<?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[Index Data - 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[Index Data - 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/index-data</link>
    </image>
    <link>https://www.elastic.co/search-labs/blog/category/index-data</link>
    <atom:link href="https://www.elastic.co/search-labs/rss/category/index-data.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <lastBuildDate>Sat, 24 Oct 2026 10:12:05 GMT</lastBuildDate>
  <item>
    <title><![CDATA[GPU-accelerated vector indexing in Elasticsearch with NVIDIA cuVS: 138M vectors in under 10 minutes]]></title>
    <description><![CDATA[Moving index builds to the GPU leaves the CPU free for queries, which is how vector indexing throughput went up 7x and p90 search latency fell 6x while indexing ran, with no change to recall.]]></description>
    <content:encoded><![CDATA[<p>Modern enterprise applications are ingesting terabyte- to petabyte-scale unstructured data to power semantic search, large language model–based (LLM-based) retrieval augmented generation (RAG), and recommender systems. At this scale, <a href="https://www.vastdata.com/blog/powering-enterprise-ai-high-velocity-vector-search-sql">vector indexing on CPUs can take days or even weeks</a>, slowing experimentation and making large index updates operationally expensive. This is especially challenging for workloads that require regular index rebuilds, such as those driven by frequent data updates or frequent embedding model updates. For example, several ecommerce teams refresh their product catalog nightly and train their own embedding models, each requiring an index rebuild.</p><p></p><p>CPU-based indexing can also degrade search performance when indexing and search happen simultaneously, especially in online systems, like ecommerce platforms and ad-serving pipelines. This happens because index builds consume significant CPU resources, leaving fewer cycles available for queries and driving up search latency and latency variance. Even after indexing completes, latency can remain high due to index fragmentation. Customers often run a forced merge to consolidate segments, but it can be slow on CPUs, further delaying recovery to the expected search latency by hours to days.</p><p></p><p><a href="https://www.elastic.co/elasticsearch/vector-database">Elasticsearch</a> has <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/gpu-vector-indexing">introduced GPU-accelerated vector indexing</a> powered by <a href="https://developer.nvidia.com/cuvs">NVIDIA cuVS</a>. In this blog, we show how Elasticsearch achieves up to 7x faster vector indexing throughput by offloading hierarchical navigable small world (HNSW) index construction to GPUs, indexing 138 million vectors in under 10 minutes using eight NVIDIA RTX PRO 6000 GPUs.</p><p></p><h2>Indexing over 100M Vectors in under 10 minutes</h2><p>Figure 1 shows vector indexing throughput on 138 million 1,024-dimensional vectors from the <a href="https://github.com/elastic/rally-tracks/tree/master/msmarco-v2-vector">MS MARCO</a> dataset, representing roughly 4 TB of multimodal documents (Figure 1). The benchmark was run with <a href="https://github.com/elastic/rally">Rally</a>, Elasticsearch’s macro-benchmarking framework. The test ran on a server with 8 NVIDIA RTX Pro 6000 GPUs with a 2 socket AMD EPYC 9555. GPUs were turned on for the GPU test and turned off for the CPU test. The result was that Elasticsearch achieved 7x higher indexing throughput, lowering time to index 138M vectors from 1 hour on CPUs to under 10 minutes on GPUs.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt05167c135e5e0727/6ab4e86f7a4851d33df9803f/vector-indexing-throughput.png" alt="Bar chart of vector indexing throughput: 281K docs/s on GPU versus 38K docs/s on CPU for 138M vectors" /><p></p><h3>Does GPU vector indexing change recall?</h3><p>Index acceleration isn’t helpful if indexes built on GPU have lower search quality compared to CPUs. The throughput-versus-recall curves for GPU-built and CPU-built indexes overlap, showing that GPU indexing delivers the same recall profile as Elasticsearch’s CPU-based indexing path, with minimal accuracy or search-time performance penalty (Figure 2).</p><p></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdbb6f3deb0bcaec9/6ab4e8ebafd558dce24a0a33/qps-vs-recall.png" alt="Chart showing QPS versus recall curves overlapping for GPU-built and CPU-built vector indexes at k=100" /><p></p><h3>Search latency under concurrent indexing load</h3><p>Offloading indexing to the GPU improved search latency on the CPU by 6x when running indexing and search simultaneously (Figure 3) because indexing on GPUs frees CPU resources for faster search latency. This allows a single Elasticsearch cluster to support real-time ingestion and low-latency retrieval at the same time, without forcing a tradeoff between freshness and search performance.</p><p></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd34d2464499b03e4/6ab4e9637c5f023f526f3c67/p90-search-latency.png" alt="Bar chart of p90 search latency during indexing: 7 ms with a GPU-built index versus 45 ms on CPU" /><p></p><h3>Force merge time for HNSW indexes on GPU versus CPU</h3><p>Additionally, GPU-accelerated indexing in Elasticsearch reduced forced merge time from roughly four hours to five minutes, helping restore low-latency search in real-time (Figure 4). Note: Force merge was used to merge each shard into four segments.</p><p></p><p>Here are the full results for GPU versus CPU vector indexing in Elasticsearch:</p><p></p><p><strong>Metric</strong></p><p><strong>CPU</strong></p><p><strong>GPU</strong></p><p><strong>Change</strong></p><p>Index build time, 138 million vectors</p><p>~1 hour</p><p>Under 10 min</p><p>7x throughput</p><p>p90 search latency under indexing load</p><p>Baseline</p><p>6x lower</p><p>6x</p><p>Force merge, four segments per shard</p><p>~4 hours</p><p>~5 min</p><p>~48x</p><p>Recall at target</p><p>95%</p><p>95%</p><p>Unchanged</p><p></p><p></p><h2>What GPU vector indexing means for Elasticsearch users</h2><p>Elasticsearch’s integration of NVIDIA cuVS brings GPU-accelerated vector indexing to production AI search with over 84% reduction in search latency to deliver faster ingest with a 640% increase in indexing throughput with a lower CPU overhead and limited infrastructure tradeoffs. This unlocks a new class of applications, such as faster RAG pipelines, semantic search, multimodal retrieval, and fraud detection at scale.</p><p></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt54b3129de4981654/6ab4e9c7bf5efbe04ad38b76/force-merge-time.png" alt="Bar chart of force merge time for a 138M vector index: 5 minutes on GPU versus 249 minutes on CPU" /><p></p><h2>Get started with GPU vector indexing in Elasticsearch</h2><p>Install Elasticsearch with GPU-accelerated vector indexing by following <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/gpu-vector-indexing">these instructions</a>. Replicate the above benchmarks by running<a href="https://github.com/elastic/rally-tracks/tree/master/msmarco-v2-vector"> rally msmarco track</a> with parameters in <a href="https://github.com/elastic/rally-tracks/issues/1188">the GitHub issue</a>. Learn more about GPU-accelerated vector indexing, search, and preprocessing by visiting<a href="https://github.com/NVIDIA/cuvs"> NVIDIA cuVS</a>. </p><h2>Acknowledgments</h2><p>The authors would like to thank Chris Hegarty, Gilad Gal, and Mayya Sharipova from Elastic, as well as Nathan Stephens from NVIDIA, for their contributions to this article.</p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/vector-indexing-gpu-elasticsearch-cuvs</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/vector-indexing-gpu-elasticsearch-cuvs</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[Inside Elastic]]></category>
    <category><![CDATA[Index Data]]></category>
    <dc:creator><![CDATA[Bao Tong,Manas Singh,Corey Nolet]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2415fbdb68d01a49/6ab4e2a39a8267823dc47d59/NVIDIA.jpg" length="0" type="image/jpeg"/>
    <pubDate>Fri, 25 Sep 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Agentic workflows in Elasticsearch: pause an AI agent for human approval, resume 72 hours later]]></title>
    <description><![CDATA[Build AI agent orchestration where the workflow waits for a human approval and then executes the fix on its own, with nothing extra to provision and the whole decision trail queryable in Elasticsearch.]]></description>
    <content:encoded><![CDATA[<p>An AI agent receives a question and processes it within seconds. The agent responds before the session expires. That model works well for question and answer or code generation. And it works well for point-in-time analysis. But what happens when the agent needs to wait for a human to approve something and that human is in a meeting or dealing with another incident? The session expires and context is lost. The work starts over from scratch.</p><p></p><p>Elasticsearch Workflows solves this with a persistent execution state. A workflow can pause at a human approval gate for days and resume exactly where it left off. Every decision is persisted in Elasticsearch as searchable data.</p><p></p><p>In this article, we’ll build this in practice with a real scenario: Documents that failed during ingestion get stuck in a data stream’s failure store. </p><p></p><p>In this scenario, every step is reproducible. By the end, you’ll have an end-to-end workflow triggered by alerts, with pre-execution approval, automatic remediation, post-execution verification, and a rejection-and-revision path.</p><h2>What you’ll learn</h2><p>We’ll cover how to:</p><ul><li><p>Use the <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/failure-store">failure store</a> to capture and remediate documents that failed ingestion.</p></li><li><p>Build a long-running workflow with structured and binary approval gates using <a href="https://www.elastic.co/docs/explore-analyze/workflows/steps/flow-control-steps"><code>waitForInput</code></a> and <a href="https://www.elastic.co/docs/explore-analyze/workflows/steps/wait-for-approval"><code>waitForApproval</code></a>.</p></li><li><p>Invoke Elastic Agent Builder AI agents automatically after human approval.</p></li><li><p>Trigger workflows automatically from alerting rules.</p></li><li><p>Handle approval and rejection paths (real human-in-the-loop, not just a checkbox).</p></li></ul><h2>What’s a long-running AI agent?</h2><p>The difference between a session-bound agent and a long-running agent is the nature of the work rather than the speed of the large language model (LLM).</p><p></p><p>A session-bound agent lives within a session: It receives a question and processes it. Then it responds to the question. If the process dies, the context dies with it. </p><p></p><p>Operational processes, like data remediation or infrastructure provisioning, or like change review, are different. The processing itself is fast. What takes time are human decisions at unpredictable moments. The person who needs to approve a reindex might be handling an incident or in a different time zone. Or they may simply be at lunch. Long-running agents handle these breaks in workflow by assembling separate infrastructure: a database to store execution state, a server for the orchestrator, a UI for approvals, and integrations for notifications. </p><p></p><p>Elasticsearch Workflows runs where the data already lives. The workflow's long-term execution state is persisted in Elasticsearch, surviving restarts and waiting days between steps. AI agents come from Agent Builder and maintain a session only for the duration of each step, such as when the step ends or when the session ends. The workflow holds the context that spans across steps and days, and alerts come from the same rules already monitoring your data. The approval UI is in Kibana, and there’s no additional infrastructure to provision.</p><p></p><p>The workflow's internal execution state is managed by Kibana. The <code>remediation-runs</code> index is the audit trail that the workflow itself writes and that you can query like any other Elasticsearch index.</p><h2>What’s the Elasticsearch failure store?</h2><p>When Elasticsearch receives a document it cannot index, it has two options: Reject the document with an error, or store it somewhere safe for later analysis. The <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/failure-store">failure store</a> is that second option.</p><p></p><p>Imagine a <code>logs-demo-app</code> data stream with the <code>price</code> field mapped as <code>float</code>. If the source application sends <code>"price": "N/A"</code> instead of a number, Elasticsearch cannot index the document. With the failure store enabled, it’s redirected to a dedicated index within the data stream itself rather than losing that document.</p><p></p><p>Documents in the failure store preserve the original content and include information about the error, including the exception type, the message, the pipeline and processor that failed, and even the stack trace. You can query the failure store using the <code>data_stream::failures</code> syntax. </p><p></p><p>A typical document looks like this:</p>{
  "@timestamp": "2026-07-16T14:45:02.111Z",
  "document": {
    "id": "AZ9rY09teEMlWkReFa9e",
    "index": "logs-demo-app",
    "source": {
      "user_id": "u-test-1",
      "price": "INVALID",
      "message": "Order failed"
    }
  },
  "error": {
    "type": "document_parsing_exception",
    "message": "failed to parse field [price] of type [float]"
  }
}<p>The <code>document.source</code> field contains the original document that failed, and the <code>error</code> field describes what went wrong. This is exactly the structure that the <code>failure-analyst</code> agent will read to diagnose the problem.</p><p>But the documents remain stuck there. Someone needs to diagnose the problem and fix the pipeline or mapping. They also need to reindex the documents back into the data stream. This is exactly the kind of work that combines AI automation with human approval, and it’s what we’re going to build.</p><h2>How the AI agent orchestration works, from alert to resolution</h2><p>The workflow connects four building blocks: an alerting rule, two AI agents, human approval gates, and an audit index. The diagram below shows how these components interact.</p><p></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt50eed826ed4fd509/6ab0ff9b3a38313fbe723342/agentic-workflow-diagram.png" alt="Agentic workflow diagram: alerting rule, failure-analyst diagnosis, Gate 1, automated execution, and Gate 2 verify" /><p>The alerting rule monitors <code>logs-demo-app::failures</code> every minute. When it finds documents written within the previous five minutes, it starts a workflow execution.</p><p>The workflow first runs <code>read_failures</code> to retrieve the failed documents. It then passes them to the <code>failure-analyst</code> agent through the <code>diagnose</code> step. The agent inspects the destination mapping and identifies the root cause. It then produces a structured remediation plan without making any changes.</p><p>The workflow records this plan in the <code>remediation-runs</code> index with the status <code>awaiting_fix_approval</code> and pauses at Gate 1.</p><p>Gate 1 uses <code>waitForInput</code>, a structured form that collects the reviewer's decision and optional notes. While the workflow waits, its state remains persisted in Elasticsearch. No agent session or polling loop needs to remain active, which means that the gate can wait for hours or days without continuously consuming compute resources.</p><p>If the reviewer approves the plan, the workflow invokes the <code>remediation-executor</code> agent through the <code>execute_fix</code> step. The agent calls the <code>execute-failure-store-fix</code> skill and creates the ingest pipeline. It also performs the bounded reindex and returns a detailed execution report.</p><p>Gate 2 appears only after the execution finishes. It uses <code>waitForApproval</code> to present the agent's report and asks the reviewer to choose between <strong>Yes, mark as resolved </strong>and<strong> No, escalate</strong>. The workflow then records the final outcome in <code>remediation-runs</code> and includes the complete report in <code>agent_report</code>.</p><p>If the reviewer rejects the initial plan at Gate 1, the workflow sends the diagnosis and the reviewer's feedback back to the <code>failure-analyst</code> agent, which then produces a revised plan that’s presented at Gate 1b. If approved, the revised plan follows the same automatic execution and verification path. If the plan is rejected again, the workflow records the case as <code>fix_rejected</code> and ends without applying any changes.</p><p>The <code>remediation-runs</code> index records the diagnosis, revision requests, revised plans, execution reports, and terminal outcomes as searchable audit documents. The shared workflow <code>execution_id</code> correlates records belonging to the same remediation run.</p><h3>Production consideration: Exception-based verification</h3><p>The diagram above shows the deliberately conservative implementation used in this tutorial: Every completed remediation pauses at Gate 2, including executions that appear fully successful. This makes the human-verification mechanism explicit and may be appropriate for low-volume or high-risk environments, but it can create review fatigue at scale. A production extension would add a deterministic verification step after execution. When the expected documents are present in the destination and the reindex reports no failures or policy-defined errors, the workflow can record resolved automatically. Only partial, failed, or ambiguous outcomes should pause at Gate 2 for site reliability engineering (SRE) review and possible escalation.</p><p>The following sections walk through each part: the data stream setup, the agents and skills, the alerting rule, and the workflow.</p><h2>Prerequisites</h2><p>To follow this tutorial, you’ll need:</p><ul><li><p>An Elastic Cloud Serverless project or an Elastic Stack 9.4+ deployment.</p></li><li><p>Agent Builder (generally available [GA] on Elasticsearch projects, enabled by default).</p></li><li><p>The following environment variables configured:</p></li></ul>export ES_URL="https://&lt;your-project&gt;.es.&lt;region&gt;.gcp.elastic.cloud:443"
export ES_API_KEY="&lt;your-api-key&gt;"
export KIBANA_ENDPOINT="https://&lt;your-project&gt;.kb.&lt;region&gt;.gcp.elastic.cloud"<h2>Setting up a data stream with the failure store enabled</h2><p>All code in this tutorial, including the workflow YAML, setup scripts, and agent instructions, is available in this <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/ai-agent-orchestration-human-approval-workflow">repository</a>. </p><p>
Start by getting the code from the repository:</p># Clone the repository without checking out all files
git clone --filter=blob:none --sparse https://github.com/elastic/elasticsearch-labs.git
cd elasticsearch-labs

# Check out only this companion folder
git sparse-checkout set supporting-blog-content/ai-agent-orchestration-human-approval-workflow
cd supporting-blog-content/ai-agent-orchestration-human-approval-workflow<p>Then run the setup script:</p>./scripts/01-setup-failure-store.sh<p>The script prepares the test environment: It removes previous test resources, creates a failure-store-enabled index template, ingests three valid baseline documents, initializes the failure store, and creates the <code>remediation-runs</code> audit index. The <code>price</code> field is mapped as <code>float</code> with <code>ignore_malformed: false</code> ; nonnumeric values generate a parsing error and get redirected to the failure store instead of being silently dropped. The data stream itself is created automatically when the first document is indexed.</p><p>The failure store is enabled at the index template level, not on the data stream directly. The template includes a <strong><code>data_stream_options</code></strong> block that tells Elasticsearch to activate the failure store for any data stream created from that template:</p>"data_stream_options": {
  "failure_store": {
    "enabled": true
  }
}<p>The data stream is created automatically upon the first ingestion request, inheriting the failure store configuration from the index template. From that point on, any document that cannot be indexed is redirected to the failure store instead of being rejected with an indexing error.</p><p>The script also ingests three valid documents with numeric <code>price</code> values. These documents establish the healthy baseline: The data stream exists and contains valid data before any ingestion failures occur.</p><p>After the <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/ai-agent-orchestration-human-approval-workflow/scripts/01-setup-failure-store.sh"><code>01-setup-failure-store.sh</code></a> script completes, the environment is ready:</p>logs-demo-app           → 3 valid documents
logs-demo-app::failures → 0 documents (empty, index initialized)
remediation-runs        → created with explicit keyword mappings<p>The invalid documents that trigger the alerting rule and start the workflow are ingested later, when you run <code>02-trigger-test.sh</code> in the testing section. This sequence shows a realistic failure scenario: The data stream is operating normally with valid data, and then an upstream change causes malformed documents to arrive.</p><h3>Importing the agents and skills into Agent Builder</h3><p>Run the restore script to import the two agents and two skills, along with the workflow into your Kibana instance:</p>pip install requests python-dotenv
python3 scripts/restore.py<p>Expected output:</p>Restoring skills
  created  failure-store-remediation-planner
  created  execute-failure-store-fix
  Skills: created=2, updated=0

Restoring agents
  created  failure-analyst
  created  remediation-executor
  Agents: created=2, updated=0

Restoring workflow
  imported failure_store_remediation

Restore completed successfully<p>Running the script again updates existing components without creating duplicates.</p><h2>The AI agents that diagnose and execute the fix</h2><p>After running the <code>restore</code> script, two agents will be available in Agent Builder: one to diagnose failures and another to apply approved fixes. In Kibana, go to <strong>Agent Builder </strong>and verify that both agents and their associated skills were created correctly.</p><h3><code>failure-analyst</code>, the read-only diagnosis agent</h3><p>This agent reads failed documents from the failure store and inspects the destination mapping. It produces a structured remediation plan that includes an ingest pipeline definition and bounded replay instructions. The plan also includes a risk assessment. The agent doesn’t execute the proposed remediation.</p><p></p><p>The agent instructions establish its objective and read-only boundary. Key instructions include:</p>## Description

Analyzes failed documents from the failure store, identifies root causes, and proposes remediation pipelines.

## Instructions

# Role

You are a cautious Elasticsearch data quality analyst specializing in data
stream failure stores.

# Goal

Analyze a batch of related failure-store documents and produce a safe,
evidence-based remediation plan for human review.

Do not execute any changes.

# Required approach

- Identify the common root cause and the affected document pattern.
- Propose the smallest bounded remediation that addresses the demonstrated failure.
- Do not propose an executable remediation when required context is missing.
- Do not create pipelines, change mappings, update templates, run reindex,
  or perform any write operation.

...<p>Its associated <code>failure-store-remediation-planner</code> skill provides the specialized rules used to construct a safe replay. For example:</p>...

- A remediation pipeline that replays indexing or mapping failures from a failure store must use `recover_failure_document` as its first processor. 
- For a small human-reviewed batch, filter the reindex source using the exact failure-store document `_id` values supplied in the input. 
- Mapping changes, template changes, failure-store deletion, and upstream application changes must be listed only as manual follow-up recommendations.

...<p>The response contract is also constrained so that the workflow can consume the plan reliably:</p>Return exactly one JSON object and no additional explanatory text.
Do not use Markdown code fences, preambles, summaries, or attachments.<p>Together, the agent instructions and skill allow <code>failure-analyst</code> to produce an actionable but bounded remediation plan while leaving execution under the control of the workflow and its human approval gate.</p><h3><code>remediation-executor</code>, the agent that applies the approved fix</h3><p>The workflow invokes this agent after the remediation plan has been approved at Gate 1. Unlike the first agent, <code>remediation-executor</code> is allowed to perform the write operations required to apply the approved remediation.</p><p></p><p>Its agent instructions intentionally keep this role focused on immediate execution:</p>## Description
Executes Elasticsearch operations: creates pipelines and runs reindex.

## Instructions
You execute Elasticsearch remediation operations.

When asked to run a remediation, invoke the skill
"execute-failure-store-fix" directly and immediately.

Do not ask for confirmation. Do not ask clarifying questions.

After the skill completes, write a plain-text summary as your final message.
Include: whether the pipeline was created, how many documents were matched,
how many were successfully reindexed, and how many failed. This message is
captured by the workflow and shown to the human reviewer.<p>The associated <code>execute-failure-store-fix</code> skill defines how the approved plan is retrieved and executed. Key instructions include:</p>## Name

execute-failure-store-fix

## Description

1. The prompt will include a "Workflow execution ID". Use it to search the
   remediation-runs index for the approved plan for this specific execution.

2. Determine which field contains the remediation plan:
   - If status is "awaiting_fix_approval", read the "diagnosis" field.
   - If status is "awaiting_fix_approval_v2", read the
     "revised_diagnosis" field.

3. From that JSON object, extract:
   - remediation_pipeline.pipeline_id
   - remediation_pipeline.pipeline_definition
   - remediation_pipeline.reindex_request
   - affected_docs.data_stream

4. Create the ingest pipeline in Elasticsearch using the extracted
   pipeline_definition.

5. Execute the reindex_request exactly as specified in the approved plan.
   Do not substitute or hardcode the destination — use
   affected_docs.data_stream from the approved plan.

6. Report how many documents were matched, successfully reindexed,
   and failed..<p>The skill searches by both <code>execution_id</code> and approval status. This ensures that the executor retrieves the plan approved for the current workflow execution rather than an unrelated remediation run. The status determines whether it uses the original <code>diagnosis</code> or the <code>revised_diagnosis</code> produced during the revision path.</p><p></p><p>This separation gives the two agents distinct responsibilities: <code>failure-analyst</code> investigates the failure and proposes a bounded plan without making changes, while <code>remediation-executor</code> applies the selected plan only after the workflow has completed the required human approval step.</p><p></p><p>The setup scripts import the complete agent instructions and skill definitions. The excerpts above highlight the instructions most relevant to understanding the responsibilities and behavior of each agent. The complete definitions are available in <code>backup/save_agents.json</code> and <code>backup/save_skills.json</code> in the repository.</p><p></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0bff70653cc57d12/6ab10598f2c7a0e6b88b38df/failure-analyst-remediation-executor.png" alt="Agent Builder in Kibana showing the Failure Analyst and Remediation Executor AI agents after import" /><p>After verifying that the agents and their skills have been imported correctly, the next step is to configure the alerting rule that detects new documents in the failure store and starts the remediation workflow.</p><h2>How do I trigger a workflow from an Elasticsearch alert</h2><p>The remediation process starts with an Elasticsearch alerting rule that monitors the failure store and starts the workflow when new failures are detected. Because the <code>restore.py</code> script has already imported and enabled <code>failure_store_remediation</code>, you can connect the workflow while creating the rule.</p><h3>Creating the alerting rule that watches the failure store</h3><p>The rule checks the failure store every minute and creates an alert when it finds documents written within the configured five-minute time window.</p><ol><li><p>Go to <strong>Management → Rules → Create rule</strong>.</p></li><li><p>Select <strong>Elasticsearch query</strong>.</p></li><li><p>Configure:</p></li><ol><ul><li><p><strong>Query type</strong>: <code>ES|QL</code>.</p></li><li><p><strong>Query</strong>:</p><p></p></li><li><p>Select time field: <code>@timestamp</code>.</p></li><li><p>Select alert group: <strong>Create an alert if matches are found.</strong></p></li><li><p>Time window: <strong><code>5 minutes</code></strong>.</p></li><li><p>Check every: <strong><code>1 minute</code></strong>.</p></li></ul></ol><li><p>In the <strong>Actions</strong> section, click <strong>Add action → Workflows</strong>.</p></li><li><p>Select the <code>failure_store_remediation</code> workflow.</p></li><li><p>Under <strong>Run workflow</strong> for, select <strong>New alerts</strong>.</p></li><li><p>Set <strong>Action frequency</strong> to <strong>Run per alert</strong>.</p></li><li><p>Save and enable the rule.</p></li></ol><p>Whenever the rule creates a new alert, it starts one <code>failure_store_remediation</code> workflow execution for that alert. The failure store is still empty at this stage; the invalid documents are ingested later by <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/ai-agent-orchestration-human-approval-workflow/scripts/02-trigger-test.sh"><code>02-trigger-test.sh</code></a>.</p><p></p><p>With the alert trigger configured, let’s examine the workflow that performs the diagnosis, approval, remediation, and verification steps.</p><h2>Building the agentic workflow, step by step</h2><p>At this point, the workflow has already been created in Kibana by the <code>restore.py</code> script executed earlier. To understand how the workflow operates, we’ll examine it incrementally, using relevant YAML excerpts to explain the role of each step in the remediation process. The complete workflow definition is available in the tutorial repository.</p><h3>Declaring the alert trigger and the 7-day timeout</h3><p>The workflow declares an alert trigger that allows it to be initiated by the alerting rule created earlier.</p>version: "1"
name: failure_store_remediation
description: &gt;
  Long-running workflow that remediates failed documents in a data stream's
  failure store. Triggered by an alerting rule when failures are detected.
  An AI agent diagnoses the root cause and proposes a fix. After human
  approval, the remediation-executor agent runs the fix automatically,
  then pauses for verification.
enabled: true
tags: [failure-store, remediation, agentic]
settings:
  timeout: 7d

triggers:
  - type: alert<p>Two details are important here. First, the <code>timeout: 7d</code> defines that the workflow can remain active for up to seven days. We set this explicitly because the workflow will pause at human approval gates that may take hours or days to respond.</p><p></p><p>Second, declaring <code>type: alert</code> makes the workflow available to alerting. The workflow action configured in the rule establishes the connection between the alert and <code>failure_store_remediation</code>.</p><h3>Reading failed documents from the failure store</h3><p>The workflow begins by retrieving failed documents using the <code>::failures</code> syntax:</p>steps:
  - name: read_failures
    type: elasticsearch.search
    with:
      index: "logs-demo-app::failures"
query:
	    range:
		"@timestamp":
		gte: "now-5m"
		lte: "now"
size: 50
sort:
        - "@timestamp": "desc"<p>The range query limits the search to failure-store documents created during the same five-minute window monitored by the alerting rule. Within that window, <code>size: 50</code> bounds the batch to the 50 most recent matching documents. For larger volumes, use <code>search_after</code> or pagination.</p><h3>How the AI agent diagnoses the root cause</h3><p>The next step sends the failed documents to the <code>failure-analyst</code> agent:</p>  - name: diagnose
    type: ai.agent
    agent-id: failure-analyst
    timeout: 300s
with:
      message: |
        Analyze these failed documents from the "logs-demo-app" failure store.

        Failed documents:
        {{ steps.read_failures.output.hits.hits | json }}

        Identify the root cause, classify the failure type, propose a concrete
        fix with ingest pipeline processors, generate a remediation pipeline to
        reindex the failed documents, and assess the risk.

        YOUR RESPONSE MUST BE A SINGLE JSON OBJECT ONLY.
  Do not write any explanation, preamble, markdown, or commentary.
  Start your response with { and end with }.
  Use exactly these top-level keys: root_cause, failure_type, affected_docs,
  proposed_fix, remediation_pipeline, risk_assessment.<p>The <code>{{ steps.read_failures.output.hits.hits | json }}</code> template injects the documents retrieved in the previous step into the agent's prompt. The agent analyzes the error patterns and identifies that the <code>price</code> field received string values where floating-point values were expected. It then proposes a remediation pipeline.</p><p>The agent's output arrives in <code>steps.diagnose.output.message</code> as a single JSON object. The exact diagnosis, proposed remediation, pipeline definition, and reindex request may vary between executions because they’re generated by the AI agent from the failures it receives. In our tests, the agent consistently identified the root cause and generated working remediation pipelines.</p><p>Despite this natural variability, the response contract remains fixed so that the workflow can persist the plan and the executor can reliably extract its executable components. The response always contains the six required top-level keys: <code>root_cause</code>, <code>failure_type</code>, <code>affected_docs</code>, <code>proposed_fix</code>, <code>remediation_pipeline</code>, and <code>risk_assessment</code>.</p><h4>Recording the diagnosis for audit</h4><p>Before requesting human approval, the workflow writes the diagnosis and proposed remediation plan to the <code>remediation-runs</code> index. This creates a durable audit record that can later be retrieved by the <code>remediation-executor</code> if the plan is approved:</p>- name: record_diagnosis
  type: elasticsearch.index
  with:
    index: remediation-runs
    document:
      "@timestamp": "{{ now | date_to_xmlschema }}"
      execution_id: "{{ execution.id }}"
      data_stream: "logs-demo-app"
      triggered_by_rule: "{{ event.rule.name }}"
      failure_count: "{{ steps.read_failures.output.hits.hits | size }}"
      failure_count_total: "{{ steps.read_failures.output.hits.total.value }}"
      diagnosis: "{{ steps.diagnose.output.message }}"
      status: awaiting_fix_approval<p>The <code>execution_id</code> correlates the diagnosis with the current workflow execution. The record stores both the number of failure documents loaded into the bounded batch and the total number of failures matching the query. After human approval, the executor uses the <code>execution_id</code> together with the approval status to retrieve the correct remediation plan.</p><h3>How do human approval gates work in Elasticsearch Workflows?</h3><p>The workflow uses two kinds of pause steps for different kinds of human decisions. Gate 1 uses <code>waitForInput</code> to collect a structured approval decision and reviewer feedback. Gate 2 uses <code>waitForApproval</code> because verification requires a binary choice: Mark the case as resolved, or escalate it.</p><p>
</p><p><strong>Gate 1</strong></p><p><strong>Gate 1b</strong></p><p><strong>Gate 2</strong></p><p>Step name</p><p><code>gate_fix</code></p><p><code>gate_fix_revised</code></p><p>gate_verify (approved path) / gate_verify_v2 (revised path)</p><p>Step type</p><p><code>waitForInput</code></p><p><code>waitForInput</code></p><p><code>waitForApproval</code></p><p>Timeout</p><p>72h</p><p>72h</p><p>72h</p><p>Decision shape</p><p>Approve or reject, plus notes</p><p>Approve or reject, plus notes</p><p>Yes, mark as resolved / No, escalate</p><p>Presents</p><p>AI diagnosis and proposed plan</p><p>Revised plan</p><p>remediation-executor report</p><p>Appears</p><p>Always</p><p>Only after Gate 1 rejection</p><p>Only after execution completes</p><p></p><p>Gate 1 is defined like this:</p>- name: gate_fix
  type: waitForInput
  timeout: 72h
  with:
    message: |
      GATE 1/2 - Fix Approval

      Data stream: logs-demo-app
      Failed documents: {{ steps.read_failures.output.hits.total.value }}

      The AI agent has diagnosed the failures. Review the full diagnosis
      in the 'diagnose' step output above, then approve or reject.
    schema:
      type: object
      properties:
        approved:
          type: boolean
          title: "Approve fix"
          default: true
        notes:
          type: string
          title: "Feedback (required if rejecting)"<p>When the workflow reaches this step, execution pauses, and its state is persisted in Elasticsearch. No agent session or polling loop needs to remain active while the gate waits. Every gate accepts a response for up to 72 hours, subject to the workflow's overall seven-day timeout.</p><p></p><p>The schema collects an approval decision and an optional <code>notes</code> field. When the proposal is rejected, the workflow passes the notes to the <code>failure-analyst</code> agent as context for revising the remediation plan. The testing section shows the approval and rejection payloads at the point where each is submitted.</p><h3>What happens when the reviewer approves or rejects the plan</h3><p>When Gate 1 is submitted, <code>route_fix</code> resumes the workflow without rerunning the completed steps. The simplified excerpt below shows the first transition in each branch. (The complete executable YAML is available in the tutorial repository.)</p>- name: route_fix
  type: if
  condition: "steps.gate_fix.output.response.approved: true"
  steps:
    - name: execute_fix
      type: ai.agent
      agent-id: remediation-executor
      timeout: 300s
      with:
        message: |
          Run remediation for the "logs-demo-app" failure store.

          Workflow execution ID: {{ execution.id }}

          The fix has been approved by a human reviewer. Execute the skill
          'execute-failure-store-fix' immediately without asking for confirmation.<p>Approval sends the current workflow <code>execution_id</code> to the <code>remediation-executor</code>. The executor uses this identifier together with the approval status to retrieve the remediation plan associated with this specific execution before applying it and continuing to Gate 2.</p><p>If Gate 1 is rejected, the workflow stores the reviewer’s feedback and asks the <code>failure-analyst</code> agent to produce a revised proposal. Gate 1b (<code>gate_fix_revised</code>) then presents that proposal for another human decision. When the revised plan is approved, <code>execute_fix_revised</code> sends the same workflow execution ID to the executor:</p>- name: execute_fix_revised
  type: ai.agent
  agent-id: remediation-executor
  timeout: 300s
  with:
    message: |
      Run the revised remediation for the "logs-demo-app" failure store.

      Workflow execution ID: {{ execution.id }}

      The revised fix has been approved by a human reviewer. Execute the skill
      'execute-failure-store-fix' immediately without asking for confirmation.<p>The executor uses the approval status to determine which plan belongs to the current path: the original <code>diagnosis</code> associated with <code>awaiting_fix_approval</code> or the <code>revised_diagnosis</code> associated with <code>awaiting_fix_approval_v2</code>. Both paths then continue to Gate 2. If the revised proposal is rejected at Gate 1b, the workflow records <code>fix_rejected</code> and ends without applying any changes.</p><h3>Verifying the result at Gate 2 before closing the case</h3><p></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf5f2a8d89684da90/6ab10a9c44675f24756f7a95/gate-2-human-approval.png" alt="Gate 2 human approval in Elasticsearch Workflows: remediation-executor report with resolve or escalate buttons" /><p>Gate 2 is a verification checkpoint. By the time it appears, the <code>remediation-executor</code> agent has already created the ingest pipeline and run the reindex. It has also returned a detailed report. The gate embeds the executor's report, <code>steps.execute_fix.output.message</code> on the approved path, or <code>steps.execute_fix_revised.output.message</code> when the revised plan was executed, and asks the reviewer to select either <strong>Yes (mark as resolved) </strong>or<strong> No (escalate)</strong>.</p><p>Gate 2 uses <code>waitForApproval</code> because the workflow only needs a binary verification decision. The operation details, including the pipeline ID, documents matched and reindexed, version conflicts, failures, and execution errors, come from the <code>remediation-executor</code> agent’s report rather than from fields entered manually by the reviewer.</p><p>In our validated execution, the agent created the <code>logs-demo-app-price-remediation</code> pipeline and matched and reindexed all five approved documents. It reported zero version conflicts and zero failures. The <code>logs-demo-app</code> data stream increased from three to eight documents, while the five original records remained in <code>logs-demo-app::failures</code>. This is expected because reindex copies documents; it doesn’t remove them from the failure store.</p><p>If the reviewer selects <strong>Yes, mark as resolved</strong>, <code>route_verify</code> writes a new document to <code>remediation-runs</code> with status <code>resolved</code> and stores the complete agent report in <code>agent_report</code>. If the reviewer selects <strong>No, escalate</strong>; the workflow writes an <code>escalated</code> record with the same report. The revised path mirrors this through <code>route_verify_v2</code>, producing the same statuses and the same <code>agent_report</code> field.</p><p>Gate 2 exists because a technically completed reindex can still produce partial or unexpected results. Escalating instead of automatically marking the remediation as resolved keeps the audit trail honest. Whether to keep this second gate depends on your environment and risk tolerance.</p><h2>The complete workflow</h2><p>After reviewing the workflow, remember that the complete code is available in the repository in the <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/ai-agent-orchestration-human-approval-workflow/workflow/failure-store-remediation.yaml"><code>failure-store-remediation.yaml</code></a> file.</p><p>The workflow can finish in five ways, depending on the decisions made at Gate 1, Gate 1b, and Gate 2. The diagram below shows how the original and revised remediation paths lead to <code>resolved</code>, <code>escalated</code>, or <code>fix_rejected</code>.</p><p></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt250701b4468e5ced/6ab10bb2b29b5da685c6ad09/five-completion-paths.png" alt="Five completion paths in the failure store remediation workflow: resolved, escalated and fix_rejected outcomes" /><p>Every terminal outcome is appended to the <code>remediation-runs</code> audit history. Across the workflow paths, the audit records preserve the diagnosis, reviewer feedback when a revision is requested, the executor report, and the terminal outcome.</p><p>With the rule and its workflow action already configured, the end-to-end flow is ready to test.</p><h2>Testing the workflow end to end</h2><p>With the alerting rule enabled, run the trigger script to ingest five documents with invalid <code>price</code> values and current timestamps:</p>./scripts/02-trigger-test.sh<p>The rule searches the previous five minutes, so it detects the new <code>failure-store</code> documents during its next evaluation. Go to <strong>Workflows → Executions</strong>, and open the new execution. Confirm that <code>read_failures</code>, <code>diagnose</code>, and <code>record_diagnosis</code> have completed and that <code>gate_fix</code> is waiting for input.</p><h3>Approving the fix at Gate 1</h3><p>At Gate 1, approve the proposed remediation by submitting:</p>{  "approved": true }<p>The workflow resumes at <code>route_fix</code> and invokes the <code>remediation-executor</code> agent through <code>execute_fix</code>. It pauses at Gate 2 with the execution report.</p><p>The following clip shows Gate 1 being approved and the workflow resuming automatically at <code>route_fix</code> and <code>execute_fix</code>.</p><p></p><p></p><p>Before completing Gate 2, verify that the five remediated documents were added to the data stream:</p>GET logs-demo-app/_count<p>Expected result:</p>{
  "count": 8
}<p>The data stream started with three valid documents, so a successful replay increases the total to eight. The original records remain in the failure store because reindex copies documents to the destination rather than removing the source records.</p><p>Complete Gate 2 by selecting <strong>Yes, mark as resolved</strong> or <strong>No, escalate</strong>. The workflow stores the selected outcome and the execution report in <code>remediation-runs</code>.</p><h3>Rejecting the plan and reviewing a revised one</h3><p>To test the rejection path, reset the environment and start another execution:</p>./scripts/03-reset-full-test-environment.sh --apply
./scripts/01-setup-failure-store.sh
./scripts/02-trigger-test.sh<p>At Gate 1, reject the proposal with specific feedback:</p>{
  "approved": false,
  "notes": "The proposed remediation should distinguish numeric strings from non-numeric strings. Convert numeric strings to a float, preserve non-numeric values in price_raw, and remove price only when conversion fails."
}<p>Although <code>notes</code> is optional in the current schema, provide specific feedback whenever you reject a proposal. The workflow passes this value to the <code>failure-analyst</code> agent when requesting a revised plan.</p><p>The workflow records the feedback and asks the agent to revise the proposal. It pauses at Gate 1b. To approve the revised plan, submit:</p>{
  "approved": true
}<p>To reject it definitively, submit:</p>{
  "approved": false,
  "notes": "Explain why the revised proposal should not be executed."
}<p>Approving the revision sends it through the same execution and Gate 2 verification path described above. Rejecting it records <code>fix_rejected</code> and ends the workflow without applying the remediation.</p><h2>Conclusion</h2><p>The pattern built in this tutorial combines three elements: persistent workflow state, specialized AI agents, and human control at the points where judgment matters. The <code>failure-analyst</code> agent diagnoses the problem and proposes a bounded remediation. Gate 1 pauses before any change is made, giving the reviewer control over execution. After approval, the <code>remediation-executor</code> agent applies the fix automatically, and Gate 2 pauses again so the result can be verified before the case is marked as resolved or escalated.</p><p>Persistent execution state solves more than the problem of expiring agent sessions. The diagnosis, reviewer feedback, execution report, and final decision are stored as searchable data in Elasticsearch and correlated by the workflow <code>execution_id</code>. This correlation allows the executor to retrieve the plan approved for the current workflow run, even when multiple remediation cases exist in <code>remediation-runs</code>. It also makes it possible to analyze how many remediations were approved on the first attempt and which failure types are most frequently rejected, along with how long approval gates remain open and which cases require escalation.</p><p>This workflow pattern also applies to index promotions, mapping changes, enrichment pipeline validation, infrastructure operations, and other processes where automation and human judgment must coexist: Diagnose the problem, pause for approval, execute the approved action automatically, pause for verification, and record the final outcome.</p><p></p><h2>Resources</h2><ul><li><p><a href="https://www.elastic.co/docs/explore-analyze/workflows">Elasticsearch Workflows documentation</a></p></li><li><p><a href="https://www.elastic.co/docs/explore-analyze/ai-features/elastic-agent-builder">Agent Builder documentation</a></p></li><li><p><a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/failure-store">Failure store documentation</a></p></li><li><p><a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/ai-agent-orchestration-human-approval-workflow">Tutorial repository</a></p></li></ul><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/ai-agent-orchestration-human-approval-workflow</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/ai-agent-orchestration-human-approval-workflow</guid>
    <category><![CDATA[Agentic AI]]></category>
    <category><![CDATA[Index Data]]></category>
    <category><![CDATA[Operations]]></category>
    <dc:creator><![CDATA[Alex Salgado]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5c030f52a5d4283d/6ab0f53597bf87491eb22cf8/ai-agent-orchestration.png" length="0" type="image/png"/>
    <pubDate>Mon, 21 Sep 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[One field, one copy: How Elasticsearch columnar storage drops the inverted index]]></title>
    <description><![CDATA[Storing each field once means no inverted index, so doc values now read in bulk and skippers let queries skip whole ranges of documents, while new mapping attributes control what each field is allowed to contain.]]></description>
    <content:encoded><![CDATA[<p>As part of the 9.5.0 release, Elasticsearch introduced <a href="https://www.elastic.co/docs/reference/elasticsearch/columnar">columnar and logsdb_columnar index modes</a> in technical preview. Elasticsearch has had columnar storage using Lucene’s doc values since version 1.0.0. Lucene’s doc values power analytics and search functionalities, like group by and sorting by a field. So, what changes with the columnar index modes? </p><p>The changes are about storage and performance, along with the out-of-the-box (OOTB) experience. Up until 9.5.0, Elasticsearch operated as a document-based search engine by default. It could be set up to behave like a columnar system storage-wise, but that wasn’t the OOTB experience. <a href="https://www.elastic.co/docs/reference/elasticsearch/columnar">Columnar index modes</a> make a number of fundamental changes that allow Elasticsearch to optimize columnar analytic and search use cases:</p><ul><li><p>Fields are stored once as doc values only and are no longer indexed by default.</p></li><li><p>New multi-value semantics. The original ordering of multiple values per field per document (for example, in arrays) is preserved by default.</p></li><li><p>Mappings are always flat, and object and passthrough fields in mappings are always auto-flattened.</p></li></ul><h2>How columnar index modes fit into Elasticsearch</h2><p>Many of the columnar index mode changes originate from <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/time-series-data-stream-tsds">time series data streams (TSDSs)</a>. As part of making TSDB a competitive metrics solution, we improved doc values format on disk and only store dimensions and metric fields once as doc values. We also improved query performance. TSDB is already columnar today. Essentially, this makes TSDB’s storage mode columnar. The lessons learned from TSDB are now being applied more broadly to Elasticsearch. </p><p>Note that columnar index modes are opt-in and columnar, and document-based indices can coexist in the same cluster. An enterprise search use case can use a document-oriented index mode, while a logging use case can use logsdb_columnar index mode and be fully columnar, all in the same cluster. In fact, there are currently seven index modes, and indices can all use them in the same cluster.</p><h2>How columnar storage stays fast without an inverted index</h2><p>Indexed fields, either an inverted index for string-based fields or block k-dimensional (BKD) tree for numeric fields, allow Elasticsearch to query or filter by field very efficiently. However, the cost for this is an additional expensive data structure that uses a lot of disk space and is expensive to build at index time and at merge time. With the columnar index modes, fields are no longer indexed by default, so what did we do for query performance to be still acceptable on fields that were no longer indexed?</p><p>One major change was improving doc values scanning performance. This is key and is the cornerstone that any columnar system relies on. Previously, the scanning of doc values was essentially document by document. Lucene’s doc values API only allowed for looking up one value at a time. Historically, this fit the execution model of a search engine. In our own doc value format, we build the capability to allow bulk reading of values for Elasticsearch Query Language (ES|QL) queries. Also over recent minor Lucene releases, Lucene doc values API added support for bulk reading. Without this, fast columnar scanning wouldn’t have been possible.</p><p>Secondly, we fully adopted <a href="https://www.elastic.co/search-labs/blog/docvaluesskippers-lucene-range-queries">doc value skippers</a>, a hierarchical skiplist over doc values. Contrary to an inverted index or a BKD tree, doc values skippers are lightweight data structures. At its core, a <em>skipper</em> allows queries to skip over a range of documents that don’t match a query. It can do this because it stores information, like min and max values. So, for example, when a range query is executed, an interval of documents can be skipped based on the intermediate result and a doc value skipper’s min and max values. The effectiveness of doc value skippers depends on the order in which documents are laid out on disk. This is why <a href="https://www.elastic.co/docs/reference/elasticsearch/columnar#index-sorting">index sorting</a> should be enabled or altered to match the use case.</p><p>By significantly improving our columnar scanning and doubling down on doc values skippers, we’re able to avoid indexing fields by default. Note that a field can still be indexed; <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-index">the <code>index</code> mapping attribute</a> just defaults to <code>false</code> in columnar mode. The exceptions to this rule are text-based fields, which are still indexed by default. This is because text fields provide free text search, which includes text analysis, along with phrase and wildcard matching. This is different from just filtering.</p><h2>How Elasticsearch handles high and low cardinality fields</h2><p>When setting up a schema with string fields, an important configuration parameter is often <em>cardinality</em>; that is, whether many unique values or a few unique string values are expected. Many systems have dedicated field or column types that target low and high cardinality string fields.</p><p>Fields that have low cardinality are typically stored with a dictionary, containing all unique values. Then, for each row offset, the offset into the dictionary containing the term the row has is stored. This is often called an <em>ordinal</em>. For low cardinality fields, this works well, as storing an ordinal per document takes up much less space. Encoding techniques, like delta encoding, offset encoding, and bitpacking, work well for ordinals to compact the per-document storage to just a few bits. </p><p>However, for high cardinality fields, the dictionary and ordinal approach can work counterintuitively. If a larger percentage of the documents have a unique value, building the dictionary becomes expensive and storage savings diminish. The dictionary then becomes another level of indirection for reading values. This is why most systems in that case store values in a columnar fashion using block-based compression. For example, values of multiple rows are stored in 128KB blocks using a sliding-window dictionary-based compression algorithm (like zstandard or lz4). This, in general, is a simple and effective method to store higher cardinality fields and avoids building and maintaining a dictionary. </p><p>With document-based Elasticsearch, there are two ways to map a string: using either the keyword field mapping or one of the text-based field mappings. The former stores an inverted index and dictionary-based doc values. The latter only stores an inverted index. This is why, typically, a text field mapper is often used in combination with a keyword mapper as a <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/multi-fields">multi-field</a>. Also, keyword field mapper (as the name suggests) is meant for keywords or fields that have a lower cardinality and uses dictionary-based doc values implementation. However, in practice, keyword field mappers are also used for high cardinality.</p><p>In columnar mode, every field is only stored once by default. For keyword fields, this means only doc values are stored with no inverted index. Text-based fields now also store doc values and an inverted index by default. Text-based field mappers are different from the keyword field mapper, as these are not automatically used via Elasticsearch’s dynamic mapping logic for columnar indices, and therefore text fields keep storing an inverted index by default.</p><p>For both keyword- and text-based fields, we didn’t choose to expose a cardinality mapping attribute. It’s not always possible to know ahead of time whether a field is low or high cardinality. When flushing and merging segments to disk, Elasticsearch sees all values and can determine the cardinality of a field. This is why we’re choosing to automatically determine whether the usage of a dictionary and ordinal-based encoding is beneficial over block-based compression using a simple cardinality threshold. If a field is below this threshold, dictionary and ordinal-based encoding is used; otherwise block-based compression is used. This simplifies configuration of the mappings and makes it possible to automatically optimize storage as data evolves, since some segments may use dictionaries while others may use blocks for the same field. However, this is currently not ready yet and so, as part of 9.5.0, in columnar mode, both keyword- and text-based field mappers store values in doc values in a block-based compressed layout on disk.</p><p>The two approaches compare as follows:</p><p>
</p><p><strong>Dictionary and ordinal encoding</strong></p><p><strong>Block-based compression</strong></p><p>Suits</p><p>Low cardinality fields</p><p>High cardinality fields</p><p>What’s stored</p><p>A dictionary of unique values, plus one ordinal per document</p><p>Values for many documents compressed together in blocks</p><p>Compression</p><p>Delta encoding, offset encoding, and bitpacking reduce each ordinal to a few bits</p><p>Sliding-window dictionary compression, such as zstandard or lz4, typically over 128KB blocks</p><p>Read path</p><p>Resolve the ordinal, and then look up the value in the dictionary</p><p>Decompress the block, and then read the value directly</p><p>Cost as cardinality rises</p><p>Dictionary grows large, savings shrink, and the extra indirection stays</p><p>Stable, with no dictionary to build or maintain</p><p>Used in columnar mode tech preview</p><p>Not yet</p><p>Yes, for both keyword and text fields</p><h2>Columnar mapping attributes: multi_value, nullability, on_failure</h2><p>The columnar index modes provide more control over how data is stored as doc values. By default, Elasticsearch is lenient and accepts all non-malformed values (for example, nulls and multiple values per field and document). If documents have fields with multiple values per document or no value, doc values store additional data structures to deal with them and therefore implicitly increase costs. </p><p>With columnar, new mapping attributes provide additional control. Note that these new mapping attributes are currently only available with the columnar index modes but will eventually also be available for all index modes.</p><p>Three new mapping attributes are involved:</p><p><strong>Attribute</strong></p><p><strong>Default</strong></p><p><strong>Enforces</strong></p><p><strong>On violation</strong></p><p><strong>Available</strong></p><p><code>multi_value</code></p><p><code>true</code></p><p>One value per document per field</p><p>Document indexing fails</p><p>9.5.0</p><p><code>nullability</code></p><p><code>true</code></p><p>Field must have a value</p><p>Document indexing fails</p><p>9.5.0</p><p><code>on_failure</code></p><p><code>fail</code></p><p>How the above failures are handled</p><p>Sets <code>fail</code> or <code>ignore</code> behavior</p><p>Next minor release</p><h3>multi_value: Enforcing single-valued fields</h3><p>By default, Elasticsearch accepts multiple values per document. To understand the implications of this, we first should take a look at how Elasticsearch (using Lucene’s doc values) stores a dense numeric field where all documents have a single value: </p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3abb1f6fb702b165/6a9fa698893681d9ef3df7d6/unnamed.png" alt="Columnar storage doc values layout: value blocks and a block index resolve which value belongs to a docId" /><p>With this layout, all values are stored in blocks. The number of values per block depends on the index mode but is typically 128 values and is always the same within an index. All values in a block are encoded using various encoding techniques, like delta encoding and bit packing, so each block can have a different size, depending on how well the encoding techniques compress the values. This is why a block index is required. </p><p>Lucene has the notion of a docid (internal numbering for a document), which is essentially a row identifier.</p><ol><li><p>Queries produce matching docids.</p></li><li><p>In case of a dense field, the block id can be resolved from the docid directly.</p></li><li><p>The offset of a block can be resolved from the block index.</p></li><li><p>Once that has been looked up, the target block gets decoded and all values are available.</p></li><li><p>Finally, from docid, the ordinal within the decoded values array can be resolved, which produces the final value.</p></li></ol><p>Now let’s have a look at how the data layout changes when documents have multiple values per document:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9d5ca280a19c7a70/6a9fa6edee57e5dec9051db9/unnamed.png" alt="Multi-value doc values layout in columnar storage: offsets map a docId to values spanning value blocks" /><p>To determine how many values belong to a single docid, an offset lookup is required.</p><p>In this case, a docid has one or more offsets. Each offset points to a block index. Values for a single document are adjacent but can stretch over blocks. In general, compaction of values works well in blocks because values are similar. However, multi-value fields can cause the compaction of values to be less efficient if the number of values per field and document is large and values aren’t similar. This and the additional storage of offsets result in multi-value fields typically having a higher storage footprint on disk.</p><p></p><p>If a field is truly single-valued, you may want to enforce this property. <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/doc-values#doc-values-multi-value">The <code>multi_value</code> mapping attribute</a> makes that possible now. An example is a log level field. Logs typically have one log level (such as debug, info, or error). Enforcing that this field is single-valued in your mappings can help avoid accidentally using more storage than anticipated. A mapping snippet example that disallows the field <code>log.level</code> to have multiple values per document:</p>{
	"properties": {
		"log.level": {
			"type": "keyword",
			"multi_value": false
		}
	}
}<p></p><p>Note that even if a field allows multiple values, this doesn’t mean an offset lookup is stored. This only happens when a Lucene segment has at least one document with two or more values. The <code>multi_value</code> mapping attribute exists just for enforcement.</p><h3>nullability: Requiring every document to have a value</h3><p>By default, Elasticsearch accepts documents with fields that have no value or null value. Just as  multiple values per document require additional accounting, documents with no value require additional accounting to identify which of them have at least one value.</p><p>Doc values store a docid to offset lookup (known as IndexedDISI in Lucene) in case not all documents have a value in a segment. The offset either points directly to the block index for single-valued fields or the offset lookup in case of multi-valued fields. This lookup is compact compared to the value blocks being stored. However, if it were to be created for fields that should have at least one value per document, that would be a waste.</p><p><a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/doc-values#doc-values-nullability">The <code>nullability</code> mapping attribute</a> allows you to control whether documents are allowed to have no value. Just like the <code>multi_value</code> mapping attribute, the <code>nullability</code> attribute exists for enforcement.  Following is a mapping snippet example that requires the <code>log.level</code> field to have a value:</p>{
  "properties": {
     "log.level": {
        "type": "keyword",
        "nullability": false
     }
  }
}<h3>on_failure: What happens when validation fails</h3><p>What happens if a document has multiple values for a field and if the <code>multi_value</code> mapping attribute is set to <code>false</code> or when a field is mapped with nullability set to <code>false</code> and a document doesn’t have that field? At the moment, indexing such documents will fail with a bad request error.</p><p>As part of the next minor release, <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/doc-values#doc-values-on-failure">the <code>on_failure</code> mapping attribute</a> will be available. This allows you to indicate how to handle these validation failures, on a per-mapped field basis. This will support two values:</p><ol><li><p>Fail: Fail indexing of the entire document with a client error. This is the current behavior in Elasticsearch 9.5.0.</p></li><li><p>Ignore: Ignore the validation error, mark the field as ignored, and store values for that field in a hidden field so that it can be introspected when requesting the source. </p></li></ol><h2>Trying the columnar index modes</h2><p>The <a href="https://www.elastic.co/docs/reference/elasticsearch/columnar">columnar index modes</a> are still under active development, but we encourage you to give them a test drive. As we prepare the columnar index modes for general availability (GA), we’ll add more performance and efficiency improvements. We believe that by adapting a columnar mindset, many use cases will benefit from being more cost effective or having better performance characteristics.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/columnar-storage-elasticsearch-index-modes</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/columnar-storage-elasticsearch-index-modes</guid>
    <category><![CDATA[Lucene]]></category>
    <category><![CDATA[Index Data]]></category>
    <category><![CDATA[Mappings]]></category>
    <dc:creator><![CDATA[Martijn van Groningen]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltef1303bd86788300/6a9fa5cdf08ee10715855390/unnamed.png" length="0" type="image/png"/>
    <pubDate>Tue, 08 Sep 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[ Backfill time series data in Elasticsearch: Load months of historical metrics through the bulk API]]></title>
    <description><![CDATA[Elasticsearch works out the time boundaries and creates the past backing indices as the documents land, so a historical data migration runs on your normal ingest path.]]></description>
    <content:encoded><![CDATA[<p>You can now write documents with past timestamps straight into Elasticsearch <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/time-series-data-stream-tsds">time series data streams (TSDB)</a>. Send months of historical metrics through the <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-bulk">bulk API</a>, the <a href="https://www.elastic.co/docs/manage-data/ingest/otlp-endpoint">OpenTelemetry Protocol (OTLP) endpoint</a>, or the <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/tsds-ingest-prometheus-remote-write">Prometheus remote write endpoint</a>. Elasticsearch creates the past backing indices as the documents arrive, computing each index's time boundaries and attaching it to the data stream. Backfilled documents are stored exactly like live ones, with columnar storage and write-time deduplication, along with up to <a href="https://www.elastic.co/blog/70-percent-storage-savings-for-metrics-with-elastic-observability">70% storage savings</a>. Time series data backfill ships in Elasticsearch 9.5, disabled by default, and turns on with one cluster setting. How far back you can write depends on your lifecycle configuration, since backfill doesn’t apply to indices that are already read-only as a result of downsampling or a searchable snapshot.</p><h2>How historical metrics were loaded before backfill</h2><p>Even if loading historical metrics isn’t a very common use case, it’s an important step when teams are adopting TSDB. Two scenarios have been the most prominent: bootstrapping a new time series data stream and migrating data from a different system or data stream to a time series one.</p><h3>Bootstrapping a new time series data stream</h3><p>You want to start a new time series data stream with a week of historical data so you have something meaningful to query from the start. With existing tooling, you had to set <a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/time-series"><code>index.look_back_time</code></a> to the seven-day maximum in the index template, and all historical data would land in a single backing index. For anything beyond seven days, you needed to create past backing indices manually.</p><h3>Migrating metrics from another system</h3><p>You have months worth of metrics stored on a different system and want to move your full dataset to TSDB. You need to load months of metrics history alongside live ingestion. The workaround was to manually create all the necessary past backing indices with the right <a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/time-series"><code>time_series.start_time</code></a> and <a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/time-series"><code>time_series.end_time</code></a> and to index into it directly using the index name. You then attached it to the data stream via the <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/modify-data-stream">modify data stream API</a>. It worked, but it required understanding the index time semantics and repeating the steps for each time window, along with coordinating that process around ongoing writes.</p><p>We wanted both scenarios to feel as close to normal bulk indexing as possible.</p><h2>What time series data backfill changes</h2><p>In 9.5, Elasticsearch can create backing indices covering past time ranges, which extends the eligible write window backward.</p><p>The eligible write windowis the range of <code>@timestamp</code> values that a time series data stream accepts for new documents. </p><p>In the past, the eligible write window was determined only by the existing writable backing indices at the moment the request was received by Elasticsearch.</p><p>In 9.5, Elasticsearch can expand the eligible write window in the past by creating backing indices. This converts the eligible write window to a sliding window extending from the present back to the first read-only or destructive lifecycle action. Common examples of these actions, which are typically defined within your lifecycle configuration, are <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/downsampling-concepts">downsampling</a> or <a href="https://www.elastic.co/docs/deploy-manage/tools/snapshot-and-restore/searchable-snapshots">searchable snapshots</a>. Examples also include <a href="https://www.elastic.co/docs/manage-data/lifecycle/data-stream/tutorial-data-stream-retention">retention configurations</a>. </p><p>So, given that loading historical data is enabled in a cluster, the eligible write window of the data stream with the following lifecycle configuration is determined by the downsampling action, because it’s the first action that makes backing indices read-only. So, for this data stream Elasticsearch accepts documents whose <code>@timestamp</code> is no older than three months.</p>GET _data_stream/metrics/_lifecycle
{
  "enabled": true,
  "downsampling": [{ "after": "90d", "fixed_interval": "10m" }],
  "data_retention": "365d"
}<h3>Why loading historical data into TSDB is hard</h3><p>TSDB consists of data streams optimized for timestamped measurements. It uses a columnar storage layout and enforces immutable dimensions. It also organizes data into time-bound backing indices; each <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/time-bound-tsds">index covers a specific time range</a> and accepts only documents whose <code>@timestamp</code> falls within it.</p><p>As time passes, rollover creates new backing indices to cover upcoming ranges. Until this release, there was no corresponding mechanism for the past. Creating indices in the past is tricky because historical data might span over a long period of time and can arrive at Elasticsearch out of order. Consequently, Elasticsearch cannot determine the write timeframe that its backing index should cover. Our solution to this is to use a preconfigured interval and lazily create past backing indices.</p><h2>How Elasticsearch creates past backing indices</h2><p>When a document is detected whose timestamp isn't covered by any existing backing index, Elasticsearch determines the time boundaries for the missing indices and creates them. It then adds them to the data stream in a single atomic operation. </p><p>Lazily creating the indices ensures that a single request in the past won’t overwhelm the cluster by requiring the creation of 300 indices all at once. It also doesn’t create indices before there are docs to write into them.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5a3c89f8c228a3b6/6a9a5b6532b530b1df6d23a0/unnamed.png" alt="Time series data backfill timeline: past backing indices accept documents within the retention limit, rejecting older ones" /><h3>Proactive vs. reactive: How we chose the index creation approach</h3><p>We explored two ways to detect when a past backing index needs to be created.</p><p>The first is proactive. Inspect each incoming document's timestamp before routing, and create any missing past backing indices up front. This keeps the write path clean. By the time a document is routed, the index it needs already exists. It does require the data stream to already exist with at least one time series backing index, since that's what we inspect to determine the eligible write window and the time boundaries of the new index. The downside is that it adds work to every bulk request targeting a time series data stream, even requests that contain no past timestamps and need no backfill at all.</p><p>The second is reactive. Let the document fail the normal indexing, intercept that failure, create the missing index, and retry. This avoids any overhead on the common case, since the extra work only happens when a mismatch actually occurs. The tradeoff is more complexity in the failure handling path and a retry on every backfill document.</p><p>We ran performance tests on the proactive approach against bulk requests with no past timestamps and found no measurable regression. The overhead of inspecting timestamps turned out to be negligible. That settled it. Proactive creation is simpler and consistent with how index auto-creation already works in Elasticsearch. Plus, it adds no measurable cost to the workloads that don't use backfill.</p><h3>How Elasticsearch determines past index boundaries</h3><p>Each new past backing index has three properties to compute: its duration, its start time, and its end time.</p><p><strong>Property</strong></p><p><strong>How it's set</strong></p><p><strong>Constraint</strong></p><p>Duration</p><p>Defaults to one day, configurable via the cluster setting <a href="https://www.elastic.co/docs/reference/elasticsearch/configuration-reference/miscellaneous-cluster-settings#time-series-data-stream"><code>data_streams.past_tsdb_index_interval</code></a></p><p>Minimum one hour. If the triggering timestamp falls in a gap up to 1.3 times the configured duration, Elasticsearch collapses it into a single bridging index rather than creating many tiny ones.</p><p>Start time</p><p>Anchored to the start of the next existing backing index, working backward in multiples of the configured duration</p><p>Increased to match the end time of the previous neighboring index, where they would otherwise overlap.</p><p>End time</p><p>Start time plus the configured duration</p><p>Reduced to match the start time of the next index, where they would otherwise overlap.</p><h3>Handling concurrent writes</h3><p>In a distributed setup, multiple nodes can receive bulk requests with overlapping past timestamps at the same time. Each node collects the timestamps that aren’t matching any of the existing indices and sends a request to the master node. </p><p>The master node executes a cluster update that sorts them and then, one by one, checks whether the timestamp is covered by an existing or newly created index. Otherwise, it issues a new create index request with the time boundaries calculated as described above. The cluster updates are always sequential and guaranteed to produce valid cluster states, so new indices are guaranteed to not overlap with existing indices.</p><h3>How lifecycle age works for backfilled indices</h3><p>Past backing indices hold old data but are new indices. Without an adjustment, lifecycle features would apply downsampling and retention based on when the index was created rather than when the data is from. We account for this by using the <code>index.time_series.end_time</code> as the <a href="https://www.elastic.co/docs/reference/elasticsearch/configuration-reference/data-stream-lifecycle-settings#_index_level_settings"><code>index.lifecycle.origination_date</code></a>. As a result, the age of the index as perceived by both <a href="https://www.elastic.co/docs/manage-data/lifecycle/data-stream">data stream lifecycle</a> and <a href="https://www.elastic.co/docs/manage-data/lifecycle/index-lifecycle-management">index lifecycle management (ILM)</a> is based on the age of its data and not its creation time.</p><h2>How to use time series data backfill</h2><h3>How to enable time series data backfill</h3><p>Backfill support ships disabled by default. <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-cluster-put-settings">Enable it at the cluster level</a>:</p>PUT _cluster/settings
{
"persistent": {
"data_stream.past_tsdb_index_creation_enabled": true
	}
}<h3>Bootstrapping with historical metrics</h3><p>To load historical data into a new time series data stream:</p><ol><li><p><a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-put-index-template">Create your index template.</a> </p></li><li><p><a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-create-data-stream">Initialize your data stream.</a> (This is an important step because an existing data stream is a requirement for creating past backing indices.)</p></li><li><p>Start indexing. </p></li></ol><p>Past backing indices are created automatically as documents with historical timestamps arrive, each covering one day of data by default. No additional configuration is needed.</p><h3>Data migration into an existing data stream</h3><h4>Migrating data within the eligible write window</h4><p>For data that falls within the eligible write window of your data stream, point your migration pipeline at the data stream and let Elasticsearch manage the rest.</p><h4>Migrating data beyond a read-only action</h4><p>For data older than the write window (for example, you're migrating 18 months of metrics but downsampling kicks in after seven days), you need a separate data stream without read-only lifecycle actions. Retention isn’t an issue since the data would be deleted anyway. The pattern is:</p><p>1. Create an index template for the historical data stream, using the same mappings as the original but without a lifecycle:</p>PUT _index_template/my-metrics-historical
{
  "index_patterns": ["metrics-historical-*"],
  "data_stream": {},
  "template": {
    "settings": { "index.mode": "time_series" },
    "mappings": {
      "properties": {
        "sensor_id": { "type": "keyword", "time_series_dimension": true },
        "temperature": { "type": "half_float", "time_series_metric": "gauge" },
        "@timestamp": { "type": "date" }
      }
    }
  }
}<p>2. Create the historical data stream. If this step isn’t executed, the first indexing request might fail. During the first indexing request, Elasticsearch can create the data stream but it cannot yet create any past backing indices, so indexing a historical document might fail. Creating the data stream explicitly ensures that all indexing requests will be accepted:</p>PUT _data_stream/metrics-historical-2024<p>3. Index historical data into the historical data stream while current data continues flowing into the original.</p><p>4. When the load is complete, add lifecycle. This is only supported by data stream lifecycle since this feature functions on a data stream level:</p>PUT _data_stream/metrics-historical-2024/_lifecycle
{
"enabled": true,
"downsampling": [{ "after": "7d", "fixed_interval": "10m" }]
}<p>5. Query across both data streams with a wildcard pattern (<code>my-metrics*</code>) or a data stream alias.</p><p>6. If retention is configured, delete the historical data streams when their data expires. Data stream lifecycle will delete the data but it won't clean up the data stream itself.</p><p>As you see, the historical data needs to fit on the target tier as a whole because lifecycle will be enabled after the data is loaded. If you have a large historical import, you might choose to split it into batches. Make sure each batch can fit on the target tier as a whole at the time of indexing, to avoid running your cluster out of disk space. Lifecycle will start processing the batch's indices as soon as it's enabled, but it will need time to process the whole backlog.</p><h2>Protecting the cluster during large migrations: Downsampling floodgate</h2><p>When data stream lifecycle runs against a data stream with many indices that all qualify for downsampling, it queues them simultaneously. Downsampling is CPU and I/O intensive; it reads and rewrites all data in an index. Queuing dozens of operations at once can overwhelm the master node with persistent task updates while it coordinates them.</p><p>The downsampling floodgate scenario could occur before backfill support (for example, when adding a lifecycle policy to an existing data stream with months of accumulated data). Backfill makes it more likely by design.</p><p>In 9.5 and serverless, we added flood protection to data stream lifecycle. It now tracks how many indices per data stream are actively being downsampled. If that count reaches a threshold, data stream lifecycle pauses queuing further operations for that data stream until the count drops. The threshold is configurable via the cluster setting <a href="https://www.elastic.co/docs/reference/elasticsearch/configuration-reference/data-stream-lifecycle-settings#_cluster_level_settings"><code>data_streams.lifecycle.downsampling.max_indices_in_progress</code></a>. Other data streams aren't affected.</p><h2>Limitations and prerequisites of time series data backfill</h2><ul><li><p>Backfill doesn’t apply to read-only indices. If downsampling or a searchable snapshot transition has already run on a time period, documents for that period are still rejected.</p></li><li><p>The feature requires a preexisting time series data stream with at least one time series backing index.</p></li><li><p>System data streams are excluded.</p></li><li><p>Replicated data streams rely on the leader data stream, so no direct backfilling is possible.</p></li><li><p>Scaling remains your responsibility. Loading months of data can trigger significant storage usage, force merge operations, and lifecycle activity in parallel. Check that your cluster has the headroom to manage it before starting.</p></li></ul><h2>Conclusion</h2><p>Prior to the Elasticsearch 9.5 release, loading historical data into TSDB was a manual process. By automating the generation and management of past backing indices, we aim to transform historical data migration to a native capability of your standard ingest pipelines. The inherent complexity of managing time-bound indices remains, but it has transitioned from a user responsibility into an internal Elasticsearch function. Whether you’re bootstrapping a fresh data stream or migrating extensive historical datasets, the platform now handles the heavy lifting, allowing you to focus on analyzing your metrics. We look forward to seeing how these improvements streamline your adoption of TSDB.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/time-series-data-backfill</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/time-series-data-backfill</guid>
    <category><![CDATA[Index Data]]></category>
    <category><![CDATA[Operations]]></category>
    <dc:creator><![CDATA[Mary Gouseti]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc78b8fb3f37f3ee8/6a9a5ad6ecbe18174b1e37ac/unnamed.png" length="0" type="image/png"/>
    <pubDate>Fri, 04 Sep 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[ES95: Adaptive Compression for Elasticsearch Time-Series Metrics]]></title>
    <description><![CDATA[ES95 is Elasticsearch 9.5's new adaptive time series codec that cuts @timestamp storage by 92% and floating point fields by up to 74%, with zero configuration.]]></description>
    <content:encoded><![CDATA[<p><em>The best compression strategy is the one that understands your data.</em></p><p>Observability workloads are storage-intensive by nature, and the composition of that storage determines both cost and query performance. <code>ES95</code> introduces adaptive compression: rather than applying the same encoding to every numeric field, it automatically selects the encoding that best matches each field's structure. The result is a 33.6% reduction in total doc-values storage, 19% to 74% reduction on floating-point gauge metrics and a 92% reduction in <code>@timestamp</code>. No configuration or migration required.</p><h3>Observability data is storage-intensive</h3><p>Data processing systems are rarely limited by how fast they can compute. They’re limited by how fast they can move bytes: off disk, across the network, and through the memory hierarchy. Compression is how a storage engine trades CPU time for memory bandwidth, spending comparatively cheap CPU cycles so fewer bytes have to travel through the parts of the system that are usually constrained. In a read-heavy system like Elasticsearch, that trade-off pays back every time data is queried, often long after it was written.</p><p>Storage size and query performance move together; fewer bytes on disk means fewer bytes to read on every range query, every aggregation and every dashboard load. Compression is not just about saving storage. Every byte that is never written is also a byte that never has to be read.</p><p>The right encoding depends on the structure of the values themselves, and the largest wins come from exploiting the structure already present in the data rather than squeezing an opaque stream of bytes. Few workloads expose that structure more clearly than observability metrics.</p><p>A single host reports hundreds of metrics every few seconds, including CPU utilization, memory ratios, request latencies, and network throughput. Multiply that by thousands of hosts across weeks of retention, and the bytes accumulate fast. Most of that volume is structured but not uniform: timestamps arrive at near-constant intervals from thousands of concurrent series, counters increase monotonically, while gauges like <code>23.47</code> or<code>1.15</code> are short decimal measurements.</p><p>A fixed compression approach cannot adapt to that variety. A timestamp column and a floating-point gauge column compress through fundamentally different techniques, but a codec that applies the same approach to both will necessarily handle one of them poorly. For most of Elasticsearch's time-series codec history, gauges were on the losing end of that trade-off.</p><h3>The structure the old codec was not built to exploit</h3><p>Elasticsearch stores time-series numeric values in <em>doc values</em>: a column-oriented structure where all values for the same field sit adjacent on disk. That adjacency makes compression possible: the codec compares consecutive values of the same field, finds patterns, and exploits them.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2386e2eb944a502a/6a85671fa8b3235ccccbf4f9/unnamed.png" alt="Row-oriented vs column-oriented storage in Elasticsearch time series indices showing how field values cluster on disk" /><p>The <a href="https://www.elastic.co/search-labs/blog/time-series-data-elasticsearch-storage-wins">time-series codec before ES95</a> applied the same fixed encoding to every numeric field: delta encoding followed by normalization, GCD (greatest common divisor) reduction, and bit-packing. Each encoding technique activated where it helped and skipped where it would not. For timestamps and integer counters, this approach was remarkably effective. For floating-point gauges, it could find almost nothing to work with.</p><p>The reason is how they’re stored. To support range queries, Elasticsearch stores floating-point values as integers that preserve numeric ordering. A change of 0.01 in a CPU percentage reading translates to a jump of trillions in that integer space. The codec sees those large jumps and has no strategy to further reduce their footprint. Storage stays near the original eight bytes per value.</p><p>The codec was doing the right thing with the representation it had, but the latter was chosen for querying, not compression, and the goals conflict at the bit level.</p><h3>The cost of a fixed format</h3><p>A compression stage for floating-point values was already on the roadmap, so the interesting part wasn’t the algorithm. The obstacle was architectural.</p><p>The previous codec baked its compression approach into the storage format. Adding a new encoding meant changing the meaning of existing bytes on disk, which forced a format migration, a rollout that can last weeks or months in large production clusters. Over time, that migration burden constrains codec development itself. The question stops being <em>Is this a good compression idea?</em> and becomes <em>Is it worth another format migration?</em> That rigidity limits the cadence of codec evolution and leads to missed compression improvements.</p><h3>The right encoding without configuration</h3><p><code>ES95</code> solves this at the architecture level for time-series indices. Each field's encoding is no longer baked into the format. It is selected automatically at write time, based on what the field mapping already declares: the field's name, its data type, and its metric role. Timestamps are encoded differently than counters. Counters are encoded differently than gauges. <code>ES95</code> encodes all of them, and it chooses the right strategy for each.</p><p>Users already tell Elasticsearch everything the codec needs to know. The mapping describes the data; the codec chooses the compression strategy.</p><p>Compression strategy is a codec concern, not a user concern.</p><p>The alternative would have been to expose per-field encoding selection as a configuration parameter, letting users opt in to better compression for specific fields. That would shift the burden of knowing which encoding fits which data type onto those least equipped to make that call and would guarantee that most deployments never see the benefit. <code>ES95</code> keeps that decision inside the codec, where it belongs. This matters most in managed and serverless deployments, where users expect the system to automatically make optimal storage decisions.</p><h3>The timestamp result nobody planned for</h3><p>With the adaptive architecture in place, the team set out to ship the planned float-compression algorithm. Before it arrived, the architecture proved itself by substantially improving compression for timestamps.</p><p>A time-series index is sorted first by its time-series identifier (<code>_tsid</code> constructed by the metric’s dimensions) and then by timestamp within each series. Timestamps on disk aren’t one smooth sequence; there are many smooth sequences laid end to end, one per series, with a large jump at every boundary where one series ends and the next begins.</p><p>The codec compresses data in fixed-size blocks without regard to those series boundaries. A block straddling a series boundary holds timestamps from two different series. The jump between them breaks monotonicity, reducing delta encoding effectiveness on blocks spanning different time series. A block that would otherwise compress to near-zero bits per value ended up needing nine or more, because bit-packing encodes every value in a block using the same fixed number of bits, so one large jump sets the cost for all of them.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd7576b4b4a9e1c2d/6a85677d1eb9e5448c2c3336/unnamed.png" alt="SplitDelta encoding splits compression blocks at series boundaries, cutting @timestamp storage by 92.4%" /><p>In the ideal case, every block belongs to a single series: timestamps increase at near-constant intervals, delta encoding captures the regularity, and bit-packing compresses the result to near-zero bits per value. With few series, boundary blocks are rare and the overhead barely registers. On an observability cluster ingesting millions of documents across thousands of series, that changes. The cost scales along two dimensions: series count and data density. More series means more boundary events; sparser series means multiple jumps packed into single blocks. In high-churn environments, both compound, and boundary blocks accumulate into a standing tax on the most-read field in any time-series workload. It’s why <code>@timestamp</code> storage grew faster than the data that produced it.</p><p>The fix was to detect series boundaries and treat each run as its own independent sequence. Instead of trying to encode across the jump between two series, which forces every value in the block to pay the storage cost of that one large jump, each run is compressed on its own terms. The boundary simply becomes a seam: the jump is never seen by the encoder on either side.</p><p>That encoding is called <code>SplitDelta</code>. <code>@timestamp</code> and monotonic long counters now use it by default. No format change. No migration. Existing segments retain legacy encoding.</p><p>On the high-cardinality <a href="https://github.com/elastic/rally">Elasticsearch Rally</a> benchmark, that single unplanned encoding cut counters storage by 20%–30% and <code>@timestamp</code> storage by 92.4%, from 1.03 GB to 79 MB. Gigabytes to megabytes, and no, that isn’t a typo.</p><p>The pluggable pipeline had already paid for itself. <code>SplitDelta</code>, which wasn’t part of the original plan, slotted in without a format change or migration before ALP even shipped.</p><h3>ALP: recovering the decimal that was always there</h3><p>Most floating-point metrics can be expressed as short decimals with no loss of accuracy: CPU utilization at <code>23.47</code>, load average at <code>1.15</code>. <a href="https://dl.acm.org/doi/10.1145/3626717">ALP</a> (Adaptive Lossless floating-Point compression) recovers that decimal structure from the floating-point representation, converting values into integers that the existing pipeline already handles well. <code>ES95</code> feeds ALP's output into the same mature integer compression pipeline used for timestamps and counters, extracting additional savings rather than treating ALP as a standalone encoding. Values that don’t fit ALP's model (such as irregular high-precision floats or special values) fall back to direct bit-packing or the original representation without degrading the rest of the block.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt056bdb51022428c9/6a85689f4acc96ce082471b1/unnamed.png" alt="ALP converts floating-point time series metrics to integers for compression through the Elasticsearch encoding pipeline" /><p><code>ALP</code> lets Elasticsearch treat floating-point metrics according to the structure they actually contain rather than the binary representation they happen to use. It’s applied automatically to double-valued gauge fields, selected by field type and metric role, through exactly the door the architecture had built for it.</p><h3>What the numbers say</h3><p>Here’s what the <a href="https://github.com/elastic/rally">Elasticsearch Rally</a> benchmark looks like on a high-cardinality workload containing 2.26 billion data points. Results are from an internal <code>tsdb-metricsgen</code> benchmark.</p><p><strong>Field or metric</strong></p><p><strong>Storage reduction (%)</strong></p><p><code>@timestamp</code></p><p><strong>−92.4%</strong></p><p><code>cpu.load_average.5m</code></p><p><strong>−74.3%</strong></p><p><code>system.cpu.utilization</code></p><p><strong>−63%</strong></p><p><code>memory.utilization</code></p><p><strong>-19%</strong></p><p>Total doc values</p><p><strong>−33.6%</strong></p><p>That overall 33.6% reduction deserves context.</p><p>A time-series index contains more than metrics. Every data point also carries the labels that identify the series: host names, IP addresses, regions, container IDs. Those dimension fields are stored as keywords. <code>ES95</code> doesn’t target dimensions.</p><p>On this benchmark, two dimension fields, <code>host.ip</code> and <code>host.mac</code>, accounted for 44% of doc-values storage after <code>ES95</code> ran. The 33.6% total reflects that mix. The per-field breakdown is the honest picture. Compression for dimension fields is an active area of work.</p><p>The per-field variation is the most convincing result. Some gauges shrank by nearly three quarters, while others moved by less than a fifth. That spread is direct evidence that <code>ES95</code> matches compression to the structure actually present in each field. A fixed encoding treats every field identically and misses most of those wins.</p><h3>Better compression without extra configuration</h3><p>The storage reductions from <code>SplitDelta</code> and <code>ALP</code> are the most visible results of <code>ES95</code>. The more consequential result is the architecture that produced them.</p><p>Before <code>ES95</code>, every new compression technique required a format evolution. That reality shaped which ideas were practical to pursue. Today, new encodings become implementation decisions inside the codec rather than migration projects. Existing data never needs to move, and users gain better compression on newly written data simply by upgrading Elasticsearch. <code>SplitDelta</code> and <code>ALP</code> are the first encodings to benefit from this architecture. They will not be the last.</p><p>Asking users to choose compression algorithms would only duplicate information Elasticsearch already has. There are no per-field compression parameters to tune, and no expert knowledge is required to get good storage efficiency. Different fields get different strategies because <code>ES95</code> understands what kind of data each field contains, not because a user configured it. As the codec evolves, those decisions evolve with it. The API does not.</p><p>In Elasticsearch Serverless, good defaults are part of the product. Users expect the system, not configuration, to make storage decisions. <code>ES95</code> is designed to honor that expectation: encoding that starts right and gets better over time.</p><h3>The compression was always there</h3><p><code>ES95</code> establishes a new standard for how time-series codec evolution works. New encodings become implementation decisions, not migration projects. Users get better compression on newly written data with every Elasticsearch upgrade.</p><p>The compression was already in the data. <code>ES95</code> just removed what was hiding it.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/time-series-database-compression-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/time-series-database-compression-elasticsearch</guid>
    <category><![CDATA[Index Data]]></category>
    <category><![CDATA[Operations]]></category>
    <category><![CDATA[Inside Elastic]]></category>
    <dc:creator><![CDATA[Salvatore Campagna]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt27b1a5308344647f/6a8566caf9838a4c963bea55/unnamed.png" length="0" type="image/png"/>
    <pubDate>Wed, 19 Aug 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Two lines of JSON to replace your ILM policy: data stream lifecycle adds frozen tier support]]></title>
    <description><![CDATA[In Elasticsearch 9.5, frozen_after in data stream lifecycle moves indices to searchable snapshots on object storage on their own, keeping them queryable alongside downsampling and retention.]]></description>
    <content:encoded><![CDATA[<p>Data stream lifecycle in Elasticsearch 9.5 can move backing indices to the frozen tier as searchable snapshots on object storage, with no ILM policy required. Add frozen_after next to <code>data_retention</code> and optional downsampling in a few lines of JSON, or set it in Kibana. The feature is generally available in 9.5.</p><h2>How to configure frozen_after in data stream lifecycle</h2><p><code>frozen_after</code> sits at the top level of the lifecycle, next to <code>data_retention</code> and <code>downsampling</code>.</p>PUT _data_stream/my-data-stream/_lifecycle
{
"data_retention": "90d",
"frozen_after": "30d"
}<p>That's the whole feature, at the API level. Indices in <code>my-data-stream</code> stay on hot for 30 days, then move to frozen for the remaining 60. After 90 days they're deleted, and the backing snapshot goes with them.</p><p>It composes with the rest of the lifecycle, including downsampling:</p>PUT _data_stream/my-data-stream/_lifecycle
{
  "data_retention": "90d",
  "frozen_after": "30d",
  "downsampling": [
    { "after": "1d", "fixed_interval": "1h" }
  ]
}<p>Same options in an index template:</p>PUT _index_template/my-index-template
{
"index_patterns": ["my-data-stream*"],
"data_stream": {},
"template": {
"lifecycle": {
"data_retention": "90d",
"frozen_after": "30d"
}
  }
}<p>The order of values is enforced: <code>frozen_after</code> has to be less than <code>data_retention</code> and greater than any <code>downsampling.after</code>. The API rejects configurations that don't make physical sense.</p><h3>Where frozen tier data is stored: the default snapshot repository</h3><p>Frozen tier data is held as partially-mounted searchable snapshots, which means DLM needs a snapshot repository to write into. Rather than make you choose a repository per lifecycle, 9.5 introduces a <a href="https://www.elastic.co/docs/deploy-manage/tools/snapshot-and-restore/self-managed#snapshot-repo-default">cluster-level default snapshot repository</a>.</p>PUT _cluster/settings
{
  "persistent": {
    "repositories.default_repository": "my-snapshot-repo"
  }
}<p>DLM uses this repository for every frozen tier index in the cluster. On Elastic Cloud Hosted (ECH), the default is pre-populated with <code>found-snapshots</code> so existing clusters work out of the box. You can change it to a repository you control if you'd rather keep your frozen data in a bucket you own (useful if you want object versioning, lifecycle backups to Glacier, or anything else that needs bucket-level access). Wherever you can set <code>frozen_after</code> in Kibana, the UI shows the current default repository inline and links to the place to change it, so you can see where frozen data will be written.</p><p>If the cluster doesn't have a default repository configured, you can still write a lifecycle with <code>frozen_after</code>. The API accepts it but returns a warning:</p>{
  "acknowledged": true,
  "warnings": [
    {
      "message": "No default snapshot repository has been configured. Data will not be moved to the frozen tier until a default snapshot repository is configured."
    }
  ]
}<p>The data stays on hot until a default repository is configured and exists. The same logic applies if the cluster lacks a valid Enterprise license. Errors are visible in the <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-explain-data-lifecycle">data stream lifecycle status API</a> for that stream.</p><h3>Configuring frozen_after in Kibana</h3><p>Kibana in 9.5 lets you set <code>frozen_after</code> and the default snapshot repository from the UI. In <strong>Streams</strong>, the Retention tab shows the frozen phase on the lifecycle timeline alongside hot and any downsampling steps. Click the timeline to open the data lifecycle flyout, set <code>frozen_after</code>, and see the timeline update before you save. <strong>Index Management</strong>'s Data Streams page opens the same flyout.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt569984ebea197a38/6a7c38dff6ab871d74d15bb6/image1.jpg" alt="Kibana Streams UI showing frozen_after set to 30 days in the data stream lifecycle Edit data phases flyout" /><h2>How frozen tier conversion works in data stream lifecycle</h2><p>When a backing index ages past <code>frozen_after</code>, DLM walks through five steps in order:</p><ol><li><p><strong>Clone</strong>. Mark the index read-only and clone it to a zero-replica copy so the original stays available while conversion runs.</p></li><li><p><strong>Force merge</strong>. Merge the clone to a single segment. On completion a cluster state marker is written; duplicate force-merge requests (for instance after a master failover) are deduplicated, so a restart doesn't repeat the work.</p></li><li><p><strong>Snapshot</strong>. Write the merged clone to the default repository, and record the snapshot name in cluster state on success. If a stalled snapshot from a previous attempt is detected, DLM deletes it and re-runs the step.</p></li><li><p><strong>Mount</strong>. Create a partially-mounted searchable snapshot index from the snapshot.</p></li><li><p><strong>Swap and delete</strong>.Once the mounted index's shards are fully allocated, atomically swap it in for the original in the data stream, then delete the original. The swap is atomic, so query results don't see a data volume dip during the transition.</p></li></ol><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt35ff5577f138d9de/6a7c38fb1012c95976266824/image2.png" alt="Data stream lifecycle frozen tier conversion steps: clone, force merge, snapshot, mount, swap and delete" /><p>On failure, DLM retries from the last successful step on the next run.</p><p>Each step is idempotent, and the cluster state markers make sure work already done isn't repeated after a master failover. Throttling caps concurrent conversions so a lifecycle change covering thousands of indices doesn't overwhelm the cluster. Errors surface in the <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-explain-data-lifecycle">data stream lifecycle status API</a> and roll up to the lifecycle health indicator.</p><h2>Scope and limitations of frozen_after</h2><ul><li><p><strong>Data indices only</strong>. <code>frozen_after</code> applies to a data stream's data indices. Failure store indices don't currently support the frozen tier and continue to be governed by their own <code>data_retention</code> setting.</p></li><li><p><strong>Enterprise license required</strong>. Frozen tier in DLM is implemented with searchable snapshots and requires an Enterprise license. You can write <code>frozen_after</code> on any license, but data won't move to frozen until the license is valid.</p></li><li><p><strong>Serverless ignores the field</strong>. In Elastic Cloud Serverless, <code>frozen_after</code> is accepted but ignored - Serverless manages tiering on your behalf. Built-in templates may include the field, so we don't reject it, but the step is skipped.</p></li></ul><h2>Getting started with frozen_after</h2><ol><li><p>In Kibana, open <strong>Streams</strong> or <strong>Index Management</strong> and choose a data stream backed by data stream lifecycle.</p></li><li><p>Open the <strong>Edit data lifecycle</strong> flyout, set a frozen-after value, and save. The lifecycle timeline shows the new phase.</p></li><li><p>On a self-managed cluster, set <code>repositories.default_repository</code> to a repository you control. On ECH, <code>found-snapshots</code> is already configured if you want zero setup.</p></li><li><p>For declarative workflows, write the same configuration into your index templates so new data streams pick up the lifecycle automatically.</p></li></ol><h2>Learn more</h2><ul><li><p><a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/data-stream-lifecycle">Data stream lifecycle</a></p></li><li><p><a href="https://www.elastic.co/docs/manage-data/lifecycle/data-tiers#frozen-tier">Frozen tier overview</a></p></li><li><p><a href="https://www.elastic.co/docs/deploy-manage/tools/snapshot-and-restore/searchable-snapshots">Searchable snapshots</a></p></li><li><p><a href="https://www.elastic.co/docs/solutions/observability/streams/management/retention">Manage data retention for Streams</a></p></li></ul><p><em>The release and timing of any features or functionality described in this post remain at Elastic's sole discretion. Any features or functionality not currently available may not be delivered on time or at all.</em></p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/data-stream-lifecycle-frozen-tier</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/data-stream-lifecycle-frozen-tier</guid>
    <category><![CDATA[Index Data]]></category>
    <category><![CDATA[Operations]]></category>
    <dc:creator><![CDATA[Edward Lewis]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5a5176d6139aa2d7/6a7c38c9be33783da7dadeb9/elastic-de_150810_blogheaderimage_ciscorevolutionizesai_treated_02_V1.jpg" length="0" type="image/jpeg"/>
    <pubDate>Wed, 12 Aug 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[One field, every modality: how Elasticsearch's semantic field indexes and searches images, audio, video and PDFs automatically]]></title>
    <description><![CDATA[The semantic field turns images, audio, video, PDFs and text into multimodal embeddings at ingest time. Describe a scene and find the matching image or use a video frame to surface related clips, all from one Elasticsearch field.]]></description>
    <content:encoded><![CDATA[<p>Multimodal search in Elasticsearch now works the same way text search does: define a field, index your content, and query. The <code>semantic</code> field generates embeddings automatically at ingest time for images, audio, video, and PDFs. Every modality lands in one shared vector space, so you can retrieve an image with a text description, match audio to a phrase, or find a video with a still frame, all from a single field. Available in Elasticsearch 9.5 and serverless as a tech preview.</p><h2>The palette takes shape: how multimodal search in Elasticsearch evolved from semantic_text</h2><p>The <code>semantic</code> field is a convergence of several complementary features we've introduced over the past couple of years, bringing them together to create a cohesive multimodal search experience. Each solved an important piece of the semantic search puzzle on its own; together they enable native multimodal search.</p><p>The first brushstroke was <code>semantic_text</code>. Before it, running semantic search meant manually configuring mappings, wiring up ingest pipelines with an ML model, manually chunking content, and generating query-time embeddings yourself. The <code>semantic_text</code> field folds all of that away: it performs inference automatically at ingest time, chunks long documents for you, and simplifies the queries you write against it. <a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text">Introduced in Elasticsearch 8.15</a> and <a href="https://www.elastic.co/search-labs/blog/elasticsearch-semantic-text-ga">released as GA in Elasticsearch 8.18</a>, it has become the foundation for semantic search on the platform.</p><p>Next came <a href="https://www.elastic.co/search-labs/blog/jina-embeddings-v5-omni-all-media-one-index">the model to power multimodal search</a>. <code>jina-embeddings-v5-omni</code> is our family of multimodal embedding models, capable of embedding text, images, video, audio, and PDFs into a shared vector space. Because those embeddings are semantically compatible across modalities, you can store diverse media in a single index and query across all of it at once, such as retrieving an image via a text description or matching audio against a written phrase, all without maintaining a separate pipeline for each content type. For more detailed information about how these embeddings are generated, see the <a href="https://jina.ai/models/jina-embeddings-v5-omni-small/">model documentation</a>.</p><p>We added the <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-knn-query#query-vector-builders-parameters">embedding query vector builder</a> in Elasticsearch 9.4 to handle multimodal inputs at query time. Query vector builders are general-purpose tools you can use to convert input to a vector at query time as part of your request. For example, we have the <code>text_embedding</code> query vector builder for text-only models and input, and the <code>lookup</code> query vector builder for getting a vector from an existing document. The <code>embedding</code> query vector builder is a new type that works with multimodal models and accepts multimodal input, including text or base64-encoded binaries. This allows you to pose a query in whatever modality fits, and Elasticsearch generates the matching vector on the fly.</p><p>The final piece was multimodal ingest. The <code>semantic_text</code> field brought automatic embedding to text; the <code>semantic</code> field extends that same automatic experience to images, audio, video, and PDFs from ingest through query.</p><h2>Painting the picture: creating an index with the semantic field</h2><p>Let’s create an index with a <code>semantic</code> field. This is as simple as setting the field type to semantic and defining the inference endpoint you want to use:</p>PUT example-index
{
  "mappings": {
    "properties": {
      "my_semantic_field": {
        "type": "semantic",
        "inference_id": ".jina-embeddings-v5-omni-small"
      }
    }
  }
}<p>In this example, we use the .<code>jina-embeddings-v5-omni-small</code> inference endpoint. This is our built-in <code>jina-embeddings-v5-omni</code> inference service, and it is available in all environments with access to the <a href="https://www.elastic.co/docs/explore-analyze/elastic-inference/eis">Elastic Inference Service</a> (EIS). This includes:</p><ul><li><p>Serverless.</p></li><li><p>Elastic Cloud Hosted (ECH).</p></li><li><p>Self-managed with <a href="https://www.elastic.co/docs/deploy-manage/cloud-connect">Cloud Connected Mode</a> (CCM).</p></li></ul><h3>Indexing images, audio, video and PDFs</h3><p>To index an image, provide an object with a <code>type</code> of <code>image</code> and a <code>value</code> containing the image as a base64-encoded <a href="https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data">data URL</a>:</p>PUT example-index/_doc/example_doc_1
{
  "my_semantic_field": {
    "type": "image",
    "value": "data:image/jpeg;base64,&lt;base64-encoded-image-bytes&gt;"
  }
}<p>Arrays of objects are also accepted, allowing you to index multiple images in a single field value:</p>PUT example-index/_doc/example_doc_2
{
  "my_semantic_field": [
    {
      "type": "image",
      "value": "data:image/jpeg;base64,&lt;base64-encoded-image-bytes&gt;"
    },
    {
      "type": "image",
      "value": "data:image/jpeg;base64,&lt;base64-encoded-image-bytes&gt;"
    }
  ]
}<p>The <code>semantic</code> field also supports text values, just like <code>semantic_text</code>. You can provide such values standalone or intermix them with image values:</p>PUT example-index/_doc/example_doc_3
{
  "my_semantic_field": "a cat on a windowsill"                                                                                                                                                                                                                }

PUT example-index/_doc/example_doc_4
{
  "my_semantic_field": [
    "a cat on a windowsill",
    {
      "type": "image",
      "value": "data:image/jpeg;base64,&lt;base64-encoded-image-bytes&gt;"
    },
    "a dog running in a park"
  ]
}<p>Text values are handled just like they are with <code>semantic_text</code>: long passages are chunked according to the <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-field-reference#semantic-params">chunking settings configured on either the inference service or field mapping</a>. Multimodal values, such as images, are not chunked. Each multimodal value is represented as one chunk.</p><p>Other modalities are supported as well. Change the type value to match your content’s modality. Currently we support:</p><ul><li><p><code>image</code></p></li><li><p><code>audio</code></p></li><li><p><code>video</code></p></li><li><p><code>pdf</code></p></li></ul><p>For example, to index a video, the request would look like:</p>PUT example-index/_doc/example_doc_5
{
  "my_semantic_field": {
    "type": "video",
    "value": "data:video/mp4;base64,&lt;base64-encoded-video-bytes&gt;"
  }
}<p></p><h3>Image search and cross-modal retrieval with a text query</h3><p>To find multimodal content using a text description, run a <code>match</code> query on the <code>semantic</code> field:</p>GET example-index/_search
{
  "query": {
    "match": {
      "my_semantic_field": "a cat on a windowsill"
    }
  }
}<p>Just like with <code>semantic_text</code>, Elasticsearch automatically generates an embedding for the query text using the inference endpoint associated with the field. That query embedding is used to return semantically similar matches.</p><p>This query pattern enables easy text-to-image search. Just index an image and use a <code>match</code> query to retrieve it via text description! It also works for any other modality: index the multimodal input and search by description to retrieve it.</p><h3>Querying with images, video, and other multimodal inputs</h3><p>We can also search using a multimodal input by using the <code>knn</code> query with an <code>embedding</code> query vector builder. For example, we can search using an image:</p>GET example-index/_search
{
  "query": {
    "knn": {
      "field": "my_semantic_field",
      "query_vector_builder": {
        "embedding": {
          "input": {
            "type": "image",
            "value": "data:image/jpeg;base64,&lt;base64-encoded-image-bytes&gt;"
          }
        }
      }
    }
  }
}<p>The <code>input</code> object format is the same as when providing an image to index: set the <code>type</code> to <code>image</code> and <code>value</code> to a base64-encoded data URL.</p><p>Similar to when querying by text description, Elasticsearch automatically generates an embedding for the query image using the inference endpoint associated with the field. That query embedding is used to return semantically similar matches.</p><p>Just like with indexing, other modalities are supported, but are limited to those supported by your inference endpoint. For example, a search using a video clip would look like:</p>GET example-index/_search
{
  "query": {
    "knn": {
      "field": "my_semantic_field",
      "query_vector_builder": {
        "embedding": {
          "input": {
            "type": "video",
            "value": "data:video/mp4;base64,&lt;base64-encoded-video-bytes&gt;"
          }
        }
      }
    }
  }
}<h2>Extending the composition: highlighting, retrievers, and other semantic field features</h2><p>The <code>semantic</code> field didn't start from a blank canvas. It's built on the same foundation as <code>semantic_text</code>, inheriting its behavior and its ergonomics, and extending them to multimodal content. In practice, that means nearly everything you already know about working with <code>semantic_text</code> carries over unchanged. If you've built with <code>semantic_text</code> before, the <code>semantic</code> field will feel immediately familiar.</p><p>Here’s a selection of the features that come along for the ride. See <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-field">the documentation</a> for a complete list.</p><h3>Highlighting the best-matching chunks</h3><p>If you index multiple values in a <code>semantic</code> field, you may want to know <em>which</em> value best matches the query. The <code>semantic</code> highlighter can be used to return the most relevant chunks as highlight fragments:</p>GET example-index/_search
{
  "query": {
    "match": {
      "my_semantic_field": "a cat on a windowsill"
    }
  },
  "highlight": {
    "fields": {
      "my_semantic_field": {
        "number_of_fragments": 2,
        "order": "score"
      }
    }
  }
}<p>Setting <code>order</code> to <code>score</code> returns the fragments ranked by relevance, while <code>number_of_fragments</code> caps how many chunks come back. The response looks like:</p>{
  "hits": {
    "hits": [
      {
        "_index": "example-index",
        "_id": "example_doc_4",
        "_source": {...},
        "highlight": {
          "my_semantic_field": [
            "a cat on a windowsill",
            "data:image/jpeg;base64,&lt;base64-encoded-image-bytes&gt;"
          ]
        }
      }
    ]
  }
}<p>Note how highlighted multimodal values are represented using their data URLs.</p><h3>Controlling vector quantisation with index options</h3><p>The <code>semantic</code> field stores its embeddings in an underlying vector field, and <code>index_options</code> lets you control how that vector field is indexed. For example, choosing a non-default quantization strategy:</p>PUT example-index
{
  "mappings": {
    "properties": {
      "my_semantic_field": {
        "type": "semantic",
        "inference_id": ".jina-embeddings-v5-omni-small",
        "index_options": {
          "dense_vector": {
            "type": "int8_hnsw"
          }
        }
      }
    }
  }
}<h3>Multi-field retrievers</h3><p>The <code>semantic</code> field participates in the <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrievers">multi-field query format</a> supported by the <code>linear</code> and <code>rrf</code> retrievers. Rather than hand-writing an inner retriever per field, you supply a single <code>query</code> and a list of <code>fields</code>, mixing lexical fields and semantic fields freely:</p>GET example-index/_search
{
  "retriever": {
    "linear": {
      "query": "a cat on a windowsill",
      "fields": ["title", "my_semantic_field"],
      "normalizer": "minmax"
    }
  }
}<p>The retriever automatically separates lexical fields from semantic fields, queries each group, and normalizes the results so that each group contributes equally to the final ranking, preventing lexical matches from drowning out semantic ones.</p><h3>Cross-cluster search</h3><p>The <code>semantic</code> field supports <a href="https://www.elastic.co/docs/solutions/search/cross-cluster-search">cross-cluster search (CCS)</a>, enabling use of the field in large, multi-cluster deployments. Simply list the indices to query using the standard <code>&lt;cluster&gt;:&lt;index&gt;</code> format:</p>GET example-index,remote-cluster:remote-index/_search
{
  "query": {
    "match": {
      "my_semantic_field": "a cat on a windowsill"
    }
  }
}<p>The fields queried across indices and clusters can use a mix of different inference endpoints that produce different query embeddings. The search request will automatically apply the proper query embedding to each individual field queried.</p><h2>Off the easel, into the world: optimising multimodal embeddings for production</h2><p>When you move multimodal search from experiment to production, the size of your multimodal inputs becomes a practical concern. Multimodal data is supplied as base64-encoded data URLs, and that data is stored in the index. Those strings can grow large in a hurry: a single high-resolution file can balloon into several megabytes of encoded text, which has several side effects:</p><ul><li><p>The index size on disk can increase significantly.</p></li><li><p>Requests and responses containing multimodal data are larger, increasing transmission time and ingress/egress costs.</p></li><li><p>Inference on larger multimodal inputs is slower.</p></li></ul><p>The good news is that you don’t need that much fidelity. Multimodal embedding models reduce each input to a compact representation before generating a vector anyway, so a smaller, lower-fidelity version of a multimodal input (such as a downscaled image or a lower-bitrate audio clip) produces a very similar embedding, and similar search quality, to its full-size original. This also applies to PDF input. PDFs are generally processed visually by multimodal models, so the quality only needs to be good enough to perform operations like image embedding and OCR. Long PDFs should be broken up into chunks of smaller inputs, so the embeddings generated more accurately represent each chunk. Feeding the model small inputs keeps your documents lean, trims index and response sizes, and speeds up ingestion, all without meaningfully affecting relevance. </p><p>Elasticsearch reinforces this practice with a guardrail: the <code>indices.inference.max_binary_input_size</code> cluster setting caps the size of each binary input, defaulting to 1 MB. Any individual value that exceeds the limit is rejected with a clear error, so oversized inputs surface as an actionable problem at index time rather than as silent bloat. This setting is adjustable in self-hosted and ECH through the <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-cluster-put-settings">cluster settings API</a>. It is not adjustable in our serverless offering, where 1 MB is the hard limit for binary sizes.</p><p>When possible, it is also advised to use <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrieve-selected-fields#source-filtering">source filtering</a> to exclude <code>semantic</code> fields from responses. For example:</p>GET example-index/_search
{ 
  "_source": {
    "excludes": ["my_semantic_field"]
  },
  "query": {
    "match": {
      "my_semantic_field": "a cat on a windowsill"
    }
  }
}<p>This makes responses smaller, more performant, and easier to parse because multimodal data is not returned with each.</p><h2>Try out the semantic field</h2><p>The <code>semantic</code> field is available in Elasticsearch 9.5 and Serverless. <a href="https://cloud.elastic.co/registration?onboarding_token=search&amp;cta=cloudregistration&amp;tech=trial&amp;plcmt=cross%20module&amp;pg=search-labs">Start a free trial</a> and try it out today.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/semantic-field-multimodal-search-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/semantic-field-multimodal-search-elasticsearch</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[Index Data]]></category>
    <category><![CDATA[Jina AI]]></category>
    <category><![CDATA[Mappings]]></category>
    <dc:creator><![CDATA[Mike Pellegrini]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltac28de8857eafbc4/6a6f090aca9a724b3c614914/image1.png" length="0" type="image/png"/>
    <pubDate>Mon, 03 Aug 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elastic Connectors: Performance impact of incremental syncs]]></title>
    <description><![CDATA[Learn about full sync and incremental sync for connectors. Discover how incremental sync can boost the performance of Elastic connectors.]]></description>
    <content:encoded><![CDATA[<h2>Elastic Connectors overview</h2><p><a href="https://www.elastic.co/guide/en/enterprise-search/current/connectors.html">Elastic Connectors</a> are a type of Elastic integrations that sync data from an original data source to an Elasticsearch index. Connectors enable you to create searchable, read-only replicas of your data sources.</p><p>There are a number of connectors that are supported for variety of 3rd-parties, such as:</p><ul><li><p><a href="https://www.elastic.co/guide/en/enterprise-search/current/connectors-mongodb.html">MongoDB</a></p></li><li><p>Various SQL DBMS such as <a href="https://www.elastic.co/guide/en/enterprise-search/current/connectors-mysql.html">MySQL</a>, <a href="https://www.elastic.co/guide/en/enterprise-search/current/connectors-postgresql.html">PostgreSQL</a>, <a href="https://www.elastic.co/guide/en/enterprise-search/current/connectors-ms-sql.html">MSSQL</a> and <a href="https://www.elastic.co/guide/en/enterprise-search/current/connectors-oracle.html">OracleDB</a></p></li><li><p><a href="https://www.elastic.co/guide/en/enterprise-search/current/connectors-sharepoint-online.html">Sharepoint Online</a></p></li><li><p><a href="https://www.elastic.co/guide/en/enterprise-search/current/connectors-s3.html">Amazon S3</a></p></li><li><p>And many more. The full list is available <a href="https://www.elastic.co/guide/en/enterprise-search/current/connectors.html#connectors-build">here</a>.</p></li></ul><h2>Connectors content synchronization jobs</h2><p>Connectors support two types of content synchronization jobs: full syncs and incremental syncs.</p><h3>1. Full syncs</h3><p>Full sync is a sync that extracts all desired documents from a 3rd-party service and ingests them into Elasticsearch. So if you've set up your Network Drive connector to ingest all documents from a folder "\Documents/Reports\2022**.docx", during a full sync the connector will fetch all the documents that match this criteria and send all of them to Elasticsearch. Simplified pseudocode for this would look like:</p>connector = NetworkDriveConnector(
  host="192.168.0.105",
  path="\\Documents\Reports\2025\**.doc"
)

for incoming_document_metadata in connector.extract_documents():
    content = connector.download(incoming_document_metadata)

    document = {
      "id": incoming_document_metadata["id"]
      "content": content
    }

    elasticsearch.ingest(document)
<p>This works well until the sync starts to take too long. This could happen because the connector fetches more data than needed. For instance, why fetch old files that have not changed and send them to Elasticsearch? One could argue that the metadata for files could be unreliable, so all files need to be fetched again and sent to Elasticsearch. Indeed, that could be the case, but if we can trust the metadata of the data fetched from 3rd-party, we can ingest less data. Incremental sync is the way to do so.</p><h3>2. Incremental syncs</h3><p>Most of the time, if written well, connector spends doing IO operations. Returning to the example code there are 3 places where IO happens:</p>connector = NetworkDriveConnector(
  host="192.168.0.105",
  path="\\Documents\Reports\2025\**.doc"
)

# Place #1: reading document metadata from 3rd-party system
for incoming_document_metadata in connector.extract_documents():
    # Place #2: reading document content from 3rd-party system
    content = connector.download(incoming_document_metadata)

    document = {
      "id": incoming_document_metadata["id"]
      "content": content
    }

    # Place #3: ingesting the resulting document into Elasticsearch
    elasticsearch.ingest(document)
<p>Each of these places can become a bottleneck and take a significant amount of time during the sync.</p><p>Here's where incremental sync comes into play. Its purpose is to decrease the amount of IO on any of the stages, if possible.</p><h2>Potential optimizations for incremental sync</h2><h3>Fetch fewer documents from 3rd-party systems</h3><p>Modifying the example above, the code could look like this:</p>connector = NetworkDriveConnector(
  host="192.168.0.105",
  path="\\Documents\Reports\2025\**.doc"
)

# We can store last sync time somewhere
last_sync_time = connector.fetch_last_sync_time()

# And later use it querying Network Drive
for incoming_document_metadata in connector.extract_documents(from=last_sync_time):
    content = connector.download(incoming_document_metadata)

    document = {
      "id": incoming_document_metadata["id"]
      "content": content
    }

    elasticsearch.ingest(document)
<p>In cases where only a small number of documents change in our 3rd-party system, we can speed up the ingestion process significantly. However, for Network Drive it's not possible - its API does not support filtering documents by metadata. We won't be able to avoid scanning through the full content of Network Drive.</p><h3>Skip download of content of files that haven't changed since previous sync</h3><p>Downloading file content takes a significant amount of time in the syncs. If files are reasonably large, the connection is unstable or throughput is low, downloading the content of files would take most of the time when syncing the content from the 3rd-party. If we skip downloading some of them, it can already significantly speed up the connector.</p><p>Consider the following example pseudocode:</p>connector = NetworkDriveConnector(
  host="192.168.0.105",
  path="\\Documents\Reports\2025\**.doc"
)

last_sync_time = connector.fetch_last_sync_time()

for incoming_document_metadata in connector.extract_documents():
    # If document timestamp did not change then not fetching
    # document content can save us a lot of time
    if incoming_document_metadata["last_updated_at"] &gt; last_sync_time
        content = connector.download(incoming_document_metadata)

    document = {
      "id": incoming_document_metadata["id"]
      "content": content
    }

    elasticsearch.ingest(document)
<p>If no documents were updated, the sync will actually be magnitudes faster than fully syncing the content.</p><h3>Skip ingestion of non-modified documents into Elasticsearch</h3><p>While it may seem minor, ingestion of data into Elasticsearch takes a significant amount of time - albeit normally less than downloading the content from the 3rd-party system. We can start storing timestamps per each document and not send the documents into Elasticsearch if their timestamp did not change.</p><p>We can combine this approach with the previous approach to save the most time possible during the sync.</p>connector = NetworkDriveConnector(
  host="192.168.0.105",
  path="\\Documents\Reports\2025\**.doc"
)

# We need to fetch only IDs and timestamps as it's sufficient to make a decision.
# For large indices it can still take a good amount of RAM, but that's the price.
existing_documents = connector.fetch_existing_documents(fields=["id", "_timestamp"])

for incoming_document_metadata in connector.extract_documents():
    existing_document_metadata = existing_documents[document_metadata["id"]]
    
    # If a document for this 3rd-party record exists in Elasticsearch index
    # and timestamp did not change, then skip downloading its content
    # and skip ingesting the document
    if existing_document_metadata:
        incoming_document_timestamp = incoming_document_metadata["last_updated_at"]
        existing_document_timestamp = existing_document_metadata["_timestamp"]

        if incoming_document_timestamp == existing_document_timestamp:
            # Skip the document for good
            continue;

    content = connector.download(incoming_document_metadata)

    document = {
      "id": incoming_document_metadata["id"]
      "content": content,
      "_timestamp" = incoming_document_metadata["last_updated_at"]
    }

    elasticsearch.ingest(document)
<p>This approach helps save even more time when running a sync. Now let's take a look into performance considerations for such improvements.</p><h2>Measuring incremental sync performance</h2><p>Now since we've taken a look into simplified code that shows how incremental syncs can work, we can try to estimate potential performance improvements.</p><p>For some connectors, incremental sync is implemented in a certain manner that optimizes the way data is fetched from a 3rd-party. For example, the Sharepoint Online connector fetches some data via delta API - only collecting documents that changed after the last sync. This improves performance in an obvious manner - less data -&gt; less time to sync the data to latest.</p><p>For other connectors (currently all connectors except Sharepoint Online connector) incremental sync is done by framework in a generic way which was described in one of previous sections <a href="https://www.elastic.co/search-labs/blog/elastic-connectors-performance-impact-of-incremental-syncs#skip-ingestion-of-non-modified-documents-into-elasticsearch">"Skip ingestion of non-modified documents into Elasticsearch"</a>.</p><p>Connectors still collect all the data from 3rd-party data source (as it does not provide a way to fetch only the changed records). However if this data contains timestamps, the connector framework compares document IDs and timestamps of already ingested documents with incoming documents. If the document exists in Elasticsearch with the same timestamp that was received from the 3rd-party data source, then this document will not be sent to Elasticsearch.</p><p>We've described abstract approach for the performance improvements with incremental syncs, but we already have these implemented in connectors, so let's dive into real numbers!</p><h3>Performance tests</h3><p>We will estimate the rough magnitude of improvement for incremental syncs with these performance tests, not aiming at high precision.</p><p>The two connectors chosen for this test, Google Drive and Github, were chosen because they have different IO profiles.</p><p>Google Drive acts like a file storage. It:</p><ul><li><p>Has a fast API that does not throttle too soon</p></li><li><p>Normally stores a lot of binary content of variable size - from small to really large</p></li><li><p>Normally stores a small number of records - tens or hundreds of thousands rather than millions</p></li></ul><p>GitHub data is ingested via a more of a classic API, that:</p><ul><li><p>Throttles quite often</p></li><li><p>Contains many records that are much smaller than those in Google Drive</p></li><li><p>Does not send binary content at all</p></li></ul><p>Due to these differences, the incremental sync performance will majorly differ.</p><p>Both tests will contain these mandatory steps:</p><ol><li><p>Do a full sync against a 3rd-party system</p></li><li><p>Modify some documents on the 3rd-party system</p></li><li><p>Run an incremental sync and check the amount of time it takes</p></li></ol><p>This setup is very bare bones but will already give a good indication of the magnitude of performance improvement. Both tests will be slightly different and I will provide results with commentary in the next section.</p><h4>Setup #1 - Google Drive Connector</h4><p>Initial setup will be:</p><ul><li><p>1 folder is on Google Drive with 1553 files (100 of them are 2MB in size, 1443 are 5KB in size)</p></li><li><p>A full sync is executed and this data gets into Elasticsearch</p></li><li><p>More files are added into Google Drive to make it 10144 files (100 of them are 2MB in size, all the rest are 5KB in size)</p></li><li><p>Incremental sync is executed again to pull the new data</p></li><li><p>Then some minor changes are made to files on Google Drive (1 added, 2 deleted)</p></li><li><p>Incremental sync is executed again</p></li><li><p>Full sync is executed to compare the run time against incremental sync again</p></li></ul><p>The following table contains the results of the described test with commentary:</p><p>Sync Description</p><p>Run time</p><p>Documents Added</p><p>Documents Deleted</p><p>Comment</p><p>Initial Full Sync</p><p>0h 4m 0s</p><p>1553</p><p>0</p><p>This is initial sync - it pulls all documents</p><p>Incremental Sync after more data was added to Google Drive</p><p>0h 20m 9s</p><p>7939</p><p>0</p><p>Run time was high as expected - a lot of documents went in</p><p>Incremental Sync after some data was slightly changed in Google Drive</p><p>0h 1m 25s</p><p>1</p><p>2</p><p>Run was very fast. It still called Google Drive API a lot, but did not have to ingest 200+MB of data into Elasticsearch</p><p>Full Sync to compare performance</p><p>0h 23m 23s</p><p>10144</p><p>0</p><p>As expected, it takes a lot of time - all the data is downloaded from Google Drive and is sent to Elasticsearch, even if it did not change. We can assume that it takes 22 minutes to download and then upload the data into Elasticsearch for the setup</p><p>In summary, incremental sync significantly improved the performance of the connector because most of the time is spent on the connector downloading the content of the files and sending this content to Elasticsearch. Full sync brings 2 * 100 + 1443 * 5 / 1024 = 207MB of content - both downloaded by connector and ingested into Elasticsearch. If only 1 large file is changed, this amount changes to only 2MB - a magnitude of 100 change. This explains the performance improvement well.</p><h4>Setup #2: GitHub connector</h4><p>The GitHub connector is very different since the actual volume of data it syncs is relatively small - issues, pull requests and such are reasonably small, while there are lots of them. Additionally, GitHub has strict throttling policies and throttles connector a lot.</p><p>To give a good real-world example we’ll use the Kibana Github repository with the GitHub connector and observe its performance.</p><p>Sync Description</p><p>Run time</p><p>Documents Added</p><p>Documents Deleted</p><p>Comment</p><p>Initial Full Sync</p><p>8h 40m 1s</p><p>147421</p><p>0</p><p>---</p><p>Incremental Sync ran immediately after</p><p>9h 6m 7s</p><p>59</p><p>0</p><p>This sync took even more time to run, mostly because it was constantly throttled. Connector had to fetch all the data from GitHub but sent only 59 records with a total volume of less than 1MB</p><p>Next incremental sync</p><p>9h 2m 52s</p><p>191</p><p>1</p><p>This sync was triggered immediately after previous incremental sync finished. Run time is the same due to data being almost the same and throttling being a major factor in the connector run time</p><h3>Key takeaways</h3><ul><li><p>As you can see, there is no performance improvement for incremental sync for the Github connector - there is barely any space for optimization as most of the time is spent by the connector querying the system and waiting for the throttling to stop.</p></li><li><p>Documents that are extracted are reasonably small, so network throughput usage is minimal. To improve the connector run time, the incremental sync would actually have to limit the number of queries to Github, but at this point it's not implemented in the connector.</p></li></ul><h2>Summary</h2><p>What is the primary factor that impacts the performance of incremental sync? In simplified terms, it's the raw volume of data that is ingested.</p><p>For Sharepoint Online connector there is a special logic to fetch less data via the <a href="https://learn.microsoft.com/en-us/graph/api/driveitem-delta?view=graph-rest-1.0&amp;tabs=http">delta API</a>. This saves good amount of time because the delta API allows connectors to not fetch files that were not changed. Files tend to be large, thus not downloading and ingesting them will save a lot of time.</p><p>For other connectors, incremental sync is generic - it just checks document timestamps before ingesting them to Elasticsearch - if this document is already in the index and the timestamp did not change, then it is not ingested. It saves less time than the previous approach that Sharepoint Online employs but works generically for all connectors. Some connectors - ones that contain large documents - benefit from this logic a lot, while others - that get throttled by a 3rd-party system and contain relatively small documents - get no benefit from incremental syncs.</p><p>Additionally, if Elasticsearch is under heavy load, incremental sync is less likely to be throttled by Elasticsearch, thus making it more performant under load.</p><p>Let's look at the following graph:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc3a8743a67731c2f/6a171206dc55de4ca7e00f0d/b027636b0c73487b828a6c0390c808cc7da44420-1440x467.png" alt="" /><p>In the graph you can see how much time each part of content extraction and ingestion takes on the timeline. In the example above the connector is spending the most time on ingesting the data, even pausing for extraction and content download. In this case incremental sync has a potential of improving the run time of the sync by 30-40%.</p><p>Let's look at another example - a system that has throttling and low throughput, but stores very little data in Elasticsearch (Sharepoint Online, GitHub, Jira, Confluence):</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbaf686e760c07694/6a17120766c4f9a936f8c14b/c29df18ba0b125df44bf8e5895d910da85f9a585-1440x467.png" alt="" /><p>This system will not benefit from generic incremental syncs a lot - most of the time is spent extracting content from the 3rd-party system.</p><p>And the last example - fast and accessible system that stores huge amounts of data in Elasticsearch (Google Drive, Box, OneDrive, Network Drive):</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd2fc28e350c89c02/6a1712092b835fa96ff4b333/524f5a5c1a547dc59d65fcdcb591a432dd8dfa11-1440x467.png" alt="" /><p>If there aren't too many items that change in such a system between syncs, this system will benefit a lot from generic incremental syncs.</p><p>Currently connectors that potentially get the most of incremental sync are:</p><ul><li><p>Azure Blob Storage</p></li><li><p>Box</p></li><li><p>Dropbox</p></li><li><p>Google Cloud Storage</p></li><li><p>Google Drive</p></li><li><p>Network Drive</p></li><li><p>OneDrive</p></li><li><p>S3</p></li><li><p>Sharepoint Online</p></li></ul><p>Other connectors will benefit less from incremental syncs, or will not benefit at all, but there's no one-size-fits-all answer here. Performance heavily depends on the profile of data ingested. The bigger each individual document is, the bigger the benefit.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elastic-connectors-performance-impact-of-incremental-syncs</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elastic-connectors-performance-impact-of-incremental-syncs</guid>
    <category><![CDATA[Index Data]]></category>
    <dc:creator><![CDATA[Artem Shelkovnikov]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc53d5b63416b4a59/6a17120ab0367df6a672be40/c5f9995397d0425d6e66399d4818a259bdeacc40-1280x611.jpg" length="0" type="image/jpeg"/>
    <pubDate>Mon, 27 May 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[How to detect which index template Elasticsearch will use before an index creation]]></title>
    <description><![CDATA[Learn about Elasticsearch index templates and how to detect which index template Elasticsearch will use before creating the index itself.]]></description>
    <content:encoded><![CDATA[<h2>Overview</h2><p>Elasticsearch offers two types of index templates: <code>legacy</code> and <code>composable</code>. Composable templates introduced in Elasticsearch 7.8 that are set to replace legacy templates, both can still be used in Elasticsearch 8.</p><p>This article explores the differences between these templates and how they interact. In particular, we will focus on how you can detect which template will be used when you are creating an index. Let's get started by looking at how to create the different types of index templates.</p><h2>Index templates in Elasticsearch</h2><p>Legacy templates can be created using the following API:</p>PUT _template/t1
{
  "order": 1,
  "index_patterns": [...],
  "mappings": {...},
  "settings": {...},
  "alias": {...}
}
<p>Composable templates can be created using this API:</p>PUT _index_template/ct1
{
  "priority": 1,
  "index_patterns": [...],
  "template": {
    "mappings": {...},
    "settings": {...},
    "alias": {...}
  }
}
<p>Component templates are a third type, which are typically used for managing multiple templates with similar structures. For example, if you need to create hundreds of templates with similar structures, you can create a component template with the common settings, mappings, and aliases, and then include it in your index templates. Component templates can be created using this API:</p>PUT _component_template/template_1
{
  "template": {
    "mappings": {...},
    "settings": {...},
    "alias": {...}
  }
}
<h3>Important!</h3><strong>When both legacy and composable templates exist and they match with the same index pattern, the legacy template will be ignored.</strong> If two composable templates point to the same index pattern, the template with the highest priority will be used. If two legacy templates point to the same index pattern, the templates are merged, with higher-order templates overriding lower-order ones. If the order is the same, the templates are sorted by name and merged accordingly.<h2>Determining which template an index will use when it is created</h2><p>To determine which template an index will use upon creation, you can use the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-simulate-index.html"><code>_simulate_index</code></a> API. This API will return the template that will be used, along with any overlapping templates. However, if no composable templates are present, the API will return an empty body. In that case, you can create a dummy index and check the logs of the elected master node to determine which template will be used.</p><h2>What happens if you have both legacy templates and composable templates?</h2><p>As noted above, if you have both legacy and composable templates, the legacy template will be ignored as if it did not exist.</p>PUT _template/t1
{
  "index_patterns": [
    "test_index-*"
  ],
  "mappings": {
    "properties": {
      "field_1": {
        "type": "integer"
      },
      "field_2": {
        "type": "integer"
      }
    }
  }
}
<p>In such a case, you would get a warning message like the following when you run the command:</p>legacy template [t1] has index patterns [test_index-<em>] matching patterns from existing composable templates [ct1] with patterns (ct1 =&gt; [test_index-</em>]); this template [t1] may be ignored in favor of a composable template at index creation timePUT _index_template/ct1
{
  "index_patterns": [
    "test_index-*"
  ],
  "template": {
    "mappings": {
      "properties": {
        "field_1": {
          "type": "integer"
        }
      }
    },
    "settings": {
      "number_of_shards": 1,
      "number_of_replicas": 0
    }
  }
}
<p>If a newly created composable template matches an existing legacy template with the same or includes an index pattern you will get a warning message like the following:</p>index template [ct1] has index patterns [test_index-<em>] matching patterns from existing older templates [t1] with patterns (t1 =&gt; [test_index-</em>]); this template [ct1] will take precedence during new index creationPOST _index_template/_simulate_index/test_index-1
#response:
{
  "template": {
    "settings": {
      "index": {
        "number_of_shards": "1",
        "number_of_replicas": "0",
        "routing": {
          "allocation": {
            "include": {
              "_tier_preference": "data_content"
            }
          }
        }
      }
    },
    "mappings": {
      "properties": {
        "field_1": {
          "type": "integer"
        }
      }
    },
    "aliases": {}
  },
  "overlapping": [
    {
      "name": "t1",
      "index_patterns": [
        "test_index-*"
      ]
    }
  ]
}
<p>Use this command if you want to test it:</p>PUT test_index-1
GET test_index-1
<h3>Notes from real life scenario</h3><p>Conflicts can be annoying, and they can crash the application. Imagine that you have <code>logstash-dev-*</code>, <code>logstash-prd-*</code>, <code>logstash-stg-*</code> legacy templates that all working fine. If someone adds a single composable template that include index pattern like a <code>logstash-*</code> all legacy templates will be ignored, the fields types can be change and finally it can break the application. Because of that, it’s recommended to switch from legacy to composable templates if you are using Elasticsearch 7 and onwards.</p><p>Another good point to keep in mind is that if you run the Logstash in Elasticsearch 8 or higher, Logstash will add it's template as composable template by default. Because <a href="https://www.elastic.co/guide/en/logstash/current/plugins-outputs-elasticsearch.html#plugins-outputs-elasticsearch-manage_template"><code>manage_template</code></a> is set to <code>true</code> by default and Logstash <a href="https://www.elastic.co/guide/en/logstash/current/plugins-outputs-elasticsearch.html#plugins-outputs-elasticsearch-template_api"><code>template_api</code></a> is set to<code>composable</code> for Elasticsearch 8 and onwards. It will create a Logstash composable template with <code>logstash-*</code> index pattern if the composable template does not exist. Yes, it will ignore all legacy templates covering <code>logstash-*</code> and overlap them.</p><p>Template Overlapping</p><h2>1. What happens if you have two composable templates that point to the same index pattern?</h2><p>As previously mentioned, if you have two composable templates that point to the same index pattern, the composable template with the highest priority will take precedence.</p>PUT _index_template/ct1
{
  "priority": 0,
  "index_patterns": [
    "test_index-*"
  ],
  "template": {
    "mappings": {
      "properties": {
        "field_1": {
          "type": "integer"
        }
      }
    },
    "settings": {
      "number_of_shards": 1,
      "number_of_replicas": 0
    }
  }
}
PUT _index_template/ct2
{
  "priority": 1,
  "index_patterns": [
    "test_index-*"
  ],
  "template": {
    "mappings": {
      "properties": {
        "field_1": {
          "type": "keyword"
        },
        "field_2": {
          "type": "integer"
        }
      }
    },
    "settings": {
      "number_of_shards": 2,
      "number_of_replicas": 0
    }
  }
}
POST _index_template/_simulate_index/test_index-1
#response:
{
  "template": {
    "settings": {
      "index": {
        "number_of_shards": "2",
        "number_of_replicas": "0",
        "routing": {
          "allocation": {
            "include": {
              "_tier_preference": "data_content"
            }
          }
        }
      }
    },
    "mappings": {
      "properties": {
        "field_1": {
          "type": "keyword"
        },
        "field_2": {
          "type": "integer"
        }
      }
    },
    "aliases": {}
  },
  "overlapping": [
    {
      "name": "ct1",
      "index_patterns": [
        "test_index-*"
      ]
    }
  ]
}
<p>In this example, you have two templates—ct1 and ct2—both targeting the same index pattern test_index-<em>. However, ct2 has a higher priority (1) than ct1 (0). Therefore, when you create an index that matches the pattern test_index-</em>, the settings and mappings defined in ct2 will be applied before ct1. If there are the same settings in the ct1 and ct2 templates, the ct2 template will overwrite.</p><h2>2. What happens if you have two legacy templates that point to the same index pattern?</h2><p>As highlighted above, if you have multiple templates that point to the same index pattern, the templates with lower-order values are merged first. Templates with higher-order values are merged later, overriding templates with lower values.</p><p>If two legacy templates have the same order value, they will be sorted by name. For example, in a case with [t2, t1], t1 would be merged first, t2 would be merged later, and t2 would override t1 if there are any same mapping/settings/aliases.</p>PUT _template/t1
{
  "index_patterns": ["test_index-*"],
  "mappings": {
    "properties": {
      "field_1": {
        "type": "integer"
      }
    }
  },
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0
  }
}
PUT _template/t2
{
  "index_patterns": [
    "test_index-*"
  ],
  "mappings": {
    "properties": {
      "field_1": {
        "type": "geo_point"
      },
      "field_2": {
        "type": "long"
      }
    }
  },
  "settings": {
    "number_of_shards": 2,
    "number_of_replicas": 0
  }
}
POST _index_template/_simulate_index/test_index-1
#response
{}
<p>Unfortunately, if you don't have composable templates, this API call responds with an empty body. So how you can check that?</p><p>The answer is to create a dummy index and check the Elasticsearch elected-master logs.</p>PUT test_index-test
2023-11-14 14:14:27 {"@timestamp":"2023-11-14T11:14:27.535Z", "log.level": "WARN",  "data_stream.dataset":"deprecation.elasticsearch","data_stream.namespace":"default","data_stream.type":"logs","elasticsearch.event.category":"templates","event.code":"index_template_multiple_match","message":"index [test_index-1] matches multiple legacy templates [t1, t2], composable templates will only match a single template" , "ecs.version": "1.2.0","service.name":"ES_ECS","event.dataset":"deprecation.elasticsearch","process.thread.name":"elasticsearch[elasticsearch][masterService#updateTask][T#3]","log.logger":"org.elasticsearch.deprecation.cluster.metadata.MetadataCreateIndexService","trace.id":"85e0a432ec11e2f2d3c7883f510376ac","elasticsearch.cluster.uuid":"Jc-a46VUSjOwuxWmbnSDZQ","elasticsearch.node.id":"MTX1x5-OTlWhiGa9lwUJPw","elasticsearch.node.name":"elasticsearch","elasticsearch.cluster.name":"elasticsearch-cluster1"}

2023-11-14 14:14:27 {"@timestamp":"2023-11-14T11:14:27.605Z", "log.level": "INFO", "message":"[test_index-1] creating index, cause [api], templates [t2, t1], shards [2]/[0]", "ecs.version": "1.2.0","service.name":"ES_ECS","event.dataset":"elasticsearch.server","process.thread.name":"elasticsearch[elasticsearch][masterService#updateTask][T#3]","log.logger":"org.elasticsearch.cluster.metadata.MetadataCreateIndexService","trace.id":"85e0a432ec11e2f2d3c7883f510376ac","elasticsearch.cluster.uuid":"Jc-a46VUSjOwuxWmbnSDZQ","elasticsearch.node.id":"MTX1x5-OTlWhiGa9lwUJPw","elasticsearch.node.name":"elasticsearch","elasticsearch.cluster.name":"elasticsearch-cluster1"}
<p>From the logs, we can see that "[test_index-1] creating index, cause [api], templates [t2, t1]".</p>GET _cat/templates/t*?v
name index_patterns order version composed_of
t2   [test_index-*] 0
t1   [test_index-*] 0
<p>As you can see, both legacy templates t1 and t2 have the same order; so, which one will override the other?</p><p>In this case, Elasticsearch will sort the legacy index templates according to their names and apply them. Both templates will be applied, and the first one in the list, which is t2 in this example, will override the template.</p><h4>Bonus: What happens if you have two legacy templates that point to the same index pattern with same field name but inappropriate type?</h4><p>Attempting to merge attributes within the legacy template, regardless of the order, is likely to fail since field definitions should remain atomic. This issue is a primary motivator for introducing the new composable templates. See the below example. We thank Philipp Krenn for adding these comments to the article.</p>PUT _template/test1
{
  "order": 3,
  "index_patterns": [
    "test-*"
  ],
  "mappings": {
    "properties": {
      "my_field": {
        "type": "integer",
        "ignore_malformed": true
      }
    }
  }
}
PUT _template/test2
{
  "order": 2,
  "index_patterns": [
    "test-*"
  ],
  "mappings": {
    "properties": {
      "my_field": {
        "type": "keyword",
        "ignore_above": 1024
      }
    }
  }
}
PUT test-1/_doc/1
{
  "my_field": "a string..."
}
#response:
{
  "error": {
    "root_cause": [
      {
        "type": "mapper_parsing_exception",
        "reason": "unknown parameter [ignore_above] on mapper [my_field] of type [integer]"
      }
    ],
    "type": "mapper_parsing_exception",
    "reason": "Failed to parse mapping: unknown parameter [ignore_above] on mapper [my_field] of type [integer]",
    "caused_by": {
      "type": "mapper_parsing_exception",
      "reason": "unknown parameter [ignore_above] on mapper [my_field] of type [integer]"
    }
  },
  "status": 400
}
<h2>Notes and good things to know</h2><ol><li><p>Using legacy templates in the same order can cause a lot of confusion. That’s why it's recommended to add order to the template.</p></li><li><p>Templates with lower-order values are merged first. Templates with higher order values are merged later, overriding templates with lower values.</p></li><li><p>You can't create two composable templates with the same priority.</p></li></ol>{
  "type": "illegal_argument_exception",
  "reason": "index template [ct2] has index patterns [test_index-*] matching patterns from existing templates [ct1] with patterns (ct1 =&gt; [test_index-*]) that have the same priority [0], multiple index templates may not match during index creation, please use a different priority"
}
<h2>Conclusion</h2><p>In conclusion, understanding how Elasticsearch's index templates work is crucial for effective index management. By knowing how to determine which template an index will use upon creation, you can ensure that your indices are created with the correct settings, mappings, and aliases.</p><h4>Resources</h4><p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-put-template.html">https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-put-template.html</a> <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates-v1.html">https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates-v1.html</a> <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-simulate-index.html">https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-simulate-index.html</a></p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-index-template</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-index-template</guid>
    <category><![CDATA[Index Data]]></category>
    <dc:creator><![CDATA[Musab Dogan]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt298e839e708ca11c/6a170b0fb339d560c2769fc2/38bc0377a6adce7eae0099f61902fdbbe644eb4a-1440x960.jpg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 09 May 2024 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>