<?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[Operations - 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[Operations - 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/operations</link>
    </image>
    <link>https://www.elastic.co/search-labs/blog/category/operations</link>
    <atom:link href="https://www.elastic.co/search-labs/rss/category/operations.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <lastBuildDate>Sat, 17 Oct 2026 22:12:40 GMT</lastBuildDate>
  <item>
    <title><![CDATA[How we’re building a context engine to cut agent token use by 71%]]></title>
    <description><![CDATA[With a context engine, the same agent and model answered in 64 seconds using 9 tool calls. The baseline took 213 seconds, 33 calls, and missed a key relationship entirely.]]></description>
    <content:encoded><![CDATA[<p>Agents that query enterprise data burn most of their tokens on discovery: working out schemas, what fields mean, and how entities relate across complex and potentially large documents and data sources. Then they do it all again on the next query, even when nothing has changed. </p><p>A <em>context engine</em> understands your data ahead of the question. It reads your sources and stores what it learns as small structured units of context, so when a task arrives, an agent gets the relevant ones and shortcuts discovery. We built one on Elasticsearch, and every stage of it turned out to be a search problem. Automations search and precompute knowledge, storing it in a specialized <a href="https://www.elastic.co/search-labs/blog/ai-index-building-context-agents"><em>AI index</em></a> with native hybrid search and retrieval enabled over it. With a context engine, agents pull what they need rather than digging through raw data. In an example, with the same model and data, tool calls dropped from 33 to 9, and input tokens fell 71%. Plus, latency went from 213s to 64s, and the agent’s response was more complete, surfacing relationships that would otherwise be missed.</p><h2>What an agent does without a context engine</h2><p>Ask a data agent a strategic business question like: <em>Give me a summary of the issues hitting our largest customers.</em> Without help, a capable agent starts by listing indices to see what data exists. It reads mappings to work out which fields mean "customer" and which mean "issue." It scans contracts to figure out account value and find the largest accounts. Along the way, it misses that five accounts roll up under the same parent company, so it undercounts one of your biggest relationships. It also doesn't know that <code>account_id</code> is sometimes null in the tickets index, so part of its query quietly returns the wrong rows.</p><p>The model's reasoning was fine, but the agent spent most of its budget working out what the data is before it could reason about what the data says, and it will repeat all of that on the next question.</p><p>Provided with the right context however, the agent can be much more efficient. A context engine built on Elasticsearch precomputes knowledge from the raw data sources like tickets and contracts, and surfaces it to the agent upfront so it avoids the costly steps before actually reasoning about an answer. The table below demonstrates this benefit for that business question above, with the same system prompt, model, and data. The only difference is whether the agent had a context engine to read from. We’ll explore what it means to build a context engine and how it provided this benefit through this post. </p><p><strong>Same prompt and model*</strong></p><p><strong>Without Context Engine</strong></p><p><strong>With Context Engine</strong></p><p>Tool calls</p><p>33</p><p>9</p><p>Input tokens</p><p>2,279,253</p><p>653,511</p><p>Latency</p><p>213s</p><p>64s</p><p>Found parent-company relationship</p><p>No</p><p>Yes</p><p>Cost</p><p>Baseline</p><p>71% lower</p><p><em>Across multiple dimensions, Context Engine improves agent performance. The agent reduces tool calls, tokens, and latency, while finding more complete answers. </em></p><p><em>* Runtime model: claude-sonnet-5, using LangChain Deep Agent Harness, precompute ~636K input tokens with claude-haiku-4-6.</em></p><h2>Why AI agents need a context layer</h2><p>Most context engineering work so far has been about what goes into one agent's context window: prompts, tool definitions, conversation history, compaction. That work matters, but when we worked with customers building on <a href="https://www.elastic.co/docs/explore-analyze/ai-features/elastic-agent-builder">Elastic Agent Builder</a>, the context that decided whether an agent succeeded usually wasn't in the conversation at all. It was business knowledge, like what each data source is for, how data sources relate across systems, what trends or anomalies exist in the data, and how to build efficient queries against that data.</p><p>When there's nowhere to keep that knowledge, the same problems show up in production, including:</p><ul><li><p><strong>Per-query discovery cost.</strong> Agents pay a discovery cost on every query, spending tokens to work out what data exists, what fields mean, and how sources relate, even when nothing has changed.</p></li><li><p><strong>Context drift. </strong>Whatever context someone does write down in markdown files or catalog entries drifts out of date as data and usage change.</p></li><li><p><strong>Siloed knowledge.</strong> What one agent learns never helps another, so quality doesn't compound and costs don't fall. Plus, the gap between demo and production stays open.</p></li></ul><h2>What a context engine does</h2><p>A context layer sits between your raw data and your agents. It does the work of understanding your data ahead of time and stores the results as small structured units of context. Then hands agents the relevant ones when they need them.</p><p>We call these units Knowledge Indicators (KIs). A KI might describe:</p><ul><li><p>An entity and its relationships, such as "LongHaul (ACC-1007) is a subsidiary of Orion Holdings".</p></li><li><p>A data source profile, such as "the tickets index holds customer support issues; search it semantically on the issue description".</p></li><li><p>A data quirk, such as "<code>account_id</code> is not always present, so filter out nulls".</p></li><li><p>A fact, anomaly, or pattern that would otherwise take a full scan to find, such as “what is the peak and median query load over the last 30 days”. </p></li></ul><p>The layer creates and serves KIs in a continuous loop:</p><ol><li><p><strong>Gather.</strong> Read each source, and learn its shape. Find the content that matters for what your agents need to do.</p></li><li><p><strong>Build.</strong> Turn that content into KIs (metadata, entities, relationships, anomalies, facts, and query guidance), and store them.</p></li><li><p><strong>Retrieve.</strong> When an agent gets a task, it pulls a small, ranked set of relevant KIs instead of doing schema discovery and document scans itself.</p></li><li><p><strong>Improve.</strong> Every agent interaction produces a trace, and traces feed back in as a new source, so context keeps up with how agents are actually used.</p></li></ol><h2>Why every stage of context engineering is a search problem</h2><p>Search usually comes up in these discussions as the retrieval step at the end. In practice, each stage of the loop depends on finding the right information.</p><p><strong>Stage</strong></p><p><strong>The search problem</strong></p><p>Gather</p><p>You're working with gigabytes to petabytes of data and can't reprocess all of it continuously. Narrowing to what matters takes high recall and high precision: agentic search to understand intent, lexical search for precision, semantic search for recall, and workflows to automate it.</p><p>Build</p><p>KIs precompute what an agent would otherwise work out at runtime. What to keep and what to throw away depends on how the KI will be retrieved, so KIs need embeddings, metadata fields, and combined fields designed for retrieval.</p><p>Retrieve</p><p>What goes into the context window has to be relevant and complete, as well as concise. If it's noisy, the agent ignores it and goes back to exploring the raw source. Tuning this is a relevance problem.</p><p>Improve</p><p>Traces and transcripts are unstructured. To surface recurring failures and the patterns they share, you need hybrid search.</p><p>We built the context layer on Elasticsearch because its stages rely on <a href="https://www.elastic.co/search-labs/blog/hybrid-search-elasticsearch">hybrid search</a>, Elasticsearch Query Language (ES|QL), and <a href="https://www.elastic.co/docs/solutions/search/vector">vector search</a>, often within the same request. <a href="https://www.elastic.co/search-labs/blog/pre-computed-context-llm-agent-costs">Earlier blog posts</a> covered each of these individually, and this post shows how Context Engine runs them together.</p><h2>How this compares to approaches like agentic RAG and longer context windows</h2><p>Context engineering isn’t a new practice, and there are many existing approaches to help manage the same challenges. Creating context, through the loop above, augments and helps address many of the tradeoffs and challenges with existing approaches today. </p><p><strong>Approach</strong></p><p><strong>What happens at query time</strong></p><p><strong>Tradeoff</strong></p><p>Static context files or catalogs</p><p>Agent reads hand-written descriptions</p><p>Goes stale and doesn't learn from use</p><p>Agentic retrieval augmented generation (RAG)</p><p>Agent searches raw chunks and loops until satisfied</p><p>Reassembles knowledge on every task and misses relationships that span documents</p><p>Coding agent with grep</p><p>Agent navigates files directly</p><p>Slow and token-hungry; struggles to tell which match is the right one</p><p>Longer model context window</p><p>Agent loads data directly into the context window to reason over</p><p>Longer context windows struggle with problems of context rot and attention, and as context builds, each subsequent turn becomes more expensive. </p><p>A context layer trades the cost of precomputing and processing data for the benefits of improved efficiency at runtime. Because precomputation can be defined ahead of time, it can be more prescribed and use smaller, cheaper models, making this tradeoff much more manageable while addressing many of the efficiency and effort concerns of the approaches above. </p><h2>Building a context engine</h2><p>Context Engine consists of five components, as shown in the following image: sources, automations, AI index, agent memory (agents), and the feedback loop.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt981f09c5455051b1/6ac73154d31d47dea1b8c2bb/image2.png" alt="Context engine architecture: sources feed automations, which write Knowledge Indicators to an AI index in Elasticsearch" /><p>Within Elastic, we’re developing a context engine to deliver the boosts in accuracy and efficiency that agents need. Let’s explore each component in detail.</p><h3>Sources</h3><p>Context can come from any Elasticsearch index or data stream, so you build it where your data already lives. It can also come from external applications and data sources, through connectors, and from agent traces captured in Agent Builder or sent from other agent frameworks over OpenTelemetry (OTel).</p><h3>How automations build context</h3><p>Automations populate the AI index. They're <a href="https://www.elastic.co/docs/explore-analyze/workflows">Elastic Workflows</a> with specialized steps that search raw content, process results with a large language model (LLM) or other models, structure the output for retrieval, verify provenance, apply access control, track state, and write KIs to the AI index.</p><p>You don't have to write them from scratch. A setup agent looks at your connected sources and suggests automations. They can run on small, inexpensive models or plain ES|QL, which keeps build costs low while the savings show up on every query afterward.</p><h3>The AI index, built on the Elasticsearch vector database</h3><p>The AI index is the context store. It runs on Elasticsearch as a vector database and includes ES|QL and hybrid search. It holds the KIs that automations produce. Its schema and mappings are designed for agents to retrieve from.</p><h3>How agent memory works</h3><p>As users complete tasks, agents can store and retrieve memories with remember, recall, and forget tools. Memory consolidation and organization-wide sharing are next on the roadmap.</p><h3>How agent traces feed back into context</h3><p>Every agent interaction produces a trace that records how the agent worked through the task, including where it succeeded and where it got stuck, in addition to where it spent tokens. The feedback loop looks for inefficiencies in these traces and suggests changes to your automations, so KIs improve from real usage as well as from any test set you wrote up front.</p><h2>From raw data to a better answer with a context engine</h2><p>Here’s how the context engine was used to achieve the results at the top of this post. We evaluated a simulated account analysis agent on questions like the one above. It had to pull from several sources:</p><ul><li><p>Four Elasticsearch indices holding customer tickets, subscribers, an internal knowledge base, and support page search query logs.</p></li><li><p>A Google Drive folder with about 50 simulated contracts between the enterprise and its customers or vendors.</p></li></ul><p>Automations created two kinds of KIs from these sources. <em>Index profiles</em> describe what each source is for and how to query it. <em>Account entities</em> pull accounts, projects, and people, along with their relationships to other entities, out of raw contract and ticket text. That produced about 80 KIs, each holding the extracted content plus metadata that tracks provenance, lifecycle, and how to query the context. The automations ran on smaller, cheaper LLMs, so extraction cost stayed low.</p><p>We connected the AI index to two agent frameworks, Elastic Agent Builder and LangChain Deep Agents, both over Model Context Protocol (MCP).</p><h3>How the baseline agent spent 33 tool calls</h3><p>By using the following request, <em>Give me a summary of the issues hitting our largest customers</em>, the baseline agent:</p><ol><li><p>Ran discovery, looking at each data source, its fields, and sample documents to learn the structure.</p></li><li><p>Worked out what "customer" means in the data (there are subscribers and contracted accounts) and how to define "largest" (there are fields for annual contract value [ACV] and monthly recurring revenue [MRR]).</p></li><li><p>Queried the tickets index to find problems.</p></li><li><p>Pulled the full text of every contract that mentioned an affected customer, looking for relationships to other organizations.</p></li></ol><p></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt236022f35cb8706a/6ac73179701a1ab5838abfd2/image1.png" alt="Flow diagram: baseline agent uses 33 tool calls and 2.3M tokens, context engine agent 9 calls and 659K" /><p>The discovery and contract reads are the expensive part, and they account for most of the token cost in the baseline run.</p><h3>What changed with the context layer</h3><p>With the AI index, the agent started with knowledge that automations had precomputed. It already knew which fields identify customers and which define the largest ones. The entity KIs also captured relationships from the raw contract text, such as one parent company sitting over several smaller ones. Reading the KIs up front let the agent skip the expensive queries and full document scans, and run lightweight checks against the original data instead. As a result, the run used 71% fewer input tokens, and the context window stayed free of noise.</p><h2>Why context engineering for agents is a search problem</h2><p>The first wave of context engineering was about the context window: what to put in the prompt and what to compact, along with what to drop. That's still important, but for agents that work across enterprise data, the harder problem is building the context in the first place. Business knowledge has to be gathered, shaped, kept current before a conversation starts, and shared across agents so each one doesn't rebuild it.</p><p>A context layer does that job, and it changes how agents work. Agents get consistent guidance on where to look and how to query, so answers still come from live data without the dead ends. Context refreshes from the source, so it adapts to changes in data, user behavior, and use cases without someone maintaining it by hand. Every agent run also shows what was missing, so the knowledge base gets better with use and every agent that shares it benefits.</p><p>All of this depends on search. Finding the signal in large datasets, shaping context so it can be found again, retrieving only what's relevant, and spotting patterns across thousands of traces are all search problems. If you're designing the context layer for your agents, build it on search.</p><p><em>We're actively partnering with developers who want to optimize their agents in production. If you're interested in exploring Context Engine for your agents, you can request </em><a href="https://www.elastic.co/elasticsearch/context-engine/sign-up"><em>access here</em></a><em>.</em></p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/context-engineering-for-agents</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/context-engineering-for-agents</guid>
    <category><![CDATA[Agentic AI]]></category>
    <category><![CDATA[Hybrid Search]]></category>
    <category><![CDATA[Operations]]></category>
    <dc:creator><![CDATA[Anish Mathur]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt31a696fd1f82649f/6ac730f3c665662b2a7d5b88/cover.png" length="0" type="image/png"/>
    <pubDate>Thu, 08 Oct 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Signed, sealed, delivered: ECK certificate management with Vault and cert-manager]]></title>
    <description><![CDATA[Replace ECK's self-signed certificates with your enterprise PKI without renewing a certificate by hand, as Vault signs with a CA key sealed outside Kubernetes and cert-manager delivers fresh certificates to every Elasticsearch pod.]]></description>
    <content:encoded><![CDATA[<p>You can run<a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s"> Elastic Cloud on Kubernetes (ECK)</a> on your own enterprise public key infrastructure (PKI) without your certificate authority’s (CA's) private key ever entering the Kubernetes cluster. By default, ECK generates self-signed certificates for Elasticsearch and Kibana. This post hands ECK certificate management for Elasticsearch to HashiCorp Vault, which holds the intermediate CA and signs every certificate, and to cert-manager, which requests and renews them. In the example, HTTP certificates last seven days and renew an hour before they expire. Transport certificates come from the cert-manager container storage interface (CSI) driver, which issues one directly into each Elasticsearch pod as it starts, so new nodes get certificates automatically and their private keys are never stored as Kubernetes Secrets.</p><p>ECK's defaults secure every cluster with Transport Layer Security (TLS) and rotate their own certificates, which works well until your security policy says that every certificate must come from your enterprise PKI. At that point, you need central control over issuance and trust, and you still don't want to renew certificates by hand. Vault and cert-manager give you both.</p><h2>Self-signed certificates that ECK generates by default</h2><p>The ECK operator generates self-signed certificates for various components of the Elastic Stack. Below is a high-level overview of the different types of self-signed certificates generated by ECK for Elasticsearch and Kibana:</p>ECK Certificate Management
├── Elasticsearch
│   ├── HTTP
│   │   ├── HTTP CA
│   │   └── HTTP Server Certificate
│   │
│   └── Transport
│       ├── Transport CA
│       └── Node Transport Certificates
│
└── Kibana
    └── HTTP
        ├── HTTP CA
        └── HTTP Server Certificate<h2>Where should your CA private key live?</h2><p>Using custom certificates with ECK introduces a tradeoff between the risk of exposing a CA private key and automating certificate lifecycle management.</p><h3>Bring your own CA and private key to ECK</h3><p>The ECK operator automates certificate issuance and renewal when provided with the CA certificate and private key through a Kubernetes Secret. ECK then manages the certificate lifecycle across the Elastic Stack, including both HTTP and transport certificates for Elasticsearch. However, storing the CA private key within the Kubernetes cluster is considered a security risk and may not align with enterprise security and PKI policies.</p><h3>Use manually issued certificates with ECK</h3><p>ECK can consume externally issued certificates provided through Kubernetes Secrets, keeping the CA private key outside the Kubernetes cluster. However, certificate issuance and renewal must be managed separately, as should certificate rotation. This creates an operational bottleneck, particularly for transport certificates, which need to be provisioned dynamically as Elasticsearch pods scale up or down.</p><h3>Use Vault PKI with cert-manager</h3><p>Cloud-native tooling removes the tradeoff between bringing your own CA and key and using manually issued certificates. HashiCorp Vault and cert-manager move certificate lifecycle management on Kubernetes out of  the ECK operator, while keeping the enterprise CA private key secure.</p><ul><li><p><a href="https://developer.hashicorp.com/vault"><strong>HashiCorp Vault</strong></a> is the external PKI system, securely managing the CA private keys and issuing certificates signed by the enterprise CA. Certificate signing operations stay secure without ever exposing the CA private key as a Kubernetes Secret.</p></li><li><p><a href="https://cert-manager.io/docs/"><strong>cert-manager</strong></a> can automate the certificate lifecycle within Kubernetes. It can request certificates from Vault and monitor their validity. It can also automatically renew them before expiration. Once issued, cert-manager can create and update the corresponding Kubernetes Secrets which, in turn, are consumed by Elastic components.</p></li></ul><p>Using Vault PKI with cert-manager gives each component a single responsibility: Vault manages the trust and signing authority, and cert-manager manages the certificate lifecycle. ECK components consume the resulting certificates. </p><p>Vault and cert-manager let organizations keep their existing enterprise PKI controls while reducing the operational overhead associated with certificate issuance, renewal, and rotation.</p><p>The table below compares the three approaches:</p><p><strong>Approach</strong></p><p><strong>Where the CA key lives</strong></p><p><strong>Certificate lifecycle</strong></p><p><strong>Transport certificates as pods scale</strong></p><p><strong>Fit with PKI policy</strong></p><p>Bring your own CA and key</p><p>Kubernetes Secret</p><p>Automated by ECK</p><p>Automatic</p><p>Depends on whether policy allows CA keys in the cluster</p><p>Manually issued certificates</p><p>Outside the cluster</p><p>Manual</p><p>Manual</p><p>Keeps the CA key outside the cluster</p><p>Vault PKI with cert-manager</p><p>Vault</p><p>Automated by cert-manager</p><p>Automatic via the cert-manager CSI driver</p><p>Keeps the CA key outside the cluster</p><h2>ECK certificate management architecture</h2><p>The following diagram illustrates the proposed architecture for the solution:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltac2f91d469b4cabc/6abe1f6923d19350e05b6398/unnamed_(1).png" alt="ECK certificate management architecture: cert-manager requests Elasticsearch TLS certificates from HashiCorp Vault PKI" /><h2>Prerequisites and tested versions</h2><p>This blog assumes that the user has a basic understanding of the Vault and cert-manager setup. In our post, we focus only on the configuration required to implement this setup.</p><p>Refer to the official Vault and cert-manager documentation for installation instructions related to <a href="https://developer.hashicorp.com/vault/install">Vault</a>, <a href="https://cert-manager.io/docs/installation/">cert-manager</a>, and <a href="https://cert-manager.io/docs/usage/csi-driver/installation/">csi-driver</a>.</p><h3><strong>Test versions</strong></h3><p>We used the following software versions for the demonstration in this blog. For compatibility considerations or configuration differences with other versions, refer to the respective official documentation.</p><ul><li><p>Elastic Stack 9.5.3</p></li><li><p>cert-manager 1.21.2</p></li><li><p>HashiCorp Vault 2.0.3 (Community Edition)</p></li></ul><h2>HashiCorp Vault as your enterprise PKI</h2><p>On the Vault server, the <a href="https://developer.hashicorp.com/vault/docs/secrets/pki">PKI secrets engine</a> needs to be enabled to securely store CA certificates and its private key that’s used to sign leaf certificates.</p><p>As a best practice, the Root CA private key remains outside Vault. The Root CA is used to sign the Intermediate CA certificate signing request (CSR), after which the Intermediate CA certificate and private key are stored securely in Vault. Vault then uses this Intermediate CA and key to issue and sign leaf certificates.</p><p>In addition to the PKI secrets engine, the Kubernetes authentication method is also enabled so Vault can authenticate requests originating from Kubernetes. A dedicated Vault role is configured with narrowly scoped permissions, ensuring that it issues certificates only for requests from the specified Kubernetes ServiceAccount and namespace.</p><p>At a high level, the following tree structure illustrates the Vault components required for this setup:</p>HashiCorp Vault
│
├── PKI
│   └── PKI Engine
│       ├── CA / Intermediate CA / Intermediate Key
│       └── Sign Role
│           └── Certificate issuance policy
│
└── Authentication
    └── Kubernetes Auth Method
        ├── Kubernetes API Server
        └── Kubernetes Auth Role
            ├── Bound ServiceAccount
            └── Bound Namespace<h3>Configure Vault PKI and Vault Kubernetes auth</h3><p>1. On a Linux host that has access to the Vault command line interface (CLI), generate a root CA certificate and key:</p>openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes -keyout eck-root-ca.key -out eck-root-ca.crt -subj "/C=SG/ST=SG/L=SG/O=Elastic Demo/OU=ECK/CN=ECK Root"<p><strong>NOTE:</strong> This is used for demonstration purposes only. In a production implementation, skip the above step and use your organization's existing enterprise CA and key instead. </p><p>2. Enable the PKI secrets engine at the <code>eck/esintca</code> path, and configure a maximum certificate lease duration of one year. This PKI engine is used to store the intermediate CA and issue certificates for the Elastic Stack components:</p>vault secrets enable -path=eck/esintca pki &amp;&amp; \
vault secrets tune -max-lease-ttl=8760h eck/esintca<p>3. Generate an internal Intermediate CA key and CSR using the <code>eck/esintca</code> PKI engine. The generated response includes the CSR and is stored in <code>eck_esint.json</code> for use in the subsequent signing step:</p>vault write -format=json eck/esintca/intermediate/generate/internal common_name="ES Intermediate" organization="Elastic Demo" ou="ECK" country="SG" province="SG" locality="SG" key_type="rsa" key_bits=2048 &gt; eck_esint.json<p>4. Extract the Intermediate CA CSR from the Vault response, and save it as <code>eck_esint.csr</code>. This CSR is submitted to the Root CA for signing.</p>jq -r 
'.data.csr' 
eck_esint.json &gt; eck_esint.csr<p>5. Create an OpenSSL extensions configuration file defining the Intermediate CA attributes. The configuration marks the certificate as a CA and limits the certificate path length to zero. It also enables the required key usage and certificate identification extensions. This configuration is used when signing the Intermediate CA CSR with the Root CA.</p>cat &gt; eck-ext.cnf &lt;&lt;'EOF'
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
EOF<p>6. Sign the Intermediate CA CSR using the Root CA certificate and private key to generate the Intermediate CA certificate. The certificate is valid for 365 days and uses SHA-256, with the CA extensions defined in <code>eck-ext.cnf</code>. The resulting <code>eck_esint.crt</code> is used by Vault to issue and sign leaf certificates.</p>openssl x509 -req -in eck_esint.csr -CA eck-root-ca.crt -CAkey eck-root-ca.key -CAcreateserial -out eck_esint.crt -days 365 -sha256 -extfile eck-ext.cnf<p>7. Combine the Intermediate CA certificate with the Root CA certificate to create the complete CA certificate chain. Vault uses this chain when issuing certificates so that clients can trust the chain back to the Root CA.</p>{ cat eck_esint.crt; printf '\n'; cat eck-root-ca.crt; } &gt; eck_es_cachain.crt<p>8. Upload the signed Intermediate CA certificate chain to the Vault PKI engine. This completes the Intermediate CA configuration and enables Vault to use it for issuing certificates signed by the Elasticsearch Intermediate CA.</p>vault write eck/esintca/intermediate/set-signed certificate=@eck_es_cachain.crt<p>9. Configure the Vault PKI engine with the URLs used to retrieve the issuing CA certificate and Certificate Revocation List (CRL). Replace <code>&lt;replace-with-vault-server-endpoint&gt;</code> with the Vault server endpoint specific to your environment. Clients use these URLs to retrieve the CA certificate and to check certificate revocation status when required.</p>vault write eck/esintca/config/urls issuing_certificates="https://&lt;replace-with-vault-server-endpoint&gt;:8200/v1/eck/esintca/ca" &amp;&amp; \

vault write eck/esintca/config/urls crl_distribution_points="https://&lt;replace-with-vault-server-endpoint&gt;:8200/v1/eck/esintca/crl"<p>10. From the Linux host that has access to Kubernetes cluster, retrieve the Kubernetes API server endpoint from the current <code>kubectl</code> context. This endpoint is used to configure Vault's Kubernetes authentication and to allow Vault to communicate with the Kubernetes API server for ServiceAccount token validation.</p>export KUBE_API_SERVER=$(kubectl config view --minify \
  -o jsonpath='{.clusters[0].cluster.server}{"\n"}')<p>11. Extract the Kubernetes API server's CA certificate from the current <code>kubectl</code> context, and decode it into <code>kubeapi-ca.crt</code>. Vault uses this CA certificate to establish a trusted TLS connection with the Kubernetes API server.</p>kubectl config view --raw --minify \
  -o jsonpath='{.clusters[0].cluster.certificate-authority-data}' \
  | base64 -d &gt; kubeapi-ca.crt<p><strong>NOTE:</strong> If the same Linux host has access to both the Kubernetes cluster and the Vault server, run the following commands from that host. Otherwise, copy the Kubernetes API server endpoint, audience url, and CA certificate to the host from which the Vault server is accessible, and run the commands there.</p><p>12. Enable the Kubernetes authentication method at the <code>eck</code> path. Vault then authenticates requests from Kubernetes workloads using their ServiceAccount tokens.</p>vault auth enable -path=eck kubernetes<p>13. Configure Vault with the Kubernetes API server endpoint and its CA certificate. Vault uses the CA certificate to establish and verify a trusted TLS connection to the Kubernetes API server.</p>vault write auth/eck/config kubernetes_host="$KUBE_API_SERVER" kubernetes_ca_cert=@kubeapi-ca.crt<p>14. Create a Vault PKI role that defines the domains and certificate parameters allowed for Elasticsearch certificates. The role permits the specified Kubernetes service domains and subdomains, and it accepts the common name (CN) and subject alternative names (SANs) from the CSR. It also limits issued certificates to a maximum time to live (TTL) of 168 hours (seven days).</p>vault write eck/esintca/roles/elasticsearch allowed_domains="elastic-cluster.svc,elastic-cluster.svc.cluster.local,elasticsearch-es-http" allow_subdomains=true allow_bare_domains=true use_csr_common_name=true use_csr_sans=true max_ttl="168h"<p><strong>Note:</strong> In this example, the Elasticsearch cluster is named <code>elasticsearch</code> and is deployed in the <code>elastic-cluster</code> namespace. Therefore, the Elasticsearch HTTP service is referenced as <code>elasticsearch-es-http</code> and the domain as <code>elastic-cluster.svc</code>. Replace the cluster name, namespace, and corresponding service name with the values applicable to your environment.</p><p>15. Create a Vault policy that grants permission to use the <code>elasticsearch</code> PKI signing role. The policy provides only the required <code>update</code> capability on the certificate signing endpoint, following a least-privilege approach.</p>cat &gt; eck-es-policy.hcl &lt;&lt;'EOF'
path "eck/esintca/sign/elasticsearch" {
  capabilities = ["update"]
}
EOF<p>16. Create the <code>eck-es-policy</code> policy in Vault using the previously defined policy file. This policy is associated with the Kubernetes authentication role and grants the permissions required to request Elasticsearch certificates.</p>vault policy write eck-es-policy eck-es-policy.hcl<p>17. Create the <code>eck-es-role</code> role, and bind it to the <code>cert-manager-vault-access</code> ServiceAccount in the <code>elastic-cluster</code> namespace. The role uses the specified Kubernetes token audience and attaches the <code>eck-es-policy</code>, so the ServiceAccount can authenticate to Vault and request Elasticsearch certificates. The authentication token is configured with a 10-minute TTL.</p>vault write auth/eck/role/eck-es-role bound_service_account_names=cert-manager-vault-access bound_service_account_namespaces=elastic-cluster audience=
"vault://elastic-cluster/eck-es-issuer" 
policies=eck-es-policy ttl=
10m<p><strong>Note:</strong> In the Kubernetes cluster, ensure that Vault authentication requests originate from the <code>elastic-cluster</code> namespace and use the <code>cert-manager-vault-access</code> ServiceAccount, along with its associated token.</p><h2>Automate certificate renewal with cert-manager</h2><p>cert-manager is used to manage the lifecycle of certificates required by the Elastic Stack. It monitors Kubernetes <code>Certificate</code> resources and requests certificates from the configured <a href="https://cert-manager.io/docs/configuration/vault/"><code>Issuer</code></a>, which, in this solution, is backed by Vault.</p><p>cert-manager authenticates to Vault using a dedicated Kubernetes ServiceAccount. Vault then validates the identity of the requesting workload and authorizes certificate issuance based on the configured Vault role and policy.</p><p>Once Vault issues the certificate, cert-manager creates and maintains the corresponding Kubernetes Secret containing the certificate and private key. cert-manager also monitors certificate validity and automatically manages renewal and rotation before certificates expire.</p><h3>Issue transport certificates with the cert-manager CSI driver</h3><p>For Elasticsearch transport certificates, the cert-manager <a href="https://cert-manager.io/docs/usage/csi-driver/">CSI driver</a> provides certificates directly to the Elasticsearch pods. The CSI driver requests certificates and mounts them directly into pods through the Kubernetes CSI interface, so the certificate and private key never need to be persisted as a Kubernetes Secret.</p><p>With the CSI driver, cert-manager still manages the certificate lifecycle and obtains certificates from the Vault-backed <code>Issuer</code>. The CSI driver handles the delivery of these certificates to the Elasticsearch pods, where they can be used for secure transport communication between Elasticsearch nodes.</p><h3>Create the Vault Issuer and HTTP certificate</h3><p>1. On the Kubernetes cluster, create the <code>elastic-cluster</code> namespace and a dedicated <code>cert-manager-vault-access</code> ServiceAccount within it. This ServiceAccount is used to obtain the Kubernetes token that cert-manager presents to Vault for authentication.</p>kubectl create ns elastic-clusterkubectl create sa cert-manager-vault-access -n elastic-cluster<p>2. Create the required role-based access control (RBAC) permissions in the <code>elastic-cluster</code> namespace to support Vault authentication. The <code>Role</code> and <code>RoleBinding</code> allow the cert-manager ServiceAccount to request a token for the dedicated <code>cert-manager-vault-access</code> ServiceAccount.
The <code>ClusterRoleBinding</code> lets Vault use this ServiceAccount to validate Kubernetes ServiceAccount tokens through the Kubernetes API, so Vault can verify the identity of the requesting workload.</p>kubectl apply -f - &lt;&lt;'EOF'
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: cert-manager-vault-token-request
  namespace: elastic-cluster
rules:
  - apiGroups: [""]
    resources:
      - serviceaccounts/token
    resourceNames:
      - cert-manager-vault-access
    verbs:
      - create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: cert-manager-vault-token-request
  namespace: elastic-cluster
subjects:
  - kind: ServiceAccount
    name: cert-manager
    namespace: cert-manager
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: cert-manager-vault-token-request
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: cert-manager-vault-access-token-review
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:auth-delegator
subjects:
  - kind: ServiceAccount
    name: cert-manager-vault-access
    namespace: elastic-cluster
EOF<p>3. Generate a token for the <code>default</code> ServiceAccount, and decode its JSON Web Tokens (JWT) payload to inspect the token claims. This is used to identify the token audience (<code>aud</code>) when configuring Vault's Kubernetes authentication.</p>export 
aud=$(kubectl create token default | cut -d. -f2 | base64 -d 
2
&gt;/dev/null | sed -n 
's/.*"aud":\["\([^"]*\)".*/\1/p'
)<p>4. Create an <code>Issuer</code> object that connects cert-manager to the Vault PKI signing endpoint. Configure the Vault server endpoint and PKI signing path, along with the Vault server CA certificate, so cert-manager can establish a trusted connection to Vault.
The Kubernetes authentication configuration specifies the <code>eck-es-role</code> Vault role and the <code>cert-manager-vault-access</code> ServiceAccount. cert-manager uses this identity and audience to authenticate to Vault and to request Elasticsearch certificates.
In the YAML below, replace the Vault server endpoint and CABundle, as well as the audience generated in the previous step, and save it in the issuer.yaml file:</p>apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: eck-es-issuer
  namespace: elastic-cluster
spec:
  vault:
    server: https://&lt;replace-with-vault-server-endpoint&gt;:8200
    path: eck/esintca/sign/elasticsearch
    caBundle: &lt;ca-cert-of-vault-endpoint-in-base64&gt;
    auth:
      kubernetes:
        role: eck-es-role
        mountPath: /v1/auth/eck
        serviceAccountRef:
          name: cert-manager-vault-access
          audiences:
            - $aud<p>Apply the manifest file to create the object:</p>kubectl create -f issuer.yaml<p>5. Create a cert-manager <code>Certificate</code> resource in the <code>elastic-cluster</code> namespace to request the Elasticsearch HTTP certificate from the <code>eck-es-issuer</code> Issuer. 
The certificate is configured with the required domain name system (DNS) names for Elasticsearch service access and a seven-day validity period, in addition to renewal 60 minutes before expiry. cert-manager stores the generated certificate and private key in the <code>eck-es-http</code> Kubernetes Secret, using an RSA 2048-bit private key encoded in Public-Key Cryptography Standards #1 (PKCS#1) format.</p>kubectl apply -f - &lt;&lt;'EOF'
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: eck-http-certificate
  namespace: elastic-cluster
spec:
  duration: '168h'
  renewBefore: '60m'
  isCA: false
  secretName: eck-es-http
  issuerRef:
    name: eck-es-issuer
    kind: Issuer
  commonName: elasticsearch-es-http.elastic-cluster.svc
  dnsNames:
    - elasticsearch-es-http
    - elasticsearch-es-http.elastic-cluster.svc
    - elasticsearch-es-http.elastic-cluster.svc.cluster.local
  privateKey:
    algorithm: RSA
    encoding: PKCS1
    size: 2048
EOF<p>6. Verify that cert-manager has successfully processed the <code>Certificate</code> resource and created the corresponding Kubernetes Secret. The Secret should contain the certificate and private key generated for the Elasticsearch HTTP endpoint.
The <code>Certificate</code> should show a <code>READY</code> status of <code>True</code>, indicating that the certificate was successfully issued and the Secret was created.</p>kubectl get certificate eck-http-certificate -n elastic-cluster &amp;&amp; \ kubectl get secret eck-es-http -n elastic-cluster<h2>Make your custom CA available to ECK</h2><p>Vault and cert-manager already provide the <code>ca.crt</code> file needed for transport trust between Elasticsearch nodes, mounted through the CSI driver. However, in scenarios where the CA changes frequently or remote cluster setup requires the CA to be trusted across clusters, the custom CA also needs to be made available as a ConfigMap in the <code>elastic-cluster</code> namespace so that the ECK operator itself is aware of it.</p><p>There are two options for providing this ConfigMap.</p><h3>Sync the CA automatically with trust-manager</h3><p><a href="https://cert-manager.io/docs/trust/trust-manager/"><code>trust-manager</code></a> is a Kubernetes operator that keeps trust bundles up to date automatically. It adds a <code>Bundle</code> custom resource that reads the CA from a source, such as a Kubernetes Secret, and publishes it into a ConfigMap, keeping it in sync whenever the source changes.</p><p>Install trust-manager with its source secret namespace set to <code>elastic-cluster</code> so that it can read the <code>eck-es-http</code> Secret used in the Bundle below. Refer to its <a href="https://cert-manager.io/docs/trust/trust-manager/installation/#installation-options">official documentation</a> for more information.</p>kubectl apply -f - &lt;&lt;'EOF'
apiVersion: trust.cert-manager.io/v1alpha1
kind: Bundle
metadata:
  name: custom-ca-trust
spec:
  sources:
  - secret:
      name: "eck-es-http"
      key: "ca.crt"
  target:
    configMap:
      key: "ca.crt"
    namespaceSelector:
      matchLabels: 
        kubernetes.io/metadata.name: elastic-cluster
EOF<h3>Create the CA ConfigMap manually</h3><p>The <code>eck-es-http</code> secret created earlier by cert-manager already contains the CA certificate under its <code>ca.crt</code> key. You can extract it directly and use it to create the ConfigMap.</p>kubectl get secret eck-es-http -n elastic-cluster -o jsonpath='{.data.ca\.crt}' | base64 -d &gt; ca.crt &amp;&amp; \
kubectl create configmap custom-ca-trust --from-file=ca.crt=ca.crt -n elastic-cluster<h2>Deploy Elasticsearch with enterprise PKI certificates</h2><p>Deploy the Elasticsearch cluster using the following ECK configuration. The cluster is configured to use the certificates provisioned through the previously configured cert-manager and Vault integration rather than relying on ECK's default self-signed certificate management. The <a href="https://www.elastic.co/docs/deploy-manage/security/k8s-https-settings">HTTP certificate</a> is provided through the Kubernetes Secret created by cert-manager, while the Elasticsearch <a href="https://www.elastic.co/docs/deploy-manage/security/k8s-transport-settings">transport certificates</a> are delivered through the cert-manager CSI driver.</p><p>Because the transport certificates arrive through the CSI driver as mounted files rather than as a Kubernetes Secret, the configuration below points <code>xpack.security.transport.ssl.key</code> and <code>xpack.security.transport.ssl.certificate</code> directly at those file paths:</p>kubectl apply -f - &lt;&lt;'EOF'
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: elasticsearch
  namespace: elastic-cluster
spec:
  version: 9.5.3
  nodeSets:
    - name: default
      count: 3
      config:
        xpack.security.transport.ssl.key: /usr/share/elasticsearch/config/cert-manager-certs/tls.key
        xpack.security.transport.ssl.certificate: /usr/share/elasticsearch/config/cert-manager-certs/tls.crt
      podTemplate:
        spec:
          containers:
          - name: elasticsearch
            volumeMounts:
            - name: transport-certs
              mountPath: /usr/share/elasticsearch/config/cert-manager-certs
          volumes:
          - name: transport-certs
            csi:
              driver: csi.cert-manager.io
              readOnly: true
              volumeAttributes:
                csi.cert-manager.io/issuer-name: eck-es-issuer
                csi.cert-manager.io/issuer-kind: Issuer
                csi.cert-manager.io/common-name: "${POD_NAME}.${POD_NAMESPACE}.svc.cluster.local"
                csi.cert-manager.io/dns-names: "${POD_NAME}.${POD_NAMESPACE}.svc.cluster.local"
  transport:
    tls:
      certificateAuthorities:
        configMapName: custom-ca-trust
      selfSignedCertificates:
        disabled: true
  http:
    tls:
      certificate:
        secretName: eck-es-http
EOF<h3>Verify that Elasticsearch is using your enterprise PKI certificates</h3><p>Verify that Elasticsearch is using the custom certificate issued through the Vault-backed cert-manager configuration. First, retrieve the certificate from the Kubernetes Secret created by cert-manager, and then inspect its subject and issuer, along with its validity period.</p><p>The output should show the expected Elasticsearch service name in the certificate's CN/SANs, the Vault-managed Intermediate CA as the issuer, and the certificate's <code>notBefore</code> and <code>notAfter</code> values.</p>kubectl get secret eck-es-http -n elastic-cluster -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -subject -issuer -dates -ext subjectAltName<p>Verify that Elasticsearch is presenting the certificate issued through the Vault-backed cert-manager configuration. The output should show the expected Elasticsearch service name in the certificate details and the Vault-managed Intermediate CA as the issuer. It should also show the certificate's validity period.</p>kubectl exec -n elastic-cluster elasticsearch-es-default-0 -- curl -v --cacert /usr/share/elasticsearch/config/http-certs/ca.crt https://elasticsearch-es-http.elastic-cluster.svc:9200 2&gt;&amp;1 | grep -E 'subject:|issuer:|start date:|expire date:|subjectAltName'<p>Use the following command to verify the health status of the Elasticsearch cluster:</p>kubectl -n elastic-cluster get elasticsearch<h2>Extend ECK certificate management across the Elastic Stack</h2><p>The approach we described in this blog post can be summarized in the following diagram:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6c8d6ff3d40fde3b/6abe1fc40f460d0968d5b276/unnamed_(2).png" alt="Sequence diagram: cert-manager stores a Vault-signed certificate as a Secret, then ECK uses it to start Elasticsearch" /><p>We focused here on configuring custom certificates for Elasticsearch. The same approach can be extended to other Elastic Stack components, such as Kibana, by using HashiCorp Vault and cert-manager to manage their certificate lifecycle.</p><p>Replacing ECK's default self-signed certificates with certificates backed by an enterprise PKI can be implemented using cloud-native tooling, such as Vault and cert-manager. The configuration we’ve demonstrated here provides a practical starting point that can be adapted to different enterprise PKI environments and certificate requirements across the entire ECK stack.</p><p>Try the approach in your own environment, starting with a non-production ECK cluster, and explore how it can fit into your organization's existing certificate management and security practices.</p><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/eck-certificate-management-enterprise-pki</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/eck-certificate-management-enterprise-pki</guid>
    <category><![CDATA[Kubernetes]]></category>
    <category><![CDATA[Operations]]></category>
    <dc:creator><![CDATA[Anand Vyas]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf2600856d536e9e2/6abe1f2dcd0bd94930d4daef/unnamed.png" length="0" type="image/png"/>
    <pubDate>Thu, 01 Oct 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[How Elasticsearch Serverless hollow shards cut indexing-node shutdowns by 30%]]></title>
    <description><![CDATA[Idle indexing shards in Elasticsearch Serverless now drop their Lucene writers and segment readers from memory until the next write, freeing indexing-tier heap at the cost of a median 218ms wait on that first write.]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/cloud/serverless">Elasticsearch Serverless</a> now unloads idle indexing shards from memory. We call them hollow shards; the shard stays allocated, but its<a href="https://lucene.apache.org/"> Lucene</a> <code>IndexWriter</code> and segment readers are gone until the next write arrives. Hollow shards build on<a href="https://www.elastic.co/search-labs/blog/thin-indexing-shards-elasticsearch-serverless"> thin indexing shards</a>, which already moved Lucene files off the local disk. In the first month in production, indexing-node shutdown times dropped by up to 30% at high percentiles across the fleet, and hundreds of thousands of shards went hollow. On one project with many idle data stream backing indices, shutdown time and indexing-tier heap improved by up to 3x. Search latency is unchanged, and the first write to a hollow shard waits a median 218ms while the shard reloads.</p><p>Thin shards write Lucene (the search library that Elasticsearch uses for indexing and search) segments locally and upload them to the object store as batched compound commits. Then they delete them from disk.</p><p>A thin shard that isn't ingesting and isn't expected to do so in the near future still holds an <code>IndexWriter</code>, segment readers, mappings, and the rest of its engine. At <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams">data stream</a> scale, those idle engines make up most of the indexing heap and add time to the relocations that we need for balancing and for shutting nodes down during autoscaling.</p><p>A <em>hollow indexing shard</em> is a thin allocated primary that keeps only the last commit's metadata and doesn't create its <code>IndexWriter</code> or segment readers until the next write. Search never used them on the indexing node anyway.</p><h2>Why do idle indexing shards use memory in Elasticsearch Serverless?</h2><p>Elasticsearch Serverless stores Lucene commits and translogs in the object store, along with cluster state. Indexing nodes and search nodes are separate tiers. Durability no longer depends on replica shards, and relocating an indexing shard doesn’t copy files from node to node. The <a href="https://www.elastic.co/search-labs/blog/elasticsearch-serverless-stateless-architecture">Symposium on Cloud Computing (SoCC) paper</a> covers that architecture.</p><p>After the files left the node, Lucene's in-memory view of them stayed:</p><ul><li><p>The standard <code>IndexEngine</code> holds an <code>IndexWriter</code> (even with an empty RAM buffer) and a <code>DirectoryReader</code> with its <code>SegmentReader</code>s opened so the writer can see existing documents for updates and deletes, in addition to version checks.</p></li><li><p>It kept per-index mappings and field infos. It also kept shard commit state.</p></li></ul><p>Heap dumps of idle primary shards showed most of the retained memory in the engine's segment readers, on the order of a few megabytes each. The writer and mappings were a smaller slice. A hollow engine cut that footprint, and the node's retained heap fell with it.</p><p>On projects with heavy data stream usage and many backing indices, that’s the difference between the indexing node you need while idle and the one a live engine formula predicts. </p><p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-ingest-autoscaling">Ingest autoscaling</a> still sizes the indexing tier from memory formulas that assume a live engine. Hollow shards exist so we can stop assuming that, once those formulas catch up.</p><h2>How hollow shards differ from fully materialized indexing shards</h2><p>A fully materialized indexing shard runs an <code>IndexEngine</code>. A hollow shard runs a <code>HollowIndexEngine</code> over the same Lucene files, with no search or write path. Table 1 compares the two:</p><p></p><p>
</p><p><strong>IndexEngine (fully materialized)</strong></p><p><strong>HollowIndexEngine (hollow)</strong></p><p>Lucene <code>IndexWriter</code></p><p>Open</p><p>None</p><p>Segment readers</p><p>Open</p><p>None</p><p>Accepts writes</p><p>Yes</p><p>No; ingestion is blocked until the shard is unhollowed</p><p>Local search</p><p>Theoretically possible</p><p>Not possible</p><p>Merges</p><p>Run on the indexing node</p><p>Require unhollowing first</p><p>Engine stats</p><p>Computed live</p><p>Served from stats stored in the hollow commit</p><p>Recovery</p><p>Translog replay and cache prewarm</p><p>No replay, no prewarm, no blob store LIST</p><p>Translog node ID in commit</p><p>Set</p><p>Empty (this is the hollow flag)</p><p><em>Table 1. IndexEngine versus HollowIndexEngine on an indexing node.</em></p><p></p><p>When we hollow, we flush everything to Lucene, commit, and upload to object storage with a hollow flag. We reuse an existing metadata field, the "translog node ID," and leave it empty. <em>Empty</em> means that this is a hollow commit and that recovery has no translog to replay, so it opens a <code>HollowIndexEngine</code> instead of the standard <code>IndexEngine</code>.</p><p>Having no readers makes the <code>HollowIndexEngine</code> a useful trip wire if some call site tries to search on the indexing tier. It also throws if indexing is attempted against it, but ingestion is gated before it reaches the engine, so we can unhollow the shard and reload an <code>IndexEngine</code>.</p><p>Search routing is unchanged; and queries continue to run on the search tier. Indexing routing still points at this primary. Since ingestion is blocked, a write queues until we unhollow.</p><h3>When Elasticsearch Serverless hollows a shard</h3><p>A shard can be hollowed only when all of these hold:</p><ul><li><p>The index isn’t a system index.</p></li><li><p>It has been idle long enough for its type. (A regular index or the current write index of a data stream needs to be idle for one day by default, while older data stream backing indices need to be idle for 15 minutes.)</p></li><li><p>There’s no in-flight ingest or translog upload.</p></li><li><p>No merge is running or scheduled.</p></li></ul><h3>Why the first write to a hollow shard is slower</h3><p>The first write is slower than a write against a warm engine because we unhollow first. We get less idle heap and faster shutdowns, and we accept a hitch on the first bulk to a cold backing index. On data streams that hitch almost never happens, because rolled-over backing indices aren’t the write index. They sit hollow until someone runs an <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-update-by-query">update-by-query</a> or a similar correction.</p><h2>Hollowing a shard during  relocation</h2><p>We only hollow a shard when it relocates. Relocations already block ingestion and already flush, so we reuse that window.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb9f13cdc4d1a9ce0/6abbd0bab2d147043277178e/image8.jpg" alt="Diagram of hollowing a shard during shard relocation: a hollow commit in the object store loads as a HollowIndexEngine" /><p>On the source node:</p><ul><li><p>Ingestion is blocked (for the relocation).</p></li><li><p>If the shard can be hollowed, we force-flush a hollow commit and upload it. If it was already hollow, we skip the flush.</p></li><li><p>The target recovers that commit and sees the empty translog node ID (the hollow flag). It then loads a <code>HollowIndexEngine</code>.</p></li><li><p>We keep ingestion blocked on the target so the first write cannot sneak into the hollow engine.</p></li></ul><p>Hollow recovery skips translog replay and cache prewarm because an idle shard doesn’t need them. If an indexing node restarts, a hollow commit on the object store comes back as a hollow engine.</p><h2>How the first write unhollows a shard</h2><p>The hollow engine cannot index. The first ingest, or a force-merge, has to put an <code>IndexEngine</code> back.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltcda5cef461180d8d/6abbd0d5d89724529109ad55/image1.jpg" alt="Diagram of the first write unhollowing an Elasticsearch Serverless shard, from HollowIndexEngine back to an IndexEngine" /><p>Incoming writes wait behind the ingestion block and trigger one asynchronous unhollowing that:</p><ul><li><p>Runs the recovery work we skipped while hollow, including blob cache prewarming.</p></li><li><p>Closes the <code>HollowIndexEngine</code> and opens an <code>IndexEngine</code>, which is where Lucene's <code>IndexWriter</code> and <code>DirectoryReader</code> get created.</p></li><li><p>Skips translog replay. A hollow commit is clean; there’s nothing to replay.</p></li><li><p>Force-flushes a non-hollow commit, stamped with this node's translog ID, so a crash before the next regular commit upload still knows where the translog lives.</p></li><li><p>Releases the ingestion block. The waiting ingest continues against a real writer.</p></li></ul><p>Reads don’t unhollow, and in general, they don’t come to the indexing tier. Real-time gets can be answered from the search tier. The data is already in the last hollow commit.</p><p>Force-merge is the other explicit materialization point. Serverless does not expose force-merge as a user-facing API, but we still use it internally; for example, on data stream backing indices. A merge needs an <code>IndexWriter</code>, so we unhollow and do the work. We let the shard go hollow again on a later relocation.</p><h2>Swapping the engine on a live shard</h2><p>The hardest part was an invariant that Elasticsearch has had for a long time; that is, once an <code>IndexShard</code> is started and live, the engine it opens is the engine it keeps.</p><p>There was no concept of changing the engine type under a started shard. Hollowing and unhollowing have to swap the engine on a live shard. Call sites that do <code>getEngine().foo()</code> assume the reference that they just got will still be open a few lines later. Two failure modes showed up immediately:</p><ol><li><p>A single engine call in the middle of a reset (for example, <code>AlreadyClosedException</code> on a real-time get).</p></li><li><p>A call site that captures the engine and invokes it more than once (flush, block ingestion, and then flush again) across a swap.</p></li></ol><p>We shipped a new <code>resetEngine()</code> on <code>IndexShard</code> that takes the shard and engine mutexes, plus a <code>withEngine</code> read/write lock so reset is the only writer. Getting there meant chasing deadlocks through refresh listeners, which now receive the reader they need without re-entering the engine, and putting <code>withEngine</code> guards on the call sites that assumed the engine couldn’t vanish. The result is fenced and a bit ugly.</p><h3>Keeping stats and recovery off the Lucene readers</h3><p>Telemetry would have reopened the readers that we had dropped. Doc stats wanted to open a searcher. After we hollow a shard, we compute the basic stats and store them in the hollow commit. We serve them from there, with no reading on the indexing node. </p><p>Recovering a hollow commit used to LIST the blob store and prewarm cache chunks for files that the hollow engine will never read. We skip LIST now and keep the minimum file chunks inside the hollow commit. We skip prewarming unless we’re unhollowing.</p><h2>Current limitations of hollow shards</h2><p>There are two operational risks that we designed for and have not fully taken on yet: </p><ul><li><p>Autoscaling still counts hollow shards as full when it sizes the indexing tier, which is conservative. Later, we’ll charge them for the small footprint they actually have and keep a reservation for shards that might unhollow.</p></li><li><p>The related problem is a burst. After the autoscaling formulas tell the truth, a large update that wakes many backing indices at once could try to materialize more heap than the node has. We plan to throttle concurrent unhollowings against reserved heap.</p></li></ul><h2>Shutdown times and memory usage in production</h2><p>In the first month after the rollout, fleet-wide indexing-node shutdown times improved at high percentiles by up to 30%, as Figure 3 shows. Unhollowings stay rare, while hundreds of thousands of shards have aged into a hollow state.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc964c2002c344f0e/6abbd1182693de74607120cd/image7.png" alt="Charts of hollow shards rising past 600,000 and p85 to p95 indexing node shutdown times dropping after the rollout" /><p>Indexing tier heap usage also dropped across the fleet, though we’re still working out how to publish this footprint to the autoscaler. The stability effect that we actually felt was shutdown time. Nodes finish leaving the fleet faster, which is what autoscaling needed.</p><p>On one project, shutdown times and indexing tier heap improved by up to 3x, as Figure 4 shows:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6a08d8d2718eb1a7/6abbd131d1edc65d4857043a/image3.png" alt="Dashboard of one Elasticsearch Serverless project where shutdown time and indexing tier heap usage dropped about 3x" /><p>We also took heap dumps from that project. Before rollout, segment readers dominated the retained heap. After rollout, those types dropped off the top of the dump and the indexing tier scaled down to smaller pods.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9f473cec04b38e6d/6abbd1437eb65f5810f0e2b2/image6.png" alt="Heap dumps before and after hollow shards: Lucene SegmentReaders dominate an 8GB pod and are gone from a 4GB pod" /><h2>What hollow shards mean for Elasticsearch Serverless projects</h2><p>Thin indexing shards moved Lucene files off the local disk and onto the object store. Hollow shards move Lucene objects off the heap until a write needs them. A hollow commit is a <a href="https://www.elastic.co/search-labs/blog/elasticsearch-refresh-costs-serverless">batched compound commit</a> (BCC) whose empty translog node ID means <em>recover metadata, not a writer</em>. It also uses a live engine swap, which <code>IndexShard</code> didn’t previously allow on a started shard, so the first bulk waits instead of racing the swap.</p><p>In production, high-percentile fleet shutdown times dropped by up to 30%, and we got back some of the indexing tier heap. The cost shows up on the rare first write to an idle shard or when we run a merge.</p><p>If you’re running Elasticsearch Serverless, some of your shards may already be hollow. Idle backing indices should relocate faster during upgrades and scale events, in line with the shutdown time drop in Figure 3, and they should sit cheaper on the indexing tier while they wait for the next write (or never get one).</p><h2>Acknowledgments</h2><p>Hollow shards were built by the Elasticsearch Distributed team. In addition to the authors, we would like to thank Francisco Fernández Castaño, Tanguy Leroux, Benjamin Lerer, Albert Zaharovits, Artem Prigoda, and Henning Andersen, who designed, implemented, and productionized the work. We would also like to thank the wider Elasticsearch engineering team.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-serverless-hollow-shards</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-serverless-hollow-shards</guid>
    <category><![CDATA[Elastic Cloud Serverless]]></category>
    <category><![CDATA[Inside Elastic]]></category>
    <category><![CDATA[Operations]]></category>
    <dc:creator><![CDATA[Iraklis Psaroudakis,Eugene Cistiakovas]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt774d7c59a05d8932/6abbd030e80c0b53acaf0305/image2.jpg" length="0" type="image/jpeg"/>
    <pubDate>Tue, 29 Sep 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Ask Elastic Agent Builder why it's slow: Natural-language trace analysis]]></title>
    <description><![CDATA[Four agent performance questions your Agent Builder traces can answer, covering token spend by model, tool error rates, slow conversation turns, and recent prompts. The ES|QL for each is here, including the type cast SUM() needs.]]></description>
    <content:encoded><![CDATA[<p>Ask Elastic Agent Builder how many tokens your agents burned today, and it writes the Elasticsearch Query Language (ES|QL) and runs it against your OpenTelemetry (OTel) trace data. Then it answers in the chat UI. The same holds for your other agent performance questions: Which tool fails most often? Which conversation turns are slowest? What have users actually been asking? Elastic Agent Builder is designed for this purpose. It allows users to ship agents grounded in your data in minutes. </p><p>If tracing is on, the <code>agent-builder-traces</code> <a href="https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview">skill</a> is already loaded on every agent in your space and there’s nothing to install. The <a href="https://www.elastic.co/search-labs/blog/opentelemetry-tracing-agent-builder">first post</a> in our series covered enabling OTel tracing and the out-of-the-box (OOTB) dashboards, along with threshold alerts. This post is about asking.</p><h2>What is the agent-builder-traces skill?</h2><p><code>agent-builder-traces</code> is a built-in Agent Builder skill. It takes a natural-language question, generates an ES|QL query from it, executes that query against your trace index, and returns a plain-language summary. The index it targets is <code>traces-agent_builder.otel-&lt;space-id&gt;</code>, where <code>&lt;space-id&gt;</code> is the Kibana space that you’re working in. Using the exact space-scoped index pattern, rather than a wildcard, keeps data from other spaces out of your results.</p><p>Under the hood, the skill uses a single inline tool: <code>agent-builder-traces.generate_esql</code>. Instead of calling this tool directly, you ask a question and the agent calls the tool on your behalf, passing your question as the prompt for ES|QL generation. The tool resolves the current space's trace index, builds a query using the default model, executes it against Elasticsearch, and returns the result.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt556675b25cdd19b9/6ab4ad2b69958c862adc8c79/unnamed.png" alt="Elastic Agent Builder chat calling the agent-builder-traces skill to answer a token usage question in plain English" /><h2>Which privacy settings control what your traces capture?</h2><p>Several fields containing sensitive information are off by default. You can enable them in <strong>Gen AI Settings</strong> (<strong>Stack Management</strong> &gt; <strong>AI Assistants</strong>), under the "Agent Builder Traces" section. Expand the <strong>Advanced privacy</strong> settings to find them.</p><p><strong>Setting key</strong></p><p><strong>What it captures</strong></p><p><code>agentBuilder:tracing:includeUserPrompts</code></p><p>User messages</p><p><code>agentBuilder:tracing:includeLlmResponses</code></p><p>Large language model (LLM) response text</p><p><code>agentBuilder:tracing:includeToolDetails</code></p><p>Tool call arguments and results</p><p><code>agentBuilder:tracing:includeSystemPrompt</code></p><p>The agent's system prompt</p><p><code>agentBuilder:tracing:includeRealNames</code></p><p>Real tool, agent, and conversation names (hashed when off)</p><p><code>agentBuilder:tracing:includeRealIds</code></p><p>Real conversation and workflow IDs (hashed when off)</p><p><code>agentBuilder:tracing:includeUserData</code></p><p>Real user IDs and usernames (hashed when off)</p><h3>Why is my trace analysis query returning empty rows?</h3><p>If the skill returns empty rows or tells you that a field is unavailable, check two things: whether the relevant setting is enabled in your configuration, and whether your query time window overlaps with any recorded spans. The skill reports exactly what the query returned; it doesn’t fabricate content when fields are empty..</p><h2>What agent performance questions can you ask?</h2><p>Examples of questions that you can ask include:</p><h3>How many tokens have my agents used, by model?</h3><p><strong>Ask:</strong> <em>How many input and output tokens have my agents used in the last 24 hours, broken down by model?</em></p><p><strong>ES|QL:</strong> </p><p>The <code>TO_LONG()</code> cast around each token field is required. These fields can surface as mixed integer and long types across index generations, and ES|QL’s <code>SUM()</code> needs an explicit numeric conversion before it can aggregate them. If you write your own queries against this index and hit unexpected type errors, this is usually why.</p><h3>Which tools are failing most often?</h3><p><strong>Ask:</strong> <em>What is the error rate for each tool over the last 7 days?</em></p><p><strong>ES|QL: </strong></p><p>The tool error rate query aggregates across spans named <code>‘execute_tool’</code> and compares <code>status.code == "Error"</code> counts to total counts per tool name. The result shows which tools are failing most often. </p><h3>Which conversation turns are slowest?</h3><p><strong>Ask:</strong> <em>Show me the 10 slowest conversation turns in the last hour.</em></p><p><strong>ES|QL: </strong></p><p>The skill queries spans where <code>span.name LIKE "invoke_agent *"</code> and <code>attributes.elastic.inference.span.kind == "CHAIN"</code>. These correspond to individual conversation turns, from the moment a user sends a message to when the agent returns a response. Durations are stored in nanoseconds, so the skill converts to seconds before sorting.</p><h3>What have users been asking my agents?</h3><p><strong>Ask:</strong> <em>What questions have users been asking in the last 30 minutes?</em></p><p><strong>ES|QL:</strong> </p><p>The recent-prompts query returns data only when <code>agentBuilder:tracing:includeUserPrompts</code> is set to <code>true</code>. When the setting is off, the <code>attributes.gen_ai.input.messages</code> field is empty and the skill will suggest that you check your<strong> Include User Prompts</strong> privacy setting.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt207b044228f7d38b/6ab4adecf2c7a075d08b4ba0/unnamed.png" alt="Agent Builder trace analysis showing empty input messages because the Include User Prompts privacy setting is off" /><h2>When should you use Discover or Kibana Lens instead?</h2><p>The skill targets one index pattern: <code>traces-agent_builder.otel-&lt;space-id&gt;</code>. Two scenarios fall outside that boundary.</p><h3>Can the skill query my own application indices?</h3><p>If you want to run ad hoc questions against data that you've indexed yourself, use a general data exploration skill or write ES|QL directly in Discover. The <code>agent-builder-traces</code> skill won’t query outside the Agent Builder traces index.</p><h3>Can the skill build or edit dashboards?</h3><p>The skill focuses on ad hoc queries and generating summary text. If your goal is to build a permanent visualization from your trace data, use Lens or the <code>dashboard-management</code> skill instead. We covered that specific process in depth in the first post in our series.</p><p>The skill sits alongside two other ways of watching your agents.</p><p><strong>Tool</strong></p><p><strong>Best for</strong></p><p><strong>You use it when</strong></p><p><code>agent-builder-traces</code> skill</p><p>Ad hoc agent performance questions</p><p>You notice something and want an answer without leaving the chat</p><p>OOTB dashboards</p><p>Ongoing visibility</p><p>You want trends over time without asking anything</p><p>Threshold alerts</p><p>Automated monitoring</p><p>You define a condition once and get notified when it's breached</p><h2>How to build evaluation pipelines from agent trace data</h2><p>Spans in <code>traces-agent_builder.otel-&lt;space-id&gt;</code> capture real execution data from every conversation turn and LLM call running in your space. That record is the raw material for evaluation pipelines: automated checks on response quality and latency budgets per agent type, along with regression detection when you update a system prompt. The next post in our series covers how to build those eval loops from trace data that Agent Builder is already collecting.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/agent-performance-trace-analysis-agent-builder</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/agent-performance-trace-analysis-agent-builder</guid>
    <category><![CDATA[Agentic AI]]></category>
    <category><![CDATA[Operations]]></category>
    <category><![CDATA[ES|QL]]></category>
    <dc:creator><![CDATA[Meghan Murphy]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfc61c395d7859ef8/6ab4acdee2d12a17d37d48c9/unnamed.jpg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 24 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[ 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[No more allocation delays: Decoupling snapshots from shard relocation in stateless Elasticsearch]]></title>
    <description><![CDATA[Clusters scale out under load without waiting for a snapshot to finish, because snapshots now read straight from the object store and no longer pin shards in place.]]></description>
    <content:encoded><![CDATA[<p>Stateless snapshots now read directly from the object store. Server-side "undesired allocation due to snapshot" warnings stopped entirely after the release. Primary shards stay free to relocate while a snapshot runs, so clusters scale out under load without waiting for one to finish. Across the fleet, cache misses dropped by more than 60% and median cache population throughput rose roughly 50%. </p><h2>Why snapshots pin primary shards in stateful Elasticsearch</h2><p>In traditional stateful Elasticsearch, snapshots lock primary shards to their active nodes, completely preventing relocation. That works fine when cluster topology is stable and nodes stay online between maintenance windows, but stateless Elasticsearch works differently. Index data lives in an external object store, with local disk as a cache, and the cluster scales automatically based on CPU, memory, and data size, both vertically (upsizing nodes) and horizontally (adding nodes).</p><p>During vertical scale-up, existing nodes must vacate all shards and shut down before new hardware takes over. Since Elasticsearch version 8.13, shard snapshots can pause during node shutdowns and resume after relocation, so a long-running snapshot doesn't block an infrastructure update.</p><p>Horizontal scale-out is a different story: No nodes shut down, so pause logic never triggers. New nodes sit idle while existing nodes finish their snapshots, and as soon as a snapshot is queued, the primary shard is pinned to its node, significantly delaying relocation.</p><p>Clusters typically scale out because they're already under heavy load. Blocking shard relocations at that moment limits the cluster's ability to reduce pressure, which shows up as degraded indexing throughput and higher latency. The resource imbalance can also trigger unexpected autoscaling behavior. And even when overall topology stays the same, shard locking disrupts hotspot mitigation and workload distribution. These failures used to surface as server-side warnings: "undesired allocation due to snapshot."</p><p>
</p><p><strong>Stateful Elasticsearch</strong></p><p><strong>Stateless Elasticsearch</strong></p><p>Snapshot reads from</p><p>Local shard data on the node holding the primary</p><p>The object store, using file locations recorded in the commit</p><p>Primary shard during snapshot</p><p>Pinned to its node until the snapshot completes</p><p>Free to relocate at any time</p><p>Effect on horizontal scale-out</p><p>New nodes wait for in-flight snapshots before taking shards</p><p>New nodes take shards immediately, regardless of snapshot state</p><p></p><h2>How stateless snapshots read directly from the object store</h2><p>A shard snapshot pins primary shards because it needs to read local shard data. In stateless Elasticsearch, that data already lives in the object store, so reading from local disk is unnecessary. Letting snapshots read directly from the object store removes the requirement to lock primary shards. They can relocate freely, and backup is decoupled from cluster balancing.</p><p>Stateless commits include location information for each data file in the object store, so snapshots can read and stream directly to the snapshot repository (a separate object store bucket). In the future, we plan to look at server-side ranged copies, which object stores support natively, to skip the local copy step entirely.</p><h2>Tracking commits when shards relocate mid-snapshot</h2><p>A snapshot is bound to a specific commit point that determines which files to back up, and those files must remain accessible for the full duration of the operation. In stateful clusters, this is simple: The snapshotting node and the data node are the same, so the commit is managed locally and held until completion.</p><p>In a stateless model, the snapshotting node and the data node can be entirely separate, or they can diverge if a shard relocates mid-snapshot. To handle this, we added a transport action that acquires commits on remote data nodes over the network. The data node tracks which commit belongs to which snapshot and releases it once cluster state signals completion.</p><p>There's a wrinkle during relocation. A stationary shard relies on its commit point to preserve files. A relocating shard must release its commit so its local store can close cleanly. To keep files accessible through that transition, a newly recovered primary temporarily preserves all existing data files in the object store until notified of snapshot completion via cluster state. This handles both graceful relocations and ungraceful recovery from node or engine failures.</p><h2>No more allocation delays and improved cache stats</h2><p>After stateless snapshots shipped, the server-side "undesired allocation due to snapshot" warnings stopped. The chart below shows the before and after, with the release marked by the red arrow. Hotspot mitigation became more responsive because shard relocations no longer had to wait for backup operations.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0c08d57928ea1c6c/6a97a33f2707c544c5329695/unnamed.png" alt="Bar chart showing undesired allocation due to snapshot warnings dropping to zero after stateless snapshots shipped" /><p>Cache use is also improved. Snapshots that bypass local shard data stop competing with indexing for cache space. After the release (also marked in the chart), we observed the following two positive changes in cache metrics:</p><ol><li><p>The median cache population throughput, defined as bytes per second for filling the local disk cache from the object store, increased about 50%.</p></li><li><p>Cache misses, where data must be retrieved from the object store to fill local disk cache, have dropped more than 60%. </p></li></ol><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0b85caa56b35485d/6a97a3683eabd0c326440fab/unnamed_(1).png" alt="Charts showing cache population throughput rising 50% at p50 and cache misses falling over 60% after release" /><h2>What comes after stateless snapshots</h2><p>Object-store-native architectures are increasingly the standard for cloud-native data systems, and stateless snapshots are a step toward fully exploiting that model across Elasticsearch operations. Backups read from the object store, and shards move freely. Neither process waits on the other. Removing the local shard dependency is a step toward further modularizing the stateless architecture.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/stateless-snapshots-shard-relocation</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/stateless-snapshots-shard-relocation</guid>
    <category><![CDATA[Elastic Cloud Serverless]]></category>
    <category><![CDATA[Inside Elastic]]></category>
    <category><![CDATA[Operations]]></category>
    <dc:creator><![CDATA[David Turner,Yang Wang]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt685f7746435d0451/6a97a2aaf08ee14b39853cbb/unnamed.png" length="0" type="image/png"/>
    <pubDate>Wed, 02 Sep 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Dashboard activity logs: Find out which Kibana dashboards get used]]></title>
    <description><![CDATA[Kibana now logs who viewed, edited or deleted each dashboard, how long it took and what failed, so you can catch a broken dashboard before anyone reports it.]]></description>
    <content:encoded><![CDATA[<p>Kibana logs every dashboard view, edit, create, delete, and refresh, along with the user behind each one. Two lines in <code>kibana.yml</code> enable this logging. When you point Discover at the index, you can find the dashboards that nobody opened in 30 days and rank them by load time or see who edited the one that broke this morning. Dashboard <a href="https://www.elastic.co/docs/reference/kibana/user-activity">activity logs</a> run on self-managed clusters today, with Elastic Cloud support coming.</p><h2>How dashboard activity logs differ from the Kibana audit log</h2><p>Dashboard activity logs and the <a href="https://www.elastic.co/docs/reference/kibana/kibana-audit-events">Kibana audit log</a> both write structured logs about user actions, but they answer different questions.</p><p>
</p><p><strong>Kibana audit log</strong></p><p><strong>Dashboard activity logs</strong></p><p>Answers</p><p>Who accessed what, and when</p><p>Which dashboards are used, and how well they perform</p><p>Built for</p><p>Security and compliance teams</p><p>Kibana admins and dashboard owners</p><p>Tracks</p><p>Security-relevant events across Kibana</p><p>Five dashboard actions: create, update, delete, view, refresh</p><p>Enabled by</p><p>Its own setting in <code>kibana.yml</code></p><p><code>user_activity.enabled: true</code> in <code>kibana.yml</code></p><h2>What dashboard activity logs capture</h2><p>The user activity service records structured events every time a user interacts with a dashboard. Each event captures <em>what happened</em> and <em>to which dashboard</em>, in addition to <em>who did it</em>. Five actions are tracked:</p><p><strong>Action</strong></p><p><strong>Fires when</strong></p><p><strong>Includes duration</strong></p><p><code>dashboard_create</code></p><p>A dashboard is created</p><p>No</p><p><code>dashboard_update</code></p><p>An edit is saved</p><p>No</p><p><code>dashboard_delete</code></p><p>A dashboard is removed</p><p>No</p><p><code>dashboard_view</code></p><p>A user opens a dashboard</p><p>Yes, time on the dashboard until they navigate away</p><p><code>dashboard_refresh</code></p><p>A user changes filters or time range, or auto-refresh runs</p><p>Yes, refresh duration</p><p>Very often, a <code>dashboard_view</code> event also triggers a refresh.</p><p>Every event carries the same core fields, with two that appear conditionally:</p><p><strong>Field</strong></p><p><strong>What it holds</strong></p><p><strong>Present on</strong></p><p><code>user.name</code></p><p>Name of the user who performed the action</p><p>Every event</p><p><code>user.email</code></p><p>Email address of the user</p><p>Every event</p><p><code>user.roles</code></p><p>Roles assigned to the user</p><p>Every event</p><p><code>object.name</code></p><p>Dashboard name</p><p>Every event</p><p><code>object.id</code></p><p>Dashboard ID</p><p>Every event</p><p><code>kibana.space</code></p><p>Kibana space the dashboard belongs to</p><p>Every event</p><p><code>client.ip</code></p><p>IP address the request came from</p><p>Every event</p><p><code>event.action</code></p><p>Which of the five actions occurred</p><p>Every event</p><p><code>event.outcome</code></p><p>Whether the action succeeded or failed</p><p>Every event</p><p><code>event.duration</code></p><p>Time taken, in nanoseconds</p><p><code>dashboard_view</code> and <code>dashboard_refresh</code></p><p><code>error.type</code> / <code>error.message</code></p><p>Error class and message when something fails</p><p>Events where <code>event.outcome</code> is <code>failure</code></p><h2>How Kibana records dashboard activity</h2><p>Under the hood, Kibana plugins report events from the browser or server through a core client, and valid events are written to a dedicated logger. No data is sent to a third party; because events are standard JSON logs, you control where they go and how they’re ingested.</p><h2>What you can do with dashboard usage data</h2><p>Dashboard activity data answers five operational questions that would otherwise require further investigation:</p><ul><li><p><strong>Clean up unused dashboards.</strong> Filter for dashboards with zero <code>dashboard_view</code> events. If nobody's looking at it, archive it. This is critical for customers who are managing thousands of dashboards. </p></li><li><p><strong>Troubleshoot performance.</strong> The <code>event.duration</code> field tells you exactly how long each dashboard load or refresh takes. Sort by duration to find your slowest dashboards.</p></li><li><p><strong>Edit history.</strong> Every create, update, and delete is logged with the user who made the change. You no longer have to wonder who modified a critical dashboard or when it happened.</p></li><li><p><strong>Plan capacity.</strong> Identify users running heavyweight queries during peak hours. If one user's auto-refresh is hammering the cluster every 10 seconds, you'll see it.</p></li><li><p><strong>Monitor errors proactively.</strong> Dashboards throwing errors surface immediately through <code>error.type</code> and <code>error.message</code> fields, so you don’t need to wait for users to report them.</p></li></ul><h2>How to enable dashboard activity logs in Kibana</h2><p>Add two lines to your <code>kibana.yml</code> ( the service is disabled by default):</p>user_activity:
  enabled: true<p>Events will start flowing immediately using a default JSON console appender. You can customize the output appender and filter specific actions using the same logging configuration schema that Kibana already uses:</p>user_activity:
  enabled: true
  appenders:
    console_json_default_appender:
      type: console
      layout:
        type: json
  filters:
    - policy: keep
      actions: [dashboard_view, dashboard_refresh]<p>Ship these logs into an Elasticsearch index (for example, via Filebeat), and you have a fully queryable dataset of dashboard usage.</p><h2>How to query dashboard activity in Discover</h2><p>Once your activity logs are indexed, open Discover and point it at your user activity index pattern. You'll immediately see every dashboard interaction as a structured event, and they’re filterable by action type, user, dashboard name, and time range.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4c49281e39a0301e/6a950fc6a3077ca19c3fe663/1.png" alt="Kibana Discover showing dashboard activity logs with event.action, object.name, duration and outcome columns" /><p>From here, you can quickly answer specific questions like the examples below. </p><h3>How many times was a dashboard viewed? </h3><p>Type your question in natural language in the Discover query editor, and press <strong>Cmd+J</strong> to automatically generate the Elasticsearch Query Language (ES|QL) query, as shown below:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt68dce42018ead590/6a9510d5a16336aae0371701/2.gif" alt="Generating an ES|QL query from plain language in Discover to search dashboard activity logs" /><h3>Which dashboards had zero views in the last 30 days?</h3><p>Dashboards with no activity simply don't appear in the logs, so you can't filter directly for zero views. Instead, this query works backward, pulling every dashboard created (and not deleted) in the past year and then checking which of those had zero views in the last 30 days.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta5fe21fc80d3e276/6a9510f5923082d373c4a4f0/3.png" alt="ES|QL query on dashboard activity logs listing 30 Kibana dashboards with zero views in the last 30 days" /><h3>Which dashboards took longer than 10 seconds to load?</h3><p>Note that <code>event.duration</code> is recorded in nanoseconds, so the query converts to seconds before filtering:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltaa956eb37e5278e8/6a95113b6fe1457f2bb1bf89/4.png" alt="ES|QL query ranking slow Kibana dashboards by load time, topped by Host Metrics Overview at 69 seconds" /><h3>Which dashboards are throwing errors, and what's failing?</h3><p>This query shows dashboards with one or more panels throwing errors during <code>dashboard_refresh</code> events, so you can quickly spot recurring issues and prioritize fixes:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8911faf07ccd694f/6a951153ecbe18f7691e19c0/5.png" alt="Dashboard activity logs showing failed dashboard refreshes grouped by error, with 17 errors on one dashboard" /><h2>Build a dashboard usage overview with AI chat</h2><p>We’re planning to add out-of-the-box dashboards along with the activity logs, but in the meantime, instead of manually building visualizations, open the <a href="https://www.elastic.co/docs/explore-analyze/ai-features/agent-builder/chat">AI chat</a> in Kibana and ask it to create a dashboard from your user activity data. </p><p>The generated dashboard gives you at-a-glance visibility into your most-viewed dashboards, heaviest users, slowest-performing panels, and recent errors; that is, exactly the operational view that large deployments need.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0734798e18f535af/6a9511928814aa03da89c1be/6.gif" alt="Building a dashboard usage overview from the user-activity-logs index using Agent Chat in Kibana" /><h2>Get started with dashboard activity logs</h2><p>User activity logs are available in Kibana 9.5. Begin by enabling the service. Then ship the logs into an index, and start building the operational visibility that your team has been asking for. For full configuration details and the complete event schema, see the <a href="https://www.elastic.co/docs/reference/kibana/user-activity">user activity documentation</a>.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/dashboard-activity-logs-kibana</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/dashboard-activity-logs-kibana</guid>
    <category><![CDATA[Kibana]]></category>
    <category><![CDATA[Operations]]></category>
    <category><![CDATA[Analytics]]></category>
    <dc:creator><![CDATA[Teresa Alvarez Soler,Rudolf Meijering]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt505cee76dae8eff8/6a950f70e657a3cdea75aeb7/image4.png" length="0" type="image/png"/>
    <pubDate>Mon, 31 Aug 2026 15:15:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Three SLOs every search team needs: monitoring search latency, availability and quality with OpenTelemetry]]></title>
    <description><![CDATA[Your OpenTelemetry search spans already carry the signals for SLOs, burn rate alerts, anomaly detection and incident response, and this post shows how to build all four in Elastic Observability.]]></description>
    <content:encoded><![CDATA[<p>Every search request your API handles already emits an OpenTelemetry span with latency, error, and result count data. You built that instrumentation for product analytics. Turns out it also gives you search monitoring for free. This post takes those spans and turns them into three SLOs (99% of queries under 250ms, 99.9% availability, zero-results rate below 15%), then layers on alerting, anomaly detection and an incident response workflow, all with Elastic Observability's built-in tooling. If you instrumented your search API following Blogs 2-4, you can set this up in an afternoon.</p><h2>What you'll discover</h2><p>In this post, you'll learn how to:</p><ul><li><p>Use Elastic APM's built-in views to explore search latency and throughput and to explore errors.</p></li><li><p>Define Service Level Objective (SLOs) for search, including latency targets and availability, along with search quality.</p></li><li><p>Create SLOs in Kibana that track your search health over time, with burn rate alerting.</p></li><li><p>Build operational dashboards with Elasticsearch Query Language (ES|QL) that show latency percentiles and time breakdowns and that include trends.</p></li><li><p>Set up alerts for latency regressions and error spikes, along with zero-results rate increases.</p></li><li><p>Establish an incident response pattern for search degradation.</p></li></ul><h2>What you'll need</h2><ul><li><p>Search instrumentation from <a href="https://www.elastic.co/search-labs/blog/series/search-analytics-opentelemetry">Blogs 2–4</a> (search spans with <code>search.*</code>  attributes in Elastic).</p></li><li><p>Kibana access with permissions to create SLOs and alert rules.</p></li><li><p>Basic understanding of SLOs. (We'll explain the search-specific parts.)</p></li><li><p>An Elastic cluster with an Enterprise subscription, an <a href="https://www.elastic.co/cloud?utm_campaign=G-TXT-EMEA-UK+CA-Core-EN-Lead_Gen-CloudTrials-BR&amp;utm_content=Brand-Cloud&amp;utm_source=google&amp;utm_medium=cpc&amp;device=c&amp;utm_term=elastic%20cloud%20trial&amp;utm_id=701610000005lJVAAY&amp;gad_source=1&amp;gad_campaignid=22979576770&amp;gbraid=0AAAAADrDgoKn2OUpnHv5-QMO2ZrcBzj4K&amp;gclid=Cj0KCQjwjb3SBhDgARIsAMKiWziycspjFFEKHsgcIEsVdAYu6qNwIrTL27hQoMuX4eZbcm9oFox0tBYaAmH1EALw_wcB">Elastic Cloud trial</a>, or <a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/local-development-installation-quickstart">a local deployment</a> with the trial activated.</p></li></ul><h2>Why search monitoring matters beyond cluster health</h2><p>Search is the primary navigation path for a significant share of visitors, and search-initiated sessions tend to show stronger purchase intent than browse sessions. A search outage is a revenue event, rather than a minor feature degradation. A latency regression from 100ms to 500ms changes user behavior before anyone files a ticket.</p><p>Most platform teams monitor search at the infrastructure level, checking whether the Elasticsearch cluster is healthy and whether nodes are responding. They also determine whether the disk is full. These are all necessary but not sufficient. A cluster can be green while search quality silently degrades; for example, queries returning stale data after a bad index deployment or latency creeping up as the index grows. This could also include zero-results rates climbing because a synonym list wasn't updated.</p><p>The gap is between "search is up" and "search is working well."</p><p><strong>A note on examples:</strong> As we have throughout this series, we use ecommerce search for concrete examples, but these reliability patterns apply equally to any search application, including content platforms, internal knowledge bases, job boards, and support portals.</p><h3>OpenTelemetry search spans as monitoring signals</h3><p>If your team followed <a href="https://www.elastic.co/search-labs/blog/series/search-analytics-opentelemetry">Blogs 2–4</a> in this series, every search request already emits an OTel span with <code>search.*</code> attributes. These include the query text, result count, Elasticsearch execution time, and error status. Those spans land in <code>traces-generic.otel-default</code>  in Elastic.</p><p><strong>Following along with code?</strong> The <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/search-analytics-otel">reference project</a> has all the instrumentation from <a href="https://www.elastic.co/search-labs/blog/series/search-analytics-opentelemetry">Blogs 2–4</a>. Generate traffic, and then follow along with the SLO and dashboard setup below. See <code>queries/blog6_reliability.esql </code> for ready-to-run queries.</p><p>The search team built that instrumentation for <em>product analytics</em>; that is, understanding what users search for and measuring click-through rates (CTRs) and conversion rates. They’ve prioritized relevance work, but the same spans contain everything you need for operational monitoring. Span duration gives you a latency signal, and <code>search.result_count == 0</code> value reflects quality. Span errors point to availability signals.</p><p>This post shows how to put this operational value to work, beginning with what you can see right now in Kibana and then building SLOs and alerting on top of it, along with incident response.</p><h2>Search monitoring out of the box with Elastic APM</h2><p>Before building anything new, let's look at what Elastic APM already gives you out of the box.</p><p>If your search API is instrumented with Elastic Distribution of OpenTelemetry (EDOT) (as in <a href="https://www.elastic.co/search-labs/blog/search-analytics-opentelemetry-esql">Blog 2</a>), it appears automatically as a service in the Elastic APM UI. Open <strong>Observability </strong>&gt;<strong> APM </strong>&gt;<strong> Services</strong> in Kibana, and select your search service (named <code>search-analytics-demo</code> if you're using the reference project). You'll immediately see the following:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt901b364e81dae2dc/6a8d6f3e0897900dc2ef9425/image6.gif" alt=" Elastic APM service overview page for an OpenTelemetry-instrumented search API showing Overview, Transactions and Errors tabs" /><h3>Elastic APM service overview for search</h3><p>The service overview page shows latency distribution and throughput over time, along with error rate, and doesn’t require configuration. You can see at a glance whether search is healthy, and the time-series charts make regressions obvious. If latency crept up after yesterday's deployment, you'll see it here.</p><h3>Trace waterfall: breaking down search request latency</h3><p>Click into any transaction, and you'll see the <em>trace waterfall</em>, which is a visual breakdown of every span in the request. For a search API call, this typically shows:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb005066de9a98d55/6a8d6f82b74e9d482044a3b3/blog6-trace-waterfall-span-details.gif" alt="Elastic APM transaction view for POST /api/search showing search latency distribution, throughput, and failed request rate" /><p>The waterfall makes the invisible visible. You can see that the 503ms API response breaks down into HTTP handling, a 241ms query rules lookup, and a 260ms Elasticsearch query,  plus the custom <code>search</code> span (36ms) carrying all of our <code>search.*</code> attributes. Click any span, and the metadata flyout shows exactly what was captured: <code>search.query: "usb hub"</code><code>,</code> <code>search.result_count: 33</code>, <code>search.took_ms: 43</code>, the index name, hit IDs, and more.</p><p><a href="https://www.elastic.co/search-labs/blog/search-analytics-opentelemetry-esql">Blog 2</a> discussed the gap between <code>search.took_ms </code> and span duration. The waterfall shows you exactly where that gap lives, without writing any queries.</p><h3>Automatic search error capture with OpenTelemetry</h3><p>One of the most valuable things OTel auto-instrumentation gives you is <em>automatic error capture</em>. When an Elasticsearch query fails because of issues like a tripped circuit breaker or a timeout, or if an index isn’t found,  the span records the exception type and message, along with the stack trace. <a href="https://www.elastic.co/search-labs/blog/search-conversion-tracking-opentelemetry">Blog 4</a> mentioned this as a side benefit of span-based conversion tracking; here it becomes an operational lifeline.</p><p>The errors tab on your service page automatically aggregates these, grouped by error type and frequency. The instrumentation captures the details for you, so you don't need custom error handling or logging. During an incident, this is often the fastest way to understand what's actually failing.</p><h3>Service map: search API and Elasticsearch dependencies</h3><p>The <a href="https://www.elastic.co/docs/solutions/observability/apm/service-map">service map</a> shows dependencies between your search API and Elasticsearch, making it easy to see whether a latency problem is in your service or in the cluster it depends on.</p><p>All of this is available the moment you deploy the instrumentation from <a href="https://www.elastic.co/search-labs/blog/search-analytics-opentelemetry-esql">Blog 2</a>, without building any dashboards or writing any queries. This is the foundation everything else in this post builds on.</p><h2>Defining service level objectives for search</h2><p>An SLO defines <em>good enough</em> in measurable terms. You define what <em>working</em> means, measure it continuously, and alert when you're burning through your error budget too fast, instead of reacting when something breaks.</p><p>Elastic Observability has a <a href="https://www.elastic.co/guide/en/observability/current/slo.html">built-in SLO framework</a> that handles <a href="https://www.elastic.co/guide/en/observability/current/slo.html#slo-important-concepts">Service Level Indicator</a> (SLI) calculation and budget tracking. It also takes care of burn rate alerting. You create SLOs directly in Kibana. No ES|QL or custom pipelines are required for the core indicators.</p><h3>Three SLOs every search service needs</h3><p>Navigate to <strong>Observability </strong>&gt; <strong>SLOs</strong> in Kibana, and click <strong>Create SLO</strong>. The <a href="https://www.elastic.co/guide/en/observability/current/slo-create.html">SLO creation workflow</a> walks you through three steps: Define the SLI (what to measure), set the objective (the target), and describe the SLO.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2ea1fc523a27cb57/6a8d7037bc1d3a69d7701bba/blog6-slo-creation-form.gif" alt="Creating a search latency SLO in Kibana showing SLI preview chart, rolling time window, and 99% target objective setting" /><h4>1. Search latency SLO: 99% of queries under 250ms</h4><p>Indicator type: Elastic APM latency target: 99% of searches complete in under 250ms.</p><p>The Elastic APM latency indicator is purpose-built for this. Select your search service (<code>search-analytics-demo</code>), and set the threshold to 250ms.  Elastic handles the rest, including calculating the percentage of transactions below the threshold and tracking your error budget over time.</p><p>Note: The Elastic APM latency indicator measures all HTTP transactions for the <code>search-analytics-demo</code> service, including health checks and click/cart/checkout endpoints, along with static asset requests, not just the <code>POST /api/search</code> endpoint. For a search-only latency SLO, use a custom Kibana Query Language (KQL) indicator with <code>name: "search" AND attributes.search.query: *</code> on the <code>traces-generic.otel-default</code><code> index</code>. The Elastic APM indicator is still valuable for whole-service health. For full coverage, combine both.</p><p>This measures end-to-end span duration; that is, what the user actually experiences. If your Elasticsearch query takes 50ms but the user waits 300ms because of network overhead or slow application logic, this SLO catches it. Use <code>search.took_ms</code> in the Elastic APM waterfall to diagnose <em>where</em> the latency lives when the SLO starts burning.</p><h4>2. Search availability SLO: 99.9% success rate</h4><p>Indicator type: Elastic APM availability <strong>t</strong>arget: 99.9% of searches succeed.</p><p>The Elastic APM availability indicator calculates the percentage of successful transactions for your service. When the Elasticsearch client throws an exception or the search endpoint returns a 5xx, the span's status records an error and this SLO counts it.</p><p>Note: Like the latency SLO, the Elastic APM availability indicator covers all HTTP transactions on <code>search-analytics-demo</code>, not just <code>POST /api/search</code>. Click/cart/checkout errors will consume this budget. For a search-only availability SLO, use a custom KQL indicator with <code>name: "search" AND attributes.search.query: *</code> for good events and <code>name: "search"</code> as the total query.</p><p>An 0.1% error budget on 100,000 daily searches means that you can tolerate 100 errors per day. That's tight, but search errors are hard failures and the user gets nothing. Availability SLOs should be stricter than latency SLOs.</p><h4>3. Search quality SLO: tracking zero-results rate</h4><p>Indicator type: Custom KQL target: 85% of searches return at least one result (zero-results rate &lt; 15%); index: <code>traces-generic.otel-default</code>; <strong>g</strong>ood query: <code>name: "search" AND attributes.search.result_count &gt; 0</code>; total query:<code>name: "search" AND attributes.search.query: *</code>.</p><p></p><p>Note on KQL versus ES|QL: The SLO framework uses KQL for its indicator filters rather than ES|QL. KQL uses <code>field: value</code>syntax and is the same language you see in the Kibana search bar. The ES|QL queries throughout this series are for ad hoc analysis and dashboards; KQL here is the SLO indicator's document filter. Both query the same <code>traces-generic.otel-default</code> index.</p><p>This is the SLO that surprises most teams. A search that returns an empty result set isn't an error; HTTP status is 200 and the span status is OK. Plus, no exception was thrown. But from the user's perspective, it failed. They asked for something and got nothing.</p><p>The quality SLO uses the custom KQL indicator type because it relies on our custom <code>search.result_count</code> attribute, which the built-in Elastic APM indicators don't know about. But the SLO framework handles everything else, including budget tracking and burn rate calculation, along with alerting.</p><p>A sudden spike in zero-results rate, such as from 12% to 40% over an hour, is almost always an infrastructure event, like a failed index deployment or a mapping change that broke queries. It could also be a synonym list misconfiguration. That's an operational problem, not a relevance problem.</p><h3>Reading your search health in the SLO overview</h3><p>Once the latency, availability and quality SLOs are created, the SLO overview page shows your search health at a glance:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9e9ed72bf5840749/6a8d706fd05d4c5a8957f946/blog6-slo-overview-drill-in.gif" alt="Search Latency SLO detail showing 50% observed against 99% objective, burn rate windows, historical SLI, and error budget" /><p></p><p>Each SLO shows the current value, the target, the remaining error budget, and the burn rate. Green means <em>healthy</em>:  Search availability is at 100%, and Search quality is just above its 85% target. Red means <em>violated</em>: Search latency is at 50% against a 99% objective, with the burn rate breached at 200x the sustainable rate. When a budget bar starts shrinking faster than expected, you know something changed, even before users complain.</p><p>Clicking into the SLO detail shows burn rate across multiple time windows (1h, 6h, 24h, 72h) and the historical SLI trend. It also shows remaining error budget. For the latency SLO, the Elastic APM latency indicator tracks the percentage of transactions below your 250ms threshold. For the quality SLO, the custom KQL indicator uses <code>traces-generic.otel-default</code> with the good query filtering for <code>attributes.search.result_count &gt; 0</code>. This is where the custom <code>search.*</code> attributes from <a href="https://www.elastic.co/search-labs/blog/search-analytics-opentelemetry-esql">Blog 2</a> pay off, since they're the foundation of meaningful SLOs.</p><h2>Burn rate alerting for search SLOs</h2><p>When you create an SLO through the Kibana UI, a default burn rate alert rule is automatically created. This is where the real operational value lives.</p><p><a href="https://www.elastic.co/guide/en/observability/current/slo-burn-rate-alert.html">Burn rate alerts</a> improve on threshold alerts ("error rate &gt; 1%"), which are noisy and miss slow degradation. : Burn rate alerts measure how fast you're consuming your error budget relative to the SLO window.</p><p>A burn rate of 1.0 means that you're spending budget at exactly the sustainable rate, but a burn rate of 10.0 means that you're burning 10x too fast and you'll exhaust the budget in 1/10th of the window.</p><p>The default burn rate rule uses a multi-window approach, with four severity levels:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5794fbcc015d4076/6a8d709811115542160cfc88/blog6-burn-rate-alert-config.gif" alt="Selecting alert rule types in Elastic Observability including anomaly detection, APM anomaly, custom threshold and SLOs" /><p></p><p><strong>Severity</strong></p><p><strong>Burn rate</strong></p><p><strong>Long window</strong></p><p><strong>Short window</strong></p><p><strong>What it means</strong></p><p>Critical (page)</p><p>&gt; 14.4x</p><p>1 hour</p><p>5 minutes</p><p>Exhausts budget in ~50 hours</p><p>High (ticket)</p><p>&gt; 6.0x</p><p>6 hours</p><p>30 minutes</p><p>Exhausts budget in ~5 days</p><p>Medium (review)</p><p>&gt; 3.0x</p><p>24 hours</p><p>120 minutes</p><p>Exhausts budget in ~10 days</p><p>Low (awareness)</p><p>&gt; 1.0x</p><p>72 hours</p><p>360 minutes</p><p>Trending toward exhaustion</p><p>The short window prevents alerting on brief spikes that self-resolve, and the long window catches sustained degradation. Together, they balance responsiveness with alert fatigue.</p><h3>Routing search alerts to PagerDuty, Slack and Jira</h3><p>Alerts are only useful if they reach the right people in the right tools. Elastic's <a href="https://www.elastic.co/guide/en/kibana/current/alerting-getting-started.html">alerting framework</a> supports a wide range of <a href="https://www.elastic.co/guide/en/kibana/current/action-types.html">connectors</a> out of the box, including:</p><ul><li><p><strong>Incident management:</strong> PagerDuty, Opsgenie, xMatters for on-call routing.</p></li><li><p><strong>Chat:</strong> Slack, Microsoft Teams for team notifications.</p></li><li><p><strong>Case management:</strong> <a href="https://www.elastic.co/guide/en/kibana/current/jira-action-type.html">Jira</a>, <a href="https://www.elastic.co/guide/en/kibana/current/servicenow-action-type.html">ServiceNow</a> for automatic ticket creation when SLOs breach.</p></li><li><p><strong>Custom:</strong> Webhooks for integrating with any system via HTTP.</p></li></ul><p>You can also use Elastic's built-in <a href="https://www.elastic.co/guide/en/kibana/current/cases.html">cases</a> to track incidents directly within Kibana, linking alerts and traces in one place, along with investigation notes, with push to Jira or ServiceNow when escalation is needed.</p><p>A typical routing setup:</p><p></p><p><strong>Alert</strong></p><p><strong>Severity</strong></p><p><strong>Channel</strong></p><p>Latency SLO burn rate &gt; 14.4</p><p>Page</p><p>PagerDuty</p><p>Availability SLO burn rate &gt; 14.4</p><p>Page</p><p>PagerDuty + Slack</p><p>Quality SLO burn rate &gt; 6</p><p>Ticket</p><p>Jira (auto-create) + Slack</p><p>CTR anomaly (machine learning [ML] job)</p><p>Notification</p><p>Slack (search team)</p><h2>Anomaly detection for search quality</h2><p>Some search degradations are gradual shifts that slip past threshold-based alerts, rather than sudden spikes. A relevance regression after a model update might reduce CTR by 15% over a week, and latency might creep up by 5ms per day as the index grows. These are real problems, but they don't trigger burn rate alerts until it's too late.</p><p>Elastic's <a href="https://www.elastic.co/guide/en/machine-learning/current/ml-ad-overview.html">anomaly detection</a> is built for exactly this. It learns normal patterns in your search metrics and flags deviations automatically, and you don’t have to configure any thresholds. </p><h3>Detecting search quality degradation with ML anomaly detection</h3><ul><li><strong>Latency anomalies:</strong></li></ul><p> <a href="https://www.elastic.co/docs/reference/machine-learning/ootb-ml-jobs-apm">Elastic APM anomaly detection</a> can be enabled directly from the Elastic APM UI for your search service. It learns the typical latency distribution, including daily and weekly patterns, and alerts when behavior deviates. A gradual 5ms/day creep will eventually register as anomalous before it hits your SLO threshold.</p><ul><li><strong>CTR drops:</strong></li></ul><p>A relevance regression is invisible to traditional monitoring; latency is fine and errors are zero, plus the result counts are normal, but the ranking changed and users aren't clicking. Anomaly detection on click volume per query is a practical proxy: When a query that normally receives 20 first-click events per hour drops to 5, something likely changed.</p><p>To set this up: In <strong>Kibana</strong> → <strong>Machine Learning</strong> → <strong>Anomaly Detection</strong>, create a new job. Use the <strong>Multi-metric</strong> wizard, and select <code>traces-generic.otel-default</code> as the index. Configure a <code>count</code> detector on <code>attributes.search.first_click</code> split by <code>attributes.search.query</code>. This creates a per-query click-volume baseline and alerts when individual query engagement drops outside the expected range.</p><p>Note: This job detects click-volume anomalies per query, not CTR (which requires dividing clicks by searches). Click volume is a useful proxy (a CTR regression usually manifests as a drop in absolute click count), but be aware that a traffic surge with flat click volume would show as a CTR drop without triggering this alert. For true CTR anomaly detection, use a scheduled ES|QL transform to materialize hourly CTR values and run anomaly detection on the computed ratio.</p><p>Route the resulting ML alert rule to your Slack search channel.</p><ul><li><strong>Throughput shifts:</strong></li></ul><p>A sudden drop or unexpected surge in search volume can indicate upstream problems (like load balancer changes or traffic shifts) or downstream issues (such as search becoming unresponsive or users retrying).</p><p>Configure <a href="https://www.elastic.co/guide/en/machine-learning/current/ml-configuring-alerts.html">ML anomaly alert rules</a> to route these to your notification channels. These complement your SLO burn rate alerts; burn rates catch budget consumption, and anomaly detection catches pattern changes.</p><h2>Building a search monitoring dashboard with ES|QL</h2><p>SLOs tell you <em>whether</em> search is healthy. When they indicate a problem, you need a dashboard that tells you <em>why</em>.</p><p>The search team and the on-call team need different views of the same data. A search engineer wants query-level detail, such as which queries have low CTR and which ones return nothing. They’re also interested in where to invest in relevance. But an on-call SRE wants the operational picture, including whether search is fast and whether it’s up. It also wants to know whether search is degrading, and if so, since when.</p><h3>Search monitoring panels for the on-call dashboard</h3><p>Build it in <a href="https://www.elastic.co/guide/en/kibana/current/dashboard.html">Kibana dashboards</a> using <a href="https://www.elastic.co/guide/en/kibana/current/lens.html">Kibana Lens</a> panels. Lens supports <a href="https://www.elastic.co/docs/explore-analyze/visualize/esorql">ES|QL as a data source</a>, so the queries from <a href="https://www.elastic.co/search-labs/blog/series/search-analytics-opentelemetry">Blogs 2–4</a> can power dashboard panels directly. The key panels include:</p><ul><li><p><strong>Search throughput over time:</strong>  A sudden drop is often the first sign of a problem.</p></li><li><p><strong>Latency percentiles (p50, p95, p99) over time:</strong> When they diverge (p50 flat, p99 spikes), you have a subset of slow queries.</p></li><li><p><strong>Error rate over time:</strong> Spikes here mean hard failures.</p></li><li><p><strong>Zero-results rate over time:</strong> A step change upward, especially correlated with a deployment, means something changed in the index or query pipeline.</p></li></ul><p>The ES|QL for each panel follows the patterns from earlier blogs. For example, a latency percentile panel:</p>FROM traces-generic.otel-default
| WHERE name == "search"
AND attributes.search.query IS NOT NULL
| EVAL bucket = DATE_TRUNC(5 minutes, @timestamp)
| STATS
    p50 = PERCENTILE(attributes.search.took_ms, 50),
    p95 = PERCENTILE(attributes.search.took_ms, 95),
    p99 = PERCENTILE(attributes.search.took_ms, 99)
BY bucket
| SORT bucket<p>This includes three lines on one chart. When they diverge, such as when p50 stays flat but p99 spikes, you likely have a subset of queries that are slow while the majority are fine. That's a different diagnosis than all queries slowing down (cluster-level pressure).</p><h3>Drill-down panels: slowest queries and top zero-result queries</h3><p>For investigation, add a few detail panels, such as:</p><p><strong>Slowest queries:</strong> A table showing the queries with the highest p95 latency and their search volume. During an incident, this narrows the problem from "search is slow" to "these specific queries are slow."</p><ul><li><p><strong>Top zero-result queries:</strong> A table showing which queries most frequently return nothing. When zero-results rate spikes, this panel immediately shows which queries are responsible.</p></li></ul><p>These drill-down panels use the same ES|QL patterns as Blogs <a href="https://www.elastic.co/search-labs/blog/search-analytics-opentelemetry-esql">2</a> and <a href="https://www.elastic.co/search-labs/blog/search-click-tracking-opentelemetry-esql">3</a>, just surfaced on a persistent dashboard instead of run ad hoc.</p><h2>Search incident response using OpenTelemetry traces</h2><p>As an example, an alert fires, noting that search latency has spiked. What happens now?</p><p>The trace data from Blog 2's instrumentation gives you a structured path from symptom to root cause.</p><h3>Step 1: Assess scope</h3><p>Start at the on-call dashboard, and get answers to the basics:</p><ul><li><p><em>When did it start?</em> Narrow the time range to the degradation window.</p></li><li><p><em>How bad is it?</em> Is p50 affected (all queries slow) or just p99 (a subset)?</p></li><li><p><em>Is it just search?</em> Check the Elastic APM service map to determine whether the Elasticsearch dependency is also degraded.</p></li></ul><h3>Step 2: Find the problem queries</h3><p>If the problem is a subset of queries (p99 spike but p50 is fine), use the slowest queries panel or run:</p>FROM traces-generic.otel-default
| WHERE name == "search"
AND attributes.search.query IS NOT NULL
  AND attributes.search.took_ms &gt; 100
| STATS
    count = COUNT(*),
    avg_ms = ROUND(AVG(attributes.search.took_ms), 0),
    max_ms = MAX(attributes.search.took_ms)
BY attributes.search.query
| SORT count DESC
| LIMIT 10<p>Adjust the <code>100</code> ms threshold to match your environment's normal range. It can be lower for a fast cluster or higher if your data volume makes 100ms typical. This narrows the problem from "search is slow" to "these specific queries are slow." That's the difference between restarting the cluster and investigating a specific query pattern.</p><h3>Step 3: Drill into the trace waterfall</h3><p>Pick a slow query, and open it in the Elastic APM trace view. The waterfall shows exactly where time was spent. (Refer back to the trace waterfall GIF above to see a real example of a <code>POST /api/search</code>  trace broken down into its component spans.)</p><p>The overhead gap between <code>search.took_ms</code> (Elasticsearch time) and span duration (end-to-end time) is your diagnostic tool:</p><p></p><p><strong>Scenario</strong></p><p><strong>search.took_ms</strong></p><p><strong>Span duration</strong></p><p><strong>Diagnosis</strong></p><p>Elasticsearch slow</p><p>400ms</p><p>430ms</p><p>Elasticsearch problem: Check slow log, cluster metrics.</p><p>App slow</p><p>50ms</p><p>350ms</p><p>Application / network overhead: Check serialization, network.</p><p>Both slow</p><p>400ms</p><p>700ms</p><p>Multiple issues: Investigate both.</p><p></p><p>If the problem is in Elasticsearch, drill into the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-profile.html">Search Profile API</a> or <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/monitor-elasticsearch-cluster.html">cluster monitoring</a>. If it's application overhead, look at the spans around the search span in the waterfall.</p><h3>Step 4: Correlate with events</h3><p>Check whether the degradation correlates with:</p><ul><li><p><strong>Deployments:</strong> Did someone deploy a new version of the search service or push a new index?</p></li><li><p><strong>Cluster events:</strong> Is Elasticsearch under memory pressure, or are there long garbage collection pauses? Or maybe the disk is I/O saturated?</p></li><li><p><strong>Network:</strong> Is latency between the search service and Elasticsearch elevated?</p></li></ul><p>Elastic Observability's unified platform makes this correlation straightforward because traces, logs, metrics, and infrastructure data all live in the same Kibana instance. You're adding filters in the same interface, rather than switching between tools.</p><h2>Going further: Infrastructure metrics and cost attribution</h2><p>This post focuses on what you get from trace data; that is, the spans your search API already emits. But OTel and Elastic Observability support a wider instrumentation picture that becomes valuable as your search infrastructure matures.</p><ul><li><p><strong>Infrastructure metrics.</strong> Adding host and container metrics (like CPU, memory, disk I/O, and network) alongside your traces lets you correlate search performance with infrastructure use. When p99 latency spikes, you can immediately see whether the Elasticsearch nodes are under memory pressure and whether garbage collection  pauses are increasing. You can also check whether disk I/O is saturated, and you can do all this in the same Kibana interface. The <a href="https://www.elastic.co/guide/en/fleet/current/elastic-agent-installation.html">Elastic Agent</a> collects these automatically for your infrastructure, and the <a href="https://www.elastic.co/guide/en/observability/current/analyze-hosts.html">infrastructure monitoring UI</a> surfaces them alongside your Elastic APM data.</p></li></ul><ul><li><p><strong>Total cost attribution (TCA).</strong> With infrastructure metrics flowing alongside traces, you can start attributing infrastructure costs to specific services and operations. How much compute does your search service consume? How does that correlate with query volume? If a new ranking model doubles CPU usage per query, you can see the cost impact directly. This is particularly valuable for teams running search on cloud infrastructure where costs scale with resource consumption; understanding the cost per search helps justify infrastructure investment and identify optimization opportunities.</p></li></ul><ul><li><p><strong>Logs correlation.</strong> OTel auto-instrumentation injects trace context (such as trace ID and span ID) into your application logs. This means that when you're investigating a slow search in the trace waterfall, you can click through to the exact log lines from that request, including Elasticsearch slow log entries and application debug output. It also includes error details that don't fit in span attributes. The <a href="https://www.elastic.co/guide/en/observability/current/application-logs.html">logs correlation</a> feature automatically ties them together.</p></li></ul><p>These are natural next steps once you have traces working. Each one extends the same unified platform, without new tools or separate pipelines.</p><h2>How search analytics and search monitoring share one data pipeline</h2><p>Here's how it all fits together:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt540076694fa1cbcc/6a8d717236492a486785f109/image3.png" alt="Search monitoring workflow: out-of-the-box APM features, SLO and alert definition, dashboard building, incident response" /><p></p><p>The data flows from the instrumentation you built in Blog 2. This one investment supports two audiences: The search team gets product analytics (<a href="https://www.elastic.co/search-labs/blog/series/search-analytics-opentelemetry">Blogs 2–5</a>), and the SRE team gets operational monitoring (this post). Neither team needs separate data pipelines.</p><h2>Getting started with search monitoring in Elastic</h2><p>This is the last post in the series, and it brings us full circle. <a href="https://www.elastic.co/search-labs/blog/search-analytics-opentelemetry">Blog 1</a> describes the vision: Instrument search once with OTel, and send spans to Elastic. Then use ES|QL to answer any question about search behavior. <a href="https://www.elastic.co/search-labs/blog/series/search-analytics-opentelemetry">Blogs 2–4</a> build the instrumentation and analytics, and <a href="https://www.elastic.co/search-labs/blog/search-analytics-relevance-click-streams">Blog 5</a> shows how to feed that data back into relevance improvements. This post shows how the same data powers operational monitoring, including SLOs, alerting, anomaly detection, and incident response.</p><p>The key takeaway for search engineers is that the instrumentation you built for analytics already generates the signals. The SLOs and alerts are built-in capabilities of Elastic Observability, as are the dashboards. You're closer to production-grade search monitoring than you might think. Observability isn't a separate discipline you need to learn from scratch. </p><p>If you've been following along and built the instrumentation from Blogs 2–4, start here:</p><ol><li><p><strong>Open Elastic APM:</strong> Look at your search service, and explore a trace waterfall. You can also check the errors tab.</p></li><li><p><strong>Create three SLOs:</strong> Latency (Elastic APM latency), availability (Elastic APM availability), and quality (custom KQL for zero-results).</p></li><li><p><strong>Enable anomaly detection:</strong> One click in the Elastic APM UI for latency anomalies.</p></li><li><p><strong>Build the on-call dashboard:</strong> Four Lens panels with the queries from this post.</p></li></ol><p>By the end of an afternoon of work, your search service can have the same observability coverage as any other critical production system.</p><h2>Get started</h2><h3>Working code</h3><ul><li><p><a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/search-analytics-otel">Reference project:</a> Working code for the entire blog series; clone, configure, and run.</p></li></ul><h3>Elastic APM and traces</h3><ul><li><p><a href="https://www.elastic.co/guide/en/apm/guide/current/apm-overview.html">Elastic APM Overview:</a> Elastic APM concepts and trace analysis.</p></li><li><p><a href="https://www.elastic.co/guide/en/observability/current/apm-ui.html">Elastic APM UI:</a> Service overview, transactions, dependencies, errors.</p></li><li><p><a href="https://www.elastic.co/docs/solutions/observability/apm/service-map">Service Maps:</a> Dependency visualization and health.</p></li><li><p><a href="https://www.elastic.co/guide/en/observability/current/open-telemetry.html">OpenTelemetry Integration:</a> OTel ingestion in Elastic.</p></li></ul><h3>SLOs and alerting</h3><ul><li><p><a href="https://www.elastic.co/guide/en/observability/current/slo.html">SLOs in Elastic Observability:</a> Creating and managing SLOs.</p></li><li><p><a href="https://www.elastic.co/guide/en/observability/current/slo-create.html">Create an SLO:</a> SLI types (like Elastic APM latency, Elastic APM availability, or custom KQL), time windows, budgeting.</p></li><li><p><a href="https://www.elastic.co/guide/en/observability/current/slo-burn-rate-alert.html">SLO Burn Rate Alerts:</a> Multi-window burn rate alerting.</p></li><li><p><a href="https://www.elastic.co/guide/en/kibana/current/action-types.html">Alert Connectors:</a> Slack, PagerDuty, webhook, email integrations.</p></li><li><p><a href="https://www.elastic.co/guide/en/kibana/current/alerting-getting-started.html">Alerting Framework:</a> Rule types and configuration.</p></li><li><p><a href="https://www.elastic.co/guide/en/kibana/current/jira-action-type.html">Jira Connector:</a> Automatic ticket creation from alerts.</p></li><li><p><a href="https://www.elastic.co/guide/en/kibana/current/servicenow-action-type.html">ServiceNow Connector:</a> ITSM integration.</p></li><li><p><a href="https://www.elastic.co/guide/en/kibana/current/cases.html">Elastic Cases:</a> Built-in incident tracking with external push.</p></li></ul><h3>Anomaly detection</h3><ul><li><p><a href="https://www.elastic.co/guide/en/machine-learning/current/ml-ad-overview.html">Anomaly Detection Overview:</a> Unsupervised time series anomaly detection.</p></li><li><p><a href="https://www.elastic.co/docs/reference/machine-learning/ootb-ml-jobs-apm">Elastic APM Anomaly Detection:</a> Enable ML for latency, throughput, error rate.</p></li><li><p><a href="https://www.elastic.co/guide/en/machine-learning/current/ml-configuring-alerts.html">ML Anomaly Alert Rules:</a> Alerting on detected anomalies.</p></li></ul><h3>Dashboards and ES|QL</h3><ul><li><p><a href="https://www.elastic.co/guide/en/kibana/current/dashboard.html">Kibana dashboards:</a> Building operational dashboards.</p></li><li><p><a href="https://www.elastic.co/guide/en/kibana/current/lens.html">Lens:</a> Visualization editor.</p></li><li><p><a href="https://www.elastic.co/docs/explore-analyze/visualize/esorql">ES|QL in Lens:</a> ES|QL-powered dashboard panels.</p></li><li><p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/esql.html">ES|QL Overview:</a> Language reference.</p></li></ul><h3>Elasticsearch operations</h3><ul><li><p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-slowlog.html">Slow Log Configuration:</a> Threshold-based query slow logging.</p></li><li><p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-profile.html">Search Profiling:</a> Profile API for query execution analysis.</p></li><li><p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/monitor-elasticsearch-cluster.html">Monitoring Elasticsearch:</a> Cluster stats, search rate, latency.</p></li></ul><h3>From this series</h3><ul><li><p><a href="https://www.elastic.co/search-labs/blog/search-analytics-opentelemetry">Modern search analytics with OpenTelemetry:</a> The vision.</p></li><li><p><a href="https://www.elastic.co/search-labs/blog/search-analytics-opentelemetry-esql">Instrument your search API </a>: Search spans and <code>search.*</code> attributes.</p></li><li><p><a href="https://www.elastic.co/search-labs/blog/search-click-tracking-opentelemetry-esql">Measuring search quality </a>: CTR, MRR, click distribution.</p></li><li><p><a href="https://www.elastic.co/search-labs/blog/search-conversion-tracking-opentelemetry">From clicks to conversions</a>: Conversion tracking and revenue attribution.</p></li><li><p><a href="https://www.elastic.co/search-labs/blog/search-analytics-relevance-click-streams">Personalizing search from behavior</a>: Judgment lists, rank features, Learning To Rank (LTR).</p></li></ul><p><em>This is the final post in a six-part series on search analytics with OpenTelemetry and Elastic. Start from the beginning: </em><a href="https://www.elastic.co/search-labs/blog/search-analytics-opentelemetry"><em>Modern search analytics with OpenTelemetry,</em></a><em> or to start building, jump to </em><a href="https://www.elastic.co/search-labs/blog/search-analytics-opentelemetry-esql"><em>Instrument your search API.</em></a></p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/opentelemetry-search-monitoring-slos</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/opentelemetry-search-monitoring-slos</guid>
    <category><![CDATA[Operations]]></category>
    <category><![CDATA[Analytics]]></category>
    <category><![CDATA[ES|QL]]></category>
    <dc:creator><![CDATA[Matthew Adams]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb4ac8bfdb49b5f32/6a8d5eb7da6aeaa5fa37aae2/image1.png" length="0" type="image/png"/>
    <pubDate>Tue, 25 Aug 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[The mystery stress your heap chart can't see: AutoOps now watches vector off-heap memory]]></title>
    <description><![CDATA[Dense vectors use off-heap memory your heap chart never shows. AutoOps detects memory pressure before vector RAM stress causes OOM.]]></description>
    <content:encoded><![CDATA[<p>AutoOps now raises a <strong>Vector memory pressure</strong> insight when dense vector off-heap footprint, heap heat, and operational stress converge on the same Elasticsearch node. We validated on a 4 GiB node under sustained k-nearest neighbor (kNN) ingest: The insight fired at ~75% heap with thread-pool stress, roughly an hour before saturation. Heap charts alone still looked moderate at that point. Dense vectors for kNN live outside the Java heap, so heap monitoring and circuit breakers never show the full vector RAM picture. Below, we walk through what the insight measures and why heap on its own misses this. We also discuss what to do when it fires.</p><h2>Why dense vectors create off-heap memory pressure that heap charts miss</h2><p>Semantic search and kNN rely on <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector"><code>dense_vector</code> fields</a>. Elasticsearch stores much of that data in off-heap memory. It’s related to how the Java Virtual Machine (JVM) operates, but it isn’t the same thing as heap usage.</p><p>In production, the heap versus off-heap split shows up in a familiar pattern:</p><ul><li><p>Heap looks fine for weeks, while the dense vector off-heap footprint quietly grows.</p></li><li><p>Heap circuit breakers stay quiet or only spike late because the pressure sits outside the JVM.</p></li><li><p>kNN search and bulk ingest slow down, queues build, and nothing on the dashboard points at vector RAM as the cause.</p></li></ul><p>Heap limits protect Java allocations. They don’t tell you whether vector off-heap footprint still fits the RAM envelope that your deployment actually runs in. AutoOps already watches cluster health broadly; Vector memory pressure adds a focused read for vector-heavy nodes when memory and load signals line up.</p><h2>How AutoOps measures vector RAM, heap, and headroom</h2><p>AutoOps works from the same node stats metrics you already use for Stack Monitoring. For each node, it tracks three derived numbers:</p><p><strong>Symbol</strong></p><p><strong>Meaning</strong></p><p><strong>Source (typical)</strong></p><p><strong>Chart (see below)</strong></p><p><strong>V</strong></p><p>Vector off-heap footprint</p><p><code>indices.dense_vector.off_heap.total_size_bytes</code></p><p>First, green line</p><p><strong>A</strong></p><p>Available RAM in the product view</p><p>Delta between <code>os.mem.total_in_bytes</code> and <code>os.mem.used_in_bytes</code></p><p>Second, green line</p><p><strong>H</strong></p><p>Headroom</p><p><strong>A − V</strong> (headroom_bytes)</p><p>First, blue line</p><p>H &gt; 0 means there’s a modeled runway: Vector use still fits comfortably in that accounting. H ≤ 0 means that you’re in a <em>compression</em> regime: Vector footprint (V) meets or exceeds the free RAM (A) picture that AutoOps can align in telemetry. On small tiers, that can be common under load. The insight emphasizes trends, growth in vector off-heap footprint, and corroborating stress, not a single negative snapshot.</p><p>AutoOps also tracks a compression regime flag (fraction of recent samples where H ≤ 0), so brief flickers don’t dominate the story (see third chart below):</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt437840e9160da903/6a73173af9a79dca2a61f13e/image4.png" alt="AutoOps charts showing vector off-heap footprint growth, shrinking RAM headroom, and compression regime over 48 hours" /><h2>How vector memory pressure detection works: Expansion and compression</h2><p>Vector memory pressure is a single HIGH severity AutoOps event, which adapts to the compression regime:</p><ul><li><p><strong>Expansion (</strong><strong>H &gt; 0</strong><strong>):</strong> Emphasize shrinking headroom, hence a growing vector off-heap footprint.</p></li><li><p><strong>Compression (</strong><strong>H ≤ 0</strong><strong>):</strong> Emphasize ΔV, heap context, I/O, and latency. "Hours until H hits zero" isn’t the main narrative when headroom is already gone.</p></li></ul><p>The detector requires three layers before it fires:</p><ol><li><p><strong>Memory carriers:</strong> Compression regime, shrinking headroom, or sustained growth in vector off-heap footprint.</p></li><li><p><strong>Operational latch:</strong> Search or indexing latency versus rolling baselines, filesystem read stress (paired with latency or heap), indexing throttle, thread-pool queues or rejections, segment creep, or heap circuit breakers when paired with other stress, as circuit breakers alone don’t provide enough evidence to be escalated without corroborating stress.</p></li><li><p><strong>Heap hot:</strong> Heap usage elevated versus a 24-hour rolling median on that node, so compression alone on a calm heap doesn’t fire the insight.</p></li></ol><p>That pairing is intentional. Vector pressure without load might be capacity planning, and load without vector pressure might be a different root cause. Together, vector memory, operational stress, and heap heat surface the vector RAM story when the node is actually in trouble, not on every compressed mapping while the heap stays normal.</p><h2>Validation: Memory pressure detection on a 4 GiB node under kNN load</h2><p>We stress-tested vector memory pressure detection on 4 GiB Elastic Cloud Hosted deployments with throttled dense-vector ingest (~2,000 docs per minute) and steady kNN search (~8 queries per second). Across <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector">Hierarchical Navigable Small World (HNSW)</a>, <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/bbq#bbq-hnsw">Better Binary Quantization (BBQ) HNSW</a>, and <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/bbq#bbq-disk">DiskBBQ</a> mapping profiles over 24–48 hours:</p><ul><li><p>Vector off-heap footprint grew from near zero to about 6 GiB on the tightest runs (more than 4.7 million vectors indexed) in both HNSW test runs.</p></li><li><p>Nodes spent most of each run in compression (H ≤ 0), which is expected when vector footprint exceeds total RAM in this model.</p></li><li><p>Vector memory pressure stayed off while heap held near 50%, even with compression and pool stress building.</p></li><li><p>On both HNSW and BBQ HNSW, the insight fired once heap climbed past ~75% with memory compression and thread-pool queue stress, roughly an hour before heap neared saturation. Node out of memory (OOM) and circuit breakers followed in the same window, as did slow search/indexing. As we can see on the dashboard below, performance drops drastically due to corroborating stress toward the end of the test run:</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8a99d9296a444dbb/6a73176d0da67336ea57999d/image2.png" alt="HNSW validation dashboard showing heap climbing past 75% with search and indexing performance degradation" /><ul><li>On DiskBBQ, where compression was chronic but heap stayed normal, the insight didn’t fire,since storage rather than memory was the limiting factor. Disk and watermark signals are the right path to follow for that profile. As we can see on the screenshot below, all indicators stayed steady with constant performance throughout the test, even though we filled up the disk with more than 70 million vectors on the same instance type:</li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3f30c606a7005a82/6a731789ef5befd2394f7896/image5.png" alt=" DiskBBQ validation dashboard showing stable heap, steady search and indexing performance over 10 days" /><p>That timing is the point: Operators get a vector-first read tied to real RAM stress, with subsystem context, rather than an alert on every compressed index or only a red heap chart after the node is fighting on every front.</p><h2>What to do when AutoOps raises vector memory pressure</h2><p>Here’s the insight that AutoOps now raises when it detects vector memory pressure:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6a5bcf4dd55dc5fb/6a7317abe35d0255150319be/image3.png" alt="AutoOps Vector memory pressure insight with detection summary and recommendations for an Elasticsearch node" /><p>Recommendations in the product map to concrete actions:</p><ol><li><p><strong>Reduce vector footprint</strong> where quality allows: Fewer dimensions, quantized mappings, archive or split indices, reindex with a leaner mapping.</p></li><li><p><strong>Tune kNN load:</strong> Lower <code>num_candidates</code>, reduce concurrent query rate, narrow filtered kNN where possible.</p></li><li><p><strong>Consider DiskBBQ</strong> when HNSW in RAM is the bottleneck (evaluate recall/latency trade-offs for your use case). If you’re already on DiskBBQ and the heap memory is calm, treat disk and watermark insights as the primary signals. Note that DiskBBQ requires an Enterprise license.</p></li><li><p><strong>Right-size RAM</strong> when vector off-heap footprint (V) trends up and headroom stays tight.</p></li></ol><p>AutoOps links affected nodes and summarizes regime and stress in plain language. Treat it as “act now, rather than waiting for red on every chart.”</p><h2>Where AutoOps vector memory pressure monitoring is available</h2><p>Vector memory pressure is available wherever AutoOps runs against Elasticsearch 9.2+, including:</p><ul><li><p>Elastic Cloud Hosted (ECH).</p></li><li><p>Elastic Cloud Serverless (coming soon).</p></li><li><p>Self-managed via <a href="https://www.elastic.co/docs/deploy-manage/monitor/autoops/cc-autoops-as-cloud-connected">Cloud Connect</a>.</p></li></ul><p>AutoOps is included at all subscription levels for supported deployment types and doesn’t consume ECUs on ECH.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/vector-memory-pressure-autoops</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/vector-memory-pressure-autoops</guid>
    <category><![CDATA[AutoOps]]></category>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[Operations]]></category>
    <dc:creator><![CDATA[Valentin Crettaz]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1ec5b14f053bdc56/6a73170a89eb5c6c9bab24c6/image1.png" length="0" type="image/png"/>
    <pubDate>Thu, 06 Aug 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Your agents have been keeping receipts: turning Elastic Agent Builder's built-in OTel traces into token cost dashboards in Kibana]]></title>
    <description><![CDATA[Your Agent Builder agents already log every LLM call as an OTel trace, and that agent tracing data can power token cost dashboards and budget alerts before one runaway conversation quietly wrecks your month.]]></description>
    <content:encoded><![CDATA[<p>Every Elastic Agent Builder conversation already generates a full OpenTelemetry trace. LLM calls, tool executions, token counts, all logged by default into Elasticsearch data streams you can query with ES|QL. Most teams don't look at this data until something breaks, which means they're sitting on usage trends, latency bottlenecks, and cost signals they could have caught earlier. This post covers how to build token cost dashboards in Kibana, set alerts that fire when a conversation blows past 256,000 tokens, and use the waterfall timeline to see exactly where your agent spent its time.</p><h2>What is an Agent Builder OTel trace and what does it capture?</h2><p>When your agent runs, Agent Builder records everything that happened as an <a href="https://opentelemetry.io/docs/concepts/signals/traces/">OpenTelemetry (OTel) trace</a>. Think of a trace as a receipt for a single conversation turn. Every LLM request, tool call, and agent action is recorded as an individual span in Elasticsearch, which is a unit of work or operation. When opted in, additional details like user prompts, LLM responses, tool outputs, and conversation IDs are captured as structured span attributes on the chat span. All of this is scoped to your Kibana space.</p><h2>How to enable agent tracing and privacy controls in Kibana</h2><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc85524772be260c0/6a6a33e610787d6661a77fa1/a0200d33905eee1439500fc3b7bd0c6a74af1fc8-1999x1266.png" alt="Agent Builder Traces settings in Kibana showing tracing toggle and advanced privacy controls for OTel trace data" /><p>To begin capturing trace data, ensure the following toggles under <strong>Agent Traces</strong> in Gen AI Settings are active within your environment:</p><ul><li><p><strong><code>agentBuilder:tracing:enabled</code></strong> — This gen AI setting manages the collection of traces and is enabled by default.</p></li></ul><p>Advanced privacy controls, located under the default tracing toggle, also let you collect message content. While prompts and tool outputs are masked by default, you may choose to enable them to support more robust traces:</p><ul><li><p><strong><code>agentBuilder:tracing:includeUserPrompts</code></strong></p></li><li><p><strong><code>agentBuilder:tracing:includeLlmResponses</code></strong></p></li><li><p><strong><code>agentBuilder:tracing:includeToolDetails</code></strong></p></li><li><p><strong><code>agentBuilder:tracing:includeSystemPrompt</code></strong></p></li><li><p><strong><code>agentBuilder:tracing:includeRealNames</code></strong><strong>:</strong> Retains real agent/tool names instead of anonymizing to custom</p></li><li><p><strong><code>agentBuilder:tracing:includeRealIds</code></strong>: Retains the actual conversation identifiers instead of the default hashed versions. This means trace data collects original IDs, which can link traces to specific user sessions (PII).</p></li></ul><p>Only enable these if you understand what data your agents handle and have appropriate data governance in place.</p><h2>How Agent Builder stores OTel trace data in Elasticsearch</h2><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltadcdb2eeb64bb453/6a6a33e515fc5c4fa99e4935/0b22d6473e530801a7433a662a77051ecbc22814-1999x1436.png" alt="Waterfall view of an Agent Builder OTel trace showing span hierarchy, LLM call durations, and tool executions" /><p>The Agent Builder utilizes OpenTelemetry semantic conventions. This results in a structured hierarchy of spans that provides a granular view of the agent's internal logic:</p><p>Span</p><p>Type</p><p>What it captures</p><p>`invoke_agent &lt;name&gt;`</p><p>CHAIN</p><p>Full turn lifecycle, from user input to final reply</p><p>`invoke_agent &lt;name&gt;`</p><p>AGENT</p><p>Single agent execution: reasoning, tool calls, reply</p><p>`chat &lt;model&gt;`</p><p>LLM</p><p>One LLM request: model, latency, token counts</p><p>`execute_tool &lt;toolName&gt;`</p><p>TOOL</p><p>Tool invocation: arguments, duration, result</p><p>Trace data is written to a dedicated data stream per Kibana space, keeping conversation data cleanly isolated. To query your traces in Discover, target the index for your space directly:</p><p>For the default space, that’s <code>traces-agent_builder.otel-default</code>. If the advanced privacy controls are turned on, then those span attributes will also be shipped to the traces data stream with the original spans. This index lets you query the raw message content to see what's actually being said in conversations. It is best practice to avoid using wildcards to prevent mixing data from unrelated spaces.</p><p>Agent Builder ships with a built-in skill called <code>agent-builder-traces</code>, installed automatically when<code>agentBuilder:tracing:enabled</code> is on. You can use it to ask questions directly about your trace data, making it easy to explore agent behavior without writing ES|QL from scratch.</p><h2>How to debug agent behaviour with the OTel trace waterfall view</h2><p>The trace waterfall shows every step of an Agent Builder session as a timeline. To open it, navigate to the specific turn in the conversation UI and select the trace icon.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0c19c6699996667b/6a6a33e704e0aa0bbe5528f9/29984c0b4f0c290170ceb48e01e404bdc25db92d-602x158.png" alt="Trace icon in the Agent Builder conversation UI used to open the OTel trace waterfall view" /><p>This launches a waterfall timeline breaking down every step of your agent's execution. At the top level, you'll see the <code>invoke_agent</code> parent span with the full end-to-end duration of your agent run. Nested beneath it are chat spans, each representing a single LLM request and showing exactly how long the model took to respond. Alongside those are <code>execute_tool</code> spans, one per tool call, where you can see which tool was called, what arguments it received, and how long it ran. This allows you to trace the exact sequence your agent followed, pinpoint timing bottlenecks, and see where errors occurred.</p><h2>How to build token cost dashboards from trace data</h2><p>Discover gives you raw trace data, but most teams want answers to operational questions like "how many tokens did we burn today?", "which tool is called most often?", and "how many unique users interacted with the agent this week?". These would require a dashboard built directly against the trace data.</p><p>There is an Elastic-managed out-of-the-box dashboard called <em>[Elastic] Agent Builder Overview</em> that can be installed by clicking in the top-right corner of the <em>Agent Traces</em> section within GenAI Settings.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt21bd488325991b1f/6a6a33e8d57c1dc19ac13eec/805391d2c18083589b4e64f450e2bc33ba32be9a-1999x117.avif" alt="" /><p>It contains basic details spanning Token Usage and Cost, Conversation Volume and Latency, Agent Execution, and Tool Call Frequency and Errors.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5775187fb11ae16e/6a6a33e955755b13022bd23e/70b96d6ff2e96358831a1bf8a08aeecec8c41b0e-1999x1115.png" alt="Elastic Agent Builder Overview dashboard showing token usage, cost metrics, and LLM request counts" /><p>However, a custom dashboard may be more efficient. If you want something more tailored, build <a href="https://www.elastic.co/docs/explore-analyze/visualize/lens">Lens</a> panels directly against the OTel trace index. Some useful panels to start with:</p><ol><li><p>Most active conversations by token spend</p></li></ol><p>create a horizontal bar chart against <code>traces-agent_builder.otel-&lt;space-id&gt;</code>. Set the x-axis to <code>gen_ai.conversation.id</code> and sort descending and limit to the top 10. Set the y-axis to a sum of <code>gen_ai.usage.input_tokens</code>plus<code>gen_ai.usage.output_tokens</code>. Input the formula as: </p><p>Conversations with the most LLM round-trips</p><p>Option to create this visualization with a simple ES|QL query that would look like:</p><p>There is also a <code>dashboard-management</code> skill that can be used to help create traces visualizations using natural language.</p><h2>How to set token cost alerts for Agent Builder conversations</h2><p>Token consumption is the most direct cost lever for LLM-based agents. A single runaway conversation can blow through your monthly budget before anyone notices.</p><p>Elastic alerting lets you define a threshold rule directly against the trace data. Navigate toObservability &gt; Alerts &gt; Manage Rules &gt; Create Ruleand selectElasticsearch queryas the rule type.</p><p>A rule that fires when any single conversation exceeds 256,000 tokens looks like this as an ES|QL rule:</p><p>Set the schedule to run every 15 minutes and configure the action to send a Slack notification or open a PagerDuty incident. The <code>gen_ai.conversation.id</code> value in the alert payload gives you the exact conversation to inspect.</p><h2>What’s coming next for Agent Builder observability</h2><p>Agent traces give you visibility that goes far beyond debugging. Once you've built dashboards and configured alerts against Agent Builder trace data, you have a live pulse on how your agents are behaving in production. If you haven't already, spin up Agent Builder in your Kibana space, make sure tracing is enabled, and run a few conversations. Check Discover, pull up the waterfall view, and see what your agent is actually doing under the hood.</p><p>This is the first in a series of posts on Agent Builder observability. Coming up, we'll go deeper on using the<code>agent-builder-traces</code> skill to query your data conversationally, building custom evaluation pipelines from trace data, and using traces to feed conversation history back into your agents. Your agents have been keeping secrets. It's time to make them talk.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/opentelemetry-tracing-agent-builder</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/opentelemetry-tracing-agent-builder</guid>
    <category><![CDATA[Agentic AI]]></category>
    <category><![CDATA[Operations]]></category>
    <dc:creator><![CDATA[Meghan Murphy,Pablo Neves Machado]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte3979255ddfc7f45/6a17e25ffaa913812f93c7cb/92c517a2e7b36122a18feee317a0215981b62b6b-720x420.jpg" length="0" type="image/jpeg"/>
    <pubDate>Tue, 28 Jul 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Faster Elasticsearch issue triage with redesigned AutoOps]]></title>
    <description><![CDATA[AutoOps introduces clearer severity, updated page layouts, and simpler issue triage for Elastic Cloud Hosted deployments and Cloud Connect clusters.]]></description>
    <content:encoded><![CDATA[<p>AutoOps has a redesigned experience for Elastic Cloud Hosted deployments and Cloud Connect clusters. The update adds a new Critical severity level and refreshes every page, including Template Optimizer, Nodes, Shards and Overview. Updated layouts and navigation make Elasticsearch issues easier to scan and triage. This post covers the redesigned UI and where AutoOps is headed next, including a headless, agentic experience.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd9be3378b7b18d00/6a6a33c6d137a512563e106b/86ddf69cfc68919fb0f708eb190c18fdc2b9479a-1999x1200.png" alt="AutoOps Deployment view for an Elasticsearch cluster showing events over time, open events list and resource metrics including JVM memory, CPU and storage across hot and cold tiers" /><h2>Why AutoOps for Elasticsearch needs clearer prioritization</h2><p>Running Elasticsearch at scale requires administrators to monitor cluster health, performance, capacity, and configuration at the same time. AutoOps now provides a clearer way to distinguish conditions that threaten cluster functionality from significant but less urgent degradation. The redesigned interface also follows familiar Elastic Cloud Console patterns, making active issues easier to find and investigate.</p><h2>What changed in AutoOps: severity, navigation, configuration, and page design</h2><p>The monitoring engine remains the same. The redesigned layout, navigation, and workflows now follow familiar Elastic patterns.</p><h3>A clearer severity model</h3><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbd9cd94b620ce367/6a6a33c70a222b3af3877f27/0304e67b9028d72ea31408712e6e778c866edfac-1780x632.png" alt="AutoOps events over time heatmap showing Critical Status Red, High Cluster Pending Tasks and Medium severity events including Unbalanced Shards and Template Optimization across an Elasticsearch deployment over 10 days" /><p>We added <strong>Critical</strong> as a new severity level for conditions that pose an immediate threat to cluster functionality and require urgent intervention. Several events previously classified as High are now Critical. Others are now Medium because they represent potential risk rather than active, significant degradation. The reclassified events are:</p><ul><li><p><strong>Promoted from High to Critical:</strong> Disk Watermark Flood Stage, Master Not Discovered, and Status Red.</p></li><li><p><strong>Demoted from High to Medium:</strong> Disk Watermark Low Threshold, Disk Watermark Low, and Disk Watermark Configuration Incorrect.</p></li></ul><p>Severity</p><p>What it means</p><p>Critical</p><p>Immediate threat to cluster functionality. Urgent intervention required.</p><p>High</p><p>Significant degradation to usability, performance, or stability.</p><p>Medium</p><p>Potential risk that can escalate if left unaddressed.</p><p>Low</p><p>Minor anomalies with minimal operational impact.</p><p>Info</p><p>Routine operational updates and configuration changes. No action required. (Coming in a near-future update).</p><p>Every severity level ships with an updated icon set and color palette. Levels are fixed so teams can build consistent runbooks and notification filters: route Critical and High events to PagerDuty or Slack, keep Medium and Low in the console for periodic review, and when Info arrives, use it for awareness without alert fatigue.</p><h3>Deployment view: open events and history, side by side</h3><p>The redesigned deployment view presents the existing Open events and Event history tabs in a clearer layout.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4e5132e8519703e1/6a6a33c88c87dc30dc0d0678/54a6c72a7024dc46a6c458d801d85a066fd3af8f-1780x1664.png" alt="AutoOps Deployment view showing the Event history tab with an events over time heatmap for Critical, High and Medium Elasticsearch events including Status Red, Data Node Disconnected and Index Queue Size" /><h3>Event flyout: a clearer view of what matters</h3><p>The event detail flyout is redesigned around action. High-severity events include a notification callout and an interactive badge that shows whether alerts are configured and links directly to setup. Recommendations collapse by default so the core event stays in focus. Settings live in the flyout menu; share is a separate icon in the header. The Dismiss action appears only when your role has the required admin permissions and the event is dismissible.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt18f642f08729fef9/6a6a33c940a4941189ca5c96/c275a87fc7ba3125599f7be5dfa170915aa0c567-1999x1202.png" alt="AutoOps Deployment view showing an open High severity event flyout for a high index queue on an Elasticsearch node, with recommendations and event timeline" /><h3>AutoOps overview: triage active events across your Elasticsearch fleet</h3><p>The Overview page is reorganized around how operators scan an estate. Elasticsearch context sits directly under the page header, and active events appear as <strong>event ribbons</strong> below the deployments table. Each ribbon shows the latest active event in your selected time range; if the same event type is open on other deployments, a new badge lets you expand the view without opening each resource individually. Event search moved to the left for quicker filtering.</p><p>The “Events over time” chart moved off Overview to keep this page focused on fleet-level triage; open a single deployment when you need that timeline.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc8fbe2571e164c56/6a6a33cad57c1d09d8c13ee4/f73d8b413c66662f890232ecfc57383be35c3203-1999x1202.png" alt="AutoOps Overview page showing a fleet of 7 Elasticsearch deployments with ES status, priority events, node and shard counts, and a Top events list filtered by Critical, High and Medium severity" /><h3>Nodes, Shards, and Indices are designed with easier navigation and information hierarchy</h3><p><strong>Nodes view</strong> now uses updated chart components and the Elastic UI color scheme, with clear expansion indicators on accordion sections. Event and instance lists that duplicated deployment-level views were removed to reduce noise.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt56b42b5dd910a347/6a6a33cb820dedb2ad12936a/dbbdca29d142a23e46acf5ed2c150cdfc18a2ba3-1999x1203.png" alt="AutoOps Nodes view for an Elasticsearch deployment showing disk usage, shards count, segments count, and documents count charts across 24 nodes over a two-day period" /><p><strong>Shards view </strong>improves node selection and groups view controls in the upper-right corner. A horizontal scrollbar supports wider layouts, and the time slider now uses native Elastic UI components. Node selection in Shards view now works across larger clusters and presents up to 100 nodes at a time.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt40d54785dc72234b/6a6a33cc776a4d7a5b51ddc5/a00dc567f48eefa06839ba294e5592a41904ba8c-1999x1202.png" alt="AutoOps Shards view for an Elasticsearch cluster showing hot and cold tier nodes with an indexing rate tooltip for a specific index on instance-181, displaying 3K/sec indexing rate and 56 million documents" /><p><strong>Index view</strong> keeps the Indices table experience you already use, including sorting, time-range brushing, and chart zoom behavior tuned for meaningful ranges.</p><h3>Template Optimizer</h3><p>The <a href="https://www.elastic.co/guide/en/cloud/current/ec-autoops-template-optimizer.html">Template Optimizer</a> now provides a searchable list of templates ordered by the most recently identified recommendations. You can open each recommendation directly or expand the JSON panel to inspect the complete template.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9b0f64ce58e9783e/6a6a33cd065b160c08701ff5/22e5371e3174233cfe942094cca3dac9444a3550-1999x1202.png" alt="AutoOps Template Optimizer showing a codec compression recommendation alongside the full JSON template configuration for autoops_standard_index_settings" /><h3>Configure notifications and event settings</h3><p>Notification settings now include connector search, clearer filters, and a simpler connector editing flow. Event settings moved from a popup to a flyout, matching the pattern used across AutoOps. Notification reports retain the same 10-day history window with minor layout updates, and dismiss events use updated confirmation components aligned with Elastic UI.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb8d711e0f3814e95/6a6a33cec9699ab1cef4b1c8/11a3cb4646f1d1efa2a384b8ee82b6b736aede29-1999x1203.png" alt="AutoOps Events settings page showing the Edit event settings flyout with index filter pattern, empty indices threshold, and per-deployment configuration options" /><h3>Navigation and controls</h3><p>The deployment picker now shows deployment ID and real-time cluster status, with copy actions for deployment name and ID in the dropdown sub-menu. Node selection supports select-all, select-by-tier grouping, and clear master node indication. The date picker follows the same relative-range and custom-range model used in Kibana and other Cloud Console monitoring views.</p><h2>AutoOps roadmap: API, MCP, CLI, and agentic experience</h2><p>Looking ahead, we are building toward a headless, agentic AutoOps experience. A forthcoming public <a href="https://github.com/elastic/roadmap/issues/144">AutoOps API </a>will make insights and raw metrics available outside the AutoOps interface. Administrators and agents will be able to query the API directly or store its data in Elasticsearch. The API will also provide the foundation for integrations with MCP, Elastic Agent Builder, the Elastic CLI, Kibana, and native AutoOps chat.</p><ul><li><p><strong>Hosted MCP server: </strong>Make AutoOps insights available to MCP clients such as Claude and Cursor.</p></li><li><p><strong>Native Elastic Agent Builder tool</strong>: Use AutoOps insights in Elastic Agent Builder.</p></li><li><p><strong>Elastic CLI support:</strong> Access the AutoOps API through the Elastic CLI.</p></li><li><p><strong>AutoOps in Kibana:</strong> Surface relevant insights and metrics within Kibana.</p></li><li><p><strong>Native AutoOps chat</strong>: Investigate cluster issues through an agentic chat experience within AutoOps UI in Elastic Cloud Console.</p></li></ul><p>The application redesign is the foundation; these surfaces will meet operators where automation and AI already live. Read more about what is coming on the <a href="https://github.com/orgs/elastic/projects/2066/views/2?sliceBy%5Bvalue%5D=Monitoring+and+diagnostics">Elastic public roadmap</a>.</p><h2>How to start using the redesigned AutoOps in Elastic Cloud Console</h2><p>Sign in to <a href="https://cloud.elastic.co">Elastic Cloud Console</a>, open a deployment, project, or connected cluster, and select <strong>AutoOps</strong> from the navigation. Learn more in the <a href="https://www.elastic.co/guide/en/cloud/current/ec-autoops.html">AutoOps documentation</a>.</p><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/autoops-elasticsearch-cluster-monitoring-redesigned</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/autoops-elasticsearch-cluster-monitoring-redesigned</guid>
    <category><![CDATA[AutoOps]]></category>
    <category><![CDATA[Elastic Cloud Hosted]]></category>
    <category><![CDATA[Operations]]></category>
    <dc:creator><![CDATA[Ori Shafir,Arnon Stern]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd9be3378b7b18d00/6a6a33c6d137a512563e106b/86ddf69cfc68919fb0f708eb190c18fdc2b9479a-1999x1200.png" length="0" type="image/png"/>
    <pubDate>Thu, 23 Jul 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[98.9% faster queries, 4x more indexing throughput: a systematic Elasticsearch performance diagnosis]]></title>
    <description><![CDATA[Use AutoOps, the Profile API and ES Rally together to find cluster hotspots, slow queries and index bottlenecks, with real benchmarks showing a 98.9% latency cut and 4x indexing gain.]]></description>
    <content:encoded><![CDATA[<p>Three Elastic tools (AutoOps, the Profile API and ES Rally) can systematically diagnose Elasticsearch performance problems at every layer of the stack. In a delivery logistics scenario, they revealed a shard imbalance causing 30-second search spikes, a deep-pagination query wasting 98.9% of its execution time, and index settings limiting bulk ingestion to a quarter of achievable throughput. This post walks through each tool, what it surfaces, and how to use the findings to fix the problem.</p><p>When dealing with dozens of users who actively connect and use your platform backed by an Elasticsearch cluster, it’s important to quickly grasp what potential bottlenecks are and how to overcome them. The main question is: Where to start? Here’s your potential decision path:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfdbd8c5fe90b986a/6a6119e2de948111c2d173b8/a7fd476e740648e7b92b0ddd27c69e47a4b37181-1240x1720.png" alt="Flowchart showing steps for diagnosing reported slowness. It starts with “Slowness reported” and then branches through decisions labeled “Cluster issue?,” “Slow query?,” and “Index settings?,” leading to boxes for AutoOps, Profile API, ES Rally, and Client-side bottleneck." /><p>The path starts with AutoOps to detect cluster-level problems, like resource pressure or bad shard distribution. If the cluster is healthy, the Profile API helps identify slow queries; candidates can come from the slow query log or application-side logging. Next, ES Rally benchmarks index settings that may limit throughput. If all three come back clean, the bottleneck is on the client side.</p><h2>How to detect cluster-level performance problems with AutoOps</h2><p>The first question is always the same: Is the cluster itself the problem?</p><p>AutoOps is designed to provide real-time cluster diagnostics, deliver tailored advice, and help you improve the health of your clusters quickly. It comes by default in Elastic Cloud, and it has been added recently as a <a href="https://www.elastic.co/blog/autoops-free">free option for self-hosted configurations</a>.</p><p>It’s very easy to set it up, and it takes no more than five minutes to see data flowing into a comprehensive list of graphs that AutoOps offers you. The idea behind it is to install an <a href="https://www.elastic.co/docs/deploy-manage/monitor/stack-monitoring/collecting-monitoring-data-with-elastic-agent">Elastic Agent</a> close to your cluster that reports back to the AutoOps platform which is connected with your Elastic Cloud account. Here’s the <a href="https://www.elastic.co/docs/deploy-manage/monitor/autoops/cc-connect-self-managed-to-autoops">installation guide</a>.</p><p>What’s nice about it is that it provides you with instant warnings and suggestions that are hard to spot through existing monitoring cluster technology. What’s really useful is that it also shows solutions to the issues. </p><p>In our delivery logistics scenario, AutoOps surfaced one finding that explained the user-reported slowness: severe load imbalance across the cluster. Node es01 was handling nearly all traffic while the other three sat idle, with search latency spiking to 30 seconds, as we'll see in the graphs in the next section.</p><h3>How AutoOps surfaces node hotspotting and shard imbalance</h3><p>AutoOps node performance graphs revealed that es01 was handling nearly all indexing traffic (5.6 docs/sec) while es02, es03 and es04 were idle.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt65864a5d75b374a5/6a6119e33c3320410f0b60d1/634bea76e349915ed037b03e0fe7b6c868ed0267-1999x704.png" alt="Four line graphs, side by side, showing indexing and search rates and latencies for entities labeled es01, es02, es03, and es04 between 07:35 am and 07:45 am. Each graph has colored lines on a dark background, with legends listing rate and latency values in seconds and milliseconds." /><p>Below, we see that CPU usage was concentrated on a single node (es01) which was hosting the heavier index, while the other three nodes were mostly idle.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc9a24d81c58d7d2b/6a6119e4f817927d5d07f5f5/d50af8e45bf876144f8af0239a3e1d9e133baa69-706x1038.png" alt="Line graph labeled “CPU,” showing usage percentages for es01, es02, es03, and es04 between 04:25 pm and 04:35 pm. The es01 line rises to about 25% after 04:25 pm, drops near 0%, and then spikes to roughly 75% at 04:35 pm. The other lines remain flat at 0% throughout." /><p>The next signal of unusual behavior appeared in the search latency graph. Using the AutoOps per-node latency view, we uncovered some unexpected results.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3f72717314212aba/6a6119e5ae3a7c30f483eef6/3015134fd9f3684e57bff9593eb916990bc25ce7-706x1022.png" alt="Line graph titled “Search latency” with a dropdown set to “Max.” The vertical axis ranges from 0 ms to 40 sec, and the horizontal axis shows times from 04:25 pm to 04:35 pm. Four lines represent es01, es02, es03, and es04. The es01 line spikes twice to about 30 seconds, while the other three remain flat near 0 ms." /><p>While other nodes show no latency, the heavy-loaded node shows symptoms of latency that affects search apps. Not only was it a write node, but also it was the one serving all search requests. During analysis, we found that a delivery index was stored on only one node, instead of being distributed across all four. That was the root cause of the issue. By reindexing the data (with increased number of primary shards), we balanced the query load and eliminated the primary issue.</p><p>This article focuses on a single AutoOps finding to keep the diagnostic flow clear. For deeper dives into the kinds of issues AutoOps surfaces, see the dedicated articles on <a href="https://www.elastic.co/search-labs/blog/hotspot-elasticsearch-autoops">hotspotting</a>, <a href="https://www.elastic.co/search-labs/blog/elasticsearch-cpu-usage-high">high CPU usage</a>, and <a href="https://www.elastic.co/search-labs/blog/slow-search-elasticsearch-query-autoops">long-running queries</a>.</p><h2>How to find slow Elasticsearch queries using the Profile API</h2><p>Once the cluster is healthy, the next question is: Are there specific, expensive queries? Finding candidates is the first step. You can explore them either by using <a href="https://www.elastic.co/docs/deploy-manage/monitor/logging-configuration/slow-logs">Elasticsearch’s slow query log</a> or through application side logging, by measuring time required for every search request. In our case, infinite scroll on a tracking delivery screen was a problem while users would scroll deeper into results.</p><p>The <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-profile">Profile API</a> is a powerful tool for analyzing long-running queries and finding search bottlenecks. Getting started is simple: Just set <code>profile=”true”</code> in any of your search queries, and the responses will contain a profile section with detailed timing breakdown. It highlights which phase dominates execution time: the query phase, aggregations, or data fetching.</p><p>Let’s take a closer look at the <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/paginate-search-results">deep pagination case</a>. In a case of infinite scroll in a mobile application, the user continues scrolling and the app responds by fetching 100 documents per request.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltccf8201277682a9c/6a6119e65144f742bab98961/f7470705f495daa62eee4a3ae7c956bcd02405b2-720x1280.jpg" alt="Vertical interface showing a product list with a search bar labeled “Search products…” and a cart icon. Each entry includes a square placeholder, product name, optional category text, and price. Items listed include prices from $1400 to $1990. A “Show more” button appears at the bottom." /><p>As users continues scrolling, the requests can reach very deep pagination levels, while the <code>from</code> parameter grows:</p>GET delivery-records/_search
{
    "profile": true,
    "from": 9000,
    "size": 100,
    "query": {
        "bool": {
            "filter": [
                {
                    "term": {
                        "payment_type": "1"
                    }
                },
                {
                    "range": {
                        "tax_amount": {
                            "gte": 5
                        }
                    }
                }
            ]
        }
    },
    "sort": [
        {
            "delivery_pickup_datetime": {
                "order": "desc"
            }
        }
    ]
}<p>The profile data, summarized below and converted from nanoseconds to milliseconds, makes the bottleneck clear: Most of the time is spent outside the query and fetch phases on coordination work caused by the large <code>from</code> offset. Elasticsearch must collect 9,100 matching documents, sort them, discard the first 9,000, and return only the requested 100.</p><p>Component</p><p>Phase</p><p>Time</p><p>ConstantScoreQuery contains</p><p>Query</p><p>34.4ms</p><p>BooleanQuery</p><p>Query</p><p>31.1ms</p><p>QueryPhaseCollector contains</p><p>Collect</p><p>22.1ms</p><p>SimpleFieldCollector (9,100-doc priority queue)</p><p>Collect</p><p>19.1ms</p><p>FetchPhase</p><p>Fetch</p><p>12.1ms</p><p>Other steps, like request parsing and deserialization, queue wait time, response building</p><p></p><p>1,885.4ms</p><p>Total</p><p></p><p>2,004.2ms</p><h3>Fixing deep pagination with search_after</h3><p>In this case, the <code>search_after</code> approach is up to twice as fast because it avoids scanning and discarding earlier results. Instead, it resumes from the last document returned on page 90, using its <code>delivery_pickup_datetime</code> value (in epoch milliseconds) as a cursor and fetching only the next 100 records.</p>GET delivery-records/_search
{
    "profile": true,
    "size": 100,
    "track_total_hits": false,
    "query": {
        "bool": {
            "filter": [
                {
                    "term": {
                        "payment_type": "1"
                    }
                },
                {
                    "range": {
                        "tax_amount": {
                            "gte": 5
                        }
                    }
                }
            ]
        }
    },
    "sort": [
        {
            "delivery_pickup_datetime": {
                "order": "desc"
            }
        }
    ],
    "search_after": [
        1451604674000
    ]
}<p>As shown in the comparison below, performances are much better with the <code>search_after</code> approach.</p><p>Component</p><p>Phase</p><p>Time</p><p>ConstantScoreQuery contains</p><p>Query</p><p>6.8ms</p><p>BooleanQuery</p><p>Query</p><p>6.2ms</p><p>QueryPhaseCollector contains</p><p>Collect</p><p>2.6ms</p><p>PagingFieldCollector</p><p>Collect</p><p>2.0ms</p><p>FetchPhase</p><p>Fetch</p><p>2.2ms</p><p>Other steps like request parsing and deserialization, queue wait time, response building</p><p></p><p>9.4ms</p><p>Total</p><p></p><p>29.2ms</p><h3>Performance comparison</h3><p>Component</p><p>Deep pagination (from:9000)</p><p>search_after</p><p>Time saved</p><p>% Saved</p><p>ConstantScoreQuery</p><p>34.4ms</p><p>6.8ms</p><p>27.6ms</p><p>80.2%</p><p>BooleanQuery</p><p>31.1ms</p><p>6.2ms</p><p>24.9ms</p><p>80.1%</p><p>QueryPhaseCollector (total)</p><p>22.1ms</p><p>2.6ms</p><p>19.5ms</p><p>88.2%</p><p>FieldCollect</p><p>19.1ms</p><p>2.0ms</p><p>17.1ms</p><p>89.5%</p><p>FetchPhase</p><p>12.1ms</p><p>2.2ms</p><p>9.9ms</p><p>81.8%</p><p>Total</p><p>2,004.2ms</p><p>29.2ms</p><p>1,975ms</p><p>98.5%</p><p>The total query time drops from 1,004 ms to 29 ms (a 98.5% improvement) almost entirely because Elasticsearch no longer has to build and discard a 9,000-document priority queue on every request.</p><h2>How to benchmark Elasticsearch index settings with ES Rally</h2><p>With the cluster healthy and queries optimized, the final question is: Are the index settings themselves a bottleneck?</p><p><a href="https://github.com/elastic/rally">ES Rally</a> is the official benchmarking tool made by Elastic. Its key strength is reproducibility: You run the same workload against two configurations on the same cluster and hardware, so any difference in results is purely down to the settings you changed. ES Rally is able to measure improvements under identical conditions: same cluster, hardware, and dataset.</p><h3>Why default Elasticsearch index settings limit bulk indexing throughput</h3><p>In many cases, slow indexing is caused by default index settings that are suitable for development but not production. For instance, the default <code>refresh_interval</code> of 1 second increases resource usage because every <a href="https://www.elastic.co/docs/manage-data/data-store/near-real-time-search">refresh creates a new searchable segment</a>, and a replica count of 1 doubles the number of write operations per indexing request.</p><p>Our delivery logistics platform, for example, ingests thousands of new records daily in bulk, exactly the kind of workload where these defaults hurt. Setting <code>refresh_interval</code> to -1 disables refreshing during the bulk loading phase, and temporarily dropping replicas to 0 halves the write operations. Both settings are restored after the import is complete.</p><p>In this demo, we won’t focus on how to <a href="https://www.elastic.co/blog/creating-custom-es-rally-tracks-guide">prepare custom data</a> for benchmarking, but it’s worth mentioning a nice article about it.</p><p>For benchmarking purposes, you set up two folders: one with the current settings and another with the contender settings. The sample dataset in this case contains 1 million records. All the files described below can be found in <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/how-to-benchmark-and-diagnose-your-applications/">this repository</a>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte0a5ea88c39596e9/6a6119e71b1d4915a86f180f/7cf2cc18c22707bdc9e19e9da6d03fba05599c78-402x345.png" alt="File directory view labeled “rally-tracks,” showing two folders. Each folder contains four files, with icons indicating compressed and JSON file types" /><p>Both track folders contain an <code>index-settings.json</code> file. These files let you adjust mappings, shard counts, replica settings, and field types; for example, converting a field from <code>double</code> to <code>scaled_float</code> or from <code>text</code> to <code>keyword</code>. Because ES Rally tracks can be rerun quickly, it’s easy to experiment with different configurations and evaluate new optimization ideas as you iterate.</p><p>The next step is to run race commands (for current and contender race) to gather stats.</p>esrally race \
--track-path="delivery-records-current" \ #(name of current setup folder)
--target-hosts="es01:9200" \ #(location of es cluster)
--pipeline=benchmark-only \
--report-format=csv \
--report-file="current.csv" \
--race-id="run-current" \
--on-error=abortesrally race \
--track-path="delivery-records-contander" \
--target-hosts="es01:9200" \
--pipeline=benchmark-only \
--report-format=csv \
--report-file="contander.csv" \
--race-id="run-contander" \
--on-error=abort<p>Quick tip: Using meaningful <code>--race-id</code> values (rather than the auto-generated ones) makes the comparison command much easier to run.After both races are complete, compare the results:</p>esrally compare --baseline=run-current --contender=run-contander<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt41dd2371798de2f6/6a6119e81c2893300b6e5ceb/1c4bb14953cffcfa07542c1ae73528645388a50a-1620x1736.png" alt="Terminal-style table titled “Final Score,” comparing performance metrics between baseline and contender configurations. Columns include Metric, Task, Baseline, Contender, Diff, Unit, and Diff %. Rows list indexing, merging, refreshing, flushing, garbage collection, dataset size, and throughput data. Green text indicates improvements, and red text indicates regressions." /><p>Additionally, reports are generated into csv files that can be easily manipulated to extract important data.</p><p>Metric</p><p>Current settings</p><p>Contender</p><p>Change</p><p>Cumulative indexing time</p><p>0,826517</p><p>0,755967</p><p>+8.54%</p><p>Mean throughput</p><p>11,090 docs/s</p><p>50,921 docs/s</p><p>+359 %</p><p>Median throughput</p><p>10,821 docs/s</p><p>51,304 docs/s</p><p>+374 %</p><p>Min throughput</p><p>10,335 docs/s</p><p>41,387 docs/s</p><p>+300 %</p><p>Max throughput</p><p>13,006 docs/s</p><p>55,342 docs/s</p><p>+326%</p><p>The contender configuration delivers roughly 3–4× faster indexing throughput, purely from adjusting refresh_interval and replica count during the bulk load phase.</p><p>In practice, a single comparison is rarely enough. Common follow-up experiments include converting double fields to float or scaled_float, changing text fields to keyword, adjusting shard count, and tuning the refresh interval. Because ES Rally tracks rerun quickly, iterating through these options is straightforward.</p><h2>What to do when Elasticsearch isn't the bottleneck</h2><p>If AutoOps shows a healthy cluster, the Profile API shows fast queries, and ES Rally confirms that index settings are not the limiting factor, the bottleneck is on the client side. The Elasticsearch side is no longer the place to look. Common application-layer causes:</p><ul><li><p><strong>Network latency</strong> between the client and the cluster, especially across regions, VPNs, or proxies.</p></li><li><p><strong>Client-side deserialization</strong> overhead on large response payloads. Use <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-source-field#include-exclude"><code>_source exclude</code></a> or the <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrieve-selected-fields#search-fields-param"><code>fields</code></a> parameter to return only what the client actually needs.</p></li><li><p><strong>Client-side queueing</strong> before requests reach the cluster. The Elasticsearch client uses an HTTP connection pool. Under concurrent load, requests wait in line for a free connection. The Profile API never sees this wait because the request hasn't left the client yet.</p></li><li><p><strong>Single search calls</strong> where <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-msearch"><code>_msearch</code></a> would batch independent queries into one network round trip.</p></li><li><p><strong>Single-document indexing</strong> where the <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-bulk"><code>_bulk</code></a> API would amortize per-request overhead across many documents.</p></li></ul><p>The diagnostic value of AutoOps, the Profile API, and ES Rally is precisely that they let you definitively rule out the cluster, the query, and the index settings before turning to the application code. When the three tools come back clean, the investigation moves out of Elasticsearch and into the client.</p><p>For investigating these on the application side, <a href="https://www.elastic.co/observability/application-performance-monitoring">Elastic APM</a> is the natural next tool: It traces request paths through the client and surfaces exactly the kind of pre-cluster wait time the Profile API can't see.</p><h2>Conclusion</h2><p>Going back to the original problem, slow search on a delivery logistics platform, we traced the issue from the infrastructure level down to the query and settings level:</p><ul><li><p>AutoOps revealed an uneven shard distribution that concentrated all query load on a single node, causing latency spikes of up to 30 seconds for end users.</p></li><li><p>The Profile API showed that deep pagination was the source of the slow queries. Switching from from/size to <code>search_after</code> eliminated 98.9% of the latency.</p></li><li><p>ES Rally confirmed that optimizing index settings during bulk ingestion, specifically <code>refresh_interval</code> and replica count, can increase throughput by 3-4×.</p></li></ul><p>Each tool answers a different question. AutoOps gives you the "Is something structurally wrong?" view. The Profile API answers "Why is this specific query slow?" And ES Rally validates "Do these changes actually improve things, and by how much?" Used together and in this order, they cover the full diagnostic surface for the Elasticsearch side of the application. When all three come back clean, the next step is to look at the client, as outlined in the previous section.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-performance-diagnosis</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-performance-diagnosis</guid>
    <category><![CDATA[Operations]]></category>
    <dc:creator><![CDATA[Aleksandar Panov]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4be28a2b56f0f75b/6a6119e91b1d4902a46f1813/2997670e3ebae815e16cb1a336542a9ddd0de77e-1999x1072.png" length="0" type="image/png"/>
    <pubDate>Wed, 15 Jul 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Your compliance posture just got an upgrade: Elasticsearch now supports FIPS 140-3]]></title>
    <description><![CDATA[Elastic 9.4 brings FIPS 140-3 support for Elasticsearch and Kibana to GA. Here's what changes for federal, defense and regulated deployments, and how to migrate from 140-2.]]></description>
    <content:encoded><![CDATA[<p><strong>The latest National Institute of Standards and Technology (NIST) cryptographic standard is fully supported in Elastic 9.4, so your compliance posture and your software can move forward together.</strong></p><p><a href="https://csrc.nist.gov/pubs/fips/140-3/final">FIPS 140-3</a> support for Elasticsearch and Kibana is generally available in Elastic 9.4 for self-managed deployments. NIST has stopped accepting new FIPS 140-2 submissions, with existing certificates winding down in September 2026. Every layer of your infrastructure needs to catch up, and your search and analytics platform is no exception. Federal programs, defense integrators and regulated enterprises are actively moving procurement requirements to 140-3. With Elastic 9.4, your stack can answer "yes."</p><h2>Two problems, one deadline: why FIPS 140-2 is no longer enough</h2><p>If you've been running Elasticsearch in FIPS 140-2 mode, you already know the value of having a compliant Elastic Stack. But two pressures are converging:</p><ul><li><strong>The standard is sunsetting.</strong> FIPS 140-2 certificates are being phased out. Procurement officers, auditors, and authorization bodies are shifting their requirements to 140-3. A stack that only supports the old standard is a stack with an expiration date on its compliance story.</li><li><strong>Auditors don't accept "close enough."</strong> It's not sufficient to run FIPS-approved algorithms. Your application layer needs to be explicitly configured for FIPS mode, with nonapproved algorithms disabled and cryptographic boundaries clearly documented. Partial compliance is noncompliance.</li></ul><p><em>Can't I just put Elasticsearch behind a FIPS-compliant load balancer or run it on a FIPS-hardened OS?</em> No, and here's exactly why: FedRAMP's network security requirements apply to cryptographic operations at the application layer, not just the network boundary. Intranode communication, keystore encryption, and credential hashing all need to happen inside a validated module. A compliant perimeter around a noncompliant application layer isn't a FIPS deployment; it's an audit finding waiting to happen.</p><h2>What FIPS 140-3 mode does in Elasticsearch 9.4</h2><p>When you flip FIPS mode on, Elasticsearch and Kibana restrict every cryptographic operation to FIPS-approved algorithms and delegate all of it to the validated module in your runtime. Here's what that covers and how.</p><ul><li><strong>High-grade security Transport Layer Security (TLS) everywhere, no exceptions.</strong> Node-to-node transport, the REST API over HTTPS, Kibana talking to Elasticsearch: All of it uses only FIPS-approved cipher suites. Noncompliant suites aren't deprioritized. They're rejected.</li><li><strong>PBKDF2 replaces bcrypt for password hashing.</strong> Bcrypt isn't FIPS-approved, so in FIPS mode, Elasticsearch switches to PBKDF2 for the native realm, file realm, and any stored credentials. Your users and service accounts stay protected with an algorithm that the auditor won't flag.</li><li><strong>Keystore encryption stays inside the boundary.</strong> Secrets in the Elasticsearch keystore, API keys, repository credentials, and encryption keys for Kibana's saved objects are wrapped with FIPS-approved key derivation and encryption. No gaps between <em>the cluster is compliant</em> and <em>the secrets are compliant</em>.</li><li><strong>You supply the cryptographic module.</strong> FIPS 140-3 support in Elasticsearch is built on the <a href="https://www.bouncycastle.org/fips-java/">Bouncy Castle FIPS Java API 2.0</a>, a FIPS 140-3 validated cryptographic module that runs inside the Java Virtual Machine (JVM). Elasticsearch delegates all cryptographic operations to Bouncy Castle; it doesn't implement its own crypto functions. Elasticsearch uses your own FIPS 140-3 JVM and Bouncy Castle FIPS provider, giving you full control over your cryptographic boundary and module versioning.</li></ul><p>Kibana takes a different path, running in a Node.js environment configured with a FIPS-compliant OpenSSL 3 provider. Together, both components operate within clearly defined cryptographic boundaries clean enough to diagram for an auditor.</p><h2>Who needs FIPS 140-3 support in Elasticsearch</h2><ul><li><strong>Federal and defense teams</strong> operating Elastic inside FedRAMP boundaries, Cybersecurity Maturity Model Certification–scoped (CMMC-scoped) environments, or Defense Information Systems Agency (DISA) Security Technical Implementation Guide–hardened (STIG-hardened) infrastructure. You can now upgrade to 9.x without punching a hole in your authorization documentation. Your Authorization to Operate (ATO) package references FIPS 140-3, not a soon-to-expire 140-2 certificate.</li><li><strong>Financial services and healthcare organizations</strong> where Payment Card Industry Data Security Standard (PCI DSS), Sarbanes‑Oxley Act (SOX), or Health Insurance Portability and Accountability Act (HIPAA) audits ask how your search infrastructure handles cryptography. FIPS mode gives your compliance team a one-word answer instead of a three-paragraph explanation.</li><li><strong>Anyone fielding </strong><em>Do you support FIPS 140-3?</em><strong> in a procurement questionnaire.</strong> That question is showing up in enterprise requests for proposal (RFPs), partner security assessments, and insurance underwriting checklists. With 9.4, the answer is <em>yes</em>.</li></ul><h2>Migrating from FIPS 140-2 to FIPS 140-3</h2><p>If you're running FIPS 140-2 on Elastic 8.x today, you don't have to jump to 9.x on day one. <strong>Elastic 8.19 continues to support FIPS 140-2, and FIPS 140-3 support is available starting in 8.19.15.</strong> That gives you two paths:</p><ul><li><strong>Stay on 8.19.15+, and upgrade the standard in place.</strong> If you're not ready to move to 9.x, you can switch from FIPS 140-2 to 140-3 on your current major version. Your cluster stays put, and your compliance posture moves forward. FIPS 140-2 certificates remain valid through September 2026, so you have a window. But the earlier you transition, the less you're depending on a sunset timeline, and you can also benefit from new features!</li><li><strong>Move to 9.4, and land on 140-3 directly.</strong> If you're planning a major version upgrade anyway, 9.4 gives you FIPS 140-3 from the start, along with everything in the 9.x line: Elasticsearch Query Language (ES|QL) latest functionalities, improved ingest, updated security detections, and performance improvements. No compliance trade-off required.</li></ul><p></p><p></p><p>Stay on 8.19.15+</p><p>Upgrade to 9.4</p><p>FIPS standard</p><p>140-3 (from 8.19.15)</p><p>140-3 (from the start)</p><p>Major version change</p><p>No</p><p>Yes</p><p>New 9.x features</p><p>No</p><p>Yes</p><p>FIPS 140-2 certificates valid until</p><p>September 2026</p><p>N/A (lands on 140-3 directly)</p><p>Kibana covered</p><p>Yes</p><p>Yes</p><p></p><p>Either way, the configuration model will feel familiar. You enable FIPS mode in your YAML config, point to a FIPS-validated JVM, and the stack handles the rest. And Kibana is covered on both paths: Your visualization and analytics layer operates within the same compliant boundary as Elasticsearch.</p><h2>How to enable FIPS 140-3 in Elasticsearch 9.4</h2><p>FIPS 140-3 support is available now in Elastic 9.4 for self-managed deployments with a Platinum or Enterprise subscription, the same licensing tier as FIPS 140-2. For setup instructions, supported JVM versions, configuration details, and known limitations, see the <a href="https://www.elastic.co/docs/deploy-manage/security/fips">FIPS compliance documentation</a>.</p><p><a href="https://www.elastic.co/downloads/elasticsearch"><strong>Download Elastic 9.4</strong></a>, and give your compliance team some good news.</p><p>Questions? Your Elastic account team can help scope a FIPS deployment, or drop into the <a href="https://discuss.elastic.co/">Elastic community forums</a> to compare notes with other operators running in regulated environments.</p><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><h2>Frequently asked questions</h2><p><strong>Does Elasticsearch support FIPS 140-3?</strong></p><p>Yes. FIPS 140-3 support for Elasticsearch and Kibana is generally available in Elastic 9.4 for self-managed deployments. All cryptographic operations are delegated to the Bouncy Castle FIPS Java API 2.0, a FIPS 140-3 validated module. A Platinum or Enterprise subscription is required.</p><p><strong>When does FIPS 140-2 expire?</strong></p><p>NIST stopped accepting new FIPS 140-2 module submissions. Existing FIPS 140-2 certificates remain valid through September 2026. Organizations running Elasticsearch in FIPS 140-2 mode should plan their migration before that deadline to avoid gaps in their compliance documentation.</p><p><strong>Can I migrate from FIPS 140-2 to FIPS 140-3 without upgrading to Elasticsearch 9.x?</strong></p><p>Yes. FIPS 140-3 support is available in Elastic 8.19.15 and later. You can switch from FIPS 140-2 to FIPS 140-3 on your current major version without moving to 9.x. Alternatively, upgrading directly to Elastic 9.4 lands you on FIPS 140-3 from the start.</p><p><strong>What cryptographic changes does FIPS mode make in Elasticsearch?</strong></p><p>When FIPS mode is enabled, Elasticsearch restricts all operations to FIPS-approved cipher suites, replaces bcrypt with PBKDF2 for password hashing, and delegates all cryptographic functions to the Bouncy Castle FIPS provider. Non-approved cipher suites are rejected, not just deprioritized.</p><p><strong>Does Kibana support FIPS 140-3?</strong></p><p>Yes. Kibana operates in a Node.js environment configured with a FIPS-compliant OpenSSL 3 provider. Both Elasticsearch and Kibana run within clearly defined cryptographic boundaries when FIPS mode is enabled.</p><p><strong>Why isn't a FIPS-hardened OS or load balancer sufficient for FedRAMP compliance?</strong></p><p>FedRAMP's network security requirements apply to cryptographic operations at the application layer, not just at the network boundary. Intranode communication, keystore encryption and credential hashing must all occur inside a validated cryptographic module. A compliant perimeter around a non-compliant application layer is an audit finding, not a FIPS deployment.</p><p><strong>Is Elasticsearch FIPS 140-3 support available on Elastic Cloud?</strong></p><p>Elastic 9.4 FIPS 140-3 support covers self-managed deployments. Cloud deployment availability is not covered in this announcement. Contact your Elastic account team or check the Elastic documentation for cloud FIPS roadmap details.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/fips-140-3-elasticsearch-kibana</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/fips-140-3-elasticsearch-kibana</guid>
    <category><![CDATA[Operations]]></category>
    <category><![CDATA[Inside Elastic]]></category>
    <dc:creator><![CDATA[Fabio Busatto]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1092d0334a5b571c/6a572545f71286c608af57e4/3e67c4eeaa3d9411e97ee8b2ae74078a8177a911-2048x1143.avif" length="0" type="image/*"/>
    <pubDate>Tue, 07 Jul 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Ecommerce search optimization using margin and popularity boosting in Elasticsearch]]></title>
    <description><![CDATA[Learn how to optimize ecommerce search using margin and popularity boosting. This blog explains how a governed control plane treats economic optimization in Elasticsearch.]]></description>
    <content:encoded><![CDATA[<p>Parts 1 through 6 of this series describe a governed control plane that classifies intent, enforces constraints, resolves conflicts, personalizes results, and routes to the appropriate retrieval strategy. This post introduces a different objective: ensuring the retailer's business priorities influence which of those relevant products rank highest, with that optimization governed per query through policies rather than applied as a static global setting.</p><p>In most ecommerce deployments, economic signals, like profit margin and product popularity, are either ignored in search ranking or applied as static, global weights. A fixed margin boost might push high-margin products up across every query, which works for "chocolate" (where shoppers are open to suggestion) but backfires for "baby formula" (where shoppers want the trusted, popular brand).</p><p>The governed control plane makes it possible to treat economic optimization as a per-query decision, expressed as policy data and managed through the same admin UI as every other governance mechanism. A merchandiser can say "for chocolate queries, prioritize margin" and "for baby formula queries, prioritize popularity", without writing code, without deploying changes, and with full auditability.</p><p>For the mathematical foundation of margin and popularity boosting in Elasticsearch, including the logarithmic scaling formula and factor tuning explanation, see <a href="https://www.elastic.co/search-labs/blog/function-score-query-boosting-profit-popularity-elasticsearch">Boosting e-commerce search by profit and popularity with the function score query in Elasticsearch</a>.</p><h2>Two business signals: Margin and popularity</h2><p>Every product document in our product catalog carries two numeric fields:</p><ul><li><p><strong><code>margin</code></strong><strong>:</strong> The product's profit margin as a percentage (0 to 200 in our dataset).</p></li><li><p><strong><code>popularity</code></strong><strong>:</strong> A relative sales volume metric (0 to 10,000 in our dataset), such as weekly average units sold.</p></li></ul><p>These fields represent two fundamentally different business objectives. <em>Margin optimization</em> pushes profit per transaction. <em>Popularity optimization</em> pushes conversion probability since products that many shoppers buy are products that the current shopper is likely to buy.</p><h2>The baseline: Global boosting with business signals</h2><p>Before introducing per-query policy overrides, the system applies a default boost for both margin and popularity. These are implemented using Elasticsearch's <code>field_value_factor</code> with logarithmic scaling inside a <code>function_score</code> query as described in <a href="https://www.elastic.co/search-labs/blog/function-score-query-boosting-profit-popularity-elasticsearch">Boosting e-commerce search by profit and popularity with the function score query in Elasticsearch</a>.</p><p>The design has three properties worth noting:</p><ul><li><p><strong>Calibrated range.</strong> Each signal's factor is calibrated so that it contributes at most approximately +1.0 to the boost multiplier at the top of its range. Combined with a baseline weight of 1, the final multiplier ranges from 1.0 (a product with zero margin and zero popularity) to approximately 3.0 (maximum margin plus maximum popularity). A product with strong business signals scores roughly 3x higher than an identical product with none, regardless of the BM25 score magnitude.</p></li><li><p><strong>Logarithmic scaling.</strong> The <code>ln1p</code> modifier grows fast at small values (rewarding incremental gains) but flattens at high values (preventing runaway scores from a single dominant product). This also makes the system resilient to data distribution changes: If the maximum popularity in a dataset shifts significantly, the boost curve stretches rather than breaking.</p></li><li><p><strong>Multiplicative, not additive.</strong> The business-signal boost is applied multiplicatively against BM25 (<code>boost_mode: "multiply"</code>) rather than added to it. BM25 scores vary dramatically across queries, so an additive boost would have inconsistent impact depending on query specificity. Multiplicative scaling guarantees a consistent percentage uplift regardless of the absolute BM25 magnitude.</p></li></ul><h2>Per-query boosting overrides through policies</h2><p>The default weights (1.0 for both margin and popularity) apply to every query. But the governed control plane makes it possible to override these weights on a per-query basis through the same policy engine described in <a href="https://www.elastic.co/search-labs/blog/ecommerce-search-governance-control-plane-architecture">Part 3</a> and <a href="https://www.elastic.co/search-labs/blog/elasticsearch-percolator-search-governance">Part 4</a>.</p><p>Each policy document has two optional fields: <code>margin_boost_weight</code> and <code>popularity_boost_weight</code>. When a policy matches a query and includes weight overrides, those values flow through to the <code>function_score</code> construction, replacing the defaults.</p><h2>Why per-query control matters</h2><p>Consider two queries and why they demand different economic optimization strategies.</p><h3>Margin boosting: Chocolate</h3><p>A shopper searching for "chocolate" is browsing. They'll be satisfied by many chocolate-related products. The retailer's store-brand chocolate truffles at 60% margin might be just as appealing as the name-brand bar at 15% margin. Aggressive margin boosting pays for itself if the shopper doesn't care which chocolate they purchase and buys one of the margin-boosted hits.</p><h3>Chocolate results without margin boosting</h3><p>To isolate the effect of per-query margin boosting, we first disable margin boosting entirely for this query (margin boost weight: 0). Without any margin signal, the ranking is driven by text relevance. In our dataset, the first hit has a margin of 10 and the next one has a margin of 84 (out of a max of 200) as follows:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt356d92cab8ecddb8/6a170bbdcf4f25c6ffb2d1a4/12b36c0c2104fc30c87c993e751f469522a876b2-660x845.png" alt="A data interface displays two chocolate products, each with pricing, nutritional details, and metadata, with a focus on the margin and popularity fields for both products." /><h3>Setting a margin boost on queries for “chocolate”</h3><p>A merchandiser who decides that “chocolate” queries should prioritize margin makes that change in the admin UI, tests it against representative queries, and promotes it to production. The change takes effect on the next query. No engineering ticket, no deployment, no code change. The following "chocolate" policy sets <code>margin_boost_weight: 3.0</code>, which ensures that searches for chocolate will aggressively promote high-margin items.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt12ba9b66b64d51e1/6a170bbe8b73cbf32f18a061/a70dcbffc8fdf04d9161b327f79008548646478c-1097x1052.png" alt="A web interface titled “Edit rewrite policy” shows configuration fields for a search rewrite rule, with a focus on the Margin Boost Weight field." /><h3>Chocolate results with margin boosting</h3><p>With the above margin boost policy enabled, the higher-margin chocolates with a margin of 197 and 184 are boosted to the top of the results as follows (remember that the maximum margin in our dataset is 200):</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt56e66350d60078ec/6a170bc0cf4f252b77b2d1a8/d22245cbc5cd2198d1f27067718c4c8719d096fe-660x940.png" alt="A data interface shows two chocolate products, with pricing, nutritional details, and metadata, with a focus on the margin and popularity fields for both products." /><h3>Popularity boosting: Baby formula</h3><p>A parent searching for baby formula is not experimenting. They want the product that other parents trust. Pushing a high-margin store-brand formula above the established brand that thousands of parents are buying would feel wrong and erode trust. Popularity is the right signal here because it functions as social proof for a high-stakes purchase.</p><h3>Baby formula results without a popularity boost</h3><p>To isolate the effect of per-query popularity boosting, we first disable popularity boosting entirely for this query (<code>popularity_boost_weight: 0</code>). Without any popularity signal, the ranking is driven by text relevance. In this example, the top hit has a popularity of 50 on a scale that goes up to 10,000.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8e95da391643811c/6a170bc27d8d678f0170e74e/e8f1121de8f5982882f6ed8cef2359b2bb76e45f-651x752.png" alt="A data interface shows two infant formula products, with pricing, product details, and metadata, with a focus on the margin and popularity fields for both products." /><h3>Setting a popularity boost on queries for “baby formula”</h3><p>A "baby formula" policy sets <code>popularity_boost_weight: 5.0</code> and <code>margin_boost_weight: 0</code>; formula searches prioritize what's popular, completely ignoring margin.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt25023505bc9263ef/6a170bc4e8fbcec01639fc7f/72cdf6ac8bc24dc93d3a540a6d359a5b03145cef-1083x1042.png" alt="A web interface titled “Edit rewrite policy” shows configuration fields for a search rewrite rule, with a focus on the Popularity Boost Weight field." /><h3>Baby formula results with popularity boosting</h3><p>If we enable the above rule, then the most popular baby formula (Lactogen 2 with a popularity of 9979) will be boosted to the top of the results, as shown below.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta626161c2174e6e7/6a170bc5cdacbfd25a7d2a2c/2ff438b375cf8bf21fc5fe27ee311afe510220df-662x732.png" alt="A data interface shows two baby formula products, with pricing, product details, and metadata, with a focus on the margin and popularity fields for both products." /><h2>Disabling business signals: Clearance</h2><p>Not every query benefits from economic boosting. A shopper searching for "clearance" is looking for deals; margin and popularity are both irrelevant to that intent. A high-margin product is the opposite of what the shopper wants, and a popular product may not be on clearance at all.</p><p>A "clearance" policy sets <code>margin_boost_weight: 0</code> and <code>popularity_boost_weight: 0</code>, which disables both business signals entirely. Results are ranked on pure text relevance with no economic influence. This completes the design space: Policies can amplify either signal independently, rebalance them, or turn them off altogether.</p><h2>How overrides flow through the control plane</h2><p>When the percolator returns matching policies, the control plane checks for <code>margin_boost_weight</code> and <code>popularity_boost_weight</code> fields on the highest-priority matching policy. If present, those values replace the defaults in the <code>RewriteState</code>. If no matching policy includes weight overrides, the default values (1.0 for both) are used.</p><p>The weights then flow through to the <code>function_score</code> construction when the final Elasticsearch query is assembled. The structure of the <code>function_score</code> doesn't change; only the <code>weight</code> values on the margin and popularity functions.</p><p>Weight overrides participate in the same governance model as every other policy mechanism. They’re subject to priority ordering: A Christmas campaign policy with <code>margin_boost_weight: 0.5</code> will override a product-category policy with <code>margin_boost_weight: 3.0</code> if the campaign policy has higher priority. The cascading transformation model from <a href="https://www.elastic.co/search-labs/blog/ecommerce-search-governance-control-plane-architecture">Part 3</a> applies: Economic optimization parameters are just another field in the policy's execution plan.</p><h2>Interaction with other policies</h2><p>Per-query weight overrides compose naturally with the constraint enforcement, conflict resolution, and personalization mechanisms described in earlier parts of this series.</p><p>Consider a search for "cheap chocolate" during a Christmas campaign, with a shopper who has a purchase history and belongs to a vegan cohort. The control plane processes this query through the full governance stack:</p><ol><li><p>The "cheap" policy extracts the price constraint and removes "cheap" from the query.</p></li><li><p>The "chocolate" policy sets <code>margin_boost_weight: 3.0</code> and constrains results to chocolate categories.</p></li><li><p>The “Christmas campaign” policy (higher priority) overrides the category constraint with seasonal categories and adjusts the price ceiling.</p></li><li><p>The “vegan cohort” policy applies a soft boost to vegan-certified products.</p></li><li><p>The margin and popularity boosts are applied with the governed weights (margin at 3.0× from the “chocolate” policy, popularity at the default 1.0×).</p></li><li><p>The shopper's purchase history boosts are applied as the outermost scoring layer.</p></li></ol><p>Every layer stacks multiplicatively. The economic optimization weights are governed by the same policy framework that controls category constraints, campaign overrides, and cohort-specific boosts. A merchandiser can tune all of these through the admin UI, all without code changes.</p><p>This example also illustrates where economic optimization sits in the scoring stack. The layers nest in a deliberate order: the base query (keyword or semantic match), then governance constraints (hard filters and soft boosts from <a href="https://www.elastic.co/search-labs/blog/ecommerce-search-governance-control-plane-architecture">Part 3</a> and <a href="https://www.elastic.co/search-labs/blog/elasticsearch-percolator-search-governance">Part 4</a>), then business-signal boosts (margin and popularity with governed weights), and then purchase history personalization (<a href="https://www.elastic.co/search-labs/blog/elasticsearch-personalized-search-governed-ecommerce">Part 6</a>). Each layer wraps the previous one, and the effects compound multiplicatively. Governance controls what appears. Economic optimization influences what ranks highest from the retailer's perspective. Personalization adjusts ranking further from the shopper's perspective.</p><h2>Tuning guidance</h2><p>The factor values in the baseline <code>function_score</code> are calibrated for the demo dataset's field ranges. A production deployment with substantially different ranges for margin or popularity should recalibrate the factors so that each signal contributes a consistent maximum boost. The logarithmic scaling provides built-in resilience to outliers and distribution shifts, but the factors are worth reviewing whenever the underlying data changes significantly. For the calibration methodology, see <a href="https://www.elastic.co/search-labs/blog/function-score-query-boosting-profit-popularity-elasticsearch">Boosting e-commerce search by profit and popularity with the function score query in Elasticsearch</a>.</p><h2>From economic optimization to agentic AI</h2><p>The governed control plane now handles intent classification, constraint enforcement, conflict resolution, personalization, and economic optimization, all expressed as policy data, all managed through a business-editable admin UI, and all composable through a deterministic transformation framework.</p><p>The final post in this series asks what happens when the input to this system isn’t a search string typed by a human shopper, but an intent string extracted by an AI agent, and why the deterministic properties of the governed control plane become even more critical when the upstream decision-maker is probabilistic.</p><h2>Put governed ecommerce search into practice</h2><p>The per-query economic optimization described in this post (policy-governed margin and popularity weights composing with governance constraints, personalization, and campaign overrides) was designed and built by Elastic Services Engineering as part of our repeatable ecommerce search accelerators. Contact <a href="https://www.elastic.co/consulting">Elastic Professional Services</a>.</p><h2>Join the discussion</h2><p>Have questions about search governance, retrieval strategies, or ecommerce search architecture? Join the broader <a href="https://discuss.elastic.co/">Elastic community conversation</a>.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/ecommerce-search-optimization-query-governed</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/ecommerce-search-optimization-query-governed</guid>
    <category><![CDATA[Operations]]></category>
    <dc:creator><![CDATA[Alexander Marquardt,Honza Král,Taylor Roy]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt272ceff311a51c5e/6a170bc7e8fbce29bb39fc83/44a9dc320fa5f36f263e48c7ab2a050955e1d071-721x420.jpg" length="0" type="image/jpeg"/>
    <pubDate>Wed, 13 May 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Multi-tier search with Elastic for ecommerce search governance: Fixing poor recall]]></title>
    <description><![CDATA[Learn how to build a multi-tier retrieval strategy used to execute ecommerce governed search plans and improve recall management. We'll cover how to orchestrate semantic matching while maintaining stable results, facets, and pagination.]]></description>
    <content:encoded><![CDATA[<p>A common issue in ecommerce search is poor recall. This occurs when a system lacks a governed fallback strategy. The solution is a multi-tier execution model. This post describes a multi-tier retrieval strategy used to execute governed search plans. It explains how to orchestrate strict, relaxed, and semantic matching while maintaining stable results, facets, and pagination.</p><h2><strong>From policy logic to retrieval architecture</strong></h2><p><a href="https://www.elastic.co/search-labs/blog/ecommerce-search-governance-control-plane-architecture">Part 3</a> and <a href="https://www.elastic.co/search-labs/blog/elasticsearch-percolator-search-governance">Part 4</a> provided a technical deep dive into the governed control plane and its implementation using the <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/percolator">Elasticsearch percolator</a>. Once the logic layer has identified which policies to apply, the system must address the retrieval strategy used to execute the search.</p><p>Managing the transition from precision to recall is a critical function of any ecommerce search engine. For example, a basic search implementation often defaults to broad keyword matching. If a shopper searches for "organic Pink Lady apples", this can lead to irrelevant results, such as apple-scented dish soap, apple juice, or organic pink grapefruit, appearing at the top of the list simply because they share a common term. While these items are technically matches, they fail to satisfy the user's intent and typically lead to high bounce rates. However, a "No results" page is equally detrimental to conversion. This conflict is resolved by implementing a <strong>three-tier execution model</strong>, which uses the governed control plane to orchestrate a principled fallback strategy.</p><h2><strong>The three-tier execution model</strong></h2><p>This architecture executes up to three retrieval tiers in a sequence, each with a specific matching logic.</p><h3><strong>Highest tier: Strict matching</strong></h3><p><em>Strict matching</em> is a lexical match that requires that <strong>all</strong> query terms appear in the product metadata.</p><ul><li><p><strong>The logic:</strong> A search for "organic navel oranges" returns only products containing all three terms.</p></li><li><p><strong>Application:</strong> This tier provides the highest precision. When a customer types a precise product name, such as "organic navel oranges", they’re typically seeking that exact item rather than an alternative.</p></li></ul><h3><strong>Mid-tier: Relaxed matching</strong></h3><p>If the strict tier fails to return sufficient results, the system expands the search parameters.</p><ul><li><p><strong>The logic:</strong> This tier allows for a subset of terms to lexically match, using <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-minimum-should-match">Elasticsearch's minimum_should_match</a> logic.</p></li><li><p><strong>Application:</strong> Relaxed matching maintains lexical grounding. A search for "organic navel oranges" might surface "navel oranges" (missing the "organic" term) or "organic oranges" (missing the "navel" term). These represent intuitive, keyword-based alternatives for the shopper.</p></li></ul><h3><strong>Lowest tier: Semantic matching</strong></h3><ul><li><p><strong>The logic:</strong> This tier uses vector/semantic embeddings (such as Elastic Learned Sparse EncodeR [ELSER], E5, or Jina) to retrieve conceptually related products, regardless of direct keyword overlap.</p></li><li><p><strong>Application:</strong> A search for "organic navel oranges" might surface "mandarins" or "clementines”. This serves as the final retrieval tier, intended to provide relevant options when literal keyword matches are unavailable.</p></li></ul><p>To see this multi-tier orchestration in action and how the Engine steps down from lexical to semantic matching, watch the video: <a href="https://youtu.be/k02NHvIAHsk?si=tJKwmc4ds3zjcRPF">Eliminating Zero-Result Pages: PRISM’s Multi-Tier Search Fallback</a>.</p><h2><strong>Tier orchestration: The "bucket filling" logic</strong></h2><p>While the governed control plane provides the logic and the queries for each tier, the application layer is responsible for the execution. The application executes these tiers sequentially and excludes lower tiers once the accumulated result count on the first page reaches or exceeds 10 items (or whatever number of results you want to display on the first page). This threshold ensures a full first page of results while prioritizing the most accurate retrieval method.</p><h3>Scenario 1: High-intent search ("oranges")</h3><p>The first tier returns 15 hits. Since 15 is more than 10, the current result set is locked to only strict matches (which can be paged through) and subsequent tiers are not executed.</p>Strict tier:   [##########]##### (&gt;= 10 found: Exact matches)
Relaxed tier:  [          ]      (Tier bypassed)
Semantic tier: [          ]      (Tier bypassed)<h3>Scenario 2: Specific but limited results ("organic blood oranges")</h3><p>The strict tier finds only four items. Since this is less than 10, the system triggers the relaxed tier, which finds 12 more relevant products. The combined total (16) meets the threshold of 10, so the current result set is locked to the strict and relaxed tiers. Subsequent paging will only surface results from these two tiers (preventing lower-quality semantic hits from appearing on later pages).</p>Strict tier:   [####      ]       (4 found)
Relaxed tier:  [    ######]###### (&gt;= 6 found)
Semantic tier: [          ]       (Tier bypassed)<h3>Scenario 3: Abstract or intent-based search ("high vitamin C snacks")</h3><p>Keyword matches are limited (only five hits between tiers 1 and 2). The system triggers the semantic tier to find conceptually relevant items, such as kiwis, guavas, or red peppers, to fill the result set. The result set for this query includes products from all tiers.</p>Strict tier:   [##        ]             (2 found)
Relaxed tier:  [  ###     ]             (3 found)
Semantic tier: [     #####]######################...<p>This orchestration optimizes for latency, as the computational cost of the semantic tier is only incurred when the keyword-based tiers are insufficient. Additionally, this allows fast-responding keyword results to be displayed while semantic results are integrated shortly after, maintaining a responsive user interface.</p><h2><strong>Determining intent via tier activation</strong></h2><p>The logic used to fill the first page serves a critical secondary purpose: It acts as a diagnostic for user intent. The application uses the logic returned by the governed control plane to determine which tiers remain active for the current result set and paging.</p><p>If the strict and relaxed tiers together yield fewer than 10 results, the query is likely exploratory or abstract. In this case, activating the semantic tier is a benefit. Because the query is diagnosed as exploratory, the system allows the shopper to page through the entire depth of the semantic results. This provides access to conceptually related alternatives that lexical matching would have missed, which is appropriate for an abstract search.</p><p>Conversely, if the strict tier returns a robust set of results (for example, 30 hits), it confirms that the system has found high-precision matches. The user can page through those 30 hits and will likely find what they’re looking for. In this scenario, there’s no need to provide additional, less relevant exploratory hits. By disabling lower tiers for these high-precision queries, we ensure that a shopper deep diving into specific results isn’t distracted by irrelevant semantic fallback as they paginate through the current result set.</p><h2><strong>Governance across tiers</strong></h2><p>A critical component of this architecture is that policies apply globally across all tiers. If a user has a "vegan" preference profile, the governed control plane injects that constraint into the strict, relaxed, and semantic queries. This ensures that even when the system uses semantic fallback to return "mandarins" for an orange search, the results remain compliant with the user's broader dietary preferences or business constraints.</p><h2><strong>The problem of facet instability</strong></h2><p>A challenge with multi-tier search is maintaining consistent faceted navigation (sidebar filters). If a search for "chocolate" yields 12 strict results, the sidebar filters might show "dark" and "milk". If a user selects "dark" and the result count drops, a naive system might trigger the semantic tier to fill the page, which could suddenly introduce "red wine" into the filters due to a semantic relationship.</p><p>The governed control plane identifies which tiers contributed to the initial search and locks the facets to those tiers. This prevents the sidebar from changing unexpectedly during a filtered session, ensuring a stable user experience.</p><h2><strong>The pagination challenge: Seamless multi-tier paging</strong></h2><p>Pagination in a tiered system requires precise state management. As established, the first page determines the scope of the <strong>current result set</strong>. If the first page required semantic results, the user can page through all available results from all three tiers. On the other hand, if the first page was satisfied by high-intent keyword matches, the semantic tier is not retrieved for that specific result set.</p><p>The governed control plane manages this through:</p><ul><li><p><strong>Tier locking:</strong> The response includes an array identifying the contributing tiers. The front end returns this on subsequent requests to keep the tier composition consistent across all pages.</p></li><li><p><strong>Dynamic offset calculation:</strong> The back end calculates an offset based on the requested page and the total products returned in preceding tiers.<strong>Example:</strong> If the first page has returned seven strict matches and three relaxed matches, a request for page 2 (starting at index 10) would execute a relaxed tier query with an offset of three.</p></li><li><p><strong>ID exclusion for lower tiers:</strong> The system retrieves IDs from the higher tiers (which, by definition, will always be fewer than the page size threshold) and explicitly excludes them from lower-tier results using an ID-only query (which avoids the overhead of a full fetch phase for excluded items).</p></li></ul><h2><strong>Summary</strong></h2><p>The multi-tier approach ensures search results are precise when data is available and helpful when it is not. By providing a governed fallback sequence for the application to execute, the architecture maintains high relevance while eliminating "no results" scenarios.</p><h2><strong>What's next in this series</strong></h2><p>The next posts in this series extend the governed control plane into new territory. Part 6 explores personalization (using purchase history boosting and cohort-aware policies), and Part 7 demonstrates per-query economic optimization. Stay tuned!</p><h2><strong>Put governed ecommerce search into practice</strong></h2><p>The search architecture described in this post, where retrieval tiers, economic weights, and governance constraints compose into a single request, was designed and built by Elastic Services Engineering as part of our repeatable ecommerce search accelerators.</p><p>To learn more about applying these patterns to your business, <a href="https://www.elastic.co/contact"><strong>Contact Elastic Professional Services</strong></a><strong>.</strong></p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/multi-tier-search-ecommerce-governance</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/multi-tier-search-ecommerce-governance</guid>
    <category><![CDATA[Operations]]></category>
    <dc:creator><![CDATA[Alexander Marquardt,Honza Král,Taylor Roy]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt69260cf1c6f964e3/6a17db030b0bed6f52dd346e/5d64716981e76396b401fd069d0a635b6929ba94-720x420.jpg" length="0" type="image/jpeg"/>
    <pubDate>Wed, 06 May 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch percolator for ecommerce search governance: translating ambiguous queries into controlled retrieval strategies]]></title>
    <description><![CDATA[Learn how to use the Elasticsearch percolator to implement search governance. In this blog, we outline the patterns needed to create a governed policy engine in production and create a controlled retrieval strategy.]]></description>
    <content:encoded><![CDATA[<p>This post is a technical deep dive into the Elasticsearch implementation of the control plane architecture described in <a href="http://elastic.co/search-labs/blog/ecommerce-search-governance-control-plane-architecture">Part 3</a>, showing how to build it using the Elasticsearch percolator. It outlines the patterns used to implement a deterministic, governed policy engine in production.</p><h2><strong>From architecture to implementation</strong></h2><p><a href="http://elastic.co/search-labs/blog/ecommerce-search-governance-control-plane-architecture">Part 3</a> described the control plane architecture: reverse matching as a lookup primitive, policy documents that separate match from action, and cascading transformations that compose multiple policies into a single execution plan. This post goes hands-on with the Elasticsearch feature that powers the policy lookup: the <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-percolate-query">percolator query</a>.</p><p>The percolator is a natural fit for governance because it inverts the direction of search in exactly the way a control plane needs. This post walks through the implementation step by step, starting with a clear explanation of what the percolator does and why it matters, and then moving through index design, policy storage, query-time evaluation, and multi-policy composition.</p><h2><strong>How normal search works</strong></h2><p>In an ecommerce system, you may have hundreds of thousands or millions of product documents containing fields such as <code>title</code>, <code>category</code>, and <code>price</code>. When a user searches for matching documents, you're asking Elasticsearch to compare the user’s search string against one or more stored fields in these product documents. Elasticsearch's default analyzer, <a href="https://www.elastic.co/docs/reference/text-analysis/analysis-standard-analyzer">the standard analyzer</a>, lowercases text and splits it into tokens. A search for “oranges” matches “Oranges” because of lowercasing. With a language-aware analyzer that includes stemming, it also matches “orange” because both forms reduce to the same stem. For example, the following <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-match-query">match query</a> returns documents that have “orange” or “oranges” in their <code>“title”</code> field.</p>POST products/_search
{
  "query": {
    "match": {
      "title": "oranges"
    }
  }
}<p>So for the above query, Elasticsearch returns the product documents whose <code>title</code> field matches “oranges”, which could include results such as “Orange Fruit Spread”, “Orange Juice”, “Juicy oranges”, “Orange Marmalade”, and so on. The key point to remember is that Elasticsearch is commonly used to compare a search string against documents and to return the documents that match the search string.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt806e1c8c115bc9b6/6a170dba67045b634645c266/ba758f25616f2106d245ce0d47926c174766e028-642x318.png" alt="A diagram comparing an incoming search string to stored product titles, showing matches for three titles containing “orange” and no match for two titles that do not." /><h2><strong>The governance problem: Finding relevant policies before searching for products</strong></h2><p>As established in <a href="https://www.elastic.co/search-labs/blog/series/governed-search-patterns">Parts 1 through 3</a>, a governed search system does not send the user's search string directly to the product catalog. First, it checks whether any policies apply to that search string.</p><p>A merchandiser has decided that when someone searches for exactly "oranges", results should be restricted to the Oranges category, eliminating orange juice, orange marmalade, and orange soda. That business decision is stored as a policy. When a user types "oranges", the control plane needs to find that policy, read its instructions, and modify the search against the product catalog accordingly. In order to do this, the control plane needs to figure out which stored policies are relevant for this search string.</p><p>An enterprise deployment might have hundreds or thousands such policies. Checking them one by one with if/else logic is the application-layer anti-pattern described in <a href="http://elastic.co/search-labs/blog/ecommerce-search-governance-zero-deploy">Part 2</a>. What we need is a way to store all of those policies in an index and instantly find the ones that match a given search string. This is where the percolator comes in.</p><h2><strong>Flipping the direction: The percolator</strong></h2><p>We previously mentioned that in a normal search, Elasticsearch is commonly used to compare a search string against documents and to return the documents that contain that search string.</p><p>The percolator inverts this. With a percolator, you have an index where each document stores a query pattern, and then an incoming search string is checked against these stored queries to determine which of these stored query patterns has triggered.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1e7e2966bf46474d/6a170dbba929cf500aae0a57/1e6348531d1c0be57b385f51d248488cf58489ff-642x279.png" alt="A diagram showing several stored query patterns tested independently against an incoming search string, with “oranges” producing a match and all other patterns returning no match." /><p>For governance, the "stored query patterns" are policies. Each policy contains a pattern that describes the kind of search string it should match. For example, does the search string exactly match “oranges”, or does the search string contain “olive oil”? The incoming string is the user's search text, which arrives at query time and needs to be checked against all stored policy patterns. This is covered in a <a href="https://youtu.be/Ap5K2Y00Xjc?t=246">related PRISM video at 4:09</a>.</p><h2>Step by step: How a search for "oranges" finds its policy</h2><h3>The policy</h3><p>A merchandiser has authored a policy that matches if a user searches for exactly "oranges" without any other words. Once the percolator matches, the remainder of the document includes the rules that the control plane will use to build the Product query; in this example, one of the rules is to restrict (filter) results to the Fruits category.</p>{
  "percolator": {
    "match_phrase": { "query": "START oranges END" }
  },
  "rule_type": "filter",
  "rule_args": {
    "filters": [
      {
        "field": "categories",
        "values": ["Fruits"],
        "mode": "hard_filter",
        "on_conflict": "soft_boost",
        "on_conflict_boost_weight": 1.0
      }
    ]
  },
  "priority": 0,
  "enabled": true
}<p>The <code>percolator</code> field contains the pattern that defines when this policy should fire. In this case, it matches the phrase <code>"START oranges END"</code>. The <code>rule_type</code> and <code>rule_args</code> fields define what the policy should do when it fires. The <code>START</code> and <code>END</code> tokens are boundary markers, which we will explain shortly.</p><p>You can see how a policy is authored in the PRISM Studio UI at <a href="https://youtu.be/Ap5K2Y00Xjc?t=172">2:52 of the related PRISM video</a>.</p><h3>The user searches</h3><p>A shopper types "oranges" into the search bar.</p><h3>The control plane checks for matching policies</h3><p>Before searching the product catalog, the control plane intercepts the user search string, wraps it in boundary markers, and sends it to the percolator:</p>POST policies/_search
{
  "query": {
    "percolate": {
      "field": "percolator",
      "document": {
        "query": "START oranges END"
      }
    }
  }
}<p>The string <code>"START oranges END"</code> is checked against all stored policy patterns. Internally, Elasticsearch runs the stored policy patterns against this string and returns the ones that match. That's the percolator. The user's search string was checked against all stored policy patterns, and the ones that matched were returned. No if/else chains. No sequential evaluation. The index handles the matching.</p><h3>The control plane applies the policy</h3><p>The control plane reads the matched policies’ actions. The above policy instructs the control plane to restrict results to the Fruits category. The control plane builds the final Elasticsearch query against the product catalog as follows:</p>POST products/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title": "oranges" } }
      ],
      "filter": [
        { "terms": { "categories": ["Fruits"] } }
      ]
    }
  }
}<p>The user searched for "oranges”. The product catalog receives a query for "oranges" constrained to the Fruits category. Because of this constraint, orange juice, orange marmalade, and orange soda are excluded.</p><h3>Why "orange marmalade" does NOT trigger the oranges policy</h3><p>Suppose a different user searches for "orange marmalade”. The control plane wraps the string and percolates: <code>"START orange marmalade END"</code>. The oranges policy's pattern is <code>match_phrase: "START oranges END"</code>. The oranges policy does not match and therefore the policy isn’t applied, and the results aren’t constrained to the Fruits category.</p><p>This is the purpose of the <code>START</code> and <code>END</code> boundary markers. Without them, a policy that matches on the word "oranges" could accidentally fire on a query like "orange marmalade". By wrapping the user's search string with <code>START</code> and <code>END</code> and including those markers in the policy's pattern, we ensure that the policy only fires when "oranges" is the complete search string, without any other words. This matches both the shoppers and the merchandiser's intent.</p><h2>A second policy: "olive oil" on the stemmed field</h2><p>Not every policy needs an exact string match. The “olive oil” policy matches on a stemmed field, so it fires regardless of minor word-form variations:</p>{
  "percolator": {
    "bool": {
      "should": [
        { "match_phrase": { "query.stemmed": "START olive oil END" } }
      ]
    }
  },
  "rule_type": "filter",
  "rule_args": {
    "filters": [
      {
        "field": "categories",
        "values": ["Olive oils"],
        "mode": "hard_filter",
        "on_conflict": "soft_boost",
        "on_conflict_boost_weight": 1.0
      }
    ]
  },
  "priority": 300,
  "enabled": true
}<p>This policy's pattern matches against <code>query.stemmed</code> instead of <code>query</code>. When the user's search string arrives, it’s stored in both a <code>query</code> field (the exact text) and a <code>query.stemmed</code> field (analyzed with a stemming analyzer that reduces words to their stems, so "olives" and "olive" both reduce to the same stem, as do "oils" and "oil"). The policy's pattern is checked against the stemmed version of the string, so it fires regardless of minor word-form variations.</p><p>The <code>START</code> and <code>END</code> boundary markers work on the stemmed field, as well, ensuring this policy only fires when "olive oil" is the entire search string, not when it appears as part of something longer.</p><p>The rest of this post covers the implementation details that make this production-ready: the index mapping that supports both matching modes, how highlights drive phrase removal and consumed phrase tracking, and how multiple conflicting policies compose into a single execution plan.</p><h2><strong>The policy index mapping</strong></h2><p>The policy index needs a percolator field to hold stored query patterns and a text field that mirrors the structure of the incoming search string the percolator will match against. The mapping below is simplified for clarity. A production deployment is more complex, using custom analyzers to handle boundary markers, variable pattern matching (for example, recognizing that "under $4" contains a currency value), and other kinds of analysis.</p>PUT policies
{
  "mappings": {
    "properties": {
      "percolator": {
        "type": "percolator"
      },
      "query": {
        "type": "text",
        "fields": {
          "stemmed": {
            "type": "text",
            "analyzer": "stemming"
          }
        }
      },
      "rule_type": { "type": "keyword" },
      "rule_args": { "type": "object", "enabled": false },
      "priority": { "type": "integer" },
      "enabled": { "type": "boolean" }
    }
  }
}<p>The index is named <code>policies</code> because each document represents a complete governed policy as defined in <a href="http://elastic.co/search-labs/blog/ecommerce-search-governance-zero-deploy">Part 2</a>. This includes match criteria, action, priority, and metadata. The <code>rule_type</code> and <code>rule_args</code> fields contain the action component of the policy, which contain the instructions that the control plane will use to compose the query for execution against the product catalog.</p><p>The <code>query</code> field is the string that the percolator matches against. It has two variants: an exact version and a stemmed version. When the user's search string arrives, it’s placed into this field in the temporary in-memory index. Policies that match on <code>query</code> see the exact string; policies that match on <code>query.stemmed</code> see the stemmed version.</p><h2><strong>Percolating with highlights, filtering, and sorting</strong></h2><p>The simple examples above showed minimal percolation requests. In practice, the control plane adds highlighting, filters disabled policies, and sorts by priority:</p>POST policies/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "percolate": {
            "field": "percolator",
            "document": {
              "query": "START olive oil END"
            }
          }
        },
        {
          "term": { "enabled": true }
        }
      ]
    }
  },
  "highlight": {
    "fields": {
      "query": {
        "matched_fields": ["query.stemmed"]
      }
    }
  },
  "sort": [
    { "priority": { "order": "desc" } }
  ]
}<p>The highlight configuration uses <code>"query"</code> as the field key with <code>"query.stemmed"</code> in <code>matched_fields</code>. This tells Elasticsearch's unified <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/highlighting">highlighter</a> to return highlights on the parent <code>query</code> field but to also consider matches from the <code>query.stemmed</code> subfield when determining which tokens to highlight. This is what allows a policy that matches on the stemmed field to still produce accurate highlight spans on the original text, which the control plane needs for phrase removal and consumed phrase tracking.</p><p>The <code>enabled: true</code> filter ensures that disabled policies are skipped. The <code>sort</code> on priority ensures that higher-priority policies are returned first, so the control plane can process them in the correct order for cascading transformations. The <code>highlight</code> field is the most important addition; it tells us exactly which words in the user's search string triggered each match.</p><p>The response for an "olive oil" search may look as follows:</p>{
  "hits": {
    "hits": [
      {
        "_id": "en_2c3021c8",
        "_source": {
          "rule_type": "filter",
          "rule_args": {
            "filters": [
              {
                "field": "categories",
                "values": ["Olive oils"],
                "mode": "hard_filter",
                "on_conflict": "soft_boost",
                "on_conflict_boost_weight": 1.0
              }
            ]
          },
          "priority": 300
        },
        "highlight": {
          "query": ["&lt;em&gt;START olive oil END&lt;/em&gt;"]
        }
      }
    ]
  }
}<h2><strong>Why highlights matter</strong></h2><p>Notice the highlight in the response: <code>"&lt;em&gt;START olive oil END&lt;/em&gt;"</code>. Elasticsearch is telling us exactly which words in the user's search string caused the policy to match. This isn’t cosmetic. The highlight metadata drives two critical downstream behaviors:</p><p><strong>Phrase removal.</strong> Some policies need to remove the matched text from the search string before constructing the product catalog query. For example, a policy that matches on "cheap" removes that word and converts it into a price filter instead. The highlight identifies exactly which span of the search string the policy matched, so the system knows what to remove.</p><p><strong>Consumed phrase tracking.</strong> As described in <a href="http://elastic.co/search-labs/blog/ecommerce-search-governance-control-plane-architecture">Part 3</a>, when multiple policies match the same search string, a higher-priority policy might remove words that a lower-priority policy also matched on. By comparing each policy's highlight against the current (evolving) search string, the system can detect that a phrase has been consumed and skip the lower-priority policy. This prevents double-processing and ensures deterministic behavior.</p><p>You can learn more about how highlighting works in <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/how-es-highlighters-work-internally">this article</a>.</p><h2><strong>From percolation to execution plan</strong></h2><p>The percolator returns a set of matching policies. But as <a href="http://elastic.co/search-labs/blog/ecommerce-search-governance-control-plane-architecture">Part 3</a> described, the lookup is only half the story. The other half is composing those matches into a coherent execution plan. Here’s what that looks like for a concrete query.</p><h3><strong>Worked example: "Cheap chocolate" during a Christmas campaign</strong></h3><p>Suppose the system has two active policies: the "Cheap chocolate" policy (priority 210) and the "Christmas chocolates" policy (priority 300), both described in detail in <a href="http://elastic.co/search-labs/blog/ecommerce-search-governance-control-plane-architecture">Part 3</a>.</p><p><strong>Step 1: Percolate.</strong> The user searches for "cheap chocolate." The control plane wraps the search string as <code>"START cheap chocolate END"</code> and sends it to the percolator. Two policies match: The "Cheap chocolate" policy's pattern matches on the phrase "cheap chocolate"; and the "Christmas chocolates" policy's pattern matches on "chocolate" via the stemmed field.</p><p><strong>Step 2: Sort by priority.</strong> The percolator returns both policies, sorted by priority in descending order. The “Christmas chocolates” policy (300) is processed first, followed by the “Cheap chocolate” policy (210).</p><p><strong>Step 3: Apply the cascading transformation.</strong> This is the <code>initial state → [Policy A] → state' → [Policy B] → state'' → execution plan</code> model from <a href="http://elastic.co/search-labs/blog/ecommerce-search-governance-control-plane-architecture">Part 3</a>.</p><p>The “Christmas chocolates” policy (priority 300) applies first:</p><ul><li><p>Adds a category hard filter: "Christmas foods and drinks," "Christmas sweets".</p></li><li><p>Adds a price filter: less than $7.</p></li><li><p>Adds a category soft boost: "Advent calendars" (3x).</p></li></ul><p>The “Cheap chocolate” policy (priority 210) applies next against the modified state:</p><ul><li><p>Attempts to add a category hard filter: "Chocolates," "Milk chocolates"; but the Christmas policy already set this field with <code>on_conflict: override</code>, so the Cheap chocolate categories are dropped.</p></li><li><p>Attempts to add a price filter: $2, the Christmas policy set <code>on_conflict: restrict</code> for price, and $2 is more restrictive than $7, so $2 wins.</p></li><li><p>Removes "cheap" from the search string.</p></li></ul><p><strong>Step 4: Build the Elasticsearch query.</strong> The control plane assembles the execution plan into a single Elasticsearch query against the product catalog:</p>POST products/_search
{
  "query": {
    "function_score": {
      "query": {
        "bool": {
          "must": [
            { "match": { "title": "chocolate" } }
          ],
          "filter": [
            { "terms": { "categories": ["Christmas foods and drinks", "Christmas sweets"] } },
            { "range": { "price": { "lt": 2 } } }
          ]
        }
      },
      "functions": [
        {
          "weight": 1
        },
        {
          "filter": { "terms": { "categories": ["Advent calendars"] } },
          "weight": 3
        }
      ],
      "score_mode": "sum",
      "boost_mode": "multiply"
    }
  }
}<p>The original search string was "cheap chocolate”. The query that reaches the product catalog is a governed, intent-aware retrieval plan: The word "cheap" has been consumed and converted into a price constraint, results are restricted to Christmas seasonal categories, Advent calendar products receive a ranking boost, and the price ceiling reflects the more restrictive value from the lower-priority policy. Every transformation is deterministic, traceable, and explainable.</p><p>For a quick overview about how these multipliers interact with the base BM25 score, see <a href="https://youtu.be/Ap5K2Y00Xjc?t=525">8:45 in the related PRISM video</a>, where we briefly discuss multiplicative boosts.</p><h2><strong>Why this scales</strong></h2><p>The percolator is efficient for this use case because of the asymmetry: An enterprise ecommerce system might have millions of products but only hundreds or thousands of governance policies. The percolator is checking one incoming search string against that set of stored policy patterns, not scanning the full product catalog. The cost is proportional to the number of policies, and Elasticsearch applies internal optimizations (indexing terms from stored query patterns, short-circuiting Boolean logic) to keep matching fast.</p><p>Adding a new policy is just indexing a new document. Disabling one is a field update. No code changes, no deploys, no restarts.</p><h2><strong>From lookup to governed retrieval</strong></h2><p>The percolator provides the fast reverse-matching primitive that makes the control plane architecture from <a href="http://elastic.co/search-labs/blog/ecommerce-search-governance-control-plane-architecture">Part 3</a> practical at scale. Policies are data which are stored and indexed, and efficiently matched against incoming search strings. The control plane composes matching policies into a governed execution plan through the cascading transformation and per-field conflict resolution described in Part 3. And the retrieval engine executes the governed execution plan against the product catalog.</p><p>The result is a system where a merchandiser can author a new policy without touching application code, test it against representative queries, promote it to production, and immediately see the effect. The percolator makes the policy lookup fast; the control plane makes the policy composition deterministic; and the governed workflow makes the whole process safe.</p><h2><strong>What's next in this series</strong></h2><p>The next post in this series extends the governed control plane into new territory. It introduces a <strong>multi-tier search architecture</strong>, explaining how to orchestrate strict, relaxed, and semantic retrieval while maintaining stable pagination and facets.</p><h2><strong>Put governed ecommerce search into practice</strong></h2><p>The percolator-based control plane described in this post, from index mappings and boundary markers to highlight-driven phrase tracking and cascading policy composition, was built by Elastic Services Engineering as part of our repeatable ecommerce search accelerators. Every query example and policy structure shown here comes from a working system validated against enterprise-scale product catalogs.</p><p>If you want to implement a governed, policy-driven control plane on Elasticsearch, Elastic Services can get you there faster. Contact <a href="https://www.elastic.co/consulting">Elastic Professional Services</a>.</p><h2>Join the discussion</h2><p>Have questions about search governance, retrieval strategies, or ecommerce search architecture? Join the broader <a href="https://discuss.elastic.co/">Elastic community conversation</a>.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-percolator-search-governance</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-percolator-search-governance</guid>
    <category><![CDATA[Operations]]></category>
    <dc:creator><![CDATA[Alexander Marquardt,Honza Král,Taylor Roy]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt19fcc31ad093ad30/6a170dbd7d8d67301070e799/5e485cdd52d78419ff0ac30a4192b953f6d70c61-1280x720.png" length="0" type="image/png"/>
    <pubDate>Mon, 04 May 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Building a control plane to govern ecommerce search]]></title>
    <description><![CDATA[How to build a governed control plane for ecommerce that composes conflicting search policies into a single execution plan (without code changes).]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/search-labs/blog/ecommerce-search-governance-improve-retrieval">Part 1</a> and <a href="https://www.elastic.co/search-labs/blog/ecommerce-search-governance-zero-deploy">part 2</a> of this series established why ecommerce search needs a <em>governance layer</em>, a decision layer between the user's query and the retrieval engine that classifies intent, enforces constraints, and routes to the correct retrieval strategy (for example, BM25, semantic, hybrid). This post shows how to build that layer using a simple architectural primitive where query interpretation policies are stored as documents and retrieved at query time via fast reverse matching. Because new retrieval policies (for example, “boost brand X” or “only show category Y”) don’t require code changes, the result is a routing layer that stays stable while policies evolve and that keeps the retrieval engines safe in high-stakes environments. If you want to see the end result of this architecture before reading further, check out this video: <a href="https://www.youtube.com/watch?v=e1GuL9CYWAk">Fixing Search Relevance in Seconds: Introducing PRISM</a>.</p><h2>Why query interpretation is often a challenge</h2><p>Storing policies as code (if/else blocks in the application layer) produces tens of thousands of lines of brittle logic that lacks any indexing for efficient policy retrieval at query time. Iteration is slow (a single query behavior change may require a six-week deployment cycle), accountability is unclear (why did results change?), and business users cannot modify search behavior without engineering involvement. This is shown on the left side in the following image:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb84f89f4d9029df7/6a170f806234e077cddb1ab6/4e2cd5244ef8b9a05af6337a4825252f321a9a43-1377x768.png" alt="Image with two headings, “Policies as code” on the left and “Policies as data” on the right. The left side shows conditional code blocks defining query‑handling rules with notes about deployment, change cycles, and sequential evaluation. The right side shows JSON policy objects with titles, match terms, actions, filters, and priorities, along with notes about storage in an Elasticsearch index, update behavior, and indexed matching." /><p>Storing policies as data in an Elasticsearch index is shown on the right side of the above image. This approach solves all of the issues associated with hard-coded query resolution logic. However, for this to work, you need a way to quickly determine which policies match the user query and how conflicts should be resolved. This is where the governed control plane comes in.</p><h2>The control plane pattern</h2><p>A governed control plane sits between the raw user query and an Elasticsearch retrieval. It receives user text as its input, and its output is an execution plan that includes filters, boosts, and retrieval routing decisions.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0585c90830d63d02/6a170f82964cea7a5908bc8b/5562da5de521f3c83ed55a13e9be87ca7fa70109-546x489.png" alt="Diagram illustrating two search flows through a governed control plane: one where a text query for “oranges” is rewritten with a category constraint before product lookup, and another where a semantic query for “gift for grandpa” is rewritten and routed to retrieve matching products from a product catalog." /><p>A control plane pipeline consists of:</p><ol><li><p><strong>User query: </strong>A user enters a string of what they’re looking for, such as “oranges” or “gift for grandpa”.</p></li><li><p><strong>Policy lookup: </strong>Match the user query against the policy index.</p></li><li><p><strong>Return matching policies:</strong> Policies that match the user query are returned from the policy index.</p></li><li><p><strong>Policy application: </strong>The control plane analyzes these returned policies and composes matched policies into a single coherent execution plan that includes filters, boosts, overrides, and guardrails and that applies the appropriate retrieval method (for example, lexical versus semantic versus hybrid).</p></li><li><p><strong>Execute:</strong> The modified <em>intent-aware</em> Elasticsearch query is passed to the application to be executed against a product catalog index.</p></li><li><p><strong>Explain (optional):</strong> In addition to creating a query that provides business and intent-aligned results, the control plane provides an optional explainability payload to show which policies were triggered and how they were combined.</p></li></ol><p>Finding which policies should be applied for a user’s search string requires a fast reverse-matching primitive, which we solve with the <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-percolate-query">percolator query</a>. After retrieving relevant policies, combining multiple matched policies into a unified execution plan requires a judgment framework: priorities, conflict strategies, consumed phrase tracking, and cascading transformations that apply policies in sequence rather than independently. Additionally, the most appropriate retrieval technology needs to be selected (for example, <a href="https://www.elastic.co/elasticon/conf/2016/sf/improved-text-scoring-with-bm25">BM25</a> for “oranges” versus <a href="https://www.elastic.co/docs/solutions/search/semantic-search">semantic search</a> for “gift for grandpa”).</p><h2>Policy lookup: Checking the query before searching for products</h2><p>When a shopper types a query, a search system with a governed control plane doesn’t send that query directly to be executed against the product catalog. First, the query is checked against a set of stored policies and modified to reflect the intent of the query and business priorities.</p><h3>Policy structure</h3><p>Each policy is a simple document that defines two things:</p><ul><li><p><strong>Match criteria:</strong> What query text should cause this policy to fire. This could be an exact phrase, a single word, a pattern, or a combination.</p></li><li><p><strong>Action:</strong> What to do when the policy fires. This could be applying a category filter, excluding products, extracting a price constraint, or changing the retrieval strategy.</p></li></ul><p>The system finds all matching policies, composes them into an execution plan, and only then runs the product search. Taken together, policies act like a knowledgeable store associate who understands what you’re looking for and walks you to the right aisle.</p><h3>The policy pattern</h3><p>The first articles in this series introduced examples of policies in action: constraining "oranges" to the produce category, treating "without peanuts" as an exclusion, and routing "gift for grandpa" to semantic retrieval. The key architectural point is that in each case, the query is checked against stored policies before the product search begins. The policies determine what constraints to apply, which text to modify, and which retrieval strategy to use. The query against the product catalog comes after the policies have been applied and a new rewritten query has been created.</p><h3>Why this is fast</h3><p>An enterprise ecommerce system might have millions of products but only hundreds or thousands of policies. The policy lookup step is searching against a small curated index, not the full product catalog, and is therefore fast. And because policies are stored as data in their own index, a merchandiser adding a new policy doesn't touch the application code, and an engineer optimizing the product search doesn't touch the policy index. The two concerns evolve independently.</p><p>The examples above describe what happens conceptually. Under the hood, the policy lookup is implemented using the Elasticsearch <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-percolate-query">percolator query</a> type, which is purpose-built for this kind of pattern: matching incoming text against a set of stored queries. <a href="https://www.elastic.co/search-labs/blog/elasticsearch-percolator-search-governance">Part 4</a> in this series provides a hands-on deep dive into the percolator implementation, including index mappings, boundary markers, and highlight-driven phrase tracking. With the lookup mechanism covered in depth in Part 4, let's turn to what a policy document actually contains and how the control plane composes multiple policies into a single execution plan.</p><h2>Example policies</h2><p>Now that we've seen what policies do conceptually, let's look at what they actually contain. The two policies below have been designed to intentionally conflict, which will demonstrate the conflict resolution system described in subsequent sections.</p><h3>Cheap chocolate</h3><p>The policy shown below detects if a user has submitted a search containing the phrase “cheap chocolate”. If so, results are restricted to the “Chocolates” and “Milk chocolates” categories. This policy also applies a price filter of $2. Also, notice that this policy has a priority of 210; we’ll come back to this when we discuss conflict resolution in more detail.</p><p>The filter mode and conflict strategy settings shown here (hard_filter, soft_boost, restrict, override) are explained in detail in the conflict resolution section below.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltada4d46e2ab26208/6a170f836f7f04f91f914924/bbcd66b20fc3aa861b5880ca67daf8e809698717-1002x890.png" alt="Interface showing a rule configuration with a match phrase for “cheap chocolate,” category and price filters, a phrase‑removal field, and priority settings." /><p>When the above policy is activated, a search for “cheap chocolate” respects the price filter of $2 and restricts results to the “Chocolates” and “Milk chocolates” categories. Example results are shown below:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt368bdfbb9a6e5a5e/6a170f8566c4f975a1f8c10f/3f373af9a985864315d7639440a416e45a882a1b-1133x1146.png" alt="Interface showing a rule configuration with a match phrase for “cheap chocolate,” category and price filters, a phrase‑removal field, and priority settings." /><h3>Christmas chocolate</h3><p>The policy shown below is an example of a policy that one could imagine applying at Christmas. This example restricts results to “Christmas foods and drinks” and “Christmas sweets”, boosts any products that are also in the “Advent calendars” category, and applies a price filter of less than $7 to focus on affordable seasonal items. Additionally, notice that this policy has a priority of 300. We’ll come back to this when we discuss conflict resolution in more detail.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta3428d211f2d8304/6a170f86839dfa0049dcffb3/8f1179342d0e05cf78266d142b046021a3694368-1007x941.png" alt="Screenshot of an Elasticsearch rule query interface showing a match_phrase query for “chocolate,” filter rules based on categories and price, conflict handling options, and rule priority settings." /><p>When the above policy is activated without any conflicting policies, a search for “chocolate” respects the price filter of $7, and restricts results to the “Christmas food and drinks” and “Christmas sweets” categories, and boosts any products tagged as “Advent calendars”. Example results are shown below:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0e7f1fe91b2cadcd/6a170f8866c4f90b0af8c113/662b0e40cb3a9291c17816c33169e9ff5b68f98d-1129x1085.png" alt="Search results page showing a query for “chocolate” with category and brand filters on the left and a list of chocolate advent calendar products with images, prices, categories, and descriptions on the right." /><h2>Combining matched policies</h2><p>The policy lookup described above is half the story. The other half is what happens when multiple policies match the same query.</p><p>In any nontrivial deployment, a single query will routinely trigger several policies at once. "Cheap chocolate" will match both of the policies that we demonstrated above. Each policy is correct in isolation. The challenge is composing them into a single, coherent execution plan without contradictions, without double-counting, and without one policy silently undoing the work of another.</p><p>This isn’t a lookup problem; it’s a judgment problem. The system must decide:</p><ul><li><p><strong>Order of application:</strong> If a negation policy removes "without peanuts" from the query, does the price policy still see the original text or the modified text?</p></li><li><p><strong>Filter conflicts:</strong> If two policies set different price ceilings, which one wins? Is the loser silently dropped, or does it degrade gracefully into a soft boost?</p></li><li><p><strong>Phrase ownership:</strong> If two policies both matched on the same word and the first one already consumed it, should the second one still fire?</p></li></ul><p>A naive implementation (apply all matched policies independently, merge the results) breaks as soon as policies interact. The architecture needs an explicit model for how policies compose. The next two sections describe that model: a priority and conflict resolution framework; and a cascading transformation model that makes policy interaction deterministic.</p><p>The key insight is that policy application isn’t a set of independent operations; it’s a cascading transformation. Each policy receives the rewrite state produced by all higher-priority policies and transforms it further:</p><p>initial state → [Policy A] → state' → [Policy B] → state'' → ... → execution plan</p><p>The state carries the rewritten query text, accumulated filters, current intent, and any synonym expansions. A high-priority policy can remove text from the query, and every subsequent policy sees the modified query, not the original. Context accumulates. Order matters.</p><h2>Precedence and conflict resolution: Determinism matters</h2><p>The specific conflict strategies are a design choice. Different organizations may resolve conflicts differently, depending on their business requirements. The following approach illustrates the kind of judgment framework a control plane needs. The important thing is not these specific strategies but that the system has explicit, deterministic strategies rather than letting conflicts resolve through unpredictable interactions.</p><h3>Priority ordering</h3><p>Policies are sorted by priority (highest first). When multiple policies match the same query, they’re applied in priority order. If two policies try to set the same filter field, the higher-priority policy's declared strategy for that field takes precedence. If there are multiple policies triggered that have the same priority, then the policy with the highest ID is given precedence (as if it were assigned a higher priority); this choice ensures deterministic behavior when conflicts arise.</p><h3>Per-field resolution, not per policy</h3><p>A critical design principle: Conflict resolution operates per field (for example, brand, category, or description), not per policy. When two policies produce filters that overlap on specific fields, only those specific fields are affected by the conflict resolution strategy, and the resolution strategy is defined by the highest-priority matching policy. Non-conflicting fields from both policies survive intact.</p><p>This matters because the alternative of a per-policy approach would force the system to either accept or reject an entire policy when only one of its fields conflicts.</p><p>Per-field resolution preserves the maximum amount of useful constraint information.</p><h3>Three settings per filter field</h3><p>Each filter field in a policy has three independent settings:</p><p><strong>Filter mode:</strong> How the filter is applied when there’s no conflict.</p><ul><li><p><code>hard_filter</code> (default): Applied as an <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-bool-query#score-bool-filter">Elasticsearch </a><a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-bool-query#score-bool-filter"><code>bool.filter</code></a> clause. This is useful for excluding unrelated products entirely. For example, restricting a search for "oranges" to the produce category eliminates hits such as orange juice and orange marmalade. Non-matching documents are completely excluded from results.</p></li><li><p><code>soft_boost</code>: Applied as an <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-function-score-query">Elasticsearch </a><a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-function-score-query"><code>function_score</code></a> weight with a configurable <code>boost_weight</code>. Documents that match get a ranking boost, but non-matching documents aren’t excluded. This is useful for something like boosting a brand, without excluding other brands.</p></li></ul><h3>Conflict strategy</h3><p>What happens when a lower-priority policy sets the same field:</p><ul><li><p><code>override</code>: This high-priority policy's value wins; the lower-priority value is dropped entirely. Valid for all field types.</p></li><li><p><code>restrict</code>: Take the more restrictive numeric value (for example, the lower ceiling for price__max, the higher floor for price__min). Valid for numeric range fields only.</p></li><li><p><code>merge</code>: Combine both values into a union. Valid for non-numeric fields only.</p></li><li><p><code>soft_boost</code>: Convert the conflicting filter to a <code>function_score</code> weight with a configurable <code>boost_weight</code> instead of a hard filter. For more details on function_score boosting, see <a href="https://www.elastic.co/search-labs/blog/bm25-ranking-multiplicative-boosting-elasticsearch">Influencing BM25 ranking with multiplicative boosting in Elasticsearch</a>. This is only valid for non-negation fields.</p></li></ul><p><strong>Value:</strong> The actual filter value (for example, a categories list, a price threshold).</p><p><strong>Strategies by field type: </strong>Not all strategies make sense for all field types. For instance, an exclusion is inherently binary, so it cannot be soft-boosted. The following table shows which strategies are available for each field type:</p><p>Field type</p><p>Available strategies</p><p>Default</p><p>Negation fields (__not, __match__not)</p><p>override, merge</p><p>override</p><p>Numeric range fields (__max, __min, __gt, __lt)</p><p>restrict, override, soft_boost</p><p>restrict</p><p>All other fields (keyword, text)</p><p>soft_boost, override, merge</p><p>soft_boost</p><p>Negation fields cannot be soft-boosted because exclusions are binary. Converting "never show canned foods" to "slightly prefer not-canned-foods" fundamentally changes the semantics; a product from "canned foods" would still appear, just ranked slightly lower, which defeats the purpose of the exclusion.</p><h2>A concrete example: Searching for "cheap chocolate" during a Christmas campaign</h2><p>Suppose a merchandiser has created the two policies for chocolate that we previously demonstrated, a lower priority one for cheap chocolate and another higher-priority chocolate-related policy that will be enabled during Christmas. If both of these policies are enabled, then how these are combined depends on the filter mode and conflict strategy of the higher-precedence policy. If both of the previously discussed policies are enabled, they’ll be combined as follows:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf930b42611a6126c/6a170f8aacf088ae28be9c1b/0405e193522172bde283180df96ed3651178fafc-529x447.png" alt="Screenshot showing a transformation pipeline where an initial query “cheap chocolate” is modified by multiple rules, including added category and price filters, conflict resolution behavior, rule priorities, and a final transformed query of “chocolate.”" /><p>This shows two conflicts, one on categories and one on price. It’s worth noting that the query that will be executed after this transformation has the following characteristics:</p><ul><li><p>Only products from the “Christmas foods and drinks” and “Christmas sweets” categories will be shown.</p></li><li><p>Within those categories, if the products are also tagged as being in the “Advent calendars” category, they’ll be boosted up by 3x.</p></li><li><p>A price filter for $2 is applied, which came from the lower-priority policy (because the higher-priority policy specified to “Restrict” on conflict).</p></li><li><p>The word “cheap” is removed, only returning products matching “chocolate”.</p></li></ul><p>With both of these policies enabled, “cheap chocolate” returns results similar to the image shown below:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7e3c2ee36f963e8c/6a170f8ccdacbf5be17d2ac2/01bbab1c5bd3d0fd37e39c25973d60141f9796e9-1126x1123.png" alt="Search results page showing a query for “cheap chocolate,” with category and brand filters on the left and a list of chocolate advent calendar products with images, prices, and product details on the right." /><h3>Relaxing constraints</h3><p>Perhaps the retailer doesn’t want to exclude products in the categories of “Chocolates” and “Milk chocolates” during Christmas. The settings on the Christmas policy might have overreached and inadvertently removed categories applied by the “cheap chocolate” policy. This is an example that shows why it might be more desirable to combine lower-priority policies with conflicting higher-priority policies. For example, we could modify the Christmas chocolates promotion so that instead of “Override” on conflict, we do a soft boost. The change to that policy would be as follows:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbb7393566aeab705/6a170f8db0367d5b6472bde2/45e88311014d67933ca8cf8381d8f91de090e2b4-1090x103.png" alt="User interface showing a search policy rule with field set to Categories, operator set to Equals, values “Christmas foods and drinks” and “Christmas sweets,” conflict handling set to Soft with priority 1, and filter mode set to Hard filter." /><p>After this modification, the query rewriter transformation pipeline execution for “cheap chocolate” looks as follows:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6b4453b35b5f8ef0/6a170f8fb339d5ba9b76a09a/396b360e48327421c2c38bcf4a039fb1a6d5a8e0-519x445.png" alt="Screenshot of a transformation pipeline showing how the initial query “cheap chocolate” is modified by multiple rules, including category filters, price limits, soft boost and hard filter modes, conflict handling outcomes, rule priorities, and a final query of “chocolate.”" /><p>With the soft boost on conflict, the conflicting filters are converted into soft boosts rather than being dropped. The query that will be executed on the product catalog after this transformation has the following characteristics:</p><ul><li><p>Because “On conflict” is specified as “Soft boost” on the higher-priority policy, the conflicts will be converted to boosts as follows:</p><ul><li><p>Products from the “Christmas foods and drinks” and “Christmas sweets” categories will have a boost of 1x applied to them.</p></li><li><p>Products from the “Chocolates” and “Milk chocolates” categories will have a boost of 3x applied to them.</p></li></ul></li><li><p>As in the previous example, if the products are also tagged as being in the “Advent calendars” category, they’ll be boosted up by 3x.</p></li><li><p>As in the previous example, a price filter for $2 is applied.</p></li><li><p>The word “cheap” is removed, only returning products matching “chocolate”.</p></li></ul><p>With relaxed filtering, results look as follows:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0288336675c509ef/6a170f917d8d6723bc70e808/7a68c54d878dadfe8b1821dd3860b7b60f9ce45f-1126x1123.png" alt="Search results page for the query “cheap chocolate,” showing category and brand filters on the left and a product list on the right, with multiple chocolate items, prices, categories, and a total of 6,895 results indicated at the top." /><h3>Overriding price from a high-priority policy</h3><p>Or perhaps the retailer wants to allow slightly more expensive chocolates to be shown during Christmas by increasing the price max to $7. To ensure that the max price from the Christmas chocolates policy is not overridden if someone searches for “cheap chocolates”, we can set the conflict mode on the price to “override” rather than “restrict”, as follows:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltae1b40d312cf59e6/6a170f92cdacbfa1277d2ac6/c2621e6513281f545b84eb77362f2b93e1c46a1f-996x70.png" alt="User interface showing a search policy rule with field set to Price, operator set to Less than, value set to 7, conflict handling set to Override, and filter mode set to Hard filter." /><p>With this override, the query for “cheap chocolate” ignores maximum price that is defined in the “cheap chocolate policy” and only applies the price specified in the “Christmas chocolates” policy, as follows:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4a47aa71c925b4a3/6a170f94ab7f0863d3db9f6d/d50da7900beb3c08439e9fd79cbe2ddd98196441-511x389.png" alt="Screenshot of a transformation pipeline detailing how the initial query “cheap chocolate” is processed by two filter rules, showing added category and price filters, hard filter and soft boost modes, conflict handling outcomes, rule priorities, and removal of a price filter due to a conflict." /><p>This is similar to the previous example, with the difference being that the max price is set to the $7 value from the higher-priority policy because that policy specified “Override” on conflict. With the Christmas price filter taking precedence, the results look as follows:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2b9ac1a62437c967/6a170f96839dfa3f58dcffb9/635ee6353ba84727486e7e053764788fb26b6f44-1134x1079.png" alt="Search results page for the query “cheap chocolate,” showing category and brand filters on the left and a list of chocolate products on the right, including multiple advent calendars with images, prices, categories, and a displayed total of 10,000 results." /><p>These three variations (override, soft_boost, and override on price) demonstrate a key property of the system: A merchandiser can change how two policies interact by modifying a setting on a single field within a single policy, without deploying any code. The conflict strategy is the lever that controls business behavior.</p><h2>Consumed phrase tracking</h2><p>There’s a subtler form of conflict: two policies that match on the same phrase. If a higher-priority policy removes "without peanuts" from the query, a lower-priority policy that also matched on "without" has nothing left to act on. The system detects if the matched phrase is no longer present in the rewritten query and skips the lower-priority policy.</p><p>Intent policies are exempt from consumed phrase tracking: They set the retrieval strategy based on the original query match, regardless of what text has been removed by higher-priority policies.</p><p>Priority ordering, per-field conflict resolution, and consumed phrase tracking together give the control plane a deterministic composition model. With that foundation in place, the system can make a routing decision that would be risky without it.</p><h2>Governance makes retrieval strategy safe</h2><p>An important insight about routing to the correct retrieval method (text, semantic, or hybrid) is that it executes after governance. If your policies have already enforced "produce category”, then semantic retrieval becomes far less risky because the candidate set is constrained. A semantic search over 500 product items is a very different proposition from a semantic search over 500,000 SKUs. Governance narrows the blast radius before retrieval begins.</p><p>For example, without governance, a semantic query for “Fruit high in vitamin C under $4”, in addition to fruits, might return vitamin bottles, carrots, and green pepper. The control plane ensures that these undesired results aren’t even considered as part of the semantic expansion.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdaa3ff1bb3afaa36/6a170f97acf088954bbe9c1f/6dccd5b8a94bfa81f68e3d1c4ad8929ce8cc4e5e-990x378.png" alt="Diagram showing a search query flowing from a user through an application server and control plane, where matching rules are looked up, the query is rewritten with semantic intent including category and price constraints, and results are retrieved from a product catalog with non-matching products excluded." /><p>With that constraint in place, the control plane applies pragmatic routing logic:</p><ul><li><p><strong>Lexical</strong> for navigational and head queries where deterministic precision matters.</p></li><li><p><strong>Semantic</strong> for descriptive discovery queries where concept matching helps.</p></li><li><p><strong>Hybrid</strong> selectively, when constraints have already been enforced and the business accepts broader recall.</p></li></ul><h2>From architecture to implementation</h2><p>The governed control plane translates business intent into deterministic, composable execution plans, without embedding that logic in application code. Policies are data: matched at query time, resolved through explicit per-field conflict strategies, and applied as cascading transformations that produce explainable results. Elastic Services Engineering has built and deployed this architecture for enterprise ecommerce teams, using repeatable patterns and accelerators that compress the path from concept to production. You can see a demo of our implementation of a control plane on YouTube at: <a href="https://www.youtube.com/watch?v=e1GuL9CYWAk">Fixing Search Relevance in Seconds: Introducing PRISM</a>.</p><h3><strong>What's next in this series</strong></h3><p>The next post goes hands-on with the implementation: how the Elasticsearch percolator powers the policy lookup, including index mappings, boundary markers, highlight-driven phrase tracking, and concrete query examples.</p><h2>Put governed ecommerce search into practice</h2><p>The control plane architecture described in this post (per-field conflict resolution, cascading policy transformations, and governance-constrained retrieval routing) was designed and built by Elastic Services Engineering. Every pattern, screenshot, and transformation pipeline shown in this series comes from a working system built by Elastic Services Engineering and validated against enterprise-scale product catalogs.</p><p>If you want to implement a governed, policy-driven control plane on Elasticsearch, <a href="https://www.elastic.co/consulting">Elastic Services</a> can get you there faster.</p><h2>Join the discussion</h2><p>Have questions about search governance, retrieval strategies, or ecommerce search architecture? Join the broader <a href="https://discuss.elastic.co/">Elastic community conversation</a>.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/ecommerce-search-governance-control-plane-architecture</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/ecommerce-search-governance-control-plane-architecture</guid>
    <category><![CDATA[Operations]]></category>
    <dc:creator><![CDATA[Alexander Marquardt,Honza Král,Taylor Roy]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb84f89f4d9029df7/6a170f806234e077cddb1ab6/4e2cd5244ef8b9a05af6337a4825252f321a9a43-1377x768.png" length="0" type="image/png"/>
    <pubDate>Fri, 01 May 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Reindexing data streams due to mapping conflicts]]></title>
    <description><![CDATA[Learn how to fix Elasticsearch mapping conflicts by reindexing data streams. This blog explains the reindexing process and how to ensure new data is correctly mapped.]]></description>
    <content:encoded><![CDATA[<p>When mapping conflicts arise in fields, whether they’re Elastic Common Schema–standard (ECS-standard) or specific to the data source, reindexing your data using Dev Tools becomes necessary. These conflicts can negatively impact any downstream function following ingestion, potentially causing inaccurate results or preventing the use of the complete dataset in features like visualizations, dashboards, the Security app, and aggregations. This blog post details the steps for this reindexing process.</p><p>This blog's content was developed and verified using Elastic versions 9.2.8 and 8.19.14, along with Filestream Integration versions 2.3.0 and 1.2.0.</p><p><strong>Important note:</strong> Depending on your environment, some steps may require specific modifications. Furthermore, be aware that dynamic templates were removed from the <code>@package</code> component template starting with Filestream Integration version 2.3.3.</p><p>Before starting the reindexing process, it’s important to consider the current storage allocation in your environment. The steps outlined below involve creating a copy of the existing backing index, which will temporarily reside in the <a href="https://www.elastic.co/docs/manage-data/lifecycle/data-tiers">hot tier</a>.</p><p><u><strong>Elasticsearch data tiers</strong></u></p><ul><li><p><strong>Hot: </strong>The hot tier is the Elasticsearch entry point for time series data, storing the most recent, frequently searched data. Hot tier nodes require fast reads and writes, necessitating more resources and faster storage (SSDs). This tier is mandatory, and new data stream indices are automatically allocated here.</p></li><li><p><strong>Warm: </strong>Time series data can move to the warm tier once it’s being queried less frequently than the recently indexed data in the hot tier. The warm tier typically holds data from recent weeks. Updates are still allowed but are likely infrequent. Nodes in the warm tier generally don’t need to be as fast as those in the hot tier. For resiliency, indices in the warm tier should be configured to use one or more replicas.</p></li><li><p><strong>Cold: </strong>Data that’s infrequently searched can move from the warm to the cold tier. The cold tier, while still searchable, prioritizes lower storage costs over search speed. Alternatively, the cold tier can store regular indices with replicas instead of searchable snapshots, allowing use of less expensive hardware for older data without reducing disk space requirements compared to the warm tier.</p></li><li><p><strong>Frozen: </strong>Data that’s queried infrequently or no longer queried moves from the cold to the frozen tier for its remaining lifecycle. This tier uses a snapshot repository and partially mounted indices to store and load data, reducing local storage and costs while still allowing search. Searches on the frozen tier are generally slower than on the cold tier because Elasticsearch may need to fetch frozen data from the snapshot repository. We recommend dedicated frozen tier nodes.</p></li></ul><h2>Prerequisites: Determine which fields have conflicts</h2><p>To determine which fields have mapping conflicts, navigate to <strong>Stack Management -&gt; Data Views -&gt; logs-*</strong> (using the logs-* data view is the highest hierarchy of data present with the <em>logs-</em> prefix.) If there are any conflicts, there will be a yellow box stating that. You may either click <strong>View conflicts</strong> or, under the <strong>Field type</strong> box next to the <strong>Search </strong>box, select <strong>conflict</strong>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7aa17311023e1ae3/6a170feaa929cf24fbae0aa9/7d41594682b601a30a9544b8db678f118b0146ab-2048x720.png" alt="Interface showing a logs index pattern with a mapping conflict warning and a list of field types. Focus is on View conflicts and on field type conflict." /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb4106cf39e1be69b/6a170feb6f7f047b74914932/41ad800daa6fc244a1123ba7538820bff5de6788-747x182.png" alt="Table row showing the field name log.offset with types keyword and long marked as a conflict." /><p>Clicking the yellow <strong>Conflict</strong> button will reveal which indices are associated with which mapping types.</p><p>This situation (where the field is mapped as both a <code>keyword</code> and a <code>long</code>) typically occurs because data was ingested before a specific mapping type was defined in the <a href="https://www.elastic.co/docs/manage-data/data-store/templates#component-templates">component template</a> for the relevant <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams">data stream</a>. In such cases, Elasticsearch attempts to set the mapping based on its dynamic templates.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdcec2cd42e2e858a/6a170feda929cff2e4ae0aad/9973c1935aa52292c1ace09a8e9c0b31ad99e7a2-2048x1085.png" alt="Screen showing the field log.offset with a warning about differing types and a table listing the indices for each type." /><p>In order to determine which mapping is appropriate for the field, and if the field is an ECS field, verification with <a href="https://www.elastic.co/docs/reference/ecs/ecs-field-reference">ECS field reference</a> is needed. If the field in question is not an ECS field, its value must be reviewed to determine the correct mapping.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc22eb597f97cbb24/6a170feed7c02291c5de657c/3c77d0a1520bd1ad17e7ffa1480ecf5e224953e1-418x360.png" alt="" /><p>If a field, such as <code>log.offset</code> in this example, isn’t documented in the ECS, the next steps are to investigate the field's value, determine which conflicting mapping type has the most backing indices, and examine the component templates of the other indices.</p><p>Typically, the mapping type associated with the highest number of indices is the correct one, but we recommend you verify the value of the field in question to validate this. To confirm the validity of a mapping type (for example, <code>long</code>), you must also verify that the field's value is appropriate for that type. This verification can be done by using <strong>Discover </strong>to search for the field in question. Reviewing other data streams that contain the same field can provide additional confirmation also.</p><p>To review the values present for the field with the mapping issue, navigate back to the yellow <strong>Conflict </strong>button stated earlier, click the <strong>Conflict</strong> button, highlight one of the backing indices, and paste into a <strong>Discover </strong>session. Your Kibana Query Language (KQL) statement should look like the following screenshot, to include the <strong><code>_index</code></strong><strong>:</strong> field delimiter.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4b1966dbd35b7264/6a170ff00c4857919a01ab52/781f63b34a9abd427ceb896484da29af446e3326-2048x1063.png" alt="Screen showing the field log.offset with a warning about a type conflict, plus a table listing the indices for each type." /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdf3bb65faeb1e94b/6a170ff214b2701bfbe3c6c3/b7b0cb847c1694ab605c61a538722f5be004ec86-2048x909.png" alt="Screen showing a time‑based histogram and a table of log entries with timestamps and log.offset values." /><h2>Prepare the new backing index custom component template</h2><p>To address the mapping conflict in the data stream, first examine the relevant <code>@package</code> component template. You can find this under <strong>Stack Management -&gt; Index Management -&gt; Component Template</strong>. Search for the data stream and select the corresponding <code>@package</code> link. This template contains mappings for the fields out of the box and, while it isn’t common to have a mapping mismatch, it’s possible for the more appropriate type to be overlooked.</p><p>Review the template to confirm it contains the necessary field nesting and mapping for the field in question. For example, if the template incorrectly lists <code>log.offset</code> as a <code>keyword</code>, this is the source of the issue.</p><p><strong>Important:</strong> Because modifying <code>@package</code>/managed templates isn’t recommended, you must use or create an <code>@custom</code> component template to correct the mapping type (for example, for <code>log.offset</code>) for all future data.</p><ul><li><p>We don’t recommend modifying the <code>@package</code>/managed templates, since when you update the integration to a more recent version, any changes you make to the <code>@package</code> template will be overwritten. This is why we recommend using the <code>@custom</code> templates.</p></li><li><p>If a data stream is experiencing mapping conflicts, you need to add any missing field (ECS and non-ECS) nestings or mappings to the data stream's <code>@custom</code> component template. Create this template if it doesn't exist yet, and make sure to specify the correct mapping type for the field.</p></li><li><p>If you have multiple conflicts in your data view, apply all the necessary missing mappings for the data stream simultaneously so that the reindex is performed once versus multiple times. Having entries for proper data typing in the <code>@custom</code> component template will ensure any future data ingestion will follow the same mapping guideline.</p></li></ul><p>To create the <code>@custom</code> component template (or verify it’s in use and populated), navigate to <strong>Index Templates</strong>, type in the name of the data stream in question, and click the appropriate <code>@custom</code> template being used by the data stream. If the template is not yet created, a yellow box will appear, allowing you to create the template through the UI.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt17b6cb8aa8f905c2/6a170ff4964cea702708bc97/bea7cb172227bebc28146e3f2f016e112f34cba5-2048x720.png" alt=" Screen showing an index template with its summary, index pattern, priority value, data stream setting, and a list of component templates, with the focus on the logs‑filestream.generic@custom entry." /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf5b67199089093f4/6a170ff5d7c0220575de6580/e8f63a2e396efbe7f1e62dc08a137a22700be484-2048x296.png" alt=" Screen showing the Index Management interface with the &quot;Component Templates&quot; tab selected and a note that the custom template doesn't exist. Focus is on “Create component template.&quot;" /><p>The screenshot below shows the next page once <strong>Create component template</strong> is selected. Leave the defaults as is on the first page and click <strong>Mappings</strong> or <strong>Next</strong> until you reach the <strong>Mappings</strong> page.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltca2924bc41cac541/6a170ff7dc55decfd6e00ec5/822f1d864302aa4be438c13756b8372f43fa1b0d-2048x1275.png" alt="" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltee1067cb282ae0af/6a170ff82b835f55bff4b2e5/affa2f1214af516a5a6b571ab813628ed7649275-2048x1235.png" alt="Template mappings" /><p>To explicitly set the mapping for a new field coming in or to update a field that has a mapping conflict, when the data stream rolls over due to configuration set in the index lifecycle policy, an entry is needed for the field that the conflict exists in.</p><p>The below will set the mapping for the <code>log.offset</code> field in the <code>@custom</code> component template for the filestream data stream. Repeat the steps to add any custom fields or update necessary fields from the <code>@package</code> with the appropriate mappings, if needed, for this dataset. In this example, when setting offset to <code>Long</code>, the field type will be <code>Numeric</code> and the Numeric type will be <code>Long</code>. Click <strong>Add field</strong> and then outside of the area to continue.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltee1067cb282ae0af/6a170ff82b835f55bff4b2e5/affa2f1214af516a5a6b571ab813628ed7649275-2048x1235.png" alt="Screen showing the component template creation interface with the “Mappings” step selected in the workflow." /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt611a10bd7567d211/6a170ffa964cea257008bc9d/ea2975ee4e40ac0e10c4170d2a23125101f7f8da-2048x1136.png" alt=": Screen showing the component template creation interface with the “Mappings” step selected in the workflow" /><p>Once all needed fields have been added, click through to review, and select <strong>Create component template</strong> when ready. All new data being ingested from this step forward will have <code>log.offset</code> set to <code>long</code>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3a0c86021cc53e0e/6a170ffcc1e8a57703f8839f/bdf8b8290b0c064c9d88990194b15232ffe85709-2048x1027.png" alt=" Template review Elasticsearch" /><h2>Creating the new backing index structure</h2><p>The new backing index needs to have the existing mappings from the data stream’s  component template, as well as the ECS <code>ecs@mappings</code> component template. The <code>ecs@mappings</code> component template is applied after the data stream’s component as a catchall for additional mappings that potentially weren’t captured in the previous component templates.</p><p>Navigate to the browser tab for the data stream's <code>@package</code> mappings. (Go to <strong>Stack Management -&gt; Index Management -&gt; Component Template -&gt; </strong><strong><code>logs-filestream.generic@package</code></strong><strong> -&gt; Manage -&gt; Edit</strong>.) Once there, click on the <strong>Review</strong> section, then <strong>Request</strong>, and finally the <strong>Copy</strong> button on the right. The JSON contents of the component template copied will ensure the remaining field mappings and settings are retained while we update the <code>log.offset</code> field mapping. The JSON will form the backing structure for the newly reindexed backing index.</p><p><strong>Important: </strong>If the template’s JSON was not copied and work was continued on with the reindex, the <code>log.offset</code> conflict would be resolved but there would be new conflicts with the integration, as the integrity of the current mappings were not upheld, creating double work to resolve the original issue.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1de7b9b0e375867e/6a170ffd8b73cb61f318a0f7/402b0431b0e19374e9b28a4374ed51dfa5fa44ba-2048x897.png" alt="Screen showing the component template creation interface with the Review step selected in the workflow. " /><p>Open a second browser tab, navigate to Dev Tools, and paste the copied content. Now, to clean up what was pasted:</p><p><strong>Modifications to the request</strong></p><p><strong>1. Index name:</strong> Replace <code>_component_template/logs-filestream.generic@package</code> with the name of the backing index you intend to reindex, appending <code>-1</code> to the end. For example, use <code>PUT &lt;backing index to reindex&gt;-1</code>.</p><ul><li><p>The appended <code>-1</code> signifies a reindex and won’t conflict with the default ILM rollover settings, which are based on the index's creation date.</p></li></ul><p><strong>2. Settings:</strong> Remove the line <code>"template"</code> (line 3), as well as the very last closing brace for the entire JSON payload; Line 3 should start with <code>"settings": {</code>.</p><ul><li><p>Replace the inner contents of the settings section with <code>"index.codec": "best_compression"</code>. This action will apply Elastic's best compression to the index upon creation.</p></li><li><p>Add in <code>"index.lifecycle.name": "logs"</code>, as well as a line for <code>"index.lifecycle.rollover_alias": ""</code>.</p><ol><li><p>The <code>"index.lifecycle.name": "logs"</code> entry will apply the logs ILM policy to the new backing index. Modify the ILM policy name if you aren’t using logs.</p></li><li><p>The <code>"index.lifecycle.rollover_alias": ""</code> is blank, since this backing index won’t be rolled over, yet the setting is required to avoid ILM rollover errors into the next ILM phase after hot.</p></li></ol></li></ul><p><strong>3. Structure:</strong> The request should now include both a <code>Settings</code> section and a <code>Mappings</code> section. Inside <code>"mappings": {</code>, you should find <code>"dynamic_templates"</code> and a <code>"properties"</code> section containing hard-coded fields and their mappings.</p><p><strong>4. Dynamic templates modification: </strong>The current dynamic templates section contains entries for fields that may be overwritten when the <code>ecs@mappings </code>dynamic templates are added next, causing redundancy and extra lines that aren’t needed.</p><ul><li><p>Remove all sections in <code>"dynamic_templates"</code> except for the second section titled <code>"_embedded_ecs-data_stream_to_constant": {</code>.</p></li><li><p>Repeat the same process as described above, gathering the dynamic mappings for the <code>@package</code> component template, but this time the dynamic mappings for <code>ecs@mappings</code> component template.</p><ul><li><p>It may be easier to copy the entire contents of the mappings from the UI for the <code>ecs@mappings</code> component template, paste into the working Dev Tools <code>dynamic_templates</code> section, and remove duplicate and unnecessary lines where appropriate. Include these dynamic template setting contents after the<code>"_embedded_ecs-data_stream_to_constant": {</code> entry. The <code>dynamic_templates</code> section should look very similar to the below sample contents in Dev Tools.</p></li></ul></li><li><p><strong>If </strong><strong><code>dynamic_templates</code></strong><strong> are not included/removed altogether</strong>, other fields (review the screenshot below) will have double mappings: <code>text</code> and <code>keyword</code> versus the appropriate mappings, if the <code>dynamic_templates</code> section was left included. What’s left should be the <code>"properties"</code> section under <code>"mappings"</code>. This will also create issues in the data view by having the fields be double mapped (if not already mapped this way) and will cause additional mapping conflicts.</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfb7494f882d7e358/6a170fffa6c2b93e46e797d2/24e972cd0fc8eadf943b21cfdd80a5d435e705aa-2048x994.png" alt="Split‑screen code editor showing Elasticsearch commands on the left and index mappings on the right. An arrow points to the text field type in the mapping, and another arrow points to the keyword subfield type." /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltcaca899f8d92e72b/6a1710010c485745e901ab5a/aac13fbe882516e5ed5b5b1b5271c0ae34e80b04-1890x2048.png" alt=": Screen showing the index pattern page for logs-* with a warning about mapping conflicts, with focus on the “keyword, text” type listings for agent.ephemeral_id and agent.id in the fields table." /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2c692786ab74f1ca/6a171002964ceaa53c08bca1/c43d6f61c8ece4de2d51657f239a0c34ced07cdb-1928x1452.png" alt="Screen showing the index pattern page for logs-* with a warning about mapping conflicts. An arrow points to the type listing “ip, text” for the host.ip field." /><p><strong>5. Metadata removal:</strong> Delete the last section labeled <code>"_meta"</code>, as well as the section labeled <code>"version"</code>, if present.</p><p><strong>6. Formatting:</strong> Auto-indent the remaining sections, and adjust or remove any unnecessary curly braces that would prevent a successful execution.
</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0906ffe338df1a9d/6a1710046f7f042aac914936/ebe1573647500de75315e7655256a0db9604c40d-2048x1402.png" alt="Code editor showing Elasticsearch index settings and mappings. A dropdown menu is open on the right, and an arrow points to the “Auto indent” option in the menu." /><p><strong>7. Mapping change:</strong> Navigate to the <code>"properties"</code> section, find <code>"log"</code>, and then locate <code>"offset"</code> nested underneath. Change the type from <code>keyword</code> to <code>long</code>, and remove the line entry (comma included) labeled <code>"ignore_above": 1024,</code>. If more than one entry was added to the <code>@custom</code> component template created earlier, include them here.</p><p>Your Dev Tools console view should now be similar to the example provided below.</p>PUT .ds-logs-filestream.generic-default-2026.04.14-000001-1
{
  "settings": {
    "index.codec": "best_compression",
    "index.lifecycle.name": "logs",
    "index.lifecycle.rollover_alias": ""
  },
  "mappings": {
    "dynamic_templates": [
      {
        "_embedded_ecs-data_stream_to_constant": {
          "path_match": "data_stream.*",
          "mapping": {
            "type": "constant_keyword"
          }
        }
      },
      {
        "ecs_timestamp": {
          "mapping": {
            "ignore_malformed": false,
            "type": "date"
          },
          "match": "@timestamp"
        }
      },
      {
        "ecs_message_match_only_text": {
          "path_match": [
            "message",
            "*.message"
          ],
          "mapping": {
            "type": "match_only_text"
          },
          "unmatch_mapping_type": "object"
        }
      },
      {
        "ecs_non_indexed_keyword": {
          "path_match": [
            "*event.original"
          ],
          "mapping": {
            "index": false,
            "type": "keyword",
            "doc_values": false
          }
        }
      },
      {
        "ecs_non_indexed_long": {
          "path_match": [
            "*.x509.public_key_exponent"
          ],
          "mapping": {
            "index": false,
            "type": "long",
            "doc_values": false
          }
        }
      },
      {
        "ecs_ip": {
          "path_match": [
            "ip",
            "*.ip",
            "*_ip"
          ],
          "mapping": {
            "type": "ip"
          },
          "match_mapping_type": "string"
        }
      },
      {
        "ecs_wildcard": {
          "path_match": [
            "*.io.text",
            "*.message_id",
            "*registry.data.strings",
            "*url.path"
          ],
          "mapping": {
            "type": "wildcard"
          },
          "unmatch_mapping_type": "object"
        }
      },
      {
        "ecs_path_match_wildcard_and_match_only_text": {
          "path_match": [
            "*.body.content",
            "*url.full",
            "*url.original"
          ],
          "mapping": {
            "fields": {
              "text": {
                "type": "match_only_text"
              }
            },
            "type": "wildcard"
          },
          "unmatch_mapping_type": "object"
        }
      },
      {
        "ecs_match_wildcard_and_match_only_text": {
          "mapping": {
            "fields": {
              "text": {
                "type": "match_only_text"
              }
            },
            "type": "wildcard"
          },
          "unmatch_mapping_type": "object",
          "match": [
            "*command_line",
            "*stack_trace"
          ]
        }
      },
      {
        "ecs_path_match_keyword_and_match_only_text": {
          "path_match": [
            "*.title",
            "*.executable",
            "*.name",
            "*.working_directory",
            "*.full_name",
            "*file.path",
            "*file.target_path",
            "*os.full",
            "*email.subject",
            "*vulnerability.description",
            "*user_agent.original"
          ],
          "mapping": {
            "fields": {
              "text": {
                "type": "match_only_text"
              }
            },
            "type": "keyword"
          },
          "unmatch_mapping_type": "object"
        }
      },
      {
        "ecs_date": {
          "path_match": [
            "*.timestamp",
            "*_timestamp",
            "*.not_after",
            "*.not_before",
            "*.accessed",
            "created",
            "*.created",
            "*.installed",
            "*.creation_date",
            "*.ctime",
            "*.mtime",
            "ingested",
            "*.ingested",
            "*.start",
            "*.end",
            "*.indicator.first_seen",
            "*.indicator.last_seen",
            "*.indicator.modified_at",
            "*threat.enrichments.matched.occurred"
          ],
          "mapping": {
            "type": "date"
          },
          "unmatch_mapping_type": "object"
        }
      },
      {
        "ecs_path_match_float": {
          "path_match": [
            "*.score.*",
            "*_score*"
          ],
          "mapping": {
            "type": "float"
          },
          "path_unmatch": "*.version",
          "unmatch_mapping_type": "object"
        }
      },
      {
        "ecs_usage_double_scaled_float": {
          "path_match": "*.usage",
          "mapping": {
            "scaling_factor": 1000,
            "type": "scaled_float"
          },
          "match_mapping_type": [
            "double",
            "long",
            "string"
          ]
        }
      },
      {
        "ecs_geo_point": {
          "path_match": [
            "*.geo.location"
          ],
          "mapping": {
            "type": "geo_point"
          }
        }
      },
      {
        "ecs_flattened": {
          "path_match": [
            "*structured_data",
            "*exports",
            "*imports"
          ],
          "mapping": {
            "type": "flattened"
          },
          "match_mapping_type": "object"
        }
      },
      {
        "all_strings_to_keywords": {
          "mapping": {
            "ignore_above": 1024,
            "type": "keyword"
          },
          "match_mapping_type": "string"
        }
      }
    ],
    "properties": {
      "input": {
        "properties": {
          "type": {
            "ignore_above": 1024,
            "type": "keyword"
          }
        }
      },
      "@timestamp": {
        "ignore_malformed": false,
        "type": "date"
      },
      "ecs": {
        "properties": {
          "version": {
            "ignore_above": 1024,
            "type": "keyword"
          }
        }
      },
      "log": {
        "properties": {
          "file": {
            "properties": {
              "inode": {
                "ignore_above": 1024,
                "type": "keyword"
              },
              "path": {
                "ignore_above": 1024,
                "type": "keyword"
              },
              "device_id": {
                "ignore_above": 1024,
                "type": "keyword"
              },
              "fingerprint": {
                "index": false,
                "type": "keyword"
              }
            }
          },
          "offset": {
            "type": "long"
          },
          "level": {
            "ignore_above": 1024,
            "type": "keyword"
          }
        }
      },
      "data_stream": {
        "properties": {
          "namespace": {
            "type": "constant_keyword"
          },
          "type": {
            "type": "constant_keyword"
          },
          "dataset": {
            "type": "constant_keyword"
          }
        }
      },
      "event": {
        "properties": {
          "original": {
            "index": false,
            "type": "keyword",
            "doc_values": false
          },
          "module": {
            "type": "constant_keyword",
            "value": "filestream"
          },
          "dataset": {
            "type": "constant_keyword",
            "value": "filestream.generic"
          }
        }
      },
      "message": {
        "type": "match_only_text"
      },
      "tags": {
        "ignore_above": 1024,
        "type": "keyword"
      }
    }
  }
}<p>After your console resembles the example (with any additional custom fields included and custom values specific to your environment), execute the command to create the shell of the new backing index, pausing to resolve any errors that arise.</p><h2>Begin reindex process</h2><p>With the shell of the new backing index successfully created, the next step is to reindex and resolve the mapping conflicts.</p><p><strong>Important:</strong> If the backing index that has the mapping conflict is the most recent index and is the current write index (for example, the ending number for the backing index is -000001), the data stream needs to be rolled over. Rolling over the data stream is needed since the current write index, which is having documents fed into it, is a live backing index and cannot be modified.</p><p>With the correct field mapping now applied to the newer write index via the previously created <code>@custom</code> component template, all new documents will reflect this change.</p><p>This is performed by executing the following: </p>POST &lt;full data stream name&gt;/_rollover<p>For example: </p>POST logs-filestream.generic-default/_rollover<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0e0ae084fe6ade43/6a171006a6c2b91078e797d6/22abc1a2f6de0420aa0d56ac498894111df7f4fd-2048x330.png" alt="Rollover result" /><p>Reindexing involves copying the data from an existing backing index to a new one within the same naming convention, typically to apply necessary changes. These modifications could include updates to a component template or the addition of a new ingest pipeline for the data to be processed through.</p><p>Next, the data will be copied from the backing index that has the incorrect mappings into a new backing index. The original backing index has been rolled over, meaning no new documents can be added to it. The new backing index will follow the same naming convention, which preserves data visibility and integrity while applying the correct ILM policy, but will include a <code>-1</code> suffix to indicate that it has been reindexed.</p><p>Adjust the index names as needed and paste the following code into the console. By including <code>wait_for_completion=false</code>, you can track the progress of document copying, which helps estimate the remaining reindexing time. Without this setting, you cannot track the status using the <code>GET _tasks</code> command below and will only be able to check the document count in the newer backing index using <code>GET &lt;backing index name&gt;-1/_count</code>.</p><p><strong>Important: </strong>If issues arise during the reindex process, don’t rerun the reindex command; doing so will restart the process and create duplicate records in the index ending with <code>-1</code>. If a restart is necessary, first delete the index with the trailing <code>-1</code>, and then execute the preceding <code>PUT</code> command to recreate the new backing index shell.</p>POST _reindex?wait_for_completion=false
{
  "source": {
    "index": "&lt;source backing index&gt;"
  },
  "dest": {
    "index": "&lt;new backing index&gt;-1"
  }
}

i.e.
POST _reindex?wait_for_completion=false
{
  "source": {
    "index": ".ds-logs-filestream.generic-default-2026.04.13-000001"
  },
  "dest": {
    "index": ".ds-logs-filestream.generic-default-2026.04.13-000001-1"
  }
}<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb30fd97a045b6008/6a171007cf4f2566d6b2d22b/22f9b1f762802ecd20faa7c7c1f76c9d1444aba5-2048x530.png" alt=" Task output" /><p>Upon execution, the response will include a task ID. You can monitor the reindex progress using this ID with the command: <code>GET _tasks/&lt;task ID&gt;</code>.</p><p>The duration of the reindex depends on the volume of data in the original index. The completion can be tracked by looking for <code>"completed": true</code> when executing the <code>GET</code> command, which should yield a similar output.</p><p><code>GET _tasks/&lt;task ID&gt;</code></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4d40766e48cc0813/6a17100960084ba9043c4642/dbf0fb0a560a78236440b8c3de68cdf5c83e6d7a-2048x824.png" alt="Task summary" /><p>With the reindexing process now finished for the document count, the next step is to verify that the mappings for the new backing index and the specific field in question are correct.</p>GET &lt;backing index&gt;-1/_mapping<p>For example:</p>GET .ds-logs-filestream.generic-default-2026.04.13-000001-1/_mapping<p>You can verify that the mapping for <code>log.offset</code> is as shown below. To confirm that other fields have only a single mapping entry (not both <code>text</code> and <code>keyword</code>), compare them to a field that was not part of the dynamic template section in the preceding <code>PUT</code> command.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc156907e9635e4a9/6a17100b60084b59673c464e/db5c12c0a651e804a916d517e6e260e49a8b835a-2048x1121.png" alt=" Mapping focus" /><p>If the backing index that’s being reindexed has a large number of documents, it’s helpful to check the status of those documents being copied to the new backing index; this can be done by the following two Dev Tools commands to compare the counts.</p><p><code>GET .ds-logs-filestream.generic-default-2026.04.14-000001/_count</code></p><p><code>GET .ds-logs-filestream.generic-default-2026.04.14-000001-1/_count</code></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbc27c4da42ddc33d/6a17100c7d8d67e0ad70e816/a0e49ac79edb0abf9fe99d0e6fd35e96d0e3e0e5-2048x880.png" alt="" /><p>Once the counts are verified to match and the correct mappings are present, update the data stream to include the new backing index, preventing an orphaned backing index in index management, where the ILM policy will never occur on the backing index.</p><ul><li><p>The return should be an acknowledgment of true, if successful.</p></li></ul>POST _data_stream/_modify
{
  "actions": [
    {
      "add_backing_index": {
        "data_stream": "logs-filestream.generic-default",
        "index": ".ds-logs-filestream.generic-default-2026.04.14-000001-1"
      }
    }
  ]
}<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1bb6db6c761628fb/6a17100e7d8d67533770e81a/0aa3233377c0175258d37eaa661d56cf9f310d5e-2048x1288.png" alt="" /><p>Verify the new backing index is added with the following command, making sure the <code>ilm_policy</code> is correct:</p>GET _data_stream/logs-filestream.generic-default<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7f4208ae6d7bf331/6a171010961e696241c4cfeb/af8b75cf260f6f088c28a78da86ad31527e0bfd5-2048x839.png" alt="" /><p>Check the ILM status of the backing index next with the following command:</p><ul><li><p>It’s normal to see that the index is in hot, as it was created very recently (review line 8 or 10).</p></li></ul>GET .ds-logs-filestream.generic-default-2026.04.14-000001-1/_ilm/explain<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt953e1a062a040311/6a171012acf0885905be9c29/cd181a31001c7a3ee2b0599a7388909ce5b50baf-2048x972.png" alt="" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd398451578d070c9/6a1710140e2e492dc041a204/20f6e7632804f173533e655f0292c3c540f26597-2048x894.png" alt="" /><p>Execute the following to transition the backing index from the hot tier to the next appropriate tier that’s after the hot phase for the ILM policy for this data stream. The specific values for <code>phase</code>, <code>action</code>, and <code>name</code> in the <code>current_step</code> below can be referenced from lines 11, 13, and 15, respectively, in the provided screenshot above.</p><p>The <code>next_step</code> value indicates the subsequent ILM phase or data tier to which the index will transition to.</p><p>For example:</p>POST _ilm/move/.ds-logs-filestream.generic-default-2026.04.14-000001-1
{
  "current_step": {
    "phase": "hot",
    "action": "rollover", 
    "name": "check-rollover-ready"
  },
  "next_step": {
    "phase": "warm" 
  }
}<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt246a6538585234f1/6a1710160c4857ddc001ab60/7ae60b900ce1d0b46ce26ec301901bc8a9ef750c-2048x1249.png" alt="" /><ul><li><p>It isn’t necessary, but as a safety measure, you may execute the <code>_ilm/explain</code> command again to ensure the backing index has moved to the next phase and is no longer in hot.</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf77bba4d9b495048/6a17101867045b3b7d45c2c1/58a460cf2ec443223ea68ba7e7166a7cf9d8c97a-2048x915.png" alt="" /><p>Once the following conditions are met, you can safely delete the original backing index that had mapping conflicts:</p><ol><li><p>A new backing index has been successfully created.</p></li><li><p>Documents have been moved to the new index, and the document counts match.</p></li><li><p>Mappings have been corrected (both data stream specific and ECS).</p></li><li><p>The data stream incorporates the new backing index.</p></li><li><p>The ILM policy has been applied and has moved the index out of the hot phase.</p></li></ol><p><strong>Important:</strong> Alternatively, before deleting the original index, you can check the <strong>Data Views</strong> page. Select <code>logs-*</code> and verify that the reindexed backing index (which ends in <code>-1</code>) now appears in the <strong><code>long</code></strong> section. The original backing index should still be present under <strong><code>keyword</code></strong>. If the reindexed backing index is not in the <strong><code>long</code></strong> section, go back and review the preceding steps and make any necessary corrections.</p><p>For example:</p>DELETE .ds-logs-filestream.generic-default-2026.04.14-000001<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt835a79be274513a3/6a17101aa929cf43c6ae0ab1/09d661b20a44929b4736a43eaa3df84180b25f30-2048x1295.png" alt="" /><p>After resolving the conflicts, return to the <strong>Data Views</strong> page and select <code>logs-*</code>. If the conflict was solely related to <code>log.offset</code>, you should no longer see any conflicts listed. If there were other conflicts, the original backing index should no longer appear in the conflict list; instead, the new backing index should now be listed in the <code>long</code> section.</p><p>You can also verify in <strong>Discover</strong> that the <code>log.offset</code> field now displays the appropriate icons.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt127cb539b70acada/6a17101ba929cfbc66ae0ab5/1c3bb7029c99aa4bc6b0931f39f5648654b35ccd-2048x1204.png" alt="" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltaa1eb678773c23d9/6a17101d4a531b00a636aa3b/0af1b1aa3a031c207aa5eb083696dd081d941e67-2048x1001.png" alt="" /><p>Continue this process, repeating the above steps for every backing index that has a mapping conflict until all are successfully resolved.</p><p>References:</p><ul><li><p><a href="https://www.elastic.co/docs/reference/ecs/ecs-field-reference">ECS field reference</a></p></li><li><p><a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-reindex">Reindex documents</a></p></li></ul><h2>Final thoughts</h2><p>By following the steps in this blog, you will resolve mapping conflicts and ensure that all new data is correctly mapped. This is achieved by linking the necessary component templates to your data source. This workflow not only fixes the immediate issues but also establishes a secure and repeatable process for managing schema changes as your data and requirements evolve.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-mapping-conflicts-reindex-data-streams</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-mapping-conflicts-reindex-data-streams</guid>
    <category><![CDATA[Operations]]></category>
    <dc:creator><![CDATA[Lisa Larribas]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9654eb32edb4a44a/6a17101fcdacbf0ac17d2ad8/2f2573aa3d29b3a628e4fce606c803add2641501-720x420.jpg" length="0" type="image/jpeg"/>
    <pubDate>Fri, 24 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Why ecommerce search needs governance]]></title>
    <description><![CDATA[Learn why ecommerce search falls short without governance and how a control layer ensures predictable and intent-driven results, thus improving retrieval.]]></description>
    <content:encoded><![CDATA[<p>Ecommerce retailers need to handle various fundamentally different query types within the same system. A shopper searching for “oranges” expects the fruit, not products containing the word “orange”, such as orange juice or orange marmalade, and not semantically related citrus products. A shopper searching for a “gift for grandpa who has a sweet tooth” needs semantic discovery, not literal keyword matching.</p><p><em>Lexical retrieval</em> (text matching), <em>semantic retrieval</em> (matching concepts), and <em>hybrid retrieval</em> (combining lexical and semantic signals) don’t solve these issues on their own. Lexical retrieval may return anything containing the word “oranges”, while pure semantic retrieval on a high-intent query like “oranges” may broaden toward related items, such as lemons or grapefruits. Hybrid retrieval blends these lexical and semantic signals, but it still doesn’t determine if this query should be treated as navigational, which constraints should be enforced, or which business policies should apply. The gap isn’t the retrieval technology itself; it’s the absence of a governance layer that understands what kind of query this is and what constraints should be enforced before retrieval begins.</p><p>In this blog, we explore ecommerce search governance, why it matters, and how a control layer ensures predictable, accurate retrieval.</p><h2>What governance means in ecommerce search</h2><p><em>Governance</em>, in this context, means introducing a decision layer between the user's query and the retrieval engine. This layer performs the following functions:</p><ul><li><p>Classifies query intent: Is this navigation ("oranges") or discovery ("gift for grandpa")?</p></li><li><p>Applies business constraints: What category boundaries, eligibility rules, availability constraints, or merchandising policies apply?</p></li><li><p>Routes to the appropriate strategy: Should this use lexical retrieval, semantic retrieval, or hybrid?</p></li></ul><p>A governance layer determines which retrieval approach should be used for each query, which constraints must be enforced, and which business policies should apply before retrieval begins. It’s important not to conflate governance with hybrid retrieval: hybrid is one retrieval strategy that combines lexical and semantic signals, while governance is the upstream decision layer that determines whether lexical, semantic, or hybrid should be used.</p><h2>The status quo: The application layer "spaghetti" implementation</h2><p>Currently, many retailers attempt to solve this by adding logic directly into the application layer. This often results in <em>spaghetti code</em>, that is, thousands of lines of hard-coded if-then statements, regex, and complex search templates.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdd7b33454d925cfd/6a1710f1e8fbce25ee39fd4d/f532b099ee103458e15563a711dae92952f8df02-1024x765.png" alt="Comparison of hard‑coded application logic and Elasticsearch, showing how Elasticsearch simplifies ranking and retrieval without complex if‑then rules." /><p>This approach can provide desired search results as shown above; however, it creates significant operational friction:</p><ul><li><p><strong>Engineering dependency:</strong> Business users and merchandisers cannot modify search behavior without engineering tickets and long deployment cycles that often span several weeks.</p></li><li><p><strong>Fragmentation:</strong> Search logic becomes scattered between application code and search templates, and is difficult to explain or audit, making it risky to evolve.</p></li></ul><p>Even when teams recognize the need for routing, the debate often focuses on the wrong question: which retrieval method to pick.</p><h2>The false choice: Lexical vs. semantic vs. hybrid</h2><p>Search teams often frame the challenge as a retrieval strategy choice: lexical/BM25 versus semantic/vectors versus hybrid. That framing is understandable (retrieval methods matter), but it misses the most common failure mode in real deployments, which is that using a single retrieval approach for all queries will give suboptimal results.</p><p>Commerce search is a mix of fundamentally different intents:</p><ul><li><p><strong>Deterministic, high-intent navigation</strong> ( "oranges", “milk”, “chocolate without peanuts”, “cheap olive oil”).</p></li><li><p><strong>Exploratory discovery</strong> ("jacket for hiking in the mountains", "gift for a 12-year-old who likes robotics").</p></li><li><p><strong>Operational constraints</strong> (availability, size, price, color).</p></li><li><p><strong>Merchandising and campaigns</strong> (boost, bury, seasonal campaigns).</p></li></ul><p>When the system routes all of these through the same retrieval strategy, the results are often systematically wrong in predictable ways because the operating model lacks governance. When teams don't recognize this as a governance gap, they respond with the only lever they have: more tuning.</p><h2>Why "relevance tuning" can become cyclical</h2><p>Without a routing layer, “relevance” often turns into a never-ending backlog:</p><ul><li><p>Why is this query showing accessories above the core product?</p></li><li><p>Why did this head query suddenly start surfacing related items?</p></li><li><p>Why did results change after we added synonyms, adjusted analyzers, or enabled hybrid?</p></li><li><p>Why does the business team need an engineering release to fix a single query?</p></li></ul><p>Teams respond with more tuning: more synonyms, more boosts, more reranking experiments, more exceptions in application code. This can work for a while, but it often produces brittle behavior because the system still lacks an explicit decision layer for determining query type and enforcing the right constraints before retrieval.</p><h2>The anatomy of ecommerce intent: Head and tail</h2><p>In this section, we use “head” and “tail” as practical shorthand for common navigational and exploratory query patterns in ecommerce. In the real world, many queries contain aspects of both:</p><h3>Head queries (deterministic intent)</h3><p>These are direct, navigational queries where the user knows exactly what they want:</p><ul><li><p>Single-item intent ("oranges", "milk", "bread").</p></li><li><p>Exact brands or product families ("iPhone 15 Pro", "Diet Coke").</p></li><li><p>SKUs, model numbers, sizes ("ABC123", "air max 270").</p></li></ul><p>For these queries, lexical retrieval can handle token correspondence (matching words), but the business also expects to respect constraints, return predictable rankings, and have controllable outcomes. A merchandiser needs to ensure that a query resolves within the correct category boundaries, respects eligibility, and surfaces specific business priorities.</p><p>Governance is required to enforce the intended resolution. For example, “oranges” should map to the produce category, not to orange juice, orange marmalade, or orange soda.</p><h3>Tail queries (exploratory discovery)</h3><p>These are descriptive, intent-rich queries where shoppers are exploring:</p><ul><li><p>"Gift for grandpa who has a sweet tooth"</p></li><li><p>"Jacket for hiking in the mountains"</p></li><li><p>"Shoes for standing all day"</p></li></ul><p>Lexical retrieval often struggles here. Semantic retrieval excels because it can connect the query concept to the product, even when wording does not match. But semantic retrieval alone is rarely sufficient either. Real queries often require constraints to be enforced, regardless of which retrieval method is used.</p><h2>Constraints are orthogonal to retrieval method</h2><p>Applying constraints to semantic retrieval doesn’t mean <em>hybrid search</em>. These are orthogonal concepts. Constraints, such as filters and boosts in Elasticsearch, can be applied to any lexical, semantic, or hybrid retrieval. The challenge is deciding how the query should be interpreted, which constraints must be enforced, and which retrieval strategy should be used.</p><p>Below are some examples of queries that combine retrieval with hard constraints:</p><ul><li><p><strong>Oranges:</strong> Lexical retrieval for “oranges” plus a category constraint, such as “Fruits” or “Produce”, eliminating orange marmalade, orange juice, and orange soda.</p></li><li><p><strong>Fruits high in vitamin C under $4:</strong> Semantic retrieval for nutritional intent plus constraints limiting results to the fruit category and products under $4.</p></li><li><p><strong>Comfortable shoes for work:</strong> Semantic retrieval for contextual intent plus a category constraint limiting results to shoes.</p></li></ul><p>These queries can't be handled by a single approach:</p><ul><li><p><strong>Pure lexical retrieval</strong> is often insufficient here because phrases like “high in vitamin C” or “comfortable” may not exist as clean, structured attributes. They may need to be inferred from product descriptions, reviews, or specifications.</p></li><li><p><strong>Pure semantic retrieval</strong> is also not always sufficient because, without explicit constraints, a query like “fruits high in vitamin C” might broaden toward vitamin supplements, fruit-flavored drinks, or high-vitamin vegetables outside the intended category and price range.</p></li></ul><p>A governance layer determines whether a query needs lexical retrieval, semantic understanding, constraint enforcement, or some combination of these. Without this layer, ecommerce teams may end up:</p><ul><li><p><strong>Over-constraining:</strong> Using lexical retrieval for semantic requests (for example, "gift for grandpa").</p></li><li><p><strong>Under-constraining: </strong>Using semantic queries for high-intent head queries (for example, “oranges”).</p></li></ul><p>The governance challenge is to build a system that can make the right judgment call for each class of query.</p><h2>What happens without governance</h2><p>The most common failure mode is straightforward: Teams take the raw user query and pass it directly into a single retrieval strategy (lexical, semantic, or hybrid), without an intermediate governance layer.</p><h3>Lexical retrieval misses intended resolution</h3><p>When a user searches for “oranges”, a lexical retrieval strategy may return anything containing that token: orange juice, orange marmalade, or orange soda. The system matched the term correctly, but without governance it may not resolve the intended shopping context (the fruit).</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1b4595242ea6eb05/6a1710f35091684ba3e1bbd0/99abc7a46f9c56a26a68d0a089d7ab830b9b5568-1560x814.png" alt=" Illustration showing how a single query for “oranges” returns different related results, such as marmalade, fresh oranges, and orange soda." /><h3>Semantic retrieval broadens beyond intended constraints</h3><p>When a user searches for “oranges”, a semantic system may retrieve conceptually related items across nearby product concepts. The system may correctly understand the broader domain (fruit or produce), but without explicit governance it can still over-broaden beyond the user’s intended constraint (specifically oranges).</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltff1aba60c7b13fc8/6a1710f58b73cb3cef18a117/c9de86363ecbed499fe48259f47b3c5b2c26bc43-1568x796.png" alt="Diagram showing how a query for “oranges” routes to different fruit categories, including apples, oranges, and mixed fruit." /><h3>The gap is governance</h3><p>What’s required is an upstream decision layer that determines query intent and enforces the right constraints before retrieval begins. This fixes issues such as the following:</p><ul><li><p>Similar or related items appearing alongside what the user actually wanted.</p></li><li><p>Blurred category boundaries ("beverages" versus. "produce").</p></li><li><p>Inability to implement seasonal boosts or campaigns.</p></li><li><p>Unpredictable and unexplainable results.</p></li></ul><h2>Intent understanding and routing: The necessary control plane</h2><p>A governed search system introduces a lightweight control plane in front of retrieval (prior to executing a query in Elasticsearch). The control will be discussed in detail in parts <a href="https://www.elastic.co/search-labs/blog/ecommerce-search-governance-control-plane-architecture">3</a> and <a href="https://www.elastic.co/search-labs/blog/elasticsearch-percolator-search-governance">4</a> of this blog series; for now, we just discuss what it can do but not how it works:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt373bd838e1751998/6a1710f74a531b1e5436aa57/88c3d0f9731a128d73a765dcdffed897308110a6-2680x766.png" alt="Diagram showing how different queries route through a control plane to BM25 or semantic search results." /><p>A control plane can detect intent, apply business policies, and ensure the appropriate retrieval strategy as follows:</p><p><strong>1. Detect intent signals</strong></p><ul><li><p>Is this query likely navigation versus discovery?</p></li><li><p>Is it a known head query (milk, bread, bananas)?</p></li><li><p>Is there a known product, brand, or category interpretation (for example, “oranges” should resolve to produce).</p></li><li><p>Is the query an SKU-like pattern?</p></li><li><p>Does the query fall under an active campaign or seasonal policy (for example, during Christmas, boost turkey-related results)?</p></li><li><p>Does the query imply constraints (category, attributes, exclusions, price/size/color)?</p></li></ul><p><strong>2. Apply governance and business policies</strong></p><ul><li><p>Enforce deterministic constraints first (category/attribute/negation/availability).</p></li><li><p>Apply active merchandising policies (boost/bury/pin/override).</p></li><li><p>Resolve conflicts with precedence rules (for example, campaign overrides versus global policies).</p></li></ul><p><strong>3. Route to the appropriate retrieval strategy</strong></p><ul><li><p>Lexical (fast, deterministic) for navigational/high-intent head queries.</p></li><li><p>Semantic retrieval for true discovery queries.</p></li><li><p>Hybrid where combined lexical and semantic signals add value under explicit business constraints.</p></li></ul><p>In practice, the output of the control plane is not simply “use hybrid” or “use semantic.” It’s a governed retrieval plan: an interpretation of the shopper’s intent, the constraints and policies that should apply, and the retrieval strategy that should be executed. A few simple examples make this concrete:</p><p>Shopper query</p><p>Governed interpretation</p><p>Example retrieval plan</p><p>“chocolate without peanuts”</p><p>Product-oriented query with a hard exclusion constraint</p><p>Lexical retrieval for chocolate plus an exclusion filter for products containing peanuts</p><p>“cheap olive oil”</p><p>Product/category query with a price constraint</p><p>Lexical retrieval for olive oil plus a price filter capped at the retailer’s threshold for cheap</p><p>“fruit high in vitamin C under $4”</p><p>Discovery query requiring semantic understanding plus hard constraints</p><p>Semantic retrieval for nutritional intent, constrained to the fruit category and filtered to products priced under $4</p><p>A control plane selects the right policy and retrieval strategy for each query consistently, predictably, and at scale. This makes advanced retrieval methods more predictable in production because intent-aligned constraints are enforced first and routing decisions are explicit rather than implicit.</p><h2>How this relates to other approaches</h2><p>Some teams use improved embedding models to better capture product semantics, which can materially improve semantic retrieval quality. Others use reranking approaches, such as <a href="https://www.elastic.co/docs/solutions/search/ranking/learning-to-rank-ltr">Learning To Rank (LTR)</a>, to optimize result ordering based on engagement or business signals after retrieval. Both are valuable and often complementary. Better embeddings improve similarity matching. Reranking improves ordering among retrieved candidates.</p><p>Governance addresses a different layer of the problem: It sits upstream of retrieval. It decides which retrieval strategy to use (for example, lexical, semantic, or hybrid), what deterministic constraints are required, and which queries should combine multiple business policies.</p><h2>What a governed control plane enables</h2><p>Once a governance layer is in place, the operating model changes fundamentally. Revenue-critical queries become predictable. Business teams can update search behavior without waiting on engineering release cycles. And advanced retrieval methods, like semantic and hybrid, can be adopted incrementally, behind routing and guardrails, instead of as a global on/off switch.</p><p>The <a href="https://www.elastic.co/search-labs/blog/ecommerce-search-governance-zero-deploy">next post</a> in this series explores what that operating model looks like in practice and why it may matter as much as the retrieval technology underneath it.</p><p>If a merchandiser has to open a Jira ticket and wait for a deploy to fix a revenue-critical query, the bottleneck isn't the engine; it's the operating model. Modern ecommerce search needs a way to translate business intent into controlled, auditable search behavior quickly and safely, while still using advanced retrieval where it adds measurable value.</p><h2>What’s next in this series</h2><p>The patterns explored in this series operate upstream of retrieval: translating business intent into the right query strategy before query generation begins. In the <a href="https://www.elastic.co/search-labs/blog/ecommerce-search-governance-zero-deploy">next post</a>, we shift from the technical problem to the operational one: what happens when business teams can change search behavior without an engineering deployment, and why governance makes that safe.</p><h2>Put governed ecommerce search into practice</h2><p>Engineering bottlenecks, brittle application-layer logic, and unpredictable search results are problems that Elastic Services can help you solve in enterprise ecommerce services engagements. The governed control plane architecture described in this series was built by Elastic Services Engineering.</p><p>If your team is spending engineering cycles translating merchandising requests into code changes, or if your search relevance backlog never seems to shrink, we can help you assess your current architecture and build a path to governed, business-editable search. Contact <a href="https://www.elastic.co/consulting">Elastic Services</a>.  </p><h2>Join the discussion</h2><p>Have questions about search governance, retrieval strategies, or ecommerce search architecture? Join the broader <a href="https://discuss.elastic.co/">Elastic community conversation</a>.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/ecommerce-search-governance-improve-retrieval</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/ecommerce-search-governance-improve-retrieval</guid>
    <category><![CDATA[Operations]]></category>
    <dc:creator><![CDATA[Alexander Marquardt,Honza Král,Taylor Roy]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt840c7a0a5b92080f/6a1710f967045b3e5445c2cd/3793259b01a5653a7520393a2f006610de0d21e7-1280x720.png" length="0" type="image/png"/>
    <pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>