<?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>Wed, 16 Sep 2026 11:51:12 GMT</lastBuildDate>
  <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[Accelerating merchandising improvements with a governed control plane]]></title>
    <description><![CDATA[Search behavior changes shouldn't require an engineering ticket. Learn how a governed control plane lets business teams update search policies in hours, without deployments, without risk.]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/search-labs/blog/ecommerce-search-governance-improve-retrieval">Part 1</a> in this blog series established why ecommerce search needs a governance layer between the user's query and the retrieval engine that classifies intent, enforces business constraints, and routes to the appropriate retrieval strategy. The natural next questions are: Who operates that layer, and how fast can they move?</p><p>This post answers those questions. A governed control plane doesn't just improve search relevance; it changes the operating model. It moves search behavior changes from engineering deployment cycles to business-driven workflows, without sacrificing safety or accountability.</p><h2>The scenario that exposes the operating model</h2><p>Imagine that you’re in the weeks leading up to Christmas, and your merchandising team has identified three urgent changes that must immediately be made to search behavior:</p><ul><li><p><strong>Campaign launch.</strong> Due to an ordering error, there’s an oversupply of in-house branded turkeys. Therefore any query for "turkey" must boost the in-house brand.</p></li><li><p><strong>Product recall.</strong> A supplier has recalled a product line. Queries that would surface those products shouldn’t be shown.</p></li><li><p><strong>Seasonal reinterpretation.</strong> Queries for "stocking" are returning women's hosiery and tights. During the holiday season, "stocking" should resolve to Christmas stockings and stocking stuffers. Once the season ends, the policy can be reverted in minutes.</p></li></ul><p>Under the traditional operating model, where search logic is embedded in application code, each of these changes requires an engineering ticket, a code change, a review cycle, a staging deployment, and a production release. In organizations with conservative release processes, that's a timeline measured in weeks, not hours or minutes. The Christmas shopping window closes before engineering can ship the necessary modifications.</p><p>The bottleneck isn’t the retrieval engine; it’s the operating model. The core challenge is that business intent cannot be translated into search behavior without engineering acting as a constant intermediary, turning every strategic pivot into a technical ticket.</p><h2>The anti-pattern: Search logic in application code</h2><p><a href="https://www.elastic.co/search-labs/blog/ecommerce-search-governance-improve-retrieval">Part 1</a> described how search logic embedded in application code can turn into a "spaghetti" implementation, which creates operational friction. Here’s what that friction looks like at scale. What starts as a few targeted overrides, a filter here, a boost there, grows over time into tens of thousands of lines of if/else branching, regex patterns, and conditional query modifications. This creates problems beyond just technical debt:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt24667eaf8ada7e24/6a170b0367045bee5f45c1e2/fc4d7ea5545512667552af429023fcd7fb316e82-1408x768.png" alt="Traditional workflow Alt text: An infographic titled “The traditional model: search logic in application code,” showing an eight‑step software development workflow that includes a merchandiser describing an urgent requirement, a Jira ticket being created, engineering investigating the request, development making code changes, code review and regression testing, staging testing, staging deployment, and a production release." /><p>This model introduces four systemic frictions that hinder both organizational speed and system scalability:</p><p><strong>Coupling.</strong> Business strategy changes daily. Application infrastructure should remain highly stable. When both live in the same codebase, a merchandiser's request to boost a seasonal product becomes a deployment risk, and a scoring function refactor can silently break a campaign.</p><p><strong>Latency (organizational and computational).</strong> A single query behavior change can require a six-week deployment cycle: ticket, investigation, code change, review, staging, release. Furthermore, the application layer lacks any indexing mechanism to efficiently determine which policies apply to a given query, so policy evaluation often adds meaningful latency at query time as the system walks through sequential if/else checks.</p><p><strong>Accountability gaps.</strong> When results change unexpectedly, nobody can quickly answer <em>why</em>. Was it a synonym update? A scoring change? A new filter added three releases ago? When business logic is distributed across thousands of lines of application code, shipped by different teams across different releases, tracing a relevance change back to its root cause becomes an archaeology project.</p><p><strong>Misallocated engineering.</strong> This model turns skilled software engineers into full-time relevance mechanics. Instead of building platform capabilities, they spend their cycles translating merchandising requests into code changes and debugging interactions and conflicts between hard-coded business policies.</p><h2>The paradigm shift: Policies as data</h2><p>The solution is to decouple business policies from application code entirely. Instead of hard-coding query modifications in middleware, store governed policies as structured documents, each one expressing a discrete business intent, and evaluate them at query time in a dedicated governed control plane layer.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8cc94199fe15c1f2/6a170b05cf4f25da6db2d175/13d47992fb1d1f3f3887f3800f5ddc83742e9c9c-1408x768.png" alt="An infographic titled “The governed model: policies as data,” showing a three‑step workflow in which a merchandiser drafts a business policy, a peer reviews it for logic and conflicts, and the policy is published to take effect on the next query, with notes about versioned, auditable, and reversible policies and same‑day deployment." /><p>A policy is a first-class data object. It has match criteria (when should this policy fire?), an action (what should it do?), a priority (how does it interact with other policies?), and metadata (a title and a description). The control plane evaluates matching policies, resolves conflicts deterministically, and produces an execution plan including constraints, boosts, and routing decisions that Elasticsearch executes against a product catalog.</p><p>For each additional search requirement, the application code doesn't change. The retrieval engine doesn't change. What changes is that business decisions are no longer encoded in code. They live in a policy index as data that can be updated without a deployment.</p><p>This changes your org chart, not just your query.</p><h2>Policies vs. triggers vs. rules</h2><p>A note on terminology used in this series: a <em>policy</em> refers to this complete governed document, including a trigger (match criteria), rule (action), priority, enabled/disabled, and metadata. A <em>trigger</em> refers to the matching criteria that determines when this policy fires, and a <em>rule</em> refers specifically to the action inside the policy, such as applying a filter or changing the retrieval strategy.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4486a76d20df9592/6a170b07b339d515be769fbe/281e0fb915a7723a5619b5bd08f855abb4e2c530-966x1412.png" alt="A screenshot of a user interface for editing a rewrite policy, showing a Policy section with ID, title, description, and toggles; a Trigger section defining a match_phrase condition for the query “oranges”; and a Rule section configuring a filter on the Categories field with additional parameters and boost weights. Show less" /><h2>The workflow: Author → Test → Promote</h2><p>Moving policies out of code and into data opens the door for business-driven search management. But enabling non-technical teams to alter search behavior requires strict operational guardrails. The goal is fast and safe iteration with governance.</p><p>To empower non-technical teams to modify search behavior with confidence, we suggest a three-stage workflow: Author, Test, and Promote. Let’s examine the components of this workflow in detail.</p><p><strong>Author.</strong> A merchandiser creates a policy using structured fields: what the policy should match, what action it should take, and at what priority. The interface guides the business user through what’s expressible.</p><p><a href="https://www.elastic.co/consulting">Elastic Services</a> has built and deployed a governed framework for enterprise ecommerce customers, which has an admin UI that looks as follows:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf3f97533429e058c/6a170b0847d49c4e242d8a02/3c8cd24f24320f5661800e34686719bc7d6c78e2-1005x959.png" alt="A screenshot of a rewrite rule editor showing a rule with an ID, title, description, and an enabled toggle, a rule query defined with a match_phrase condition for “START oranges END,” and a filter on the Categories field set to “Oranges,” with additional settings for conflict handling and filter mode." /><p><strong>Test.</strong> The policy is validated in a non-production environment where the merchandiser can run representative queries and verify that the policy produces the expected behavior, including how it interacts with other active policies. Because the control plane infrastructure is identical across environments, what works in the test environment will work in production.</p><p><strong>Review.</strong> Before a policy is promoted to production, it passes through review. Depending on the organization's risk tolerance, this might be a peer review from another merchandiser, an approval from a search lead, or an automated validation that checks for conflicts with existing policies.</p><p><strong>Promote.</strong> Once approved, the policy is promoted to the production policy index. It takes effect on the next query: no code deployment, no engineering release, no staging build. The entire promotion is a data operation: the same JSON document, moved to a different index.</p><p><strong>Disable.</strong> If a production policy produces unexpected behavior, it can be disabled immediately without engineering involvement. Disabling removes the policy from query evaluation instantly, without affecting any other policy in the system.</p><p>This is the "zero-deploy" promise. It doesn't mean "no process." It means the process operates on <em>policy data</em>, not application code. This distinction compresses the change cycle from weeks to hours or minutes.</p><h2>Why "zero-deploy" matters for revenue-critical queries</h2><p>The economics of ecommerce search are asymmetric. A small number of high-volume queries ("milk," "bread," "oranges," "diapers") drive a disproportionate share of revenue. When one of these queries returns unexpected results, the cost is immediate and measurable: Conversion drops, customer complaints spike, and the merchandising team opens an urgent ticket.</p><p>Under the traditional model, the response cycle is:</p><ol><li><p>The merchant notices the problem.</p></li><li><p>The merchandiser files a ticket with engineering.</p></li><li><p>Engineering investigates, identifies the cause, and writes a fix.</p></li><li><p>The fix goes through code review, staging, and release.</p></li><li><p>Production is updated.</p></li></ol><p>Depending on the organization, steps 2 through 5 may take weeks. For a revenue-critical query during a peak sales period, that latency costs money.</p><p>Under a governed control plane, the response cycle compresses:</p><ol><li><p>The merchant notices the problem.</p></li><li><p>The merchandiser drafts a policy fix (or modifies an existing policy).</p></li><li><p>The policy goes through review and is published.</p></li><li><p>The fix is live.</p></li></ol><p>The difference isn't just speed. It's ownership. The person closest to the business context (the merchandiser who understands why "oranges" should resolve to produce, not beverages) is the person making the change. Engineering is freed from the daily merchandising loop to focus on the platform. This shift also unlocks something that's nearly impossible under the traditional model: attributing search performance changes to specific business decisions.</p><h2>Measurability: Which policy moved conversion</h2><p>When policies are discrete, versioned documents that are stored in an Elasticsearch index, each one becomes independently deployable and therefore its impact can be more easily measured. You can answer questions that are nearly impossible to answer when business logic is scattered across application code:</p><ul><li><p>Did the "cheap laptops" price threshold policy improve conversion for that query class, or did it suppress it?</p></li><li><p>What was the click-through rate impact of the holiday campaign boost?</p></li><li><p>When we rolled back the "oranges" category constraint last Thursday, what happened to add-to-cart rates?</p></li></ul><p>This turns search governance into a data-driven discipline. Instead of vague "relevance tuning," where a release contains a dozen changes and nobody can attribute the outcome, you get measurable, attributable impact per policy. Merchandisers can iterate with evidence. Engineers can evaluate whether a policy schema change produced the expected downstream effect. Leadership can see which policies are driving revenue and which are inert.</p><h2>What this means for each role</h2><h3>For merchandisers and business users</h3><p>Search behavior becomes something you can directly influence through structured policies without understanding Elasticsearch syntax or scoring algorithms. You can see what policies are triggered for a given query to understand why it produces specific results, and make changes within hours instead of weeks. The same policy mechanism also supports sponsored product placement: A merchandiser can create a boost policy that elevates a product or brand and flags it for a 'Sponsored' indicator in the UI, without requiring engineering involvement or additional infrastructure.</p><h3>For search engineers</h3><p>The control plane separates two concerns that are currently entangled: retrieval optimization and business logic. Instead of maintaining tens of thousands of lines of application code that encodes business decisions, you maintain the retrieval engine and the control plane infrastructure. When a merchandiser needs a new campaign boost, they don't need engineering to write it.</p><p>This doesn't eliminate engineering involvement. Engineers design the policy schema, maintain the control plane, set guardrails on what policies can express, add new capabilities as required, and handle edge cases that fall outside the policy framework. But the day-to-day operational cadence of modifying query behavior shifts to the people who own the business context.</p><h3>For site reliability engineers and platform teams</h3><p>Because policies are structured documents rather than application code, they fit naturally into existing operational workflows. Policies can be stored in version control, reviewed through pull requests, and deployed through the same continuous integration and continuous deployment (CI/CD) pipelines the team already uses. Conflicts between policies are detected and resolved deterministically at query time through the control plane's priority system, not through unpredictable interactions between code branches shipped in different releases.</p><p>When something does go wrong, diagnosing the cause is straightforward: Policies are discrete, named, and individually toggleable. A problematic policy can be disabled or deleted immediately without affecting anything else in the system. Compare that to debugging a relevance regression caused by an interaction between a synonym update, a scoring function change, and a new analyzer, all shipped in the same release with no clear attribution.</p><h2>Beyond manual authoring: Large language model–assisted (LLM-assisted) policy suggestions</h2><p>The policies described so far are authored by humans (a merchandiser identifying a gap and drafting a fix). But the same governed workflow supports a second mode: LLM-assisted policy suggestion.</p><p>An LLM can run offline or in the background, analyzing query logs, identifying patterns where search results underperform, such as queries with high exit rates, low click-through, or frequent reformulations. An LLM can then suggest new policies that enter the same Author → Test → Promote pipeline, where a human evaluates each one before it reaches production.</p><h2>Governance is the enabler, not the constraint</h2><p>It might seem counterintuitive: Adding a governance layer makes the system <em>faster</em> to change, not slower. This is the same pattern that works in other domains. CI/CD pipelines don't slow down software delivery; they make it safe to ship frequently. Access control doesn't slow down collaboration; it makes it safe to share broadly.</p><p>A governed control plane works the same way. The reason a query behavior change takes six weeks isn't that the code change is complex; it's that nobody is confident enough to ship it faster, because the blast radius is unclear and the rollback path is uncertain.</p><p>Governance provides that confidence. When every policy is explicit, every conflict is resolved deterministically, and every change can be instantly disabled and then rolled back (because policies are structured JSON documents that can be version controlled using existing workflows), the cost of iteration drops dramatically. Business teams move at the speed of the market. Engineering focuses on the platform.</p><h2>From operating model to architecture</h2><p>The shift from business logic in code to business policies as data is more than a technical refactoring; it's an organizational change that puts relevance ownership with the teams closest to the business context. But it raises an architectural question: How do you evaluate policies at query time without adding latency or turning the control plane itself into a new form of spaghetti?</p><p>The next post will dig into exactly that: the design pattern that enables fast, deterministic policy evaluation at query time.</p><h2>Put governed ecommerce search into practice</h2><p>The workflow described here, merchandisers authoring, testing, and promoting search policies without engineering deployments, is already available. Elastic Services Engineering designed and built it, and Elastic Services has the skills to deploy it for enterprise ecommerce teams.</p><p>If your organization is ready to move from deployment-gated relevance tuning to business-editable search with governance and auditability, we can accelerate your implementation. 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-governance-zero-deploy</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/ecommerce-search-governance-zero-deploy</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/blt76fe825555fe3842/6a170b0a66c4f927f4f8c033/dc802d2ca828ba41d6ff2a0ea1ba67eb0e3bcd10-1280x720.png" length="0" type="image/png"/>
    <pubDate>Tue, 28 Apr 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>