<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Basics - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Basics - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/search-labs/blog/category/basics</link>
    </image>
    <link>https://www.elastic.co/search-labs/blog/category/basics</link>
    <atom:link href="https://www.elastic.co/search-labs/rss/category/basics.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <lastBuildDate>Wed, 16 Sep 2026 06:33:49 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Elasticsearch query logs: One coordinator-level line per query for ES|QL, DSL, SQL, and EQL]]></title>
    <description><![CDATA[Easily understand query impact on cluster performance with Elasticsearch query logs. One coordinator-level line records ES|QL, DSL, SQL, and EQL per request and provides full query text, tracing, optional user context, and CCS hints]]></description>
    <content:encoded><![CDATA[<p>Your dashboard times out and CPU spikes, but which query actually ran? Slow logs give you one line per shard; Elasticsearch query logs give you one JSON line per request, with the same end-to-end duration as the took you already trust from the API. That single line also captures full query text for ES|QL, DSL, SQL, and EQL, outcomes, tracing, optional user context, and cross-cluster hints when relevant.</p><p>They’re ECS-aligned, ready for Discover and out-of-the-box dashboards once you ship the log, no custom schema project. Below: why we built this, how it differs from slow logs, what each line contains, and how to turn it on.</p><h2>Why we built this (you asked, a lot!)</h2><p>Coordinator-level query logging has been a very popular request; we listened and delivered! The same pain kept showing up: You want the <em>response</em> duration for Service Level Objectives (SLOs) and dashboards. You want to know the execution time of queries executed in your cluster, and you want to be able to see the full query.</p><p>If using cross-cluster search, a search that fans out across clusters looks like one operation from the app or Kibana, but operationally it’s a chain of work: coordination, remote execution, merges, timeouts, and partial results. When something is slow or flaky, teams need to know not only how long the request took but also which clusters contributed and whether the outcome was success, partial, or a hard failure.</p><p><strong>What you get:</strong> One log stream, one entry per query! Every entry has the coordinator duration (the very same <code>took</code> time that actually matches your search API response), success or failure, and the full query text. Elastic Common Schema–compliant (ECS) JSON, optional duration threshold and user/audit fields, plus <code>X-Opaque-Id</code> that lets you <a href="https://www.elastic.co/docs/troubleshoot/kibana/trace-elasticsearch-query-to-the-origin-in-kibana">trace a hot query</a> back to the saved object it originates from, and the trace ID so you can correlate with Kibana or your own tooling.</p><p><strong>What’s more:</strong> Logs follow a stable, ECS-aligned schema, which means you don’t need to design your own ingestion pipelines or field mappings. This consistency enables out-of-the-box dashboards and analytics that work immediately once logs are shipped.</p><h2>Slow logs vs. query logs: The 30-second version</h2><p>Slow logs have been the go-to tool for years. They tell you which search operation is slow, but they emit <strong>one line per shard</strong> that took part, where each line reflects that shard’s piece of the work. This means that they don’t provide a single row that says how long the query execution took, from the client’s perspective. Query logs do exactly that: <strong>one line per query</strong>, with the <strong>end-to-end (wall clock)</strong> duration that lines up with the <code>took</code> time in the search API response. This makes them much better suited for understanding workload patterns and identifying problematic queries quickly.</p><p>Slow and query logs also differ in when they fire and what they cover. Slow logs only write when a shard’s slice breaches a duration threshold; that is, you’re optimized for “show me unusually slow shard work.” Query logs can record every query (or only those above a configurable threshold you set at the cluster level), so you can tune volume for analytics versus troubleshooting. Slow logs only support DSL queries, while query logs cover <strong>ES|QL, DSL, SQL, and EQL</strong>, which matches how you reason about “what ran on my cluster” in a modern stack. Both provide the same support in terms of correlation with headers, traces, and audit information (when you turn on user context).</p><p>The table below summarizes the main differences between the historical slow logs and the new query logs features.</p><p></p><p>Slow logs</p><p>Query logs</p><p>What they’re for</p><p>Finding hot shards / slow index operations on specific indices and classic performance tuning inside one cluster.</p><p>Understanding what query ran, how long the operation took end to end from the coordinator, and whether it succeeded, which is better for SLOs, analytics, and incident investigations.</p><p>Granularity</p><p>Per shard (and per phase) for searching slow logs: One user search can produce many lines across shards/replicas.</p><p>Per coordinator-level query: One query maps to one log event.</p><p>Scope of work</p><p>Query + indexing</p><p>Query only; indexing will come soon.</p><p>What you learn</p><p>“This shard on this index exceeded N ms in query/fetch phase.”</p><p>“This query (full text), this duration, this outcome, and (when relevant) federation/cross-cluster summary fields.”</p><p>Query types</p><p>DSL only</p><p>ES|QL, DSL, SQL, and EQL</p><p>Threshold model</p><p>Often tiered (for example, multiple time thresholds per log levels) and per index.</p><p>Single duration gate at the cluster level (for example, “log if duration ≥ 500ms”)</p><h2>What you get in each log line</h2><p>Every line is one JSON object (one request) in a dedicated file (for example, <code>*_querylog.json</code> under your Elasticsearch log directory). Below is what you can <em>do</em> with the data:</p><p><strong>Did it succeed, how long did it take, and what broke?</strong> Outcome (whether the request was successful or not), duration (<code>took / took_millis</code>, in line with the API), and a clear failure or timeout when something goes wrong. That’s the core signal for alerting, SLOs, and dashboards: “Are we green? If not, what’s the error?” You also get how many rows or hits came back (<code>result_count</code>), so you can separate “slow but empty” from “slow and huge.”</p><p><strong>What actually ran?</strong> Query type (<code>esql</code>, <code>dsl</code>, <code>sql</code>, <code>eql</code>) plus the <strong>full query text</strong>. That answers “Which dashboard rule, saved search, or client pattern is hammering us?” Mix it with duration and outcome to find the worst offenders to fix or throttle.</p><p><strong>Who asked for it</strong>, and how do I trace it end to end? <strong>X-Opaque-Id</strong> and <strong>trace ID</strong> tie a line back to Kibana or your own headers. Task and optional parent task IDs help follow work that was enqueued or chained (async or nested operations).</p><p><strong>Cross-cluster search: </strong>Who participated, and did anyone misbehave? When cross-cluster search (CCS) is in play, the log can carry <strong>remote cluster aliases</strong>, per-cluster duration, and status (successful, failed, partial, skipped). You can see at a glance whether a slow search was local or a specific remote dragging the response. DSL can also record that a search was served from a remote alias; ES|QL exposes the richer cluster map; EQL logs a lighter view (for example, which remotes and how many) when remotes are involved.</p><p><strong>Security (optional).</strong> With <code>elasticsearch.querylog.include.user</code>, you get the usual identity and realm fields (plus effective user when run-as applies), and API key metadata when applicable. Pair with query text and duration for governance and capacity conversations that use names, not only IPs.</p><p>There’s more available than we covered here, including additional execution details, shard-level outcomes, and optional profiling information depending on the query type. For every field path and setting, see the <a href="https://www.elastic.co/docs/deploy-manage/monitor/logging-configuration/query-logs">Elasticsearch documentation on query logs</a>.</p><h2>Where the logs live (and how to use them)</h2><p>Logs land in your Elasticsearch log directory as <code>*_querylog.json</code> (for example, <code>mycluster_querylog.json</code>) on the coordinating node. Ship them with the <code>querylog</code> fileset in the <a href="https://www.elastic.co/docs/reference/beats/filebeat/filebeat-module-elasticsearch#_querylog_log_fileset_settings">Filebeat Elasticsearch module</a>, so you can then inspect them in Discover (filter by <code>event.dataset: elasticsearch.querylog</code>). On Elastic Cloud, you need to enable Logs on your deployment, and the query logs are shipped as soon as you enable them.</p><p><strong>Two workflows.</strong> If you need a one-off look to find out who’s hammering the cluster, what the query mix is, or a quick audit, just turn logging on, set a duration threshold so you only log what matters (for example, ≥ 1 s or ≥ 5 min), and then turn it off when you’re done. If you want <strong>ongoing query analytics</strong>, simply enable logging, point Filebeat at the log, and open a dashboard on the monitoring cluster. Two very simple steps, enable + ship, and you’re done. One request per line, one duration per request, no custom pipeline.</p><p>The dashboard below builds upon the new query logs and is provided out of the box. On the top row, you can find the P95/P99 query latencies (with an optional “acceptable latency” bar), the query type breakdown, the success and failure ratio, the user and system queries ratio, and (for DSL) hits versus aggregations. Underneath that, the latency over time (avg, p50, p95, p99, max) with a reference line so you can spot regressions, query volume over time (stacked by type), and tables for top indices, top users, and top error types. Filtering for cluster, user, or index lets you zoom into exactly what you want to focus on.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltcc2ae00bd7a37f99/6a170ae4c1e8a57104f882c4/f913f860b7a7235fea9d4eeff935bd2e2aa61c0f-1999x1406.png" alt="Dashboard showing Elasticsearch query performance metrics, including P95 and P99 latency values, pie charts for query type distribution, success versus failure, user versus system queries, and hits versus aggregations, line and bar charts for query latency and volume over time, and tables listing top indices, top users, and top error types." /><p><strong>Heads up.</strong> Logging of queries is asynchronous, so it doesn’t block the query execution. Use the duration threshold to cap volume. Also worth noting that at very high queries per second (QPS), we may drop some lines rather than slow your cluster down. For analytics, shipping to a separate monitoring cluster keeps the cluster you’re debugging from taking the extra load.</p><h2>Some configuration and code samples</h2><p>Query logging is <strong>off by default</strong>. Flip it on in <code>elasticsearch.yml</code> or via the cluster settings API. Here’s how.</p><h3>Enable query logging</h3><p>In <code>elasticsearch.yml</code>:</p>elasticsearch.querylog.enabled: true<p>Or dynamically via the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-update-settings.html">cluster settings API</a>:</p>PUT _cluster/settings
{
  "persistent": {
    "elasticsearch.querylog.enabled": "true"
  }
}<h3>Only log queries above a duration threshold</h3><p>If you don’t want to log every health check or tiny request, simply set a threshold so only queries that run at least this long get an entry. Duration is in <strong>time units</strong>:</p>PUT _cluster/settings
{
  "persistent": {
    "elasticsearch.querylog.enabled": "true",
    "elasticsearch.querylog.threshold": "1s"
  }
}<h3>Include user/audit information</h3><p>If you use the Security plugin and want to see <em>who</em> ran each query:</p>PUT _cluster/settings
{
  "persistent": {
    "elasticsearch.querylog.enabled": "true",
    "elasticsearch.querylog.include.user": "true"
  }
}<h3>Log DSL searches that hit only system indices</h3><p>By default, searches that target <em>only</em> system indices aren’t logged. To include them, enable query logging and set:</p>PUT _cluster/settings
{
  "persistent": {
    "elasticsearch.querylog.enabled": "true",
    "elasticsearch.querylog.include.system_indices": "true"
  }
}<h3>Example log entries</h3><p>One line = one JSON object = one request with the same shape for ES|QL, DSL, SQL, EQL. Below: a successful DSL search and afailed EQL query with timestamp, duration, query type, and full query. On success, you get result count and shard stats, on failure an <code>error</code> block. User-inclusion and X-Opaque-Id show up when you’ve enabled them.</p><p><strong>Success (DSL search):</strong></p>{
  "@timestamp": "2026-03-04T19:40:34.736Z",
  "log": {
    "level": "INFO",
    "logger": "elasticsearch.querylog"
  },
  "event": {
    "duration": 1000000,
    "outcome": "success"
  },
  "elasticsearch": {
    "querylog": {
      "type": "dsl",
      "query": "{\"size\":10,\"query\":{\"match_all\":{\"boost\":1.0}}}",
      "indices": ["query_log_test_index"],
      "result_count": 3,
      "search": { "total_count": 3 },
      "shards": { "successful": 1 },
      "took": 1000000,
      "took_millis": 1
    },
    "node": { "name": "node-1" },
    "cluster": { "name": "my-es-cluster" }
  },
  "http": {
    "request": {
      "headers": { "x_opaque_id": "opaque-1772653234" }
    }
  },
  "user": {
    "name": "elastic",
    "realm": "reserved"
  }
}<p><strong>Failure (EQL query):</strong></p>{
  "@timestamp": "2026-03-04T19:40:35.271Z",
  "log": {
    "level": "INFO",
    "logger": "elasticsearch.querylog"
  },
  "event": {
    "duration": 1326334,
    "outcome": "failure"
  },
  "elasticsearch": {
    "querylog": {
      "type": "eql",
      "query": "any where true",
      "indices": ["nonexistent_index_xyz"],
      "result_count": 0,
      "took_millis": 1
    },
    "node": { "name": "node-1" },
    "cluster": { "name": "my-es-cluster" }
  },
  "error": {
    "type": "org.elasticsearch.index.IndexNotFoundException",
    "message": "no such index [Unknown index [nonexistent_index_xyz]]"
  }
}<h2>Wrapping up</h2><p><strong>Elasticsearch query logs</strong> provide you with one single coordinator-level log for every query (ES|QL, DSL, SQL, EQL). One line per request, coordinator duration, full query, optional user and <code>X-Opaque-Id</code>. Enable it, set a duration threshold and user-inclusion if you want them, and you’re done. Logs live in your log dir (<code>*_querylog.json</code>), and when shipped with Filebeat, you can find them in Discover under the <code>elasticsearch.querylog</code> dataset.</p><p>Head to the <a href="https://www.elastic.co/docs/deploy-manage/monitor/logging-configuration/query-logs">Elasticsearch documentation on query logs</a> for the full list of configuration settings, and field references. Slow or broken queries can also be found in <a href="https://www.elastic.co/search-labs/blog/slow-search-elasticsearch-query-autoops">AutoOps</a>, which leverages the <code>X-Opaque-Id</code> to tie a long-running search back to its origin, such as a dashboard, a saved search, or an alerting rule.</p><p>Finally, it’s also worth noting that this new query log is an evolution of the <a href="https://www.elastic.co/docs/reference/query-languages/esql/esql-query-log">ES|QL-only query log</a> that we released in 9.2. We recommend adopting the new query log since it not only supports ES|QL queries, but also all your other queries.</p><p>Now, go see what’s actually running in your cluster.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-query-logs</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-query-logs</guid>
    <category><![CDATA[Inside Elastic]]></category>
    <category><![CDATA[Basics]]></category>
    <category><![CDATA[Query Languages]]></category>
    <dc:creator><![CDATA[Najwa Harif,Valentin Crettaz]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4cb4463cec71b906/6a170ae6ab7f0834cfdb9e73/31f1d882d6c0b62bd5ba320c89bda5700434c25c-1672x941.png" length="0" type="image/png"/>
    <pubDate>Tue, 12 May 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[How to compare two Elasticsearch indices and find missing documents]]></title>
    <description><![CDATA[Exploring approaches for comparing two Elasticsearch indices and finding missing documents.]]></description>
    <content:encoded><![CDATA[<p>When managing Elasticsearch indices, you may need to verify that all documents present in one index also exist in another, such as after a reindex operation, a migration, or a data pipeline. Elasticsearch doesn't provide a built-in "diff" command for this, but the right approach depends on one key question: <strong>Are your document IDs stable between the two indices?</strong></p><h2>The problem</h2><p>Imagine you have two indices, <code>index-a</code> (source) and <code>index-b</code> (target), and you want to find all documents that exist in <code>index-a</code> but are missing from <code>index-b</code>.</p><p>A naive approach, querying both indices and comparing results in memory, won't scale. Elasticsearch is designed to handle millions of documents, and loading them all at once isn’t practical.</p><p>There are two scenarios:</p><ol><li><p><strong>IDs are stable</strong>: Both indices use the same <code>_id</code> for the same document (for example, <code>emp_no</code> as the document ID). This is the easy case.</p></li><li><p><strong>IDs are generated</strong>: Documents were ingested through different pipelines that assigned random or sequential IDs. You can't compare by <code>_id</code>; you need to match on content.</p></li></ol><p>Let's walk through both.</p><h2>Step 0 — A lighter CLI for Elasticsearch</h2><p>All the examples in this post use <a href="https://github.com/Anaethelion/escli-rs">escli</a>, a small Rust command line interface (CLI) that wraps the Elasticsearch REST API. It reads your cluster URL and credentials from environment variables, so you don’t have to repeat authentication headers on every command.</p><p>To see why that matters, here's a typical <code>_search</code> call with raw <code>curl</code>:</p>curl -X GET \
  -H "Authorization: ApiKey $ELASTIC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":{"term":{"user.id":"kimchy"}}}' \
  "$ELASTICSEARCH_URL/my-index-000001/_search"<p>With <code>escli</code>, the same request becomes:</p>./escli search --index my-index-000001 &lt;&lt;&lt; '{"query":{"term":{"user.id":"kimchy"}}}'<p>The credentials live in a <code>.env</code> file that escli sources automatically — no <code>-H "Authorization: ..."</code> on every call, no risk of leaking secrets in shell history. The request body is passed via stdin (<code>&lt;&lt;&lt;</code>), which makes it easy to pipe in multi-line JSON built dynamically with <code>jq</code>.</p><h2>Step 1 — Count documents in both indices</h2><p>Before doing a full scan, get a quick count of each index. If the counts match, the indices are likely in sync, and there’s no need to scan at all.</p>./escli count --index index-a
./escli count --index index-b<p>The <code>_count</code> API returns:</p>{ "count": 1000000 }<p>If the counts differ, proceed to the full comparison.</p><h2>Step 2 — When IDs mean something: Use op_type=create</h2><p>If both indices use the same <code>_id</code> for the same document, for example, because you indexed documents using a functional business key like <code>emp_no</code> rather than a generated UUID, you can find and fix missing documents in a single <code>_reindex</code> call.</p><h3>Why functional IDs matter</h3><p>Using a meaningful field as <code>_id</code> (instead of a random UUID) is a best practice when the data has a natural key. It means:</p><ul><li><p>The same document always gets the same <code>_id</code>, regardless of which pipeline ingested it.</p></li><li><p>You can easily update or delete documents by ID.</p></li><li><p>You can use <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-index#operation-index-op_type"><code>op_type=create</code></a> to skip documents that already exist in the target.</p></li><li><p>No client-side scanning or comparison is needed.</p></li></ul><h3>The op_type=create trick</h3><p><code>_reindex</code> with <code>op_type=create</code> tries to create each document from the source in the target. If a document with the same <code>_id</code> already exists, Elasticsearch reports it as a <code>version_conflict</code> and moves on. It <strong>doesn’t</strong> overwrite the existing document. Setting <code>conflicts=proceed</code> tells the API to continue instead of aborting on the first conflict.</p>./escli reindex &lt;&lt;&lt; '{
  "source": { "index": "index-a" },
  "dest":   { "index": "index-b", "op_type": "create" },
  "conflicts": "proceed"
}'<p>The response tells you exactly what happened:</p>{
  "total": 1000000,
  "created": 49594,
  "version_conflicts": 950406,
  "failures": []
}<ul><li><p><code>created</code>: Documents that were missing from <code>index-b</code> and have now been added.</p></li><li><p><code>version_conflicts</code>: Documents that already existed in <code>index-b</code> and were left untouched.</p></li></ul><p><strong>No scanning, no client-side comparison, no intermediate file.</strong> Everything happens server-side in about six seconds on a 1M-document dataset.</p><h2>Step 3 — When IDs are not stable: Business-key comparison</h2><p>Sometimes you can't rely on <code>_id</code>. A document pipeline that generates IDs at ingestion time will assign a different <code>_id</code> each time the same record is processed. If <code>index-a</code> and <code>index-b</code> were populated by two such pipelines, the same employee record might have <code>_id: "abc123"</code> in one index and <code>_id: "xyz789"</code> in the other, even though the underlying data is identical.</p><p>In this case, you need to match documents by content rather than by ID. The key is to identify a set of fields that together form a unique business key.</p><p>For an employee dataset, a reasonable business key is <code>(first_name, last_name, birth_date)</code>. A document in <code>index-a</code> is "missing" from <code>index-b</code> if no document in <code>index-b</code> has the same combination of those three fields.</p><h3>3a — Scan the source with PIT + search_after</h3><p>Open a <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-open-point-in-time">point in time (PIT)</a> on the source index to get a consistent snapshot, and then paginate through it, fetching only the business-key fields:</p>./escli open_point_in_time index-a 5m
# → { "id": "46ToAwMDaWR..." }./escli search &lt;&lt;&lt; '{
  "size": 10000,
  "_source": ["first_name", "last_name", "birth_date"],
  "pit": { "id": "46ToAwMDaWR...", "keep_alive": "5m" },
  "sort": [{ "_shard_doc": "asc" }]
}'<p>The sort key <code>_shard_doc</code> is the most efficient sort for full-index pagination: it uses the internal Lucene document order with no overhead. Repeat with <code>search_after</code> until the response contains zero hits. Always close the PIT when done:</p>./escli close_point_in_time &lt;&lt;&lt; '{"id": "46ToAwMDaWR..."}'<h3>3b — Check each page against the target via _msearch</h3><p>For each page of source documents, build one <code>_msearch</code> request with one subquery per document. Each subquery uses a <code>bool/must</code> on the three business-key fields and requests <code>size: 0</code>; we only need to know whether a match exists, we don’t need to retrieve the document itself.</p>./escli msearch &lt;&lt; 'EOF'
{"index": "index-b"}
{"size":0,"query":{"bool":{"must":[{"term":{"first_name.keyword":"Alice1"}},{"term":{"last_name.keyword":"Smith"}},{"term":{"birth_date":"1985-03-12"}}]}}}
{"index": "index-b"}
{"size":0,"query":{"bool":{"must":[{"term":{"first_name.keyword":"Bob2"}},{"term":{"last_name.keyword":"Jones"}},{"term":{"birth_date":"1990-07-24"}}]}}}
EOF<p>The response contains one entry per subquery, in the same order:</p>{
  "responses": [
    { "hits": { "total": { "value": 1 } } },
    { "hits": { "total": { "value": 0 } } }
  ]
}<p><code>total.value == 0</code> means no document in <code>index-b</code> matches that business key; the document is missing. Collect the corresponding <code>_id</code> from the source page.</p><strong>Note on</strong> <strong><code>.keyword</code></strong> <strong>subfields</strong>: <code>term</code> queries require exact (keyword) matching. The <code>first_name</code> and <code>last_name</code> fields must have a <code>.keyword</code> subfield in the index mapping. The demo's <code>mapping.json</code> includes this.<h3>3c — Speed it up with split-by-date</h3><p>If the business key includes a date field, you can partition the source into date slices and run each slice as an independent job. Each slice opens its own PIT with a <code>range</code> filter on <code>birth_date</code>, runs its own msearch loop, and writes its results to a separate file. The parent script launches all slices in parallel and aggregates the results when they’re all done.</p><p>But depending on your use case, you might want to partition by a different field; for example, if you have a <code>team</code> field, you could run one slice per team. The key is to find a field that allows you to split the data into reasonably even chunks that can be processed in parallel.</p>[compare] Launching 5 slices in parallel...

  → Slice 1: 1960-01-01 → 1969-12-31 ✅ — 244408 checked, 12207 missing
  → Slice 2: 1970-01-01 → 1979-12-31 ✅ — 243624 checked, 12212 missing
  → Slice 3: 1980-01-01 → 1989-12-31 ✅ — 243551 checked, 11921 missing
  → Slice 4: 1990-01-01 → 1999-12-31 ✅ — 243895 checked, 11991 missing
  → Slice 5: 2000-01-01 → 2009-12-31 ✅ — 24522 checked, 1263 missing<h2>Performance on a 1M dataset</h2><p>To validate the approaches, the demo generates 1,000,000 documents in <code>index-a</code> and deliberately skips ~5% in <code>index-b</code> (49,594 missing documents), and then runs the full compare → reindex cycle.</p><p>Results on a MacBook M3 Pro:</p><p><strong>Comparison</strong> (<code>compare-indices.sh</code>):</p><p>Strategy</p><p>Compare</p><p>Reindex</p><p>Total</p><p>How it works</p><p>op_type</p><p></p><p>6s</p><p>6s</p><p>Full _reindex server-side, skips existing</p><p>business-key</p><p>1m 38s</p><p>4s</p><p>1m 42s</p><p>PIT scan + _msearch by business key</p><p>split-by-date</p><p>32s</p><p>4s</p><p>36s</p><p>Same as business-key, 5 slices in parallel</p><p>The <code>op_type=create</code> approach is fastest because everything is server-side and requires no client-side scanning. The <code>split-by-date</code> strategy cuts the <code>business-key</code> duration from 1m 38s down to 36s through parallelism: not bad for a comparison across two 1M-document indices.</p><h2>Decision tree</h2>Are _id values stable between both indices?
├── Yes → _reindex with op_type=create          (6s, server-side)
└── No  → Do you have a reliable business key?
          ├── Yes, simple scan is fast enough → business-key   (1m 42s)
          └── Yes, and you need more speed    → split-by-date  (36s, parallel)<h2>Conclusion</h2><p>Elasticsearch doesn't offer a native index diff command, but the right strategy depends on your data model:</p><ul><li><p><strong>Use functional</strong> <strong><code>_id</code></strong><strong>s</strong> (a natural business key like <code>emp_no</code>) whenever possible. It unlocks the simplest and fastest approach: <code>_reindex</code> with <code>op_type=create</code> finds and fills gaps in one server-side call.</p></li><li><p><strong>When IDs are unstable</strong>, match by business key using PIT + <code>_msearch</code>. Partition by a field and run slices in parallel to recover most of the performance. If you find yourself doing this regularly, consider computing a hash of your business key fields and using it as <code>_id</code> at ingestion time. You get the best of both worlds: stable IDs and efficient lookups.</p></li></ul><p>The complete demo, including dataset generation, comparison scripts, and reindex scripts, is available at <a href="https://github.com/dadoonet/blog-compare-indices/">https://github.com/dadoonet/blog-compare-indices/</a>.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-index-comparison</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-index-comparison</guid>
    <category><![CDATA[Basics]]></category>
    <dc:creator><![CDATA[David Pilato]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt87c19debc74944e3/6a170e09286714495393e3ae/099abf465250360ab741a5aa13931fa8884ded34-1376x768.png" length="0" type="image/png"/>
    <pubDate>Mon, 06 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Testing Elasticsearch. It just got simpler.]]></title>
    <description><![CDATA[Explaining how Elasticsearch integration tests have become simpler thanks to improvements in Elasticsearch 9.x, the modern Java client, and Testcontainers 2.x.]]></description>
    <content:encoded><![CDATA[<p>When I first wrote about <a href="https://www.elastic.co/search-labs/blog/series/integration-tests-using-elasticsearch">testing Elasticsearch</a> with Testcontainers for Java, the focus was very pragmatic: if you care about correctness, you should test against a real node; if you care about confidence, your integration tests should resemble production as closely as possible; and if you care about maintainability, your setup shouldn’t turn into a maze of mocks and assumptions.</p><p>That philosophy hasn’t changed.</p><p>What has changed, however, is how little effort it now takes to achieve that goal. With Elasticsearch 9.x, the modern Java client, and Testcontainers 2.x, the experience of writing integration tests feels noticeably smoother, as if a layer of incidental complexity has quietly been removed.</p><p>The example accompanying this article is intentionally modest and can be found <a href="https://github.com/pioorg/elasticsearch9-testcontainers2/blob/main/src/test/java/testing_elasticsearch/ES9TC2DemoTest.java">here</a>.</p><p>It doesn’t attempt to demonstrate sophisticated indexing strategies or elaborate data pipelines; instead, it concentrates on the essentials, because the essentials are precisely where the improvements are most visible.</p><h2>When the tooling stops getting in the way</h2><p>Anyone who has maintained a test suite for a few years will recognize the pattern: You introduce a new library, a transitive dependency pulls something unexpected, and before long, you’re negotiating between versions of testing engines rather than writing tests.</p><p>With Testcontainers 2.x, that negotiation largely disappears. The dependency structure is clearer, the modules are more explicit, and the accidental coupling to older testing frameworks no longer sneaks in behind your back. In practical terms, adding Elasticsearch support to your tests is now as straightforward as declaring:</p>&lt;dependency&gt;
  &lt;groupId&gt;org.testcontainers&lt;/groupId&gt;
  &lt;artifactId&gt;testcontainers-elasticsearch&lt;/artifactId&gt;
  &lt;version&gt;2.0.3&lt;/version&gt;
  &lt;scope&gt;test&lt;/scope&gt;
&lt;/dependency&gt;<p>And, if you’re using JUnit Jupiter integration:</p>&lt;dependency&gt;
  &lt;groupId&gt;org.testcontainers&lt;/groupId&gt;
  &lt;artifactId&gt;testcontainers-junit-jupiter&lt;/artifactId&gt;
  &lt;version&gt;2.0.3&lt;/version&gt;
  &lt;scope&gt;test&lt;/scope&gt;
&lt;/dependency&gt;<p>There are no exclusions to sprinkle in, no legacy engines to silence, and no uneasy feeling that something hidden might surface during the next upgrade. The configuration becomes almost unremarkable, which, in the context of build tooling, is a compliment.</p><h2>A real Elasticsearch node, with security intact</h2><p>In the demo test, we use the official Elasticsearch 9.3.1 Docker image:</p>var container =
    new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:9.3.1");

container.start();<p>At first glance, this may look similar to older examples, yet the subtle difference lies in what we no longer need to do. <strong>We don’t disable security.</strong> <strong>We don’t bypass SSL.</strong> We don’t simplify the environment just to make the test convenient.</p><p>Instead, once the container is started, we construct a client that uses the REST API and authenticates properly:</p>try (var client = ElasticsearchClient.of(c -&gt; c
     .host("https://" + container.getHttpHostAddress())
     .usernameAndPassword("elastic", ElasticsearchContainer.ELASTICSEARCH_DEFAULT_PASSWORD)
     .sslContext(container.createSslContextFromCa())
)) {<p>What deserves special mention here is how neat the client construction itself has become. In earlier iterations, creating an Elasticsearch client often meant juggling multiple intermediate objects, configuring transport layers explicitly, wrapping low-level clients, and dedicating some amount of code to what was essentially plumbing. Now, the signal-to-noise ratio is refreshingly high. The builder encapsulates the necessary details, the container provides what the client needs, and the resulting configuration fits comfortably within a few readable lines.</p><p>Just as importantly, the <code>ElasticsearchClient</code> is <code>AutoCloseable</code>, which means it integrates naturally with try-with-resources, ensuring proper cleanup without additional ceremony. The lifecycle is explicit, concise, and self-contained, which is exactly what you want in integration tests that should focus on behavior rather than infrastructure management.</p><p>The container exposes everything required to build a legitimate, secure connection, and the client integrates with it naturally, which means the test environment mirrors production in all the aspects that matter, without imposing additional mental overhead from the developer.</p><p>This alignment between realism and simplicity is, perhaps, one of the most meaningful improvements.</p><h2>Typed APIs change the character of tests</h2><p>The evolution of the Elasticsearch Java client has also reshaped how integration tests read and feel. Where older approaches often involved parsing JSON responses or navigating loosely typed structures, the modern client offers a builder-based, strongly typed API that guides you through valid request shapes at compile time.</p><p>In the demo, we perform a simple cluster health check:</p>var health = client.cluster().health();

Assertions.assertEquals("docker-cluster", health.clusterName());
Assertions.assertEquals(HealthStatus.Green, health.status());<p>What’s striking here is not the complexity of the operation, but the absence of friction. There’s no manual extraction from maps, no assertions built on untyped string values, and no detour into low-level response handling. The test code looks indistinguishable from application code, which subtly reinforces the idea that integration tests aren’t a special category of code with different rules, but simply another consumer of the same APIs.</p><p>When the boundary between production code and test code becomes thinner, confidence increases almost by default.</p><h2>Reading the test as a story</h2><p>If you take a look at the full test case:</p>@Test
void newClientTest() throws IOException {
    try (var container =
             new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:9.3.1")) {
        
        container.start();
        
        try (
            var client = ElasticsearchClient.of(c -&gt;
                c.host("https://" + container.getHttpHostAddress())
                    .usernameAndPassword("elastic", ElasticsearchContainer.ELASTICSEARCH_DEFAULT_PASSWORD)
                    .sslContext(container.createSslContextFromCa()))) {

            HealthResponse health = client.cluster().health();

            Assertions.assertEquals("docker-cluster", health.clusterName());
            Assertions.assertEquals(HealthStatus.Green, health.status());
        }
    }
}<p>you’ll notice that it reads less like a configuration script and more like a short narrative:</p><ul><li><p>We define the container.</p></li><li><p>We start the container.</p></li><li><p>We build a client.</p></li><li><p>We call a real API.</p></li><li><p>We assert the outcome.</p></li></ul><p>The supporting infrastructure fades into the background, leaving the intent of the test clearly visible. That clarity isn’t accidental; it’s the cumulative effect of incremental improvements across Testcontainers and the Elasticsearch client.</p><h2>The advanced patterns still apply</h2><p>None of the more advanced techniques discussed in earlier articles, <a href="https://www.elastic.co/search-labs/blog/elasticsearch-integration-tests-faster">Faster integration tests with real Elasticsearch</a> and <a href="https://www.elastic.co/search-labs/blog/elasticsearch-improve-performance-integration-tests">Advanced integration tests with real Elasticsearch</a>, have become obsolete. Reusing containers to speed up large test suites, customizing cluster settings, preloading indices, or testing role-based access scenarios remain entirely valid and, in many cases, essential.</p><p>What has improved is the baseline experience. The simplest possible integration test, the one that merely needs a real node and a real client, no longer requires defensive configuration or dependency gymnastics. It’s concise, expressive, and production-like by default.</p><h2>Progress without drama</h2><p>There was no dramatic rewrite of the ecosystem, no disruptive migration guide that forced a rethinking of everything. Instead, there has been a steady refinement of APIs and dependencies, each release smoothing a rough edge here and removing a surprise there.</p><p>The result isn’t flashy, yet it’s tangible. Writing integration tests against Elasticsearch now feels less like assembling a test harness and more like exercising a real system in miniature.</p><p>Sometimes progress announces itself loudly. Sometimes it arrives quietly, in the form of code that simply reads better and requires less explanation. In this case, it’s the latter, and for those of us who care about clean, reliable integration tests, that’s more than enough.</p><p>And what if we could do something similar with Kibana? Sounds appealing? Stay tuned!</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-integration-tests</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-integration-tests</guid>
    <category><![CDATA[Java]]></category>
    <category><![CDATA[Basics]]></category>
    <dc:creator><![CDATA[Piotr Przybyl]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc31c3bf0d453d846/6a170e12cdacbfdf6a7d2a7e/3ae41b1f2876d2ad11c8e2b79bbf79955d6902aa-1440x840.png" length="0" type="image/png"/>
    <pubDate>Thu, 19 Mar 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch Serverless pricing demystified: VCUs and ECUs explained]]></title>
    <description><![CDATA[Learn how Elasticsearch Serverless pricing works for Elastic’s fully-managed deployment offering. We explain VCUs (Search, Ingest, ML) and ECUs, detailing how consumption is based on actual allocated resources, workload complexity, and Search Power.]]></description>
    <content:encoded><![CDATA[<p><em>Navigating Elasticsearch Serverless pricing is simple... you pay for the resources you use. Getting a handle on VCUs, ECUs, and the factors that drive your consumption is key to making informed decisions about your usage. In this blog, we'll break down exactly how Elasticsearch Serverless pricing works so you can plan, monitor, and optimize your spend.</em></p><p>When we built Elasticsearch Serverless, we had to decide how to bill our users. While a charge per query may have been easier to reason about from a consumption perspective, it would be a lot harder to reason about from a resource perspective. Instead, we implemented a simple pricing scheme comprising three dimensions for compute: search, ingest, and machine learning VCUs. This means we charge users for the actual resources we allocate to fulfill your requested workloads.</p><h2>VCU, ECU, and other terms</h2><p>Let's start by defining a few terms that will keep coming back throughout this post.</p><h3>VCU</h3><p>A VCU is a <a href="https://www.elastic.co/docs/deploy-manage/cloud-organization/billing/elasticsearch-billing-dimensions#elasticsearch-billing-information-about-the-vcu-types-search-ingest-and-ml">Virtual Compute Unit</a>, representing a fraction of RAM, CPU, and local disk for caching. We separate compute by the workloads they support, so we have three flavors of VCU:</p><ol><li><p>Search VCU</p></li><li><p>Ingest VCU</p></li><li><p>Machine Learning (ML) VCU</p></li></ol><p>VCU’s are charged by the hour.</p><h3>Regional pricing</h3><p>We have different prices for different regions and different cloud providers. You can find a full list of prices <a href="https://cloud.elastic.co/cloud-pricing-table?productType=serverless">on this page</a>.</p><h3>ECU</h3><p>An ECU is an <a href="https://www.elastic.co/docs/deploy-manage/cloud-organization/billing/ecu">Elastic Consumption Unit</a>, which is the unit we bill you in. The nominal value of an ECU is $1.00 USD. All of the different components of consumption are charged at a specific rate of ECUs per time unit. For example, one Gigabyte of storage might cost 0.047 ECU per month, so 100 GB of storage will cost you 4.7 ECU = $4.70 for one month. Similarly, if your search workload consumed 10 VCUs in a day and the Search VCU rate in your region is 0.09 ECU, your cost for that day would be $0.90.</p><h3>Interactive Dataset Size</h3><p>The amount of data in your project has a direct influence on your costs. We make the distinction of “interactive dataset” primarily for time-series data, as this relates to the amount of data in the Boost Window. For non-time-series data, this is simply the amount of data in the project.</p><p></p><h2>Project settings</h2><p>We have three <a href="https://www.elastic.co/docs/deploy-manage/deploy/elastic-cloud/project-settings">project settings</a> that allow you to control your project's usage.</p><h3>Search power</h3><p>Search Power controls the speed of searches against your data. With Search Power, you can improve search performance by adding more resources for querying, or you can reduce provisioned resources to cut costs. Choose from three Search Power settings:</p><p><strong>On-demand</strong>: Autoscales based on data and search load, with a lower minimum baseline for resource use. This flexibility results in more variable query latency and reduced maximum throughput.</p><p><strong>Performant</strong>: Delivers consistently low latency and autoscales to accommodate moderately high query throughput.</p><p><strong>High-availability</strong>: Optimized for high-throughput scenarios, autoscaling to maintain query latency even at very high query volumes.</p><h3>Boost window</h3><p>For time series use cases, the boost window is the number of days of data that constitutes your interactive dataset size. The interactive dataset is the portion of your data that we keep cached, and that we use to determine how to scale the Search tier for your project. By default, the boost window is seven days.</p><h3>Data retention</h3><p>You can set the number of days of data that are retained in your project, which will affect the amount of storage we need. You can do this on a per-data stream basis in your project.</p><h2>Price components</h2><p>Serverless Elasticsearch contains a few different pricing components. For most use cases, the components you will care most about are Search, Ingest, and ML VCUs, as well as the Elastic Inference Service's token consumption.</p><h3>Search VCUs</h3><p>Search VCU consumption is the most complex part of pricing. We make this simple for you by automatically determining the right amount of VCUs that are needed to fulfill your workloads. For more details on how our autoscaling logic works, see <a href="https://www.elastic.co/search-labs/blog/elasticsearch-serverless-tier-autoscaling">our earlier blog on the topic</a>.</p><h4>Search VCU inputs</h4><p>Search VCUs are allocated based on a few factors, but mainly, we can boil it down to three inputs: the interactive dataset size, the search load on the system, and Search Power.</p><p>For traditional search use cases, the interactive dataset size will generally be your entire dataset. For time series use cases, it will be the portion of your dataset that fits inside the Boost Window.</p><p>Search load measures the amount of load being placed on the system by currently active searches. The main contributing factors are the number of searches per second, the complexity of the searches (the more that needs to be computed, the higher the load), and the size of the dataset that needs to be searched to fulfill the result. If we can get you the right number of results by scanning 10% of the dataset, then the load will be much lower than if we need to scan the full dataset.</p><p>Finally, <a href="https://www.elastic.co/docs/deploy-manage/deploy/elastic-cloud/project-settings">Search Power</a> influences the number of VCUs we allocate. Each Search Power setting defines the baseline capacity of the search tier.</p><p>In short: the larger the dataset size and the higher the search load, the more VCUs we need to fulfill your search requests. Search Power allows you to tune to what extent we will scale up and down.</p><h4>Minimum VCUs</h4><p>Elasticsearch Serverless is designed to align infrastructure costs directly with your application's demand. </p><p>For smaller workloads, the search infrastructure can scale down to zero VCUs during periods of inactivity. If the system detects fifteen minutes of total inactivity, the associated hardware resources are deprovisioned. This makes the platform highly cost-effective for development environments, bursty workloads, or applications with intermittent usage. Note that inactivity means actual inactivity: no user-initiated searches whatsoever. As soon as we need to serve a search of any kind, we need to allocate hardware resources to execute that search.</p><p>As your interactive dataset grows, the system eventually reaches a storage threshold where a baseline level of resources is required to maintain data availability and indexing readiness. A minimum VCU allocation is maintained to ensure your data remains "warm" and queryable, even if no active searches are occurring.</p><h4>VCU consumption is not linear</h4><p>Because our hardware is allocated in steps, consumption of VCUs does not necessarily scale linearly with workload size. Each scaling step can contain a wide range of workloads, and if your workload is at the bottom of that range, it may have a lot of room to grow before we need to jump to the next scaling step.</p><p>This can make estimating based on a non-representative workload hard. For example, you may be consuming 2 VCUs per hour on a small workload. It's entirely possible that you could increase your workload size by a factor of 100 and still fit in that 2 VCU per hour load before we need to start increasing the amount of VCUs we allocate to serve your workload.</p><p>We know this makes estimating your cost a little harder, and we are working on ways to make that easier for you. If you need more help estimating your likely price, you can always talk to our customer team and get more personalized assistance.</p><h2>Ingest VCUs</h2><p>Ingest VCUs are much simpler than Search VCUs.</p><h4>Ingest VCU Inputs</h4><p>Ingest VCUs have essentially three inputs: the number of indices, the ingest rate, and the ingest complexity. We need to allocate a little bit of memory for every index in your system, which is why the number of indices matters. Read indices in data streams do not count for this calculation.</p><p>The faster you ingest, the more CPU we will need to process that ingestion. And the more complex your ingest requests, the more CPU we will need. Some factors that make ingest requests more expensive to execute are complicated field mappings or a lot of post-processing.</p><h4>Minimum Ingest VCUs</h4><p>We do not have a minimum number of VCUs we allocate to your ingest. If you do not ingest data, we do not need to allocate any VCUs to processing ingestion. There is an exception for a large number of indices (think: thousands of indices), where we do need to keep some resources allocated to be responsive when indexing requests come in.</p><h4>VCU consumption is not linear</h4><p>As with Search VCUs, we allocate Ingest VCUs based on step functions. Each step can contain a wide range of workloads: it's entirely possible that if you have a minimal amount of ingest, you could increase your ingest rate by a factor of 100 and still fit in the same step, thus not actually increasing your cost.</p><h2>AI workloads</h2><p>When running machine learning tasks in Serverless, we give you three options:</p><ol><li><p>You use our Elastic Inference Service (EIS) to run your inference and completion workloads. We take care of everything, and you are charged per token.</p></li><li><p>You use traditional Elasticsearch Machine Learning capabilities to run your workloads. These use our Trained Models capabilities. We will scale up and down based on your machine learning workload requirements.</p></li><li><p>You do it yourself, outside of our systems, and just bring your vectors or other inference results to store and search in Elasticsearch.</p></li></ol><h4>EIS</h4><p>The pricing for EIS is <a href="https://cloud.elastic.co/cloud-pricing-table?productType=serverless">quite straightforward</a>: you get charged a rate per one million consumed tokens. Token consumption is generally easy to predict for inference workloads. For LLM-based tasks, particularly agentic ones, this can be more complex, and some experimentation and trial runs may be useful to determine how many tokens your workloads typically consume.</p><h4>ML VCUs</h4><p>Machine Learning VCUs work on one simple input: machine learning workloads. The more inference you require, the more VCUs we will consume. Once you stop performing inference, we will scale down. We will keep a trained model in memory for about 24 hours after you last used it so that we can be responsive, which means that the minimal amount of VCU required to keep that model available will remain up for 24 hours before scaling down entirely.</p><p>We generally recommend our customers use EIS instead of our Machine Learning nodes for inference, particularly if your usage is periodic. By switching to EIS, you will not have to wait for machine learning nodes to spin up, and we won't charge you for unused ML node time before scaling down. EIS charges on a per token basis.</p><h2>Storage</h2><p>We charge storage per gigabyte per month. Storage does serve as an input into other parts of our system, particularly Search VCUs (see Search VCU above), but the pricing for storage itself is <a href="https://cloud.elastic.co/cloud-pricing-table?productType=serverless">quite straightforward</a>.</p><h2>Data Out (egress)</h2><p>We charge you for the data you take out of the system.</p><p>To minimize your egress costs, we recommend a few optimizations on your queries:</p><ol><li><p>Do not return vectors in your query responses. We <a href="https://www.elastic.co/search-labs/blog/elasticsearch-exclude-vectors-from-source">do this by default</a> for indices created after October 2025. You can always return vectors in your responses explicitly if necessary.</p></li><li><p>Return only the fields needed for your application. You can <a href="https://www.elastic.co/search-labs/blog/displaying-fields-in-an-elasticsearch-index">do this</a> by using the <code>fields</code> and <code>_source</code> parameters.</p></li></ol><h2>Support</h2><p>We charge <a href="https://www.elastic.co/pricing/serverless-search">support</a> as a percentage of your total ECU usage. We currently have four levels of support:</p><ol><li><p>Limited support</p></li><li><p>Base support</p></li><li><p>Enhanced support</p></li><li><p>Premium support</p></li></ol><h2>Project subtype profiles</h2><p>We currently offer two project subtypes for Serverless Elasticsearch, referred to as “General Purpose” and “Vector Optimized”. All Serverless Elasticsearch projects created through the cloud console UI will be created using the “General Purpose” option. You may create a “Vector Optimized” by calling the API directly with the <code>optimized_for</code> parameter (see <a href="https://www.elastic.co/docs/api/doc/elastic-cloud-serverless/operation/operation-createelasticsearchproject">documentation</a> for all options).</p><p>The difference between the two options is the allocation of resources. We allocate approximately four times more resources (aka VCUs) to the “Vector Optimized” profile, which will result in your costs being up to four times higher. This is why we recommend starting on the “General Purpose” profile and only using the “Vector Optimized” profile when your use case demands the use of uncompressed dense vectors with high dimensionality, and quantization and <a href="https://www.elastic.co/search-labs/blog/diskbbq-elasticsearch-introduction">DiskBBQ</a> will not serve your needs.</p><p>When Serverless Elasticsearch was envisioned years ago, we thought that vector workloads would require much more resources to remain performant. However, with innovations like <code>semantic_text</code>, <code>sparse_vector</code> models, and <a href="https://www.elastic.co/search-labs/blog/elasticsearch-9-1-bbq-acorn-vector-search">Better Binary Quantization</a> (BBQ), we’ve found that many vector workloads perform well on the “General Purpose” profile at a fraction of the cost. Therefore, don’t let the “Vector Optimized” label fool you…you can get excellent price <em>and</em> performance for vector workloads on the “General Purpose” profile.</p><h2>Monitoring costs</h2><p>We recognize that keeping track of your costs, especially when you are new to Elasticsearch Serverless, is important to you. We built a few tools just for this purpose, and continue to improve them for even greater visibility.</p><h2>Cloud console billing usage</h2><p>The <a href="https://www.elastic.co/docs/deploy-manage/cloud-organization/billing/view-billing-history">Elastic Cloud Console</a> provides billing details for your cloud account, across all cloud-based resources, including Elasticsearch Serverless. There, you can find a breakdown of all the price components described in this article. Filters allow you to zoom in on specific time periods and resources.</p><p>To further monitor your costs, you can also configure custom <a href="https://www.elastic.co/docs/deploy-manage/cloud-organization/billing/manage-billing-notifications">budget alerts </a>from the Budgets and notifications tab under the Billing and subscriptions page.</p><h2>AutoOps monitoring</h2><p>We’re bringing <a href="https://www.elastic.co/docs/deploy-manage/monitor/autoops/autoops-for-serverless">AutoOps to Serverless</a>! One of the key value propositions of Elasticsearch Serverless is that we ensure everything runs smoothly, but that also means you have limited observability into the infrastructure. AutoOps for Serverless gives users visibility into what is driving usage, and, therefore, costs.</p><p>AutoOps is rolled out in new Serverless regions regularly, and we're always working to add new monitoring tools. Make sure to check out the <a href="https://www.elastic.co/docs/deploy-manage/monitor/autoops/ec-autoops-regions#autoops-for-serverless-full-regions">region coverage</a> and future planned monitoring tools.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-serverless-pricing-vcus-ecus</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-serverless-pricing-vcus-ecus</guid>
    <category><![CDATA[Basics]]></category>
    <category><![CDATA[Inside Elastic]]></category>
    <dc:creator><![CDATA[Sander Philipse,Pete Galeotti]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3b8542204a8988dc/6a170bed0e2e49cd2641a12e/46f1e3c09e17cb8aa2a1cca64624bf533e55fe1d-1746x1096.png" length="0" type="image/png"/>
    <pubDate>Fri, 19 Dec 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Parsing JSON fields in Elasticsearch]]></title>
    <description><![CDATA[Learn how to parse JSON fields in Elasticsearch using an ingest pipeline to efficiently index, query, and aggregate JSON data.]]></description>
    <content:encoded><![CDATA[<p>In this article, we will discuss how to parse JSON fields in Elasticsearch, which is a common requirement when dealing with log data or other structured data formats. We will cover the following topics:</p><ol><li><p>Ingesting JSON data into Elasticsearch</p></li><li><p>Using an Ingest Pipeline to parse JSON fields</p></li><li><p>Querying and aggregating JSON fields</p></li></ol><h2>1. Ingesting JSON data into Elasticsearch</h2><p>When ingesting JSON data into Elasticsearch, it is essential to ensure that the data is properly formatted and structured. Elasticsearch can automatically detect and map JSON fields, but it is recommended to define an explicit mapping for better control over the indexing process.</p><p>To create an index with a custom mapping, you can use the following API call:</p>PUT /my_index
{
 "mappings": {
   "properties": {
     "message": {
       "type": "keyword"
     },
     "json_field": {
       "properties": {
         "field1": {
           "type": "keyword"
         },
         "field2": {
           "type": "integer"
         }
       }
     }
   }
 }
}<p>In this example, we create an index called <code>my_index</code> with a custom mapping for a JSON field named <code>json_field</code>.</p><h2>2. Using an Ingest Pipeline to parse JSON fields</h2><p>If your JSON data is stored as a string within a field, you can use the Ingest Pipeline feature in Elasticsearch to parse the JSON string and extract the relevant fields. The Ingest Pipeline provides a set of built-in processors, including the <code>json</code> processor, which can be used to parse JSON data.</p><p>To create an ingest pipeline with the <code>json</code> processor, use the following API call:</p>PUT _ingest/pipeline/json_parser
{
 "description": "Parse JSON field",
 "processors": [
   {
     "json": {
       "field": "message",
       "target_field": "json_field"
     }
   }
 ]
}<p>In this example, we create an ingest pipeline called <code>json_parser</code> that parses the JSON string stored in the <code>message</code> field and stores the resulting JSON object in a new field called <code>json_field</code>.</p><p>To index a document using this pipeline, use the following API call:</p>POST /my_index/_doc?pipeline=json_parser
{
 "message": "{\"field1\": \"value1\", \"field2\": 42}"
}<p>The document will be indexed with the parsed JSON fields:</p>{
 "_index": "my_index",
 "_type": "_doc",
 "_id": "1",
 "_source": {
   "message": "{\"field1\": \"value1\", \"field2\": 42}",
   "json_field": {
     "field1": "value1",
     "field2": 42
   }
 }
}<h2>3. Querying and aggregating JSON fields</h2><p>Once the JSON fields are indexed, you can query and aggregate them using the Elasticsearch Query DSL. For example, to search for documents with a specific value in the <code>field1</code> subfield, you can use the following query:</p>POST /my_index/_search
{
 "query": {
       "term": {
         "json_field.field1": "value1"
       }
 }
}<p>To aggregate the values of the <code>field2</code> subfield, you can use the following aggregation:</p>POST /my_index/_search
{
 "size": 0,
 "aggs": {
   "field2_sum": {
         "sum": {
           "field": "json_field.field2"
         }
 }
}<h2>Bonus: How to deal with unparsed JSON data?</h2><p>If you are in the situation where you have already ingested unparsed JSON data into a text/keyword field, there’s a way to extract the JSON data without having to reindex everything from scratch.</p><p>You can leverage the <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-update-by-query#operation-update-by-query-pipeline">Update by Query API</a> with the ingest pipeline developed in section 2. But before running that update, you’ll first need to update your index mapping similarly to what we did in section 1 to add the <code>json_field</code> mapping, by running the command below:</p>PUT /my_index/_mapping
{
  "properties": {
    "json_field": {
      "properties": {
        "field1": {
          "type": "keyword"
        },
        "field2": {
          "type": "integer"
        }
      }
    }
  }
}<p>When done, you can simply run the command below, which will iterate over all documents in your index, extract the JSON from the <code>message</code> field and index the parsed JSON data into the <code>json_field</code> object.</p>POST /my_index/_update_by_query?pipeline=json_parser<h2>Conclusion</h2><p>In conclusion, parsing JSON fields in Elasticsearch can be achieved using custom mappings, the Ingest Pipeline feature, and the Elasticsearch Query DSL. By following these steps, you can efficiently index, query, and aggregate JSON data in your Elasticsearch cluster.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-parse-json-field-ingest-pipeline</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-parse-json-field-ingest-pipeline</guid>
    <category><![CDATA[Basics]]></category>
    <dc:creator><![CDATA[Valentin Crettaz]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6d8bd040a15da207/6a17e422414c640e67945116/ef9edded97edd7c919e617e648e62016155cde56-720x420.jpg" length="0" type="image/jpeg"/>
    <pubDate>Fri, 17 Oct 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch plugin for UBI: Analyze user data in Kibana]]></title>
    <description><![CDATA[Discover how to capture user behavior data using the Elasticsearch plugin for UBI and build a custom dashboard in Kibana to analyze it. ]]></description>
    <content:encoded><![CDATA[<p>In this article, we’ll show you how to capture and analyze user analytics data using the <strong>UBI</strong> <em>(User Behavior Insights)</em> standard in Elasticsearch.</p><p><em>You can learn more about UBI in </em><a href="https://www.elastic.co/search-labs/blog/elasticsearch-plugin-user-behavior-insights"><em>this article</em></a><em>.</em></p><p>Data collected with the UBI collector can be used on Kibana to build dashboards that open the window to users’ behavior in our application. In this blog, we will explore how to analyze UBI data in Kibana to gain insights into how our app is being used.</p><h2>Demo set up</h2><p>We can easily reproduce the demo in this blog following these steps:</p><p>1. Clone the repository</p>git clone https://github.com/Alex1795/ubi-dashboard-elasticsearch_blog.git 
cd ubi-dashboard-elasticsearch_blog<p>2. Install required libraries:</p>pip install -r requirements.txt<p>3. Run the setup script. Make sure to have the following environment variables set beforehand</p><ol><li><p>ES_HOST</p></li><li><p>API_KEY</p></li><li><p>KIBANA_HOST</p></li></ol>python setup.py<p>That’s all you need to do. If everything went well, you should see this output from the script execution:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0c568b39867bd357/6a170e3360084be8393c45ff/947a67ef7210fa76f62324a3eadb62a3e10bb887-1600x633.png" alt="" /><p>As we can see the script:</p><ul><li><p>Created two indices with the appropriate mappings</p></li><li><p>Indexed 23 documents to these indices</p></li><li><p>Uploaded some saved objects to Kibana</p></li></ul><p>Now, let’s take a look at what exactly this script did behind the scenes.</p><h2>Understanding the uploaded data</h2><p>First, we put some data in Elasticsearch before creating our visualizations.</p><p>You can reproduce the process manually in Kibana DevTools, copying the mappings and sample data and using the <strong>PUT &lt;index&gt;</strong> and <strong>PUT _bulk</strong> APIs, respectively.</p><h3>Ubi_events index</h3><p>User action data, documents are generated for every click (in this case), and it includes:</p><ul><li><p><strong>application</strong>: The client application that generated the event ("search-ui")</p></li><li><p><strong>action_name</strong>: Type of user action performed ("click")</p></li><li><p><strong>query_id</strong>: Links this event to the corresponding search query session</p></li><li><p><strong>client_id</strong>: A generated, unique ID that represents a user or session without revealing personal data. It is generated instead of using identifiable data like email addresses or usernames. This approach allows us to have privacy advantages such as safe analytics capabilities and secure data sharing without exposing PII, while still having important functionality like session continuity, behavioral analysis, or A/B testing.</p></li><li><p><strong>timestamp</strong>: ISO 8601 formatted timestamp when the event occurred</p></li><li><p><strong>message_type</strong>: Category of the event for processing ("CLICK_THROUGH")</p></li><li><p><strong>message</strong>: Human-readable description of what happened ("Clicked Fahrenheit 451")</p></li><li><p><strong>user_query</strong>: The original search term that led to this event ("fahrenheit")</p></li><li><p><strong>event_attributes</strong>: Nested object containing detailed event context:</p><ul><li><p><strong>object.device</strong>: Device type used by the user ("mobile")</p></li><li><p><strong>object.object_id</strong>: Unique identifier of the clicked item</p></li><li><p><strong>object.description</strong>: Details about the clicked item (book title, date, author)</p></li><li><p><strong>object.position.ordinal</strong>: Ranking position of the item in search results (1st)</p></li><li><p><strong>object.position.page_depth</strong>: Which page of results the item appeared on (1st page)</p></li><li><p><strong>object.user.ip</strong>: User's IP address</p></li><li><p><strong>object.user.city/region/country</strong>: Geographic location data</p></li><li><p><strong>object.user.location</strong>: Precise latitude/longitude coordinates</p></li></ul></li></ul><p>Sample document:</p>       {
         "application": "search-ui",
         "action_name": "click",
         "query_id": "2dd48446-7ca8-4510-89f4-2ebb67ed240b",
         "client_id": "8c1915fe-8ee0-4487-b801-3b1d67c25cf6",
         "timestamp": "2025-07-30T14:25:52.698Z",
         "message_type": "CLICK_THROUGH",
         "message": "Clicked Fahrenheit 451",
         "user_query": "fahrenheit",
         "event_attributes": {
           "object": {
             "device": "mobile",
             "object_id": "ZwoTM5gBPJ218VOaBpj4",
             "description": "Fahrenheit 451(1953-10-15) by Ray Bradbury",
             "position": {
               "ordinal": 1,
               "page_depth": 1
             },
             "user": {
               "ip": "192.168.1.100",
               "city": "New York",
               "region": "New York",
               "country": "United States",
               "location": {
                 "lat": 40.7128,
                 "lon": -74.006
               }
             }
           }
         }
       }<p>You can download the index mappings <a href="https://github.com/Alex1795/ubi-dashboard-elasticsearch_blog/blob/main/index_mappings/ubi_events-mappings.json">here</a></p><h3>Ubi_queries index</h3><p>Search data includes data relevant to each search executed by the users:</p><ul><li><p><strong>query_response_id</strong>: Unique identifier for this specific query response instance</p></li><li><p><strong>user_query</strong>: The original search term entered by the user ("fahrenheit")</p></li><li><p><strong>query_id</strong>: Unique identifier for the search query session</p></li><li><p><strong>query_response_object_ids</strong>: Array of object IDs that were returned as search results (["3", "9"])</p></li><li><p><strong>query</strong>: The complete Elasticsearch query object in JSON format, including search parameters, fields to search, result size, sorting, and metadata</p></li><li><p><strong>client_id</strong>: A generated unique ID that represents a user or session without revealing personal data. It is generated instead of using identifiable data like email addresses or usernames. This approach allows us to have privacy advantages such as safe analytics capabilities and secure data sharing without exposing PII, while still having important functionality like session continuity, behavioral analysis, or A/B testing.</p></li><li><p><strong>timestamp</strong>: Unix timestamp in milliseconds when the query was executed (1753885225098)</p></li></ul><p>Sample document:</p>    {
         "query_response_id": "03e8af3e-8725-49d9-99ad-36bf2a8e96d1",
         "user_query": "fahrenheit",
         "query_id": "f8b2f5bc-cb3c-49d4-86bc-19212a782ba7",
         "query_response_object_ids": [
           "3",
           "9"
         ],
         "query": """{"from":0,"size":20,"query":{"multi_match":{"query":"fahrenheit","fields":["author^1.0","name^1.0"]}},"_source":{"includes":["name","author","image_url","url","price","release_date"],"excludes":[]},"sort":[{"_score":{"order":"desc"}}],"ext":{"query_id":"f8b2f5bc-cb3c-49d4-86bc-19212a782ba7","user_query":"fahrenheit","client_id":"8c1915fe-8ee0-4487-b801-3b1d67c25cf6","object_id_field":null,"query_attributes":{}}}""",
         "client_id": "8c1915fe-8ee0-4487-b801-3b1d67c25cf6",
         "timestamp": 1753885225098
       }<p>You can download the index mappings <a href="https://github.com/Alex1795/ubi-dashboard-elasticsearch_blog/blob/main/index_mappings/ubi_queries-mappings.json">here</a><strong>.</strong></p><h3>Sample data</h3><p>We can use the <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-bulk">_bulk API</a> to index <a href="https://github.com/Alex1795/ubi-dashboard-elasticsearch_blog/blob/main/sample_documents/bulk_index.ndjson">some sample</a> data in both indices</p><p>This will create 6 documents in the <strong>ubi_queries </strong>index and 16 in the <strong>ubi_events</strong> index.</p><h3>Dashboard object</h3><p>Before going into details of the visualizations used in this example <a href="https://github.com/Alex1795/ubi-dashboard-elasticsearch_blog/blob/main/dashboards/web_analytics_dashboard.ndjson">here</a>, you can download the Saved Object of the full example dashboard and <a href="https://www.elastic.co/docs/explore-analyze/find-and-organize/saved-objects#saved-objects-import">import</a> it into your Kibana instance. This dashboard explores the most searched terms, when searches and events took place, and where they come from (in a map).</p><h2>Visualize Insights</h2><p>We are going to create a Kibana dashboard to analyze the most common metrics leveraging <a href="https://www.elastic.co/docs/explore-analyze/visualize/lens">Kibana Lens</a>. For a reference on available visualizations, visit <a href="https://www.elastic.co/docs/explore-analyze/visualize/supported-chart-types">this</a> page.</p><h3>Ubi_events</h3><p>We will start with some simple Metric visualizations created with Lens: <strong>Total events:</strong> Counts how many events were triggered in the timeframe. Uses a simple count of the documents in the index, denoted by <strong># Records</strong> in the field list.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt69d672ece8a13874/6a170e35286714284693e3be/2d06ff89f2cf43ee4102e9e01079ff63754e99fd-502x182.png" alt="" /><p><strong>Event actions: </strong>Counts actions by <code>action_name</code>. This is a simple count of documents split by <code>action_name.keyword</code>. In our sample data, we have two types of actions:</p><ul><li><p>click: Generated when a user clicks in the book link</p></li><li><p>search_input: Generated when a user enters text in the search box (debounce 300ms)</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltda49e8d513f0166c/6a170e366f7f04c66c9148fb/3735206d066f62990159c0777243f8b3d0703b6b-1188x186.png" alt="" /><p>Now on table visualizations:</p><p><strong>Top clicks: </strong>A table with a count of the number of events split by the query they come from. It uses a Top values function on the <code>user_query.keyword</code>. This can give us visibility on which queries generate more interactions on our webpage.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt81925167886ba5e3/6a170e37b339d58be776a06c/87aba3bbb9cf32aefa2aa126ba8edfb6bb456ae4-223x296.png" alt="" /><p>Finally, some other visualizations:</p><p><strong>Device types:</strong> This visualization breaks down the percentage of events by the device they come from. The device can be one of three categories: Desktop, mobile, or tablet. This visualization is a pie that uses the top values of <code>event_attributes.object.device.keyword,</code> and can give us insights into which type of devices our users have. This can generate alerts if we detect an unexpected, sudden fall of events on a specific type of device, as this might indicate that a recent change in our app resulted in errors when accessing it from a device.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt695213931fbb528c/6a170e397d8d6741e770e7c2/b9e8d1a07fcebf73b3107bcbc728b77a5d30e6a6-846x484.png" alt="" /><p><strong>Events map:</strong> A <a href="https://www.elastic.co/docs/explore-analyze/visualize/maps/maps-getting-started">map visualization</a> that shows where the events are coming from, which allows us to see the geographical distribution of our users. Right now, this shows where individual documents come from, but this can also be used to see the density of users with a heatmap, for example.</p><p>This particular visualization can provide very interesting insights when used with different filters. For example, we can see where different search terms are coming from or where most of our clicks are originating. This can be useful information for making decisions on localization efforts or establishing differences across local markets. The map uses the location at <code>event_attributes.object.user.location</code>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1fbe510ac15fc06a/6a170e3b4a531b59a036a9fb/ca87fe7d5f84b9c38d9787c899c55bd48f2af9f5-1600x759.png" alt="" /><p><strong>UBI Events: </strong>A saved search with the latest UBI events documents</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0101bc86e2a11639/6a170e3dd7c022575bde6545/73c94c7f0d238149851e066b4b53d16dff9e2e74-1309x379.png" alt="" /><h3>Ubi_queries</h3><p>Here we have visualizations from this index:</p><p><strong>Total queries:</strong> A simple document count of the index to show how many queries have been received in total. This shows the big picture and answers the question of how many total queries we had in the selected time window.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2f0e83ff0e87e104/6a170e3e286714171893e3c2/10af8288eaa278aff04625dab4d4a2c86c9ebf6d-218x90.png" alt="" /><p><strong>Unique clients: </strong>A <code>unique_count</code> of the field <code>client_id</code> to show how many different clients have used our website.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt30a15247e689a3c3/6a170e3f509168aec9e1bb82/baf7b29670f9392619dffef4800cb50efb3a0578-249x95.png" alt="" /><p><strong>Top queries (tag cloud):</strong> A Tag cloud of the top 5 most searched terms. This visualization uses the field <code>user_query.keyword</code> and allows us to easily see the main terms that our users are looking for.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt31218b03715ea15c/6a170e41e8fbce688139fd0b/833844afba1a5120ba2e83eade8f83954c34ba82-790x327.png" alt="" /><p><strong>Queries over time: </strong>A line chart of queries per hour, which uses a simple count metric in a horizontal axis of the field timestamp</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3c0731a036283fad/6a170e42a6c2b9839ce79798/01d79d29cd63bbc3be273ed3f55f49cc01859f64-873x182.png" alt="" /><p><strong>Query terms over time:</strong> Similar to the last one, but broken down by the <code>user_query.keyword</code>. This chart can show how many different terms are searched over time.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltcb263c69b0e87ecb/6a170e4467045b7cac45c288/afb1c9c19919ca9117b5dc26c6f938c425261ae9-844x209.png" alt="" /><p><strong>Top queries:</strong> A Top values table showing how many times a term was searched. It uses the <code>user_query.keyword</code> field.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfb8f5d3bf9cb7943/6a170e45a292995c17d010b6/19854feaa749b528e328e1e427aa285b2abd16b6-384x295.png" alt="" /><p><strong>Client queries:</strong> A Top values table of the <code>client_id</code> field that counts the total queries and unique queries per client.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9201d14e53e3666a/6a170e4760084b756b3c4608/af18679364d92263913febf59c792957e41e5292-382x291.png" alt="" /><p><strong>No result queries:</strong> A Top value table that shows the top <code>query_terms</code> that didn’t match any document, and a Unique Count of the field client_id. This can be very useful to determine what products our website is lacking. For example, in an e-commerce book store, seeing regular searches for a particular book title could lead us to buy copies to sell. Alternatively, it can also indicate shortcomings in our search implementation, for example, if people are using question-based searches that align better with semantic search approaches.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc59e01f67e2899a1/6a170e48dc55de5d75e00e72/75dc6908767eb86ef2e2b8ac7e25c57b55f722ee-746x574.png" alt="" /><p>Here you can see the full dashboard:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4e197f27fb09906b/6a170e4a0e2e49c69541a1b3/39a00886ed753e12e8f2966b509b08afc45b4389-1600x913.png" alt="" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt057ec5248cd60aca/6a170e4c8b73cb5b3b18a0ce/2eb980f0123d6464b94599bc01f61de542f8b8f9-1600x412.png" alt="" /><h2>Analysis of sample data</h2><p>In our dashboard, we can get some insights:</p><ul><li><p>Traffic is coming from 3 different cities in the US</p></li><li><p>Most of our users access our website from a desktop device, but we have a sizable number of users using a mobile device and even some using a tablet.</p></li><li><p>We can see the top query is “asimov,” but at the same time, we do not have any results. This might be a good indicator of what products should be prioritized for stock acquisition.</p></li></ul><p>To further this analysis, we could use Kibana’s Machine Learning capabilities to understand and predict behaviours on our website. Going even one step further, we can create alerts based on these behaviors using the different available connectors.</p><p>From a search relevance perspective, user behavior is a useful input for relevance engineering tools like <a href="https://www.elastic.co/search-labs/blog/elasticsearch-learning-to-rank-introduction">LTR</a>.</p><h2>Conclusion</h2><p>Data collected by the UBI collector can be easily used to have a better understanding of our users. The resulting dashboard becomes a live pulse of what our users are searching for and can point to data gaps to drive improvements in our search engine.</p><p><strong>Note:</strong> The o19s User Behavior Insights (UBI) plugin mentioned in this article is a third-party, community-maintained plugin and is not officially supported by Elastic. For questions or issues related to this plugin, please refer to the o19s UBI project repository at <a href="https://github.com/o19s/ubi">https://github.com/o19s/ubi</a>. </p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-plugin-user-behavior-data-kibana</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-plugin-user-behavior-data-kibana</guid>
    <category><![CDATA[Basics]]></category>
    <dc:creator><![CDATA[Eduard Martin,Alexander Dávila]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2d7e4a5bbd9427c2/6a170e4d0e2e4905b641a1b7/04f1738a38cead88c9a67b0f863171b4b43010ab-1600x913.png" length="0" type="image/png"/>
    <pubDate>Fri, 26 Sep 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Failure store: see what didn’t make it]]></title>
    <description><![CDATA[Learn about failure store, a new feature in the Elastic Stack that captures and indexes previously lost events. ]]></description>
    <content:encoded><![CDATA[<p>If a tree falls in the woods and no one is around, does it make a sound? Yes, it does. Just like if a log message is emitted but fails to process into your observability platform, <strong>that log message really did happen</strong>—and, assuming it’s something important, you’ll almost certainly hear about it eventually.</p><p>Elastic is built to adapt to all kinds of data your systems emit: logs, metrics, traces, custom telemetry, and more. But when that data doesn’t match the expected shape due to a schema change, a misconfigured agent, or a rogue service emitting unexpected fields, it can fail to process and silently disappear.</p><p>That absence is a signal. But it’s also hard to detect, hard to debug, and hard to report on. And worse, it puts the burden on the client to figure out what happened.</p><p>That’s why we built <strong>failure store</strong>: a new way to capture, debug, and analyze failed events directly in the Elastic Stack. In this blog, we’ll go over Elastic’s failure store and explain how it provides visibility into data ingestion issues, helps debug schema changes, and enables teams to monitor data quality and pinpoint failure patterns.</p><h2>About failure store</h2><p><em>Failure store</em> gives you visibility into failed events that were previously only visible to the client sending the data and to dead letter queues. It works by capturing and indexing failed documents into dedicated `::failures` indices that live in your data stream alongside your production data. You can enable it per data stream or across multiple data streams with a single cluster setting.</p><h2>Why it matters</h2><p>Teams are often downstream from the source of truth. They don’t write the code; they just keep it all running. When upstream teams ship changes that break mappings or introduce unexpected fields, failures happen. But without access to the original failed data itself, debugging becomes guesswork.</p><p>Even worse, when data fails to be indexed, it doesn’t exist in your indexes—which means it’s much harder to measure the impact. You can’t track which streams are failing most often. You can’t quantify how broken your pipelines are. And you certainly can’t alert on what’s missing if the platform never saw it (except for alerts when data goes missing).</p><p>The failure store allows a developer to understand which data failed indexing and why, giving observability engineers the tools needed to quickly understand and fix ingestion failures. Triage also happens quickly since failures are stored in Elasticsearch, with no need to gather information from remote clients or shippers.</p><h2>Get started</h2><p><strong>Set up for new data streams…</strong></p>PUT _index_template/my-index-template
{
  "index_patterns": ["my-datastream-*"],
  "data_stream": { },
  "template": {
    "data_stream_options": {
      "failure_store": { // ✨
        "enabled": true 
      } 
    }
  }
}<p><strong>…or enable for existing data streams</strong></p><p>Enable failure store for individual data streams in stack management in Kibana or leverage the _data_stream API:</p>PUT _data_stream/my-existing-datastream/_options
{
  "failure_store": {
    "enabled": true
  }
}<p><strong>Enable failure store via cluster setting</strong></p><p>If you have a large number of existing data streams, you may want to enable their failure stores in one place. Instead of updating each of their options individually, set data_streams.failure_store.enabled to a list of index patterns in the cluster settings. Any data streams that match one of these patterns will operate with their failure store enabled.</p>PUT _cluster/settings
{
  "persistent" : {
    "data_streams.failure_store.enabled" : [ "my-datastream-*", "logs-*" ]
  }
}<p><strong>A failing document response</strong></p><p>After enabling the failure store, requests that previously would fail are now processed differently. The client now receives a <code>201 created</code> instead of a <code>400 Bad Request</code>. Especially if you’re using custom applications or our <a href="https://www.elastic.co/docs/reference/elasticsearch-clients">language clients</a>, make sure to update your code accordingly. When a document goes to the failure store, the response will contain the <code>failure_store: used</code> attribute.</p>{
  "_index": ".fs-logs-generic.otel-default-2025.07.31-000010",
  "_id": "2K9IYpgBfukt97YIaUPG",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 0,
  "_primary_term": 1,
  "failure_store": "used"
}<p><strong>Search and filter failure data</strong> just like any other logs, with support for ES|QL and Kibana tools:</p>  FROM logs-generic.otel-default::failures<p>Data in the failure store comes with all the context to make debugging simple. Each ::failures index contains information about which pipeline failed, along with specific error messages, stack traces, and error types that help you identify patterns.</p><p>Are you getting lots of errors and aren't sure where to start? Use ES|QL and ML functions. With the data exposed in ES|QL, errors can be analyzed with ML capabilities such as <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/grouping-functions#esql-categorize">CATEGORIZE</a> to help parse errors and extract patterns. Read more about data remediation techniques in our <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/failure-store-recipes#failure-store-examples-remediation">documentation</a>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta20404a812af7309/6a170deda929cf3114ae0a5d/9a7c92485859ee13e971466c074bd001dbad12ba-1506x596.png" alt="Elastic failure store - example of query failures with ES|QL" /><p><strong>Control costs and retention</strong> using the same data stream lifecycle you're already using for your other data. Absent a custom retention, failure store data will stick around for 30 days.</p><p><strong>Monitor data quality over time</strong> with failure metrics and sortable dashboards by failure percentage to find new problem areas that require investigation. Read more about data quality monitoring in the <a href="https://www.elastic.co/docs/solutions/observability/data-set-quality-monitoring">documentation</a>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltcc5a3f8e6bfc435d/6a170def7d8d67042370e7a4/f6790c267346481ba3b54f203c0f30224215855b-1600x818.png" alt="Elastic failure store - example for data quality and failure summaries" /><h2>Learn more</h2><p>Failure store is available starting in Elastic 9.1 and 8.19 and will be enabled by default on <strong>logs-*-*</strong> indexes in an upcoming release. To learn more, check the <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/failure-store">documentation</a> for setup instructions and best practices.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elastic-failure-store</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elastic-failure-store</guid>
    <category><![CDATA[Basics]]></category>
    <dc:creator><![CDATA[James Baiera,Graham Hudgins]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9a02efdbda3b403b/6a170df1961e6963e4c4cf8d/9ad9833bbf5e1fd93376b955500d6cbc70e19ec0-1200x628.png" length="0" type="image/png"/>
    <pubDate>Wed, 13 Aug 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[How to migrate data between different versions of Elasticsearch & between clusters]]></title>
    <description><![CDATA[Exploring methods for transferring data between Elasticsearch versions and clusters.]]></description>
    <content:encoded><![CDATA[<p>When you want to upgrade an Elasticsearch cluster, it is sometimes easier to create a new, separate cluster and transfer data from the old cluster to the new one. This affords users the advantage of being able to test all of their data and configurations on the new cluster with all of their applications without any risk of downtime or data loss.</p><p>The disadvantages of that approach are that it requires some duplication of hardware and could create difficulties when trying to smoothly transfer and synchronize all of the data.</p><p>It may also be necessary to carry out a similar procedure if you need to migrate applications from one data center to another.</p><p>In this article, we will discuss and detail three ways to transfer data between Elasticsearch clusters.</p><p><strong>How to migrate data between Elasticsearch clusters?</strong></p><p>There are 3 ways to transfer data between Elasticsearch clusters:</p><ol><li><p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-migrate-data-versions-clusters#1.-reindexing-data-from-a-remote-cluster">Reindexing from a remote cluster</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-migrate-data-versions-clusters#2.-transferring-data-using-snapshots">Transferring data using snapshots</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-migrate-data-versions-clusters#3.-transferring-data-using-logstash">Transferring data using Logstash</a></p></li></ol><p>Using snapshots is usually the quickest and most reliable way to transfer data. However, bear in mind that you can only restore a snapshot onto a cluster of an equal or higher version and never with a difference of over one major version. That means you can restore a 6.x snapshot onto a 7.x cluster but not an 8.x cluster.</p><p>If you need to increase by more than one major version, you will need to reindex or use Logstash.</p><p>Now, let’s look in detail at each of the three options for transferring data between Elasticsearch clusters.</p><h2>1. Reindexing data from a remote cluster</h2><p>Before starting to reindex, remember that you will need to set up appropriate mappings for all of the indices on the new cluster. To do that, you must either create the indices directly with the appropriate mappings or use index templates.</p><h3>Reindexing from remote — configuration required</h3><p>In order to reindex from remote, you should add the configuration below to the elasticseearch.yml file for the cluster that is receiving the data, which, in Linux systems, is usually located here: /etc/elasticsearch/elasticsearch.yml. The configuration to add is as follows:</p>reindex.remote.whitelist: "192.168.1.11:9200"<p>If you are using SSL, you should add the CA certificate to each node and include the following in the command for each node in elasticsearch.yml:</p>reindex.ssl.certificate_authorities: “/path/to/ca.pem”<p>Alternatively, you can add the line below to all Elasticsearch nodes in order to disable SSL verification. However, that approach is less recommended since it is not as secure as the previous option:</p>reindex.remote.whitelist: "192.168.1.11:9200"
reindex.ssl.verification_mode: none
systemctl restart elasticsearch service <p>You will need to make these modifications on every node and carry out a rolling restart. For more information on how to do that, please see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.17/restart-cluster.html#restart-cluster-rolling">our guide</a>.</p><h3>Reindexing command</h3><p>After you have defined the remote host in the elasticsearch.yml file and added the SSL certificates if necessary, you can start reindexing data with the command below:</p>POST _reindex
{
  "source": {
    "remote": {
      "host": "http://192.168.1.11:9200",
      "username": "elastic",
      "password": "123456",
     "socket_timeout": "1m",
      "connect_timeout": "1m"

    },
    "index": "companydatabase"
  },
  "dest": {
    "index": "my-new-index-000001"
  }
}<p>While doing that, you may face timeout errors, so it may be useful to establish generous values for timeouts rather than relying on defaults.</p><p>Now, let’s take a look at some other common errors that you may encounter when reindexing from remote.</p><h3>Common errors when reindexing from remote</h3><h4>1. Reindexing not whitelisted</h4>{
  "error": {
    "root_cause": [
      {
        "type": "illegal_argument_exception",
        "reason": "[192.168.1.11:9200] not whitelisted in reindex.remote.whitelist"
      }
    ],
    "type": "illegal_argument_exception",
    "reason": "[192.168.1.11:9200] not whitelisted in reindex.remote.whitelist"
  },
  "status": 400
}<p>If you encounter this error, it shows that you did not define the remote host IP address or node name DNS in Elasticsearch as described above or forgot to restart Elasticsearch services.</p><p>To fix that for the Elasticsearch cluster, you need to add the remote host to all Elasticsearch nodes and restart Elasticsearch services.</p><h4>2. SSL handshake exception</h4>{
  "error": {
    "root_cause": [
      {
        "type": "s_s_l_handshake_exception",
        "reason": "PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target"
      }
    ],
    "type": "s_s_l_handshake_exception",
    "reason": "PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target",
    "caused_by": {
      "type": "validator_exception",
      "reason": "PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target",
      "caused_by": {
        "type": "sun_cert_path_builder_exception",
        "reason": "unable to find valid certification path to requested target"
      }
    }
  },
  "status": 500
}<p>This error means that you forgot to add the reindex.ssl.certificate_authorities to elasticsearch.yml as described above. To add it:</p>#elasticsearch.yml
reindex.ssl.certificate_authorities: "/path/to/ca.pem"<h2>2. Transferring data using snapshots</h2><p>Remember, as mentioned above, you can only restore a snapshot onto a cluster of an equal or higher version and never with a difference of over one major version</p><p>If you need to increase by more than one major version, you will need to reindex or use Logstash.</p><p>The following steps are required to transfer data via snapshots:</p><p>Step 1. Adding the repository plugin to the first Elasticsearch cluster – In order to transfer data between clusters via snapshots, you need to ensure that the repository is accessible from both the new and the old clusters. Cloud storage repositories such as AWS, Google, and Azure are generally ideal for this. To take snapshots, please see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/snapshot-restore.html">our guide</a> and follow the steps it describes.</p><p>Step 2. Restart Elasticsearch service (rolling restart).</p><p>Step 3. Create a repository for the first Elasticsearch cluster.</p><p>Step 4- Add the repository plugin to the second Elasticsearch cluster.</p><p>Step 5- Add repository as read only to second Elasticsearch cluster – You will need to add a repository by repeating the same steps that you took to create the first Elasticsearch cluster.</p><p>Important note: When connecting the second Elasticsearch cluster to the same AWS S3 repository, you should define the repository as a read-only repository:</p>PUT _snapshot/my_s3_repository
{
  "type": "s3",
  "settings": {
    "bucket": "my-analytic-data",
    "endpoint": "s3.eu-de.cloud-object-storage.appdomain.cloud",
    "readonly": "true"
  }
}<p>That is important because you want to prevent the risk of mixing Elasticsearch versions inside the same snapshot repository.</p><p>Step 6- Restoring data to the second Elasticsearch cluster – After taking the above steps, you can restore data and transfer it to the new cluster. Please follow the steps described in <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/snapshot-restore.html">this article</a> to restore data to the new cluster. </p><h2>3. Transferring data using Logstash</h2><p>Before starting to transfer the data with logstash, remember that you will need to set up appropriate mappings for all of the indices on the new cluster. To do that, you will need to either create the indices directly or use index templates.</p><p>To transfer data between two Elasticsearch clusters, you can set up a temporary Logstash server and use it to transfer your data between two clusters. For small clusters, a 2GB ram instance should be sufficient. For larger clusters, you can use four-core CPUs with 8GB RAM.</p><p>For guidance on installing Logstash, please <a href="https://www.elastic.co/guide/en/logstash/current/installing-logstash.html">see here</a>.</p><h3>Logstash configuration for transferring data from one cluster to another</h3><p>A basic configuration to copy a single index from cluster A to cluster B is:</p>iinput
{
elasticsearch
      {
        hosts =&gt; ["192.168.1.11:9200"]
        index =&gt; "index_name"
       docinfo =&gt; true      
      }
}

output 
{
  elasticsearch {
        hosts =&gt; "https://192.168.1.12:9200"
        index =&gt; "index_name"
        
  }
}<p>For secured elasticsearch, you can use the configuration below:</p>input
{
  elasticsearch
      {
        hosts =&gt; ["192.168.1.11:9200"]
        index =&gt; "index_name"
        docinfo =&gt; true 
        user =&gt; "elastic"
        password =&gt; "elastic_password"
        ssl =&gt; true
        ssl_certificate_verification =&gt; false
            
      }
}

output 
{
  elasticsearch {
        hosts =&gt; "https://192.168.1.12:9200"
        index =&gt; "index_name"
        user =&gt; "elastic"
        password =&gt; "elastic_password"
        ssl =&gt; true
        ssl_certificate_verification =&gt; false
  }
}<h3>Index metadata</h3><p>The above commands will write to a single named index. If you want to transfer multiple indices and preserve the index names, then you will need to add the following line to the Logstash output:</p>index =&gt; "%{[@metadata][_index]}"<p>Also if you want to preserve the original ID of the document, then you will need to add:</p>document_id =&gt; "%{[@metadata][_id]}"<p>Bear in mind that setting the document ID will make the data transfer significantly slower, so only preserve the original ID if you need to.</p><h2>Synchronization of updates</h2><p>All of the methods described above will take a relatively long period of time, and you might find that data in the original cluster has been updated while waiting for the process to complete.</p><p>There are various strategies to enable the synchronization of any updates that may have occurred during the data transfer process, and you should give some thought to these issues before starting that process. In particular, you need to think about:</p><ul><li><p>What method do you have to identify any data that has been updated/added since the start of the data transfer process (e.g., a “last_update_time” field in the data)?</p></li><li><p>What method can you use to transfer the last piece of data?</p></li><li><p>Is there a risk of records being duplicated? Usually, there is, unless the method you are using sets the document ID during reindexing to a known value).</p></li></ul><p>The different methods to enable the synchronization of updates are described below.</p><h3>1. Use of queueing systems</h3><p>Some ingestion/updating systems use queues that enable you to “replay” data modifications received in the last x days. That may provide a means to synchronize any changes carried out. </p><h3>2. Reindex from remote</h3><p>Repeat the reindexing process for all items where “last_update_time” &gt; x days ago. You can do this by adding a “query” parameter to the reindex request.</p><h3>3. Logstash</h3><p>In the Logstash input, you can add a query to filter all items where “last_update_time” &gt; x days ago. However, this process will cause duplicates in non-time-series data unless you have set the document_id.</p><h3>4. Snapshots</h3><p>It is not possible to restore only part of an index, so you would have to use one of the other data transfer methods described above (or a script) to update any changes that have taken place since the data transfer process was carried out.</p><p>However, snapshot restore is a much quicker process than reindexing/Logstash, so it may be possible to suspend updates for a brief period of time while snapshots are transferred to avoid the problem altogether.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-migrate-data-versions-clusters</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-migrate-data-versions-clusters</guid>
    <category><![CDATA[Basics]]></category>
    <dc:creator><![CDATA[Kofi Bartlett]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfc041ebca11476c6/6a16f70560084b31b93c4344/01fde3b1d714f12bf8673140c9f2f940d443de31-1440x823.jpg" length="0" type="image/jpeg"/>
    <pubDate>Mon, 14 Apr 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch new semantic_text mapping: Simplifying semantic search]]></title>
    <description><![CDATA[Learn how to use the new semantic_text field type and semantic query for simplifying semantic search in Elasticsearch.]]></description>
    <content:encoded><![CDATA[<h2>semantic_text - You know, for semantic search!</h2><p>Do you want to start using semantic search for your data, but focus on your model and results instead of on the technical details? We’ve introduced the <code>semantic_text</code> field type that will take care of the details and infrastructure that you need.</p><p><a href="https://www.elastic.co/what-is/semantic-search">Semantic search</a> is a sophisticated technique designed to enhance the relevance of search results by utilizing <a href="https://www.elastic.co/elasticsearch/machine-learning">machine learning models</a>. Unlike traditional keyword-based search, semantic search focuses on understanding the meaning of words and the context in which they are used. This is achieved through the application of machine learning models that provide a deeper semantic understanding of the text.</p><p>These models generate <a href="https://www.elastic.co/what-is/vector-embedding">vector embeddings</a>, which are numeric representations capturing the text meaning. These embeddings are stored alongside your document data, enabling <a href="https://www.elastic.co/what-is/vector-search">vector search techniques</a> that take into account the word meaning and context instead of pure lexical matches.</p><h2>How to perform semantic search</h2><p>To perform semantic search, you need to go through the following steps:</p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text#choosing-an-inference-model">Choose an inference mode</a>l to create embeddings, both for indexing documents and performing queries.</p></li><li><p><a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text#creating-your-index-mapping">Create your index mapping</a> to store the inference results, so they can be efficiently searched afterwards.</p></li><li><p><a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text#setting-up-indexing">Setting up indexing</a> so inference results are calculated for new documents added to your index.</p></li><li><p><a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text#automatically-handling-long-text-passages">Automatically handle long text documents</a>, so search can be accurate and cover the entire document.</p></li><li><p><a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text#querying-your-data">Querying your data</a> to retrieve results.</p></li></ul><p>Configuring semantic search from the ground up can be complex. It requires setting up mappings, ingestion pipelines, and queries tailored to your chosen inference model. Each step offers opportunities for fine-tuning and optimization, but also demands careful configuration to ensure all components work together seamlessly.</p><p>While this offers a great degree of control, it makes using semantic search a detailed and deliberate process, requiring you to configure separate pieces that are all related to each other and to the inference model.</p><p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-text.html"><code>semantic_text</code></a> simplifies this process by focusing on what matters: the inference model. Once you have selected the inference model, <code>semantic_text</code> will make it easy to start using semantic search by providing sensible defaults, so you can focus on your search and not on how to index, generate, or query your embeddings.</p><p>Let's take a look at each of these steps, and how <code>semantic_text</code> simplifies this setup.</p><h3>Choosing an inference model</h3><p>The inference model will generate embeddings for your documents and queries. Different models have different tradeoffs in terms of:</p><ul><li><p>Accuracy and relevance of the results</p></li><li><p>Scalability and performance</p></li><li><p>Language and multilingual support</p></li><li><p>Cost</p></li></ul><p>Elasticsearch supports both internal and external inference services:</p><ul><li><p>Internal services are deployed in the Elasticsearch cluster. You can use already included models like <a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-elser.html">ELSER</a> and <a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-e5.html">E5</a>, or import <a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-model-ref.html#ml-nlp-model-ref-text-embedding">external models</a> into the cluster using <a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-import-model.html">eland</a>.</p></li><li><p>External services are deployed by model providers. Elasticsearch supports the following:   </p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-cohere-embeddings-support">Cohere</a></p></li><li><p><a href="https://www.elastic.co/search-labs/integrations/hugging-face">Hugging Face</a></p></li><li><p><a href="https://www.elastic.co/search-labs/integrations/mistral">Mistral</a></p></li><li><p><a href="https://www.elastic.co/search-labs/integrations/open-ai">OpenAI</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-azure-ai-studio-support">Azure AI Studio</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-azure-openai-embeddings-support">Azure OpenAI</a></p></li><li><p>Google AI Studio</p></li></ul></li></ul><p>Once you have chosen the inference mode, <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/put-inference-api.html">create an inference endpoint</a> for it. The inference endpoint identifier will be the only configuration detail that you will need to set up <code>semantic_text</code>.</p>PUT _inference/sparse_embedding/my-elser-endpoint
{
  "service": "elser",
  "service_settings": {
    "num_allocations": 1,
    "num_threads": 1
  }
}
<h3>Creating your index mapping</h3><p>Elasticsearch will need to index the embeddings generated by the model so they can be efficiently queried later.</p><p>Before semantic_text, you needed to understand about the two main field types used for storing embeddings information:</p><ul><li><p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/sparse-vector.html"><code>sparse_vector</code></a>: It indexes sparse vector embeddings, like the ones generated by ELSER. Each embedding consists of pairs of tokens and weights. There is a small number of tokens generated per embedding.</p></li><li><p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html"><code>dense_vector</code></a>: It indexes vectors of numbers, which contains the embedding information. A model produces vectors of a fixed size, called the vector dimension.</p></li></ul><p>The field type to use is conditioned by the model you have chosen. If using dense vectors, you will need to <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html#dense-vector-params">configure</a> the field to include the dimension count, the similarity function used to calculate vectors proximity, and storage customizations like quantization or the specific data type used for each element.</p><p>Now, if you're using semantic_text, you define a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-text.html">semantic_text field mapping</a> by just specifying the inference endpoint identifier for your model:</p>PUT test-index
{
  "mappings": {
    "properties": {
      "infer_field": {
        "type": "semantic_text",
        "inference_id": "my-elser-endpoint"
      }
    }
  }
}
<p>That's it. No need for you to define other mapping options, or to understand which field type you need to use.</p><h3>Setting up indexing</h3><p>Once your index is ready to store the embeddings, it's time to generate them.</p><p>Before <code>semantic_text</code>, to generate embeddings automatically on document ingestion you needed to set up an <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest.html">ingestion pipeline</a>.</p><p>Ingestion pipelines are used to automatically enrich or transform documents when ingested into an index, or when explicitly specified as part of the ingestion process.</p><p>You need to use the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/inference-processor.html">inference processor</a> to generate embeddings for your fields. The processor needs to be configured using:</p><ul><li><p>The text fields from which to generate the embeddings</p></li><li><p>The output fields where the generated embeddings will be added</p></li><li><p>Specific inference configuration for text embeddings or sparse embeddings, depending on the model type</p></li></ul><p>With <code>semantic_text</code>, you simply add documents to your index. semantic_text fields will automatically calculate the embeddings using the specified inference endpoint.</p><p>This means there's no need to create an inference pipeline to generate the embeddings. Using bulk, index, or update APIs will do that for you automatically:</p>PUT test-index/_doc/doc1
{
  "infer_field": "These are not the droids you're looking for. He's free to go around"
}
<p>Inference requests in <code>semantic_text</code> fields are also batched. If you have 10 documents in a bulk API request, and each document contains 2 <code>semantic_text</code> fields, then that request will perform a single inference request with 20 texts to your inference service in one go, instead of making 10 separate inference requests of 2 texts each.</p><h3>Automatically handling long text passages</h3><p>Part of the challenge of selecting a model is the number of tokens that the model can generate embeddings for. Models have a limited number of tokens they can process. This is referred to as the model’s context window.</p><p>If the text you need to work with is longer than the model’s context window, you may <strong>truncate</strong> the text and use just part of it to generate embeddings. This is not ideal as you'll lose information; the resulting embeddings will not capture the full context of the input text.</p><p>Even if you have a long context window, having a long text means a lot of content will be reduced to a single embedding, making it an inaccurate representation.</p><p>Also, returning a long text will be difficult for the users to understand, as they will have to scan the text to check it's what they are looking for. Using smaller snippets would be preferable instead.</p><p>Another option is to use <strong>chunking</strong> to divide long texts into smaller fragments. These smaller chunks are added to each document to provide a better representation of the complete text. You can then use a nested query to search over all the individual fragments and retrieve the documents that contain the best-scoring chunks.</p><p>Before <code>semantic_text</code>, chunking was not done out of the box - the inference processor did not support chunking. If you needed to use chunking, you needed to do it before ingesting your documents or use the script processor to perform the chunking in Elasticsearch.</p><p>Using semantic_text means that chunking will be done on your behalf when indexing. Long documents will be split into 250-word sections with a 100-word overlap so that each section shares 100 words with the previous section. This overlap ensures continuity and prevents vital contextual information in the input text from being lost by a hard break.</p><p>If the model and inference service support batching the chunked inputs are automatically batched together into as few requests as possible, each optimally sized for the Inference Service. The resulting chunks will be stored in a nested object structure so you can check the text contained in each chunk.</p><h3>Querying your data</h3><p>Now that the documents and their embeddings are indexed in Elasticsearch, it's time to do some queries!</p><p>Before <code>semantic_text</code>, you needed to use a different query depending on the type of embeddings the model generates (dense or sparse). A <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-sparse-vector-query.html">sparse vector query</a> is needed to query sparse_vector field types, and either a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html">knn search</a> or a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-knn-query.html">knn query</a> can be used to search dense_vector field types.</p><p>The query process can be further customized for performance and relevance. For example, sparse vector queries can define <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-sparse-vector-query.html#sparse-vector-query-with-pruning-config-and-rescore-example">token pruning</a> to avoid considering irrelevant tokens. Knn queries can specify the number of candidates to consider and the top k results to be returned from each shard.</p><p>You don't need to deal with those details when using <code>semantic_text</code>. You use a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-semantic-query.html">single query type</a> to search your documents:</p>GET test-index/_search
{
  "query": {
    "semantic": {
      "field": "infer_field",
      "query": "robots you're searching for"
    }
  }
}
<p>Just include the field and the query text. There’s no need to decide between sparse vector and knn queries, semantic text does this for you.</p><p>Compare this with using a specific <code>knn</code> search with all its configuration parameters:</p>{
  "knn": {
    "field": "infer_field",
    "k": 10,
    "num_candidates": 100,
    "query_vector_builder": {
      "text_embedding": { 
        "model_id": "my-dense-vector-embedding-model", 
        "model_text": "robots you're searching for" 
      }
    }
  }
}
<h2>Under the hood: How <code>semantic_text</code> works</h2><p>To understand how <code>semantic_text</code> works, you can create a <code>semantic_text</code> index and check what happens when you ingest a document. When the first document is ingested, the inference endpoint calculates the embeddings. When indexed, you will notice changes in the index mapping:</p>GET test-index
{
  "test-index": {
    "mappings": {
      "properties": {
        "infer_field": {
          "type": "semantic_text",
          "inference_id": "my-elser-endpoint",
          "model_settings": {
            "task_type": "sparse_embedding"
          }
        }
      }
    }
  }
}
<p>Now there is additional information about the model settings. Text embedding models will also include information like the number of dimensions or the similarity function for the model.</p><p>You can check the document already includes the embedding results:</p>GET test-index/_doc/doc1
{
  "_index": "test-sparse",
  "_id": "doc1",
  "_source": {
    "infer_field": {
      "text": "these are not the droids you're looking for. He's free to go around",
      "inference": {
        "inference_id": "my-elser-endpoint",
        "model_settings": {
          "task_type": "sparse_embedding"
        },
        "chunks": [
          {
            "text": "these are not the droids you're looking for. He's free to go around",
            "embeddings": {
              "##oid": 1.9103845,
              "##oids": 1.768872,
              "free": 1.693662,
              "dr": 1.6103356,
              "around": 1.4376559,
              "these": 1.1396849

              …
            }
          }
        ]
      }
    }
  }
}
<p>The field does not just contain the input text, but also a structure storing the original text, the model settings, and information for each chunk the input text has been divided into.</p><p>This structure consists of an object with two elements:</p><ul><li><p><em>text</em>: Contains the original input text</p></li><li><p><em>inference</em>: Inference information added by the inference endpoint, that consists of: </p><ul><li><p><em>inference_id</em> of the inference endpoint</p></li><li><p><em>model_settings</em> that contain model properties</p></li><li><p><em>chunks</em>: Nested object that contains an element for each chunk that has been created from the input text. Each chunk contains:</p><ul><li><p>The <em>text</em> for the chunk</p></li><li><p>The calculated <em>embeddings</em> for the chunk text</p></li></ul></li></ul></li></ul><h2>Customizing <code>semantic_text</code></h2><p><code>semantic_text</code> simplifies semantic search by making default decisions about indexing and querying your data:</p><ul><li><p>uses <code>sparse_vector</code> or <code>dense_vector</code> field types depending on the inference model type</p></li><li><p>Automatically defines the number of dimensions and similarity according to the inference results</p></li><li><p>Uses <code>int8_hnsw</code> index type for dense vector field types to leverage <a href="https://www.elastic.co/search-labs/blog/evaluating-scalar-quantization">scalar quantization</a>.</p></li><li><p>Uses query defaults. No token pruning is applied for <code>sparse_vector</code> queries, nor custom <code>k</code> and <code>num_candidates</code> are set for knn queries.</p></li></ul><p>Those are sensible defaults and allow you to quickly and easily start working with semantic search. Over time, you may want to customize your queries and data types to optimize search relevance, index and query performance, and index storage.</p><h3>Query customization</h3><p>There are no customization options - yet - for semantic queries. If you want to customize queries against <code>semantic_text</code> fields, you can perform <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-semantic-query.html#advanced-search">advanced semantic_text search</a> using explicit knn and sparse vector queries.</p><p>We're planning to add <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/retrievers-overview.html">retrievers support</a> for <code>semantic_text</code>, and adding configuration options to the <code>semantic_text</code> field so they won't be needed at query time. Stay tuned!</p><h3>Data type customization</h3><p>If you need deeper customization for the data indexing, you can use the <code>sparse_vector</code> or <code>dense_vector</code> field types. These field types give you full control over how embeddings are generated, indexed, and queried.</p><p>You need to create an ingest pipeline with an inference processor to generate the embeddings. <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-search-inference.html">This tutorial</a> walks you through the process.</p><h2>What's next with <code>semantic_text</code>?</h2><p>We're just getting started with <code>semantic_text</code>! There are quite a few enhancements that we will keep working on, including:</p><ul><li><p>Better inference error handling</p></li><li><p>Customize the chunking strategy</p></li><li><p>Hiding embeddings in _source by default, to avoid cluttering the search responses</p></li><li><p>Inner hits support, to retrieve the relevant chunks of information for a query</p></li><li><p>Filtering and retrievers support</p></li><li><p>Kibana support</p></li></ul><h2>Try it out!</h2><p><code>semantic_text</code>is available on <a href="https://www.elastic.co/elasticsearch/serverless">Elasticsearch Serverless</a> now! It will be available soon on Elasticsearch 8.15 version for <a href="https://www.elastic.co/cloud">Elastic Cloud</a> and on <a href="https://www.elastic.co/downloads/elasticsearch">Elasticsearch downloads</a>.</p><p>If you already have an Elasticsearch serverless cluster, you can see a complete example for testing semantic search using <code>semantic_text</code> in <a href="https://www.elastic.co/search-labs/blog/elasticsearch-cohere-rerank">this tutorial</a>, or try it with <a href="https://colab.research.google.com/github/elastic/elasticsearch-labs/blob/main/notebooks/search/09-semantic-text.ipynb">this notebook</a>.</p><p>We'd love to hear about your experience with <code>semantic_text</code>! Let us know what you think in the <a href="https://www.elastic.co/community">forums</a>, or open an issue in the <a href="https://github.com/elastic/elasticsearch">GitHub repository</a>. Let's make semantic search easier together!</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[Basics]]></category>
    <dc:creator><![CDATA[Carlos Delgado,Mike Pellegrini]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt31515f5dc8f12092/6a170c170c48570fa101aabd/dc08f5c15b12a0e686b8922ad8d2b997ca1227d7-1024x1024.jpg" length="0" type="image/jpeg"/>
    <pubDate>Mon, 24 Jun 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Automatically updating your Elasticsearch index using Node.js and an Azure Function App]]></title>
    <description><![CDATA[Learn how to update your Elasticsearch index automatically using Node.js and an Azure Function App. Follow these steps to ensure your index stays current.]]></description>
    <content:encoded><![CDATA[<p>Maintaining an up-to-date Elasticsearch index is crucial, especially when dealing with frequently changing dynamic datasets. This blog post will guide you through automatically updating your Elasticsearch index using Node.js and an Azure Function App.</p><p>First, we'll load the data using Node.js and ensure it remains current through regular updates. Then, we'll leverage the capabilities of <a href="https://learn.microsoft.com/en-us/azure/azure-functions/functions-overview?pivots=programming-language-javascript">Azure Function Apps</a> to automate these updates, thereby ensuring your index is always fresh and reliable.</p><p>For this blog post, we will be using the <a href="https://data.nasa.gov/Space-Science/Asteroids-NeoWs-API/73uw-d9i8/about_data">Near Earth Object Web Service (NeoWs</a>), a RESTful web service offering detailed information about near-earth asteroids. By integrating NeoWs with Node.js services integrated as Azure serverless functions, this example will provide you with a robust framework to handle the complexities of managing dynamic data effectively. This approach will help you minimize the risks of working with outdated information and maximize the accuracy and usefulness of your data.</p><h2>Prerequisites</h2><ul><li><p>This example uses Elasticsearch version 8.13; if you are new to Elasticsearch, check out our Quick Start on <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started.html">Elasticsearch</a>. Any 8.0 version should work for this blog post.</p></li><li><p>Download the latest <a href="https://docs.npmjs.com/downloading-and-installing-node-js-and-npm">NPM and Node.js version</a>. This tutorial uses Node v21.6.1 and npm 10.5.0.</p></li><li><p><a href="https://api.nasa.gov/">An API key</a> for NASA's APIs.</p></li><li><p>An active <a href="https://azure.microsoft.com/en-us/">Azure account</a> with access to create a Function App.</p></li><li><p>Access to the <a href="https://azure.microsoft.com/en-us/get-started/azure-portal">Azure portal</a> or <a href="https://learn.microsoft.com/en-us/cli/azure/">Azure CLI</a></p></li></ul><h2>Setting up locally</h2><p>Before you begin indexing and loading your data locally, setting up your environment is essential. First, create a directory and initialize it. Then, download the necessary packages and create a <code>.env</code> file to store your configuration settings. This preliminary setup ensures your local environment is prepared to handle the data efficiently.</p>mkdir Introduction-to-Data-Loading-in-Elasticsearch-with-Nodejs
cd Introduction-to-Data-Loading-in-Elasticsearch-with-Nodejs
npm init
<p>You will be using the <a href="https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html">Elasticsearch node client</a> to connect to Elastic, <a href="https://www.npmjs.com/package/axios">Axios</a> to connect to the NASA APIs and <a href="https://www.npmjs.com/package/dotenv">dotenv</a> to parse your secrets. You will want to download the required packages running the following commands:</p>npm install @elastic/elasticsearch axios dotenv
<p>After downloading the required packages, you can create a .<code>env</code> file at the root of the project directory. The .<code>env</code> file allows you to keep your credentials secure locally. Check out the <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/automatically-updating-your-index-nodejs-azure/env.example">example .env file</a> to learn more. To learn more about connecting to Elasticsearch, be sure to take a look at the <a href="https://docs.npmjs.com/downloading-and-installing-node-js-and-npm">documentation on the subject</a>.</p><p>To create a <code>.env</code> file, you can use this command at the root of your project:</p>touch .env
<p>In your <code>.env </code>, be sure to have the following entered in. Be sure to add your complete endpoint:</p>ELASTICSEARCH_ENDPOINT="https://...."
ELASTICSEARCH_API_KEY="YOUR_ELASTICSEARCh_API_KEY"
NASA_API_KEY="YOUR_NASA_API_KEY"
<p>You will also want to create a new JavaScript file as well:</p>touch loading_data_into_a_index.js
<h2>Creating your index and loading your data in</h2><p>Now that you have set up the proper file structure and downloaded the required packages, you are ready to create a script that creates an index and loads data into the index. If you get stuck along the way be sure to check out the <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/automatically-updating-your-index-nodejs-azure/loading_data_into_a_index.js">full version of the file</a> you are creating in this section.</p><p>In the file <code>loading_data_into_a_index.js,</code> configure the <a href="https://www.npmjs.com/package/dotenv">dotenv</a> package to use the keys and tokens stored in your .<code>env </code>file. You should also import the <a href="https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html">Elasticsearch client</a> to connect to Elasticsearch and <a href="https://www.npmjs.com/package/axios">Axios</a> and make HTTP requests.</p>require('dotenv').config();

const { Client } = require('@elastic/elasticsearch');
const axios = require('axios');
<p>Since your keys and tokens are currently stored as environment variables, you will want to retrieve them and create a client to authenticate to Elasticsearch.</p>const elasticsearchEndpoint = process.env.ELASTICSEARCH_ENDPOINT;
const elasticsearchApiKey = process.env.ELASTICSEARCH_API_KEY;
const nasaApiKey = process.env.NASA_API_KEY;

const client = new Client({
  node: elasticsearchEndpoint,
  auth: {
    apiKey: elasticsearchApiKey
  }
});
<p>You can develop a function to retrieve data from NASA's NEO (Near Earth Object) Web Service asynchronously. You will first configure the base URL for the NASA API request and create date objects for today and the previous week to establish the query period. After you format these dates in the YYYY-MM-DD format required for the API request, set up the dates as query parameters and execute the GET request to the NASA API. Additionally, the function includes error-handling mechanisms to aid debugging should any issues arise.</p>async function fetchNasaData() {
  const url = "https://api.nasa.gov/neo/rest/v1/feed";
  const today = new Date();
  const lastWeek = new Date(today);
  lastWeek.setDate(today.getDate() - 7);

  const startDate = lastWeek.toISOString().split('T')[0];
  const endDate = today.toISOString().split('T')[0];
  const params = {
    api_key: nasaApiKey,
    start_date: startDate,
    end_date: endDate,
  };

  try {
    const response = await axios.get(url, { params });
    return response.data;
  } catch (error) {
    console.error('Error fetching data from NASA:', error);
    return null;
  }
}
<p>Now, you can create a function to transform the raw data from the NASA API into a structured format. Since the data you get back is currently nested in a complex JSON response. A more straightforward array of objects makes handling data easier.</p>function createStructuredData(response) {
  const allObjects = [];
  const nearEarthObjects = response.near_earth_objects;

  Object.keys(nearEarthObjects).forEach(date =&gt; {
    nearEarthObjects[date].forEach(obj =&gt; {
      const simplifiedObject = {
        close_approach_date: date,
        name: obj.name,
        id: obj.id,
        miss_distance_km: obj.close_approach_data.length &gt; 0 ? obj.close_approach_data[0].miss_distance.kilometers : null,
      };

      allObjects.push(simplifiedObject);
    });
  });

  return allObjects;
}
<p>You will want to create an index to store the data from the API. An <a href="https://www.elastic.co/blog/what-is-an-elasticsearch-index">index</a> inside Elasticsearch is where you can store your data in documents. In this function, you will check to see if an index exists and create a new one if needed. You will also specify the proper <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html">mapping</a> of fields for your index. This function also loads the data into the index as documents and maps the id field from the NASA data to the<code> _id</code> field in Elasticsearch.</p>async function indexDataIntoElasticsearch(data) {
  const indexExists = await client.indices.exists({ index: 'nasa-node-js' });
  if (!indexExists.body) {
    await client.indices.create({
      index: 'nasa-node-js',
      body: {
        mappings: {
          properties: {
            close_approach_date: { type: 'date' },
            name: { type: 'text' },
            miss_distance_km: { type: 'float' },
          },
        },
      },
    });
  }

  const body = data.flatMap(doc =&gt; [{ index: { _index: 'nasa-node-js', _id: doc.id } }, doc]);
  await client.bulk({ refresh: false, body });
}
<p>You will want to create a main function to fetch, structure, and index the data. This function will also print out the number of records being uploaded and log whether the data is indexed, whether there is no data to index, or whether it failed to get data back from the NASA API. After creating the <code>run</code> function, you will want to call the function and catch any errors that may come up.</p>async function run() {
  const rawData = await fetchNasaData();
  if (rawData) {
    const structuredData = createStructuredData(rawData);
    console.log(`Number of records being uploaded: ${structuredData.length}`);
    if (structuredData.length &gt; 0) {
      await indexDataIntoElasticsearch(structuredData);
      console.log('Data indexed successfully.');
    } else {
      console.log('No data to index.');
    }
  } else {
    console.log('Failed to fetch data from NASA.');
  }
}

run().catch(console.error);
<p>You can now run the file from your command line by running the following:</p>node loading_data_into_a_index.js
<p>To confirm that your index has been successfully loaded, you can check in the Elastic Dev Tools by executing the following API call:</p>GET /nasa-node-js/_search
<h2>Keeping your index updated with an Azure Function App</h2><p>Now that you've successfully loaded your data into your index locally, this data can quickly become outdated. To ensure your information remains current, you can set up an Azure Function App to automatically fetch new data daily and upload it to your Elasticsearch index.</p><p>The first step is to configure your Function app in Azure Portal. A helpful resource for getting started is the <a href="https://learn.microsoft.com/en-us/azure/azure-functions/functions-create-function-app-portal?pivots=programming-language-javascript">Azure quick start guide</a>.</p><p>After you've set up your function, you can ensure that you have environment variables set up for <code>ELASTICSEARCH_ENDPOINT</code>, <code>ELASTICSEARCH_API_KEY</code>, and <code>NASA_API_KEY</code>. In Function Apps, environment variables are called Application settings. Inside your function app, click on the "Configuration" option in the left panel under "Settings." Under" the "Application settings" tab, click on "+ New application setting."</p><p>You will want to make sure the required libraries are installed as well. If you go to your terminal on the Azure Portal, you can install the necessary packages by entering the following:</p>npm install @elastic/elasticsearch axios
<p>The packages you are installing should look very similar to the previous install, except you will be using the moment to parse dates, and you no longer need to load an env file since you just set your secrets to be Application settings.</p><p>You can click where it says create to create a new function inside your Function App select the template entitled “Timer trigger”. You will now have a file called function.json set for you. You will want to adjust it to look as follows to run this application every day at 10 am.</p>{
    "bindings": [
      {
        "name": "myTimer",
        "type": "timerTrigger",
        "direction": "in",
        "schedule": "0 0 10 * * *"
      }
    ]
  }
<p>You'll also want to upload your <code>package.json</code> file and ensure it appears as follows:</p>{
  "name": "introduction-to-data-loading-in-elasticsearch-with-nodejs",
  "version": "1.0.0",
  "description": "A simple script for loading data in Elasticsearch",
  "main": "loading_data_into_a_index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" &amp;&amp; exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/JessicaGarson/Introduction-to-Data-Loading-in-Elasticsearch-with-Nodejs.git"
  },
  "author": "Jessica Garson",
  "license": "Apache-2.0",
  "bugs": {
    "url": "https://github.com/JessicaGarson/Introduction-to-Data-Loading-in-Elasticsearch-with-Nodejs/issues"
  },
  "homepage": "https://github.com/JessicaGarson/Introduction-to-Data-Loading-in-Elasticsearch-with-Nodejs#readme",
  "dependencies": {
    "@elastic/elasticsearch": "^8.12.0",
    "axios": "^0.21.1"
  }
}
<p>The next step is to create a <code>index.js</code> file. This script is designed to automatically update the data daily. It accomplishes this by systematically fetching and parsing new data each day and then seamlessly updating the dataset accordingly. Elasticsearch can use the same method to ingest time series or immutable data, such as webhook responses. This method ensures the information remains current and accurate, reflecting the latest available data.You can can check out the <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/automatically-updating-your-index-nodejs-azure/loading_data_into_a_index.js">full code</a> as well.</p><p>The main differences between the script you run locally and this one are as follows:</p><ul><li><p>You will no longer need to load a <code>.env</code> file, since you have already set your environment variables</p></li><li><p>There is also different logging designed more towards creating a more sustainable script</p></li><li><p>You keep your index updated based on the most recent <code>close approach date</code></p></li><li><p>There is an entry point for an Azure Function App</p></li></ul><p>You will first want to set up your libraries and authenticate to Elasticsearch as follows:</p>const elasticsearchEndpoint = process.env.ELASTICSEARCH_ENDPOINT;
const elasticsearchApiKey = process.env.ELASTICSEARCH_API_KEY;
const nasaApiKey = process.env.NASA_API_KEY;

const client = new Client({
 node: elasticsearchEndpoint,
 auth: {
   apiKey: elasticsearchApiKey
 }
});
<p>Afterward, you will want to obtain the last date update date from Elasticsearch and configure a backup method to get data from the past day if anything goes wrong.</p>async function getLastUpdateDate() {
  try {
    const response = await client.search({
      index: 'nasa-node-js',
      body: {
        size: 1,
        sort: [{ close_approach_date: { order: 'desc' } }],
        _source: ['close_approach_date']
      }
    });

    if (response.body &amp;&amp; response.body.hits &amp;&amp; response.body.hits.hits.length &gt; 0) {
      return response.body.hits.hits[0]._source.close_approach_date;
    } else {
      // Default to one day ago if no records found
      const today = new Date();
      const lastWeek = new Date(today);
      lastWeek.setDate(today.getDate() - 1);
      return lastWeek.toISOString().split('T')[0];
    }
  } catch (error) {
    console.error('Error fetching last update date from Elasticsearch:', error);
    throw error;
  }
}
<p>The following function connects to NASA's NEO (Near Earth Object) Web Service to get the data to keep your index updated. There is also some additional error handling that can capture any API errors that might come up.</p>async function fetchNasaData(startDate) {

  const url = "https://api.nasa.gov/neo/rest/v1/feed";
  const today = new Date();

  const endDate = today.toISOString().split('T')[0];

  const params = {
    api_key: nasaApiKey,
    start_date: startDate,
    end_date: endDate,
  };

  try {
    // Perform the GET request to the NASA API with query parameters
    const response = await axios.get(url, { params });
    return response.data;
  } catch (error) {
    // Log any errors encountered during the request
    console.error('Error fetching data from NASA:', error);
    return null;
  }
}
<p>Now, you will want to create a function to organize your data by iterating over the objects of each date.</p>function createStructuredData(response) {
  const allObjects = [];
  const nearEarthObjects = response.near_earth_objects;

  Object.keys(nearEarthObjects).forEach(date =&gt; {
    nearEarthObjects[date].forEach(obj =&gt; {
      const simplifiedObject = {
        close_approach_date: date,
        name: obj.name,
        id: obj.id,
        miss_distance_km: obj.close_approach_data.length &gt; 0 ? obj.close_approach_data[0].miss_distance.kilometers : null,
      };

      allObjects.push(simplifiedObject);
    });
  });

  return allObjects;
}
<p>Now, you will want to load your data into Elasticsearch using the bulk indexing operation. This function should look similar to the one in the previous section.</p>async function indexDataIntoElasticsearch(data) {
  const body = data.flatMap(doc =&gt; [{ index: { _index: 'nasa-node-js', _id: doc.id } }, doc]);
  await client.bulk({ refresh: false, body });
}
<p>Finally, you will want to create an entry point for the function that will run according to the timer you set. This function is similar to a main function, as it calls the functions created previously in the file. There is also some additional logging, such as printing the number of records and informing you if the data was indexed correctly.</p>module.exports = async function (context, myTimer) {
  try {
    const lastUpdateDate = await getLastUpdateDate();
    context.log(`Last update date from Elasticsearch: ${lastUpdateDate}`);

    const rawData = await fetchNasaData(lastUpdateDate);
    if (rawData) {
      const structuredData = createStructuredData(rawData);
      context.log(`Number of records being uploaded: ${structuredData.length}`);
      
      if (structuredData.length &gt; 0) {

        const flatFileData = JSON.stringify(structuredData, null, 2);
        context.log('Flat file data:', flatFileData);

        await indexDataIntoElasticsearch(structuredData);
        context.log('Data indexed successfully.');
      } else {
        context.log('No data to index.');
      }
    } else {
      context.log('Failed to fetch data from NASA.');
    }
  } catch (error) {
    context.log('Error in run process:', error);
  }
<h2>Conclusion</h2><p>Using Node.js and <a href="https://learn.microsoft.com/en-us/azure/azure-functions/functions-overview?pivots=programming-language-javascript">Azure's Function App</a>, you should be able to ensure that your Elasticsearch index is updated regularly. By utilizing Node.js's capabilities in conjunction with Azure's Function App, you can efficiently maintain your index's regular updates. This powerful combination offers a streamlined, automated process, reducing the manual effort involved in keeping your index regularly updated. Full code for this example can be found on <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/automatically-updating-your-index-nodejs-azure">Search Labs GitHub</a>. Let us know if you built anything based on this blog or if you have questions on our <a href="https://discuss.elastic.co/">forums</a> and <a href="https://communityinviter.com/apps/elasticstack/elastic-community">the community Slack channel</a>.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-index-node-js-automatic-updates</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-index-node-js-automatic-updates</guid>
    <category><![CDATA[Javascript]]></category>
    <category><![CDATA[Basics]]></category>
    <dc:creator><![CDATA[Jessica Garson]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt373f9290a1371dd7/6a17122e0c48579b2001abba/fd87bff40e296ebce871d631c86fd0245f11c796-1440x960.jpg" length="0" type="image/jpeg"/>
    <pubDate>Tue, 04 Jun 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Keeping your Elasticsearch index current with Python and Google Cloud Platform Functions]]></title>
    <description><![CDATA[Keep your Elasticsearch index updated with Python &amp; Google Cloud Functions. Follow these steps to automatically update an index when new data is present.]]></description>
    <content:encoded><![CDATA[<h2>Background</h2><p>An <a href="https://www.elastic.co/blog/what-is-an-elasticsearch-index">index</a> inside Elasticsearch is where you can store your data in documents. While working with an index, the data can quickly grow old if you are working with a dynamic dataset. To avoid this issue, you can create a Python script to update your index and deploy it using <a href="https://cloud.google.com/">Google Cloud Platform's</a> (GCP) <a href="https://cloud.google.com/functions/docs#docs">Cloud Functions </a>and <a href="https://cloud.google.com/scheduler/docs">Cloud Scheduler</a> in order to keep your index up-to-date automatically.</p><p>To keep your index current, you can first set up a Jupyter Notebook to test locally and create a framework of a script that will update your index if new information is present. You can adjust your script to make it more reusable and run it as a Cloud Function. With Cloud Scheduler, you can set the code in your Cloud Function to run on a schedule using a cron-type format.</p><h2>Prerequisites for automating index updates</h2><ul><li><p>This example uses Elasticsearch version 8.12; if you are new, check out our Quick Start on <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started.html">Elasticsearch</a>.</p></li><li><p>Download the latest version of Python if you don't have it installed on your machine. This example utilizes Python 3.12.1.</p></li><li><p><a href="https://api.nasa.gov/">An API key</a> for NASA's APIs.</p></li><li><p>You will use the <a href="https://requests.readthedocs.io/en/latest/">Requests</a> package to connect to a NASA API, <a href="https://pandas.pydata.org/">Pandas</a> to manipulate data, the <a href="https://www.elastic.co/guide/en/elasticsearch/client/python-api/current/getting-started-python.html">Elasticsearch Python Client</a> to load data into an index and keep it up to date, and <a href="https://docs.jupyter.org/en/latest/">Jupyter Notebooks</a> to work with your data interactively while testing. You can run the following line to install these required packages:</p></li></ul>pip3 install requests pandas elasticsearch notebook
<h2>Loading and updating your dataset</h2><p>Before you can run your update script inside of GCP, you will want to upload your data and test the process you will use to keep your script updated. You will first connect to data from an API, save it as a Pandas DataFrame, connect to Elasticsearch, upload the DataFrame into an index, check to see when the index is last updated, and update it if new data is available. You can find the complete code of this section in <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/keeping-your-index-current/local_testing.ipynb">this search labs notebook</a>.</p><h3>Loading your data</h3><p>Let's start testing locally with a Jupyter Notebook to work with your data interactively. To do so, you can run the following in your terminal.</p>jupyter notebook
<p>In the right-hand corner, you can select where it says “New” to create a new Jupyter Notebook.</p><p>First, you will need to import the packages you will be using. You will import all the packages you installed earlier, plus <code>getpass</code> to work with secrets such as API keys and <code>datetime</code> to work with date objects.</p>import requests
from getpass import getpass
import pandas as pd
from datetime import datetime, timedelta
from elasticsearch import Elasticsearch, helpers
<p>The dataset you will use is<a href="https://data.nasa.gov/Space-Science/Asteroids-NeoWs-API/73uw-d9i8/about_data"> Near Earth Object Web Service (NeoWs)</a>, a RESTful web service that provides near-earth Asteroid information. This dataset lets you search for asteroids based on their closest approach date to Earth, look up a specific asteroid, and browse the overall dataset.</p><p>With the following function, you can connect to NASA's NeoWs API, get data from the past week, and convert your response to a JSON object.</p>def connect_to_nasa():
    url = "https://api.nasa.gov/neo/rest/v1/feed"
    nasa_api_key = getpass("NASA API Key: ")
    today = datetime.now()
    params = {
        "api_key": nasa_api_key,
        "start_date": today - timedelta(days=7),
        "end_date": datetime.now(),
    }
    return requests.get(url, params).json()
<p>Now, you can save the results of your API call to a variable called response.</p>response = connect_to_nasa()
<p>To convert the JSON object into a pandas DataFrame, you must normalize the nested objects into one DataFrame and drop the column containing the nested JSON.</p>def create_df(response):
    all_objects = []
    for date, objects in response["near_earth_objects"].items():
        for obj in objects:
            obj["close_approach_date"] = date
            all_objects.append(obj)
    df = pd.json_normalize(all_objects)
    return df.drop("close_approach_data", axis=1)
<p>To call this function and view the first five rows of your dataset, you can run the following:</p>df = create_df(response)
df.head()
<h3>Connecting to Elasticsearch</h3><p>You can access Elasticsearch from the Python Client by providing your Elastic Cloud ID and API key for authentication.</p>def connect_to_elastic():
    elastic_cloud_id = getpass("Elastic Cloud ID: ")
    elastic_api_key = getpass("Elastic API Key: ")
    return Elasticsearch(cloud_id=elastic_cloud_id, api_key=elastic_api_key)
<p>Now, you can save the results of your connection function to a variable called <code>es</code>.</p>es = connect_to_elastic()
<p>An index in Elasticsearch is the main container for your data. You can name your index called <code>asteroid_data_set</code>.</p>index_name = "asteroid_data_set"
es.indices.create(index=index_name)
<p>The result you get back will look like the following:</p>ObjectApiResponse({'acknowledged': True, 'shards_acknowledged': True, 'index': 'asteroids_data'})
<p>Now, you can create a helper function that will allow you to convert your DataFrame to the correct format to upload into your index.</p>def doc_generator(df, index_name):
    for index, document in df.iterrows():
        yield {
            "_index": index_name,
            "_id": f"{document['id']}",
            "_source": document.to_dict(),
        }
<p>Next, you can bulk upload the contents of your DataFrame into Elastic, calling the helper function you just created.</p>helpers.bulk(es, doc_generator(df, index_name))
<p>You should get a result that looks similar to the following, which tells you how many rows you’ve uploaded:</p>(146, [])
<h3>When was the last time you updated your data?</h3><p>Once you've uploaded data into Elastic, you can check the last time your index was updated and format the date so it can work with NASA API.</p>def updated_last(es, index_name):
    query = {
        "size": 0,
        "aggs": {"last_date": {"max": {"field": "close_approach_date"}}},
    }
    response = es.search(index=index_name, body=query)
    last_updated_date_string = response["aggregations"]["last_date"]["value_as_string"]
    datetime_obj = datetime.strptime(last_updated_date_string, "%Y-%m-%dT%H:%M:%S.%fZ")
    return datetime_obj.strftime("%Y-%m-%d")
<p>You can save the date your index was last updated to a variable and print out the date.</p>last_update_date = updated_last(es, index_name)
print(last_update_date)
<h3>Updating your data</h3><p>Now, you can create a function that checks to see if there is any new data since the last time the index was updated and the current date. If the object is valid and the data is not empty, it will update the index and let you know if there is no new data to update or if the DataFrame returns a type of <code>None</code> indicating that there may have been a problem.</p>def update_new_data(df, es, last_update_date, index_name):
    if isinstance(last_update_date, str):
        last_update_date = datetime.strptime(last_update_date, "%Y-%m-%d")

    last_update_date = pd.Timestamp(last_update_date).normalize()

    if not df.empty and "close_approach_date" in df.columns:
        df["close_approach_date"] = pd.to_datetime(df["close_approach_date"])

    today = pd.Timestamp(datetime.now().date()).normalize()

    if df is not None and not df.empty:
        update_range = df.loc[
            (df["close_approach_date"] &gt; last_update_date)
            &amp; (df["close_approach_date"] &lt; today)
        ]
        if not update_range.empty:
            helpers.bulk(es, doc_generator(update_range, index_name))
        else:
            print("No new data to update.")
    else:
        print("The DataFrame is None.")
<p>If the DataFrame is a valid object, it will call the function you wrote and update the index if applicable. It will also print out the date of the index's last update to help you debug if needed. If not, it will tell you there may be a problem.</p>try:
    if df is None:
        raise ValueError("DataFrame is None. There may be a problem.")
    update_new_data(df, es, last_update_date, index_name)
    print(updated_last(es, index_name))
except Exception as e:
    print(f"An error occurred: {e}")
<h2>Keeping your index current</h2><p>Now that you've created a framework for local testing, you are ready to set up an environment where you can run your script daily to check to see if any new data is available and update your index accordingly.</p><h3>Creating a Cloud Function</h3><p>You are now ready to deploy your Cloud Function. To do so, you will want to select the environment as a 2nd gen function, name your function, and select a cloud region. You can also tie it to a Cloud Pub/Sub trigger and choose to create a new topic if you haven't made it already. You can check out the <a href="https://github.com/JessicaGarson/Keeping-Your-Elasticsearch-Index-Current">complete code for this section on GitHub</a>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd01cb110679df057/6a170b30ab7f084f76db9e9a/5e5548faac7d7ed20166e853db9a81d4ed51d60b-1116x1188.jpg" alt="" /><h3>Creating a Pub/Sub topic</h3><p>When creating a new topic, you can name your topic ID and select the encryption using a Google-managed encryption key.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt93a679b113dc8eee/6a170b321949f751eee7aa3e/5a32c2efaf32f496fa201d3c3e7f97de29b59f66-1114x874.jpg" alt="" /><h3>Setting your Cloud Function's environment variables</h3><p>Under where it says “Runtime environment variables,” you can add in the environment variables for your <code>NASA_API_KEY,</code> <code>ELASTIC_CLOUD_ID</code>, and <code>ELASTIC_API_KEY.</code> You will want to save these as the raw values without single quotes around them. So if you entered a value of <code>'xxxxlsdgzxxxxx'</code> into your terminal earlier, you would want it to be <code>xxxxlsdgzxxxxx</code>.</p><h3>Adjusting your code and adding it to your Cloud Function</h3><p>After you enter your environment variables, you can press the button that says next, which will take you to a code editor. You will want to select the runtime of Python 3.12.1 or match the version of Python you are using. After that, update the entry point to <code>update_index</code>. The entry point serves a similar role to the main function in Python.</p><p>Instead of using <code>getpass</code> to retrieve secrets, you will want to use <code>os</code> to perform a more automated process. An example will look like the following:</p>elastic_cloud_id = os.getenv("ELASTIC_CLOUD_ID")
elastic_api_key = os.getenv("ELASTIC_API_KEY")
<p>You will want to adjust the order of your script to have the function that connects to Elasticsearch first. Afterward, you will want to know when your index was last updated, connect to the NASA API you are using, save it to DataFrame, and load any new data that might be available.</p><p>You may notice a new function at the bottom called <code>update_index</code> that ties your code together. In this function, you define the name of your index, connect to Elastic, figure out the last date the index was updated, connect to the NASA API, save the results into a data frame, and update the index if needed. To indicate the entry point function is a cloud event, you can denote it with the decorator <code>@functions_framework.cloud_event</code>.</p>@functions_framework.cloud_event
def update_index(cloud_event):
    index_name = "asteroid_data_set"
    es = connect_to_elastic()
    last_update_date = updated_last(es, index_name)
    print(last_update_date)
    response = connect_to_nasa(last_update_date)
    df = create_df(response)
    if df is not None:
      update_new_data(df, es, last_update_date, index_name)
      print(updated_last(es, index_name)) 
    else:
      print("No new data was retrieved.")
<p>Here is the full updated code sample:</p>import functions_framework
import requests
import os
import pandas as pd
from datetime import datetime
from elasticsearch import Elasticsearch, helpers


def connect_to_elastic():
    elastic_cloud_id = os.getenv("ELASTIC_CLOUD_ID")
    elastic_api_key = os.getenv("ELASTIC_API_KEY")
    return Elasticsearch(cloud_id=elastic_cloud_id, api_key=elastic_api_key)


def connect_to_nasa(last_update_date):
    url = "https://api.nasa.gov/neo/rest/v1/feed"
    nasa_api_key = os.getenv("NASA_API_KEY")
    params = {
        "api_key": nasa_api_key,
        "start_date": last_update_date,
        "end_date": datetime.now(),
    }
    return requests.get(url, params).json()


def create_df(response):
    all_objects = []
    for date, objects in response["near_earth_objects"].items():
        for obj in objects:
            obj["close_approach_date"] = date
            all_objects.append(obj)
    df = pd.json_normalize(all_objects)
    return df.drop("close_approach_data", axis=1)


def doc_generator(df, index_name):
    for index, document in df.iterrows():
        yield {
            "_index": index_name,
            "_id": f"{document['close_approach_date']}",
            "_source": document.to_dict(),
        }


def updated_last(es, index_name):
    query = {
        "size": 0,
        "aggs": {"last_date": {"max": {"field": "close_approach_date"}}},
    }
    response = es.search(index=index_name, body=query)
    last_updated_date_string = response["aggregations"]["last_date"]["value_as_string"]
    datetime_obj = datetime.strptime(last_updated_date_string, "%Y-%m-%dT%H:%M:%S.%fZ")
    return datetime_obj.strftime("%Y-%m-%d")


def update_new_data(df, es, last_update_date, index_name):
    if isinstance(last_update_date, str):
        last_update_date = datetime.strptime(last_update_date, "%Y-%m-%d")

    last_update_date = pd.Timestamp(last_update_date).normalize()

    if not df.empty and "close_approach_date" in df.columns:
        df["close_approach_date"] = pd.to_datetime(df["close_approach_date"])

    today = pd.Timestamp(datetime.now().date()).normalize()

    if df is not None and not df.empty:
        update_range = df.loc[
            (df["close_approach_date"] &gt; last_update_date)
            &amp; (df["close_approach_date"] &lt; today)
        ]
        print(update_range)
        if not update_range.empty:
            helpers.bulk(es, doc_generator(update_range, index_name))
        else:
            print("No new data to update.")
    else:
        print("The DataFrame is empty or None.")


# Triggered from a message on a Cloud Pub/Sub topic.
@functions_framework.cloud_event
def hello_pubsub(cloud_event):
    index_name = "asteroid_data_set"
    es = connect_to_elastic()
    last_update_date = updated_last(es, index_name)
    print(last_update_date)
    response = connect_to_nasa(last_update_date)
    df = create_df(response)
    try:
        if df is None:
            raise ValueError("DataFrame is None. There may be a problem.")
        update_new_data(df, es, last_update_date, index_name)
        print(updated_last(es, index_name))
    except Exception as e:
        print(f"An error occurred: {e}")
<h3>Adding a requirements.txt file</h3><p>You will also want to define a <code>requirements.txt</code> file with all the specified packages needed to run the code.</p>functions-framework==3.*
requests==2.31.0
elasticsearch==8.12.0
pandas==2.1.4
<h3>Scheduling your Cloud Function</h3><p>In Cloud Scheduler, you can set up your function to run at a regular interval using unix cron format. I have the code set to run every morning at 8 am in my timezone.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt031810158f88fd08/6a170b34a929cf4718ae09d0/a6b9c490a804389ec15366ff96adb9c992cb561a-1160x1014.jpg" alt="" /><p>You will also want to configure the execution to connect to the Pub/Sub topic you created previously. I currently have the message body set to say “hello.”</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte5c6c0061dc955fc/6a170b35b0367d9d5672bd37/b12a4a782976edbf9b6a65992f0d3d73df2d15c0-1116x518.jpg" alt="" /><p>Now that you have set up your Pub/Sub topic and your Cloud Function and set that Cloud Function to run on a schedule, your index should automatically update whenever new data is present.</p><h2>Conclusion</h2><p>Using Python, Google Cloud Platform Functions, and Google Cloud Scheduler you should be able to ensure that your index is updated regularly. You can find the complete code <a href="https://github.com/JessicaGarson/Keeping-Your-Elasticsearch-Index-Current">here</a> and <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/keeping-your-index-current/local_testing.ipynb">the search labs notebook for local testing</a>. We are also running an on-demand webinar with <a href="https://www.elastic.co/virtual-events/architecting-search-apps-on-google-cloud">Google Cloud</a> which might be a good next step if you are looking to build search apps. Let us know if you built anything based on this blog or if you have questions on our <a href="https://discuss.elastic.co/">Discuss forums</a> and <a href="https://communityinviter.com/apps/elasticstack/elastic-community">the community Slack channel</a>.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/keeping-your-elasticsearch-index-current-with-python-and-google-cloud-platform-functions</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/keeping-your-elasticsearch-index-current-with-python-and-google-cloud-platform-functions</guid>
    <category><![CDATA[Python]]></category>
    <category><![CDATA[Basics]]></category>
    <dc:creator><![CDATA[Jessica Garson]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt315cf663d39ca57f/6a170b3760084b95c23c4576/b839822139ab0769a7fcf1d62102c984af87bf0d-1440x954.jpg" length="0" type="image/jpeg"/>
    <pubDate>Wed, 13 Mar 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Adding document level security (DLS) to your internal knowledge search]]></title>
    <description><![CDATA[Learn how to secure your internal knowledge lake and offer personalized search for your end-users using document level security (DLS).]]></description>
    <content:encoded><![CDATA[<p>There's a good chance that your enterprise is drowning in internal data.</p><p>You've got your issue-tracking, your note-taking, your meeting transcripts, your wiki pages, your video recordings, your chats and IMs and DMs. And don't forget the emails!</p><p>It's no wonder that so many enterprises are trying to create workplace search experiences - giving their employees a centralized, one-stop-shop for searching for internal information.</p><p>With Elastic's catalog of <a href="https://www.elastic.co/guide/en/enterprise-search/current/connectors.html">connectors</a>, this is relatively easy to do. But after you get all your data indexed and ready to be searched, how do you ensure that it is secured? After all, Tess (from Engineering) shouldn't be looking at Bob's (from HR) notes on performance reviews. How can you make sure that each separate user who comes to this unified search bar does gets their own unique view into only the data that they're authorized to view?</p><p>Enter, Document Level Security (DLS).</p><h2>Understanding document level security (DLS) in Elasticsearch</h2><p>Folks who've followed Elasticsearch for a while may already be aware that <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/document-level-security.html">DLS</a> has been an Elasticsearch feature for quite a long time. It's part of the larger theme of <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/authorization.html">user authorization</a>, and is really quite simple. You embed metadata in Elasticsearch documents, and then you craft an Elasticsearch query, filtering based on that document metadata, that describes the user's authorization. That query is used to create an <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.12/defining-roles.html">Elasticsearch Role</a>.</p><p>At query time, when the search user authenticates, their role(s) (if any) is identified, and the embedded query filter (if any) is applied to their searches.</p><p>Let's look at a simplistic example. Say we have two documents:</p>PUT example/_doc/1
{
  "my-data": true,
  "text": "This data is mine"
}

PUT example/_doc/2
{
  "my-data": false,
  "text": "This data belongs to someone else"
}
<p>A query that would fetch only <em>my</em> data would be:</p>GET example/_search
{
  "query": {
    "term": {
      "my-data": {
        "value": true
      }
    }
  }
}
<p>That query can be embedded into a Role, like:</p>POST /_security/role/my_role
{
  "indices": [
    {
      "names": [ "example" ],
      "privileges": ["read"],
      "query": {
        "term": {
          "my-data": {
            "value": true
          }
        }
      }
    }
  ]
}
<p>So if my user is assigned the role <code>my_role</code>, if I just do</p>GET example/_search
<p>I will only see document <code>1</code>, but not document <code>2</code>.</p><p>While this example is simple in theory, it has a relatively large number of moving pieces.</p><ul><li><p>you must ensure that the documents contain the relevant metadata (<code>"my-data": true</code> vs <code>"my-data": false</code>)</p></li><li><p>you must <em>trust</em> that the metadata on those documents is accurate</p></li><li><p>you must create a Role for every search user with a finely crafted Elasticsearch query</p></li><li><p>you must ensure that every role you create correctly maps to the right user at query time</p></li><li><p>you mush ensure that all of the above stays up to date.</p></li></ul><p>That last one is particularly difficult. When people in your enterprise join, leave, switch teams, or get promoted, that requires changes - potentially to both your (meta)data AND your Roles. And if you add in data sources that support sharing or access editing, you're definitely needing to make sure that your (meta)data stays up-to-date.</p><h2>DLS with Elastic connectors</h2><p><a href="https://www.elastic.co/guide/en/enterprise-search/current/dls.html">Connector document level security</a> builds off of the Elasticsearch DLS primitives. For many connectors, this includes syncing the relevant metadata and Role Descriptors to support DLS. This results in the documents in your content index automatically containing metadata (usually in a <code>_allow_access_control</code> field) to describe the people/groups who are authorized to search for this document, as well as documents in a special <code>.search-acl-filter-&lt;index-name&gt;</code> index that contain the Role Descriptor JSON necessary to build a concrete Role or an API key for a given search user.</p><p>You can find <a href="https://www.elastic.co/guide/en/enterprise-search/current/dls.html#dls-availability-prerequisites">which connectors have DLS available here</a>. For this blog, we're going to reference an example application which utilizes the Sharepoint Online connector. This was the first connector we enabled DLS on, but the example could be easily adapted to work with any DLS-enabled connector.</p><p>If your connector is eligible, and you have a Platinum+ Elasticsearch license, you can enable DLS through a toggle on the connector configuration page.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta4d4869b2ef7a27b/6a1711f7964cea84be08bce1/a8d709d6d559891d623d108484326201a7d048d0-1440x666.png" alt="enable-dls" /><p>From there, it's just a matter of running a Full sync and an Access Control sync, and Elasticsearch will have all the data it needs.</p><h2>DLS implementation example</h2><p>And then what?</p><p>Once Elasticsearch has Role Descriptors and document data with sufficient metadata for those role filters, you're ready to build a secure search experience.</p><p>We've built <a href="https://github.com/elastic/elasticsearch-labs/tree/main/example-apps/internal-knowledge-search">an example knowledge search app</a> that we'll use for this blog, and you're welcome to go take a look at its source code. However, we do want to stress that this is an example only - it is not ready to be run in production on its own. Please exercise good judgement and do not run code that you have not read or do not understand.</p><p>This application has a pretty simple architecture.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4ddcfc0dd7551129/6a1711f867045b814245c303/2ee525509acb672b89f36a2b05ad2b75872745d6-864x408.png" alt="dls-simple-architecture" /><p>It is composed of a <a href="https://github.com/elastic/elasticsearch-labs/blob/main/example-apps/internal-knowledge-search/api/app.py">Flask backend</a> and a <a href="https://github.com/elastic/elasticsearch-labs/tree/main/example-apps/internal-knowledge-search/app-ui">React frontend</a>. The backend is configured with environment variables to establish a connection with Elasticsearch.</p>export ELASTICSEARCH_URL=...
export ELASTIC_USERNAME=...
export ELASTIC_PASSWORD=...
<p>Using this connection, the backend provides three endpoints:</p><ol><li><p><code>GET /api/persona</code> This endpoint lists the identifiers for the "identities" or "personas" that the connector found during the Access Control sync. The frontend uses this list to populate a dropdown of personas so to demonstrate how search results change depending on the selected persona.</p></li><li><p><code>GET /api/indices</code> This endpoint lists which indices have been included in your Search Application. The frontend uses this list to allow you to choose which ones to search against.</p></li><li><p><code>GET /api/api_key?persona=&lt;persona&gt;</code> This endpoint creates and returns an Elasticsearch API key based off of a selected persona. In a production system, <code>persona</code> wouldn't be a request argument, but would be inferred from the authentication credentials. This API key is then used by the frontend to issue search requests to Elasticsearch.</p></li></ol><h3>Caveats</h3><p>As stated above, this example should not be used in production. Gaps include:</p><ul><li><p>It does not implement authentication. A production-ready app would need a way for users to authenticate, and have their identities verified, rather than selecting a user from a dropdown.</p></li><li><p>It does not utilze SSL/TLS. The backend currently transmits Elasticsearch API Keys to the frontend over HTTP, not HTTPS.</p></li><li><p>The frontend issues <code>/_search</code> requests directly to Elasticsearch. Depending on the production use case, you may not want to expose Elasticsearch to your end user like this. Instead, it may be advisable to issue requests from the frontend to your backend (again, with authentication implemented), and have the backend translate those requests to Elasticsearch queries.</p></li></ul><h3>Source Code</h3><p>Below we link to the critical pieces of code that are necessary to implement search with DLS.</p><h4>Creating the authenticated user's role descriptor</h4><p><a href="https://github.com/elastic/elasticsearch-labs/blob/ebd2e96de3dc8d56624e70248de4bbac35e2ec71/example-apps/internal-knowledge-search/api/app.py#L121-L166">Code link</a></p>
            identity = elasticsearch_client.get(
                index=identities_index, id=persona)
            permissions = identity["_source"]["query"]["template"]["params"][
                "access_control"
            ]
            role_descriptor = {
                "dls-role": {
                    "cluster": ["all"],
                    "indices": [
                        {
                            "names": [search_app_name],
                            "privileges": ["read"],
                            "query": {
                                "template": {
                                    "params": {"access_control": permissions},
                                    "source": """{
                                        "bool": {
                                            "should": [
                                                {
                                                    "bool": {
                                                        "must_not": {
                                                            "exists": {
                                                                "field": "_allow_access_control"
                                                            }
                                                        }
                                                    }
                                                },
                                                {
                                                    "terms": {
                                                        "_allow_access_control.enum": {{#toJson}}access_control{{/toJson}}
                                                    }
                                                }
                                            ]
                                        }
                                    }""",
                                }
                            },
                        }
                    ],
                    "restriction": {"workflows": ["search_application_query"]},
                }
            }
<p>You may notice that the query template in this role descriptor is significantly more complex than the simple example provided earlier in this blog. This query does several things:</p><ol><li><p>It uses a query template, instead of an explicit query. This makes it easier when reading to separate a long list of permissions from the query syntax.</p></li><li><p>It uses a <code>bool</code> query. This allows us to combine several logical checks.</p></li><li><p>It grants access to any documents that do not contain the <code>_allow_access_control</code> field</p></li><li><p>It grants access to documents where the <code>_allow_access_control</code> field contains a value found in this user's <code>permissions</code></p></li></ol><h4>Creating an API Key from that Role Descriptor</h4><p><a href="https://github.com/elastic/elasticsearch-labs/blob/ebd2e96de3dc8d56624e70248de4bbac35e2ec71/example-apps/internal-knowledge-search/api/app.py#L167-L169">Code link</a></p>
        api_key = elasticsearch_client.security.create_api_key(
            name=search_app_name+"-internal-knowledge-search-example-"+persona, expiration="1h", role_descriptors=role_descriptor)
        return {"api_key": api_key['encoded']}
<h4>Searching with the API Key</h4><p><a href="https://github.com/elastic/elasticsearch-labs/blob/ebd2e96de3dc8d56624e70248de4bbac35e2ec71/example-apps/internal-knowledge-search/app-ui/src/pages/SearchPage.tsx#L76-L107">Code link</a></p>
      const apiKey = searchPersonaAPIKey;

      const client = SearchApplicationClient(
        appName,
        searchEndpoint,
        apiKey,
        {
          facets: {
            description: {
              type: "text",
            },
          },
        },
        {
          disableCache: true,
        }
      );

      const sortArray = Object.values(sorts).map((sort) =&gt; ({
        [sort.title]: sort.sortDirection,
      }));

      const rawResults = await client()
        .query(query)
        .setSort(sortArray)
        .setPageSize(10)
        .addParameter("indices", indexFilter)
        .search();

      const searchResults = rawResults.hits.hits.map((hit: any) =&gt; {
        return mapHitToSearchResult(hit);
      });
]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/dls-internal-knowledge-search</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/dls-internal-knowledge-search</guid>
    <category><![CDATA[Basics]]></category>
    <dc:creator><![CDATA[Sean Story]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1a0bf009f00b1c80/6a1711fa4a531b3db636aa9f/c7c174d6408b23fca482664c608f9e8849243d96-1440x720.jpg" length="0" type="image/jpeg"/>
    <pubDate>Mon, 22 Jan 2024 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>