<?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[Alexander Marquardt - 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[Alexander Marquardt - 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/author/alexander-marquardt</link>
    </image>
    <link>https://www.elastic.co/search-labs/author/alexander-marquardt</link>
    <atom:link href="https://www.elastic.co/search-labs/rss/author/alexander-marquardt.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <lastBuildDate>Wed, 09 Sep 2026 16:52:07 GMT</lastBuildDate>
  <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[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>
  <item>
    <title><![CDATA[Influencing BM25 ranking with multiplicative boosting in Elasticsearch]]></title>
    <description><![CDATA[Learn why additive boosting methods can destabilize BM25 rankings and how multiplicative scoring provides controlled, scalable ranking influence in Elasticsearch.]]></description>
    <content:encoded><![CDATA[<p><a href="https://en.wikipedia.org/wiki/Okapi_BM25">BM25</a> is one of the most widely used scoring models in Elasticsearch for text-based search. In many e-commerce implementations, it forms a major component of how product relevance is determined because it provides a well-understood, interpretable score that reflects how closely an item matches a shopper’s query. In addition to this text relevance, merchandising and search teams often need to influence the ranking with business metrics such as margin, stock levels, popularity, personalization, or campaign strategy, in a way that doesn’t destabilize the underlying text relevance.</p><p>The most intuitive levers for doing this are boosted <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-bool-query">should</a> clauses or <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/rank-feature">rank_feature</a> fields. These may initially appear effective, but both approaches degrade and may even fail, as query patterns shift or catalog composition changes. Their shared limitation is that they introduce additive adjustments into a scoring system whose scale varies substantially across queries. A boost like “+2” might overwhelm the base BM25 score in one query while barely registering in another. In other words, additive methods may create brittle, unpredictable ranking behavior.</p><p>In contrast, <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-function-score-query">function_score</a> with multiplicative boosting provides a stable and mathematically proportional way to shape BM25 scores without distorting their underlying structure. Your application logic determines what merits uplift; <code>function_score</code> expresses that intent in a predictable and explainable way that preserves the geometry (high-level relative ordering) of the BM25 relevance signal, nudging rankings in controlled ways rather than overwhelming the core text relevance.</p><p>This article builds on two earlier pieces that demonstrated practical uses of multiplicative boosting: (1) <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>, and (2) <a href="https://www.elastic.co/search-labs/blog/ecommerce-search-relevance-cohort-aware-ranking-elasticsearch">How to improve e-commerce search relevance with personalized cohort-aware ranking</a>. Here we step back from those examples to examine the architectural principle that underlies them: why multiplicative boosting via <code>function_score</code> is one of the most reliable and scalable ways to influence BM25-based ranking in Elasticsearch.</p><h2>Why it's important to preserve base BM25 rankings</h2><p>In many Elasticsearch-based applications, including e-commerce, BM25 remains a central component of how text relevance is assessed. It provides a signal that is interpretable and transparent for teams who need to understand why a product ranked where it did. These properties make BM25 particularly attractive in environments where explainability and operational predictability matter.</p><p>Because of this, most teams want to shape, rather than replace, the rankings produced by BM25. For example, they may want to allow higher-margin items to surface slightly more often, reduce exposure for low-stock products without hiding them, or highlight items aligned with a particular user segment. Ideally, this shaping should preserve the geometry of the rankings produced by the BM25 algorithm.</p><p>The difficulty arises when teams try to achieve these goals using mechanisms that add separate scoring streams on top of the base BM25 ranking. These additive adjustments are not always comparable to BM25’s scale and behave inconsistently as queries, data distributions, and catalog composition evolve. Over time, the ranking becomes brittle, unintuitive, and difficult to tune. A reliable influence mechanism must work with BM25’s scoring geometry rather than overpowering it.</p><p>The <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-function-score-query">function_score</a> query with multiplicative boosting provides this property. It allows teams to apply business influence in a proportional, explainable way while keeping BM25’s underlying structure intact.</p><h2>Why many approaches to influencing ranking degrade (or break) BM25</h2><p>Teams often begin with mechanisms that look straightforward: boosted <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-bool-query">should</a> clauses, <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/rank-feature">rank_feature</a> fields, or custom <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-script-score-query">script_score</a> logic. These tools can be effective in their intended use cases, which is why they seem like natural levers for adding business influence. But when they are used to shape or influence BM25-based text relevance, they may create unstable, opaque, or brittle ranking behavior.</p><p>The underlying issue is that these approaches introduce independent additive scoring contributions into a system whose base BM25 values vary widely across queries, fields, and data sets. Without respecting that variability, the influence becomes unpredictable.</p><p>Below are the three most common patterns and why they fail in practice.</p><h3>1. Additive boosts via should clauses</h3><p>A boosted <code>should</code> clause feels intuitive: “Promote items that match this business rule.” But under the hood, the behavior is fundamentally additive.</p><p>Consider a query of the form:</p>GET products/_search
{
  "query": {
    "bool": {
      "must": [ { "match": { "description": "running shoes" }}],
      "should": [ { "term": { "brand": { "value": "nike", "boost": 1 }}}]
    }
  }
}<p>This kind of query results in the following behavior:</p>final_score = base_BM25 + should_BM25<p>The problem is that <code>base_BM25</code> and <code>should_BM25</code> do not scale together. As your dataset changes, or as different queries are issued, the magnitude of BM25 can shift dramatically. For example, the base BM25 scores for three products might be 12, 8, 4 in one context, and 0.12, 0.08, 0.04 in another. Such a change might happen after a catalog update or a modification to the query structure.</p><p>A boosted <code>should</code> clause adds its own BM25-style contribution to the final score. In this situation, an additive contribution (i.e. should_BM25 = +2) behaves inconsistently:</p><ul><li><p>When base_BM25 is small (0.12), +2 dominates the score — roughly an 18× increase.</p></li><li><p>When base_BM25 is large (12), the same +2 barely shifts the document —  only about a 17% increase.</p></li></ul><p>This instability means that the combined <code>must</code> score and <code>should</code> score have no stable meaning across queries or catalogs. A rule that slightly promotes a brand for one query can dominate the ranking for another, or become irrelevant in a third. This is not a tuning issue; it is a structural property of additive scoring.</p><h3>2. Using rank_feature for business influence</h3><p>The <code>rank_feature</code> family is extremely useful for representing numeric qualities such as recency or popularity. It is fast, compressed, and operationally simple. However, when it is used to influence text relevance (BM25), it runs into the same structural limitation described in the previous section.</p><p>A <code>rank_feature</code> clause produces its own scoring contribution, which is then added to the BM25 score:</p>final_score = base_BM25 + feature_score<p>Just as with boosted <code>should</code> clauses, the two components do not scale together. BM25 values vary substantially across queries depending on term rarity and catalog statistics, while the <code>feature_score</code> follows the scale of the underlying business attribute being boosted (for example, popularity or recency), which typically bears no relationship to the scale of BM25. As a result, the two scoring streams drift apart as your corpus or query patterns evolve.</p><p>The consequence is the same as what we discussed above with relation to the should-clause problem:</p><ul><li><p>The feature score can dominate BM25 in one query and be negligible in another.</p></li><li><p>Tuning becomes fragile because you are calibrating two independent scales — BM25, which varies with query term statistics, and the feature score, which varies with the business attribute’s own distribution.</p></li></ul><p>Although <code>rank_feature</code> remains an excellent mechanism for representing raw numeric attributes, it is not well-suited for proportional influence on BM25, where the goal is not to add a second score but to gently shape the existing one.</p><h3>Custom scoring with script_score</h3><p>When boosted clauses or <code>rank_feature</code> fields become difficult to tune, teams often turn to <code>script_score</code> as a last resort. It provides complete freedom to manipulate the score, including adding, subtracting, multiplying, or replacing the BM25 value according to any business rule. A <code>script_score</code> query replaces Elasticsearch’s scoring pipeline with custom logic. Instead of shaping the BM25 score, the script builds a separate scoring mechanism whose behavior depends entirely on the code inside the script. While this can be powerful, it introduces three challenges that become more significant as the system grows.</p><p><strong>1. Opacity</strong></p><p>Scoring logic is hidden inside a script rather than expressed declaratively. When ranking behavior changes unexpectedly, it is difficult to understand whether the issue is the script itself, a data shift, or an interaction with BM25. Merchandisers and relevance engineers lose the ability to reason about why a document moved up or down.</p><p><strong>2. Performance and operational cost</strong></p><p>Script scoring bypasses many of Elasticsearch’s optimizations and caching pathways. Each document that matches the initial query must execute the script, often leading to higher CPU usage and unpredictable latency.</p><p><strong>3. Fragility when combined with BM25</strong></p><p>Because <code>script_score</code> allows arbitrary computations, it is easy to drift into scoring behaviors that no longer resemble BM25 or that fail to preserve its relative structure. As the dataset evolves or query patterns shift, the custom logic may interact with BM25 in unanticipated ways. A script that behaved reasonably early in development can produce surprising or unstable results once the catalog grows or data distributions change. Because <code>script_score</code> allows arbitrary math, two engineers working on different parts of the system may unintentionally encode competing scoring models, making ranking difficult to reason about as the organization scales.</p><h2>How function_score provides predictable influence on BM25</h2><p>BM25 already captures how well a document matches a query. It reflects text relevance, term rarity, document length, and the statistical shape of the corpus. When teams introduce business signals including margin, stock levels, popularity, personalization, or merchandising strategy, the goal is not to replace this relevance. The goal is to <em>influence it</em>.</p><p>This distinction is subtle but crucial. Most business requirements are proportional in nature:</p><ul><li><p>Promote higher-margin items modestly</p></li><li><p>Reduce exposure for low-stock products, but don’t hide them</p></li><li><p>Give this user segment a slight uplift for matching products</p></li><li><p>Boost for popularity, but not so much that textual relevance is lost</p></li></ul><p>These are naturally expressed as <em>percentage adjustments</em> rather than as fixed additive values. A merchandiser is rarely asking for “+2 points of score”; they are asking for “a little more visibility,” irrespective of the absolute numeric scale of the BM25 score. Mathematically, this means that the desired transformation is:</p>final_score = BM25 × boost_factor<p>Where <em>boost_factor</em> might be 1.05, 1.2, or 1.5, depending on the signal. Multiplicative boosting does not attempt to reinvent scoring; it simply adjusts the BM25 output by a proportional factor. A multiplicative adjustment has three properties that align well with real-world ranking control:</p><ol><li><p>The boost remains proportional. In other words, a 20% uplift is always a 20% uplift—whether BM25 is 0.12 or 12. The magnitude of the boost does not depend on the underlying BM25 scale.</p></li><li><p>BM25 retains its role as the primary signal. The multiplicative shaping nudges the ordering without overriding it. Strong textual matches still win; business logic influences but does not dominate.</p></li><li><p>Because the operation is multiplicative, not additive, changing the query or updating the corpus does not require re-tuning numeric constants. The boost has the same meaning everywhere.</p></li></ol><p>Elasticsearch’s <code>function_score</code> query provides an elegant mechanism for expressing this pattern. By using:</p><ul><li><p><strong>score_mode: “sum”</strong> to assemble a boost factor (building the multiplier), and</p></li><li><p><strong>boost_mode: “multiply”</strong> to apply the boost (multiplier) to BM25</p></li></ul><p>You can express business intent in a way that remains stable and explainable as your data and query patterns evolve. Instead of adding a second score beside BM25, <code>function_score</code> transforms BM25 itself—shaping it gently, predictably, and in line with how merchandisers and product owners think about ranking adjustments.</p><h2>Examples in practice: How multiplicative boosting behaves in real e-commerce queries</h2><p>To illustrate how multiplicative boosting works in real-world ranking scenarios, it helps to look at a small, concrete example. The goal here is not to demonstrate tuning or production-scale scoring, but rather to show how <code>function_score</code> influences BM25 in predictable, proportional ways that align with business intent.</p><p>Consider a simple catalog with three basketball shoes from three different brands: Nike, Adidas, and Reebok. The product descriptions are intentionally crafted so the BM25 scores exhibit natural differences based on query specificity and field length—just as they would in a real catalog.</p><h3>Example dataset</h3><p>For the following examples, we use a small, straightforward sample dataset with the following characteristics.</p><p>Brand</p><p>Description</p><p>nike</p><p>“Nike basketball shoes”</p><p>adidas</p><p>“New Adidas basketball shoes”</p><p>reebok</p><p>“Reebok basketball shoes”</p><p>We can create an index with the above products with the following commands from Kibana Dev Tools:</p>PUT products
{
  "mappings": {
    "properties": {
      "brand":       { "type": "keyword" },
      "description": { "type": "text" }
    }
  }
}

POST products/_bulk
{ "index": { "_id": "nike-001" } }
{ "brand": "nike",    "description": "Nike basketball shoes" }
{ "index": { "_id": "adi-001" } }
{ "brand": "adidas",  "description": "New Adidas basketball shoes" }
{ "index": { "_id": "ree-001" } }
{ "brand": "reebok", "description": "Reebok basketball shoes" }<p>With this dataset, we now evaluate three queries:</p><ul><li><p>A baseline “basketball shoes” search</p></li><li><p>The same query with a 50% promotion for Adidas and a 25% promotion for Nike</p></li><li><p>A specific “Reebok basketball shoes” query while the Adidas and Nike promotions are still active</p></li></ul><p>Each scenario highlights a different property of multiplicative boosting.</p><h3>1. Baseline ranking: No promotion</h3>GET products/_search
{
  "size": 3,
  "_source": ["brand", "description"],
  "query": {
    "match": { "description": "basketball shoes" }
  }
}<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt272891c9a78166c1/6a16f9d12b835f9e90f4b00b/050f44956c112ea9916d34946e9296480354f7d3-2322x1324.png" alt="" /><p>This query returns the following results where Nike and Reebok are ranked above adidas:</p><p>Rank</p><p>Brand</p><p>Score (BM25)</p><p>1/2 (tie)</p><p>nike</p><p>0.27845407</p><p>1/2 (tie)</p><p>reebok</p><p>0.27845407</p><p>3</p><p>adidas</p><p>0.24686474</p><h3>2. Adding 50% Adidas uplift and 25% Nike uplift with function_score</h3><p>If marketing launches a campaign where Adidas basketball shoes should receive a 50% uplift and Nike a 25% uplift, then the application layer could construct its queries to include those uplifts as follows:</p>GET products/_search
{
  "size": 3,
  "_source": ["brand", "description"],
  "query": {
    "function_score": {
      "query": {
        "match": { "description": "basketball shoes" }
      },
      "functions": [
        {
          "filter": { "term": { "brand": "adidas" } },
          "weight": 0.5
        },
        {
          "filter": { "term": { "brand": "nike" } },
          "weight": 0.25
        },
        {
          "weight": 1.0
        }
      ],
      "score_mode": "sum",
      "boost_mode": "multiply"
    }
  }
}<h3>How the multiplier is constructed</h3><ul><li><p>Base weight = 1.0</p></li><li><p>Adidas gets an additional +0.5</p></li><li><p>So Adidas’s multiplier = 1.5</p></li><li><p>Nike gets an additional +0.25</p></li><li><p>So Nike’s multiplier = 1.25</p></li><li><p>All other brands (including Reebok) get the base weight multiplier = 1.0</p></li></ul><h3>Apply multiplier:</h3><p>Final score = BM25 × multiplier</p><p>Product</p><p>BM25</p><p>Multiplier</p><p>Final score</p><p>Adidas</p><p>0.24686474</p><p>1.5</p><p>0.37029710</p><p>Nike</p><p>0.27845407</p><p>1.25</p><p>0.34806758</p><p>Reebok</p><p>0.27845407</p><p>1.0</p><p>0.27845407</p><h3>Result</h3><p>Adidas moves to the top, Nike follows, and Reebok is at the bottom with no change in its score. This is exactly the behavior that multiplicative boosting is designed to produce:</p><ul><li><p>Adidas and Nike both gain visibility, but in proportion to their configured uplifts.</p></li><li><p>The relative differences in BM25 still matter; we are reshaping the ranking, not replacing it.</p></li><li><p>The ordering changes primarily where BM25 scores are close.</p></li></ul><p>With additive boosts, the same “50% versus 25%” business intent would have to be approximated with numeric constants on an arbitrary BM25 scale, and the effect would vary drastically across queries.</p><h2>3. Specific intent still wins: “Reebok basketball shoes”</h2><p>Now run a highly specific branded query for “Reebok basketball shoes”, with the same Adidas (50%) and Nike (25%) promotions still active:</p>GET products/_search
{
  "size": 3,
  "_source": ["brand", "description"],
  "query": {
    "function_score": {
      "query": {
        "match": { "description": "Reebok basketball shoes" }
      },
      "functions": [
        {
          "filter": { "term": { "brand": "adidas" } },
          "weight": 0.5
        },
        {
          "filter": { "term": { "brand": "nike" } },
          "weight": 0.25
        },
        {
          "weight": 1.0
        }
      ],
      "score_mode": "sum",
      "boost_mode": "multiply"
    }
  }
}<p>The response shows the following results:</p><p>Rank</p><p>Brand</p><p>Final score</p><p>1</p><p>reebok</p><p>1.3011196</p><p>2</p><p>adidas</p><p>0.3702971</p><p>3</p><p>nike</p><p>0.34806758</p><h3>Result</h3><p>Reebok wins overwhelmingly because BM25 correctly detects strong intent for “Reebok basketball shoes”. Adidas and Nike still receive their 50% and 25% promotions, respectively, but those multipliers are nowhere near enough to override the BM25 score.</p><p>This is exactly the behavior that multiplicative boosting is designed to produce:</p><ul><li><p>When BM25 scores are close, boosts can shift the relative ordering.</p></li><li><p>When BM25 scores differ significantly (as they do here, due to strong text matching), the same boosts have little practical effect.</p></li></ul><p>Promotions influence the ranking, but they do not override the core text relevance signal.</p><h2>What this example demonstrates</h2><p>These real queries illustrate the key properties of multiplicative boosting:</p><ol><li><p>The influence is proportional, not arbitrary. A percentage-based uplift has the same proportional effect regardless of the underlying BM25 scale.</p></li><li><p>Text relevance remains in control. Strong brand-intent queries still surface the correct product.The system behaves intuitively. Merchandisers see exactly the ranking changes they expect.</p></li><li><p>The math is stable across queries. The same promotion works correctly whether the match is broad or highly specific.</p></li><li><p>Application logic stays clean. The business layer decides the uplift; Elasticsearch applies it predictably.</p></li></ol><p>Multiplicative boosting through <code>function_score</code> preserves relevance in a predictable and controllable way, while enabling business impact.</p><h2>Application logic remains the author of influence</h2><p>There is a clear separation between deciding what should be boosted and applying that boost in Elasticsearch. <code>function_score</code> handles the second task, but the first belongs firmly to application logic.</p><p>Your application logic is where decisions are made about:</p><ul><li><p>Which margin thresholds matter for your business</p></li><li><p>Whether popularity should rise or fall based on seasonality</p></li><li><p>How to interpret customer behavior or cohort membership</p></li><li><p>How to encode campaign rules</p></li><li><p>When to surface or suppress certain product groups</p></li></ul><p>These are <em>business</em> decisions, not scoring decisions. Elasticsearch does not infer whether a user is budget-focused or luxury-oriented, whether a promotion is active, or whether low stock requires a visibility adjustment. Those determinations occur upstream, in the part of the system that has access to user context, session features, analytics, and business configuration. After application logic produces clear numeric signals for fields such as weights, uplift factors, thresholds, and cohort tags, a <code>function_score</code> query provides a reliable way to express those signals as controlled multipliers on BM25.</p><p>This creates a clean architectural contract:</p><ul><li><p>Application logic: decides <em>what</em> should be influenced.</p></li><li><p>BM25 provides the core text relevance.</p></li><li><p><code>function_score</code> applies influence in a mathematically stable way.</p></li></ul><p>Because business logic lives outside the index, teams can adjust or experiment with uplift strategies without reindexing or restructuring documents.</p><h2>Conclusion</h2><p>E-commerce search must balance core text relevance with business considerations such as profitability, stock position, customer intent, seasonality, and personalization. BM25 provides a stable and interpretable foundation for text relevance, but influencing that score requires care. Business signals should shape the ranking, not overpower it.</p><p>However, the most commonly used levers such as boosted <code>should</code> clauses, <code>rank_feature</code> fields, and ad-hoc script scoring often behave unpredictably. These approaches can appear effective in early development, but their limitations emerge as soon as the catalog evolves or new query patterns arrive. Additive boosts fluctuate wildly because their impact depends entirely on the underlying scale of BM25, which varies dramatically across queries. A boost that produces a subtle nudge in one situation can dominate the ordering in another. Script scoring introduces its own challenges: opaque logic, reduced performance, and scoring behavior that becomes harder to understand or maintain over time.</p><p>Multiplicative boosting with <code>function_score</code> avoids these pitfalls by transforming BM25 proportionally rather than competing with it. Instead of adding a second, independent score component, it applies a controlled multiplier to BM25 itself. This produces the kind of predictable adjustments that merchandisers actually intend. For example, it allows slight promotions for high-margin items, modest reductions for low-stock products, or gentle uplifts for relevant user cohorts.</p><p>Equally important, the architecture remains clean. Application logic determines which business signals matter, and <code>function_score</code> applies them in a consistent, explainable way. Business teams can evolve business strategy without destabilizing relevance, and Engineering teams can refine relevance without disturbing business rules.</p><p>This principle is the foundation of the previous blogs that demonstrated how to influence e-commerce rankings: (1) <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>, and (2) <a href="https://www.elastic.co/search-labs/blog/ecommerce-search-relevance-cohort-aware-ranking-elasticsearch">How to improve e-commerce search relevance with personalized cohort-aware ranking</a>. Both approaches rely on the idea that business signals should guide BM25, not override it. Multiplicative boosting through <code>function_score</code> provides a practical, transparent, and scalable method for achieving that balance in real-world e-commerce search.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/bm25-ranking-multiplicative-boosting-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/bm25-ranking-multiplicative-boosting-elasticsearch</guid>
    <category><![CDATA[Inside Elastic]]></category>
    <dc:creator><![CDATA[Alexander Marquardt]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt72914b2406032448/6a170e2c60084b4e913c45f9/6150bb846170d9be926a19260846a161ed377a5f-1098x542.png" length="0" type="image/png"/>
    <pubDate>Mon, 22 Dec 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[How to improve e-commerce search relevance with personalized cohort-aware ranking]]></title>
    <description><![CDATA[Improve e-commerce search relevance with explainable, cohort-aware ranking in Elasticsearch. Learn how multiplicative boosting delivers stable, predictable personalization at query time.]]></description>
    <content:encoded><![CDATA[<h2>Overview</h2><p>In this article, we explore how to make Elasticsearch search results more relevant for different e-commerce user segments using an explainable, multiplicative boosting strategy — without any machine learning post-processing.</p><h2>Introduction: Why personalization matters</h2><p>Elasticsearch is very good at ranking results by textual relevance (BM25) and by semantic relevance (vectors). In e-commerce, that is necessary but not sufficient. Two people can type the same query and reasonably expect different results:</p><ul><li><p>A luxury shopper searching for “red lipstick” expects prestige brands near the top.</p></li><li><p>A budget shopper wants affordable options promoted.</p></li><li><p>A gift buyer may prefer popular bundles.</p></li></ul><p>The goal is to adjust ranking so that, for a given query, products that align with the user’s segment rise modestly in the list, without destroying the underlying relevance. This article shows how to add cohort-aware personalization on top of Elasticsearch’s relevance using only <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-function-score-query">function_score</a>, a keyword field, and small multiplicative boosts.</p><h2>Multiplicative boosting for cohort personalization</h2><p>The core challenge in cohort personalization is stability. You want a product that is relevant to the query to remain relevant, with a controlled, explainable uplift when it matches the user’s segment. What often goes wrong is that personalization signals are added to the score in a way that either:</p><ul><li><p>overwhelms BM25 on some queries, or</p></li><li><p>has almost no effect on others.</p></li></ul><p>This happens because most boosting approaches use additive scoring. However, BM25 scales can vary dramatically across queries and datasets, so a fixed additive adjustment (e.g., “add +2.0 for a cohort match”) is sometimes a massive change to the BM25 score, and other times is negligible. Instead, what we want is a guarantee that if a product is a good match for the query, and it aligns with the user’s cohort, then its score is increased by a controlled percentage regardless of the absolute BM25 scale. We can achieve this with a multiplicative pattern:</p>final_score = BM25 × (1 + cohort_overlap × weight_per_cohort)<p>This article shows how to implement this pattern using Elasticsearch’s function_score query, a cohorts field on the product, and a list of user cohorts passed at query time.</p><h2>Modeling cohorts in your product catalog</h2><p>The simplest way to enable cohort-aware ranking is to treat cohorts as tags. For example, a product might carry tags such as:</p><ul><li><p>Lipstick: ["female", "beauty", "luxury"]</p></li><li><p>Men’s deodorant: ["male", "personal_care", "sport"]</p></li><li><p>Glitter gloss: ["female", "beauty", "youth", "party"].</p></li></ul><p>A user or session carries a set of tags inferred from behavior and profile:</p><ul><li><p>High-income female luxury shopper: ["female", "beauty", "luxury"]</p></li><li><p>Budget-oriented female shopper: ["female", "beauty", "budget"]</p></li></ul><p>Cohort overlap is the count of shared cohort tags between the user/session and the product. No weighting, no semantic similarity — just a simple intersection. For example, if the user cohorts are [“female”, “beauty”, “budget”] and a lipstick has [“female”, “beauty”, “luxury”], the overlap is 2. If a men’s deodorant has [“male”, “personal_care”, “sport”], the overlap with that same user is 0.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta288b12a3e3fa2a8/6a170bda286714007393e33b/e88ddfa1b453327fe75211500b850b49ad3776f5-1172x844.png" alt="" /><p>The intuition is that (a) BM25 ranks documents depending on how relevant they are to the user’s query, and (b) cohort overlap boosts products based on how well each product aligns with the user's segment. To accomplish this, we transform the cohort overlap between the user cohorts and the product cohorts into a multiplicative boost that scales BM25.</p><p>To avoid field explosion, we keep all cohort tags in a single keyword field, such as follows:</p>{
  "product_id": "LIP-001",
  "description": "Premium cherry red lipstick with velvet finish",
  "cohorts": ["female", "beauty", "luxury"]
}<p>This is easy for merchandisers to understand, avoids hundreds of boolean fields like is_female or is_luxury, and works efficiently with term filters.</p><h2>Why additive boosts don’t work</h2><p>One subtle but important point is that even a standard boolean query is additive. When Elasticsearch scores a document, the base BM25 score from the main query (typically in a <code>must</code>) and every matching <code>should</code> clause contributes an additive score. This means “additive boosting” is not just about boosts, it’s fundamental to how boolean scoring works.</p><p>Personalization built on additive logic behaves inconsistently because BM25 scales differ per query and dataset. For example the base BM25 scores for three products might be 12, 8, 4 in one instance, and might be 0.12, 0.08, 0.04 after updating your dataset or modifying your query. In this case an additive boost (e.g., +2.0) becomes a dominating force when the base BM25 scores are small (a +2.0 boost on a score of 0.12 is about 18× higher) or a rounding error when the base BM25 scores are large (+2.0 boost on a score of 12 is only about 1.17× higher). This creates inconsistent, unpredictable ranking behavior.</p><h2>Why multiplicative boosting is the right shape</h2><p>If we apply a multiplicative boost, the shape is consistent:</p>final_score = BM25 × boost
boost = 1 + overlap × weight_per_cohort<p>With weight_per_cohort = 0.1, an overlap of 2 gives a boost of 1.2 (20% increase), an overlap of 1 gives a boost of 1.1 (10% increase), and an overlap of 0 gives a boost of 1.0 (no change). This means that a product that is more aligned with the user cohort gets a predictable percentage uplift, regardless of whether its BM25 score is 0.01 or 10.0. BM25 remains the primary signal; cohort alignment gently reshapes the ranking.</p><h2>How function_score gives us multiplicative behavior</h2><p>To convert cohort overlap into a controlled percentage boost, we need a way to take the normal BM25 score and scale it up by a factor such as 1.1, 1.2, or 1.3. Elasticsearch does not support multiplying a score directly inside a standard query, but <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-function-score-query">function_score</a> provides exactly this capability: it lets us compute an additional score component and combine it with the base score using a chosen strategy, which is "multiply" for this use case.</p><p>Elasticsearch’s function_score lets us implement multiplicative cohort boosting in three steps. First, each cohort match contributes a small weight (e.g., 0.1). Second, we include a baseline weight of 1.0 so that the final multiplier never drops below 1. Third, we sum all cohort contributions using score_mode: "sum" to produce a boost factor that represents (1 + overlap × weight). Finally, we combine this boost factor with the BM25 score using boost_mode: "multiply", which gives us the exact multiplicative behavior we want.</p><p>The calculation below shows how the final score is calculated, where BM25 is the base relevance; n is the number of matched cohorts; w is weight_per_cohort (e.g., 0.1); and additive baseline = 1.0:</p>sum_score = baseline + n × w
final_score = BM25 × sum_score<p>So, with 2 overlapping cohorts and w = 0.1:</p>sum_score = 1.0 + 2 × 0.1 = 1.2
final_score = BM25 × 1.2<p>This is exactly the multiplicative behavior we want.</p><h2>Putting it together: index, data, and baseline ranking</h2><p>Create a simple index:</p>PUT product_catalog
{
  "mappings": {
    "properties": {
      "product_id": {
        "type": "keyword"
      },
      "description": {
        "type": "text"
      },
      "cohorts": {
        "type": "keyword"
      }
    }
  }
}<p>Index a few products:</p>POST _bulk
{ "index": { "_index": "product_catalog", "_id": "LIP-001" }}
{ "product_id": "LIP-001", "description": "Premium cherry red lipstick with velvet finish", "cohorts": ["female", "beauty", "luxury"] }
{ "index": { "_index": "product_catalog", "_id": "LIP-002" }}
{ "product_id": "LIP-002", "description": "Affordable matte red lipstick for everyday wear", "cohorts": ["female", "beauty", "budget"] }
{ "index": { "_index": "product_catalog", "_id": "LIP-003" }}
{ "product_id": "LIP-003", "description": "Glitter red gloss for parties and festivals", "cohorts": ["female", "beauty", "youth", "party"] }<p>A baseline query for “red lipstick” might look like:
</p>POST product_catalog/_search
{
  "size": 5,
  "_source": ["product_id", "description"],
  "query": {
    "multi_match": {
      "query": "red lipstick",
      "fields": ["description"]
    }
  }
}<p>This returns a pure BM25 ranking (without any cohort boosting). In this example, the scores of LIP-001 and LIP-002 will be very close (or identical), because they match the same query terms with similar frequencies and have comparable lengths.</p><p>The relative ranking is what matters; the exact numeric scores may differ depending on shard configuration, analyzer differences, or Elasticsearch version.</p><p>Product ID</p><p>Description</p><p>BM25 score</p><p>LIP-001</p><p>Premium cherry red lipstick with velvet finish</p><p>0.603535</p><p>LIP-002</p><p>Affordable matte red lipstick for everyday wear</p><p>0.603535</p><p>LIP-003</p><p>Glitter red gloss for parties and festivals</p><p>0.13353139</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2ef304420fe7d036/6a170bdcb0367dc72072bd4b/513bfba3467fb9966ed00b6e798889eeb690fe84-1788x1306.png" alt="" /><h3>Persona A: high-income luxury shopper</h3><p>Suppose we know that Persona A fits into the following cohorts:</p>["female", "beauty", "luxury"]<p>We translate that into a set of cohort filters, each with a small weight, plus a baseline factor:</p>GET product_catalog/_search
{
  "explain": true,
  "query": {
    "function_score": {
      "query": {
        "multi_match": {
          "query": "red lipstick",
          "fields": ["description"]
        }
      },
      "functions": [
        { "filter": { "term": { "cohorts": "female" }},  "weight": 0.1 },
        { "filter": { "term": { "cohorts": "beauty" }},  "weight": 0.1 },
        { "filter": { "term": { "cohorts": "luxury" }},  "weight": 0.1 },
        { "weight": 1.0 }
      ],
      "score_mode": "sum",
      "boost_mode": "multiply"
    }
  }
}<p>For this persona LIP-001 (“Premium cherry red lipstick with velvet finish”) matches “female”, “beauty”, and “luxury” which means that the cohort overlap is 3 and therefore the boost factor is 1.3. On the other hand, LIP-002 and LIP-003 match “female” and “beauty” which results in a boost factor of 1.2.</p><p>Product ID</p><p>Description</p><p>Base BM25 score</p><p>Boost factor</p><p>New score</p><p>LIP-001</p><p>Premium cherry red lipstick with velvet finish</p><p>0.603535</p><p>1.3x (30%)</p><p>0.7845955</p><p>LIP-002</p><p>Affordable matte red lipstick for everyday wear</p><p>0.603535</p><p>1.2x (20%)</p><p>0.724242</p><p>LIP-003</p><p>Glitter red gloss for parties and festivals</p><p>0.13353139</p><p>1.2x (20%)</p><p>0.16023767</p><p>As desired for this luxury user, the luxury lipstick (LIP-001) receives the strongest uplift and will tend to rise above similar alternatives in the results.</p><h3>Persona B: budget-oriented shopper</h3><p>A budget-conscious shopper might belong to the following cohorts:</p><p>["female", "beauty", "budget"]</p><p>The query for this user is nearly identical to the previous query, except for the cohort values which now reflect “budget” rather than “luxury”:</p>GET product_catalog/_search
{
  "query": {
    "function_score": {
      "query": {
        "multi_match": {
          "query": "red lipstick",
          "fields": ["description"]
        }
      },
      "functions": [
        { "filter": { "term": { "cohorts": "female" }},  "weight": 0.1 },
        { "filter": { "term": { "cohorts": "beauty" }},  "weight": 0.1 },
        { "filter": { "term": { "cohorts": "budget" }},  "weight": 0.1 },
        { "weight": 1.0 }
      ],
      "score_mode": "sum",
      "boost_mode": "multiply"
    }
  }
}<p>For this persona LIP-002 (“Affordable matte red lipstick for everyday wear”) matches “female”, “beauty”, and “budget” which means that the cohort overlap is 3 and therefore the boost factor is 1.3. On the other hand, LIP-001 and LIP-003 match “female” and “beauty” which results in a boost factor of 1.2.</p><p>Product ID</p><p>Description</p><p>Base BM25 score</p><p>Boost factor</p><p>New score</p><p>LIP-002</p><p>Affordable matte red lipstick for everyday wear</p><p>0.603535</p><p>1.3x (30%)</p><p>0.7845955</p><p>LIP-001</p><p>Premium cherry red lipstick with velvet finish</p><p>0.603535</p><p>1.2x (20%)</p><p>0.724242</p><p>LIP-003</p><p>Glitter red gloss for parties and festivals</p><p>0.13353139</p><p>1.2x (20%)</p><p>0.16023767</p><p>As desired for this budget user, the budget lipstick (LIP-002) receives the strongest uplift and will tend to rise above similar alternatives in the results.</p><h2>How to build the cohort filter dynamically (Python example)</h2><p>You will normally inject the cohort filters at query time based on the user/session profile. For example:</p>user_cohorts = ["female", "beauty"]
functions = [
    { "filter": { "term": { "cohorts": cohort } }, "weight": 0.1 }
    for cohort in user_cohorts
]
# add baseline multiplier
functions.append({ "weight": 1.0 })<p>Using term filters on a keyword field is fast, shard-cache friendly, and fully visible in the _explain API, which shows exactly which filters fired and which weights were applied.</p><h2>How cohort assignment works</h2><p>Cohort assignment is intentionally left outside Elasticsearch, and is outside the scope of this article. However, sources could include:</p><ul><li><p>browsing events (“has viewed lipstick” → beauty)</p></li><li><p>gender inference (from preferences or marketing profile)</p></li><li><p>device characteristics (mobile shopper)</p></li><li><p>location (“urban buyer”)</p></li><li><p>historical purchases</p></li><li><p>marketing segments</p></li><li><p>personalization cookies</p></li></ul><p>All of these are input signals, but the scoring mechanism in Elasticsearch remains the same. Elasticsearch does not need to know how you inferred the segments. This separation of concerns keeps Elasticsearch focused on ranking, while your application or data science layer owns the logic for inferring segments.</p><h3>How to choose the right boost weight</h3><p>In our examples, we used 0.1 per cohort. This value is tunable. Staying between 0.05 and 0.20 will likely provide good results. You should A/B test weights based on:</p><ul><li><p>catalog diversity</p></li><li><p>number of cohort tags per product</p></li><li><p>variability in BM25</p></li><li><p>business goals (revenue vs. discovery vs. personalization)</p></li></ul><h3>Limit the number of cohorts assigned to each product</h3><p>Giving a product 20 cohort tags leads to:</p><ul><li><p>Noise in the signals</p></li><li><p>Gaming by merchandisers (“tag everything as luxury”)</p></li><li><p>Loss of explainability</p></li><li><p>Over-boosting</p></li></ul><p>As a starting point (to be confirmed by your own testing), we recommend:</p><ul><li><p>Approximately 5 cohorts per product.</p></li><li><p>Optionally, an offline validation step (ingest pipeline, CI script, or index-time check) that warns or blocks when more than 5 tags are assigned.</p></li></ul><h3>Customized cohort boosting per user</h3><p>So far, our examples assume every cohort contributes equally. In reality, some users strongly prefer certain segments. In some cases, you might know that certain cohorts are especially important for an individual user. For example:</p><ul><li><p>A user who almost always buys luxury brands.</p></li><li><p>A user who consistently picks budget options.</p></li></ul><p>You can encode this by assigning different weights per cohort instead of a flat 0.1. For example, if your application has detected a “super-luxury” shopper, then you could modify the function scoring as follows:</p>"functions": [
  { "filter": { "term": { "cohorts": "female" }},  "weight": 0.1 },
  { "filter": { "term": { "cohorts": "beauty" }},  "weight": 0.1 },
  { "filter": { "term": { "cohorts": "luxury" }},  "weight": 0.2 },
  { "weight": 1.0 }
]<p>In the above example matching “female” or “beauty” each add <code>+0.1</code> while matching luxury adds <code>+0.2</code>. In this example, a product matching all three cohorts would get:</p>boost = 1.0 + 0.1 + 0.1 + 0.2 = 1.4<p>This remains fully explainable, and you can document the configuration (“luxury is 2× as important as other cohorts for this user”). Additionally, the explain API will show exactly how those numbers contributed to the final score.</p><h2>Conclusion:</h2><p>This Elasticsearch-native approach to cohort personalization uses only lightweight metadata and standard query constructs, while preserving explainability, stability, and business control over the relevance model. This delivers precise, predictable relevance that ensures the business goals never sacrifice the quality of the search results.</p><h2>Implementation summary</h2><p>If you want to adopt this pattern in production, the high-level steps are:</p><ul><li><p>Add a single keyword field (cohorts) to each product containing 3–5 cohort tags.</p></li><li><p>Compute user/session cohorts in your application logic (from browsing, purchase history, CRM, etc.) and pass them with the query.</p></li><li><p>Inject dynamic function_score filters into your query with one per user cohort, and each with a small weight (e.g., 0.1), plus a baseline weight (1.0).</p></li><li><p>Wrap your existing BM25 query in function_score with score_mode: "sum" and boost_mode: "multiply" to apply multiplicative boosting.</p></li><li><p>Tune per-cohort weights (typically 0.05–0.20) based on A/B experiments, ensuring BM25 remains the dominant signal.</p></li></ul><p>These steps let you layer cohort personalization cleanly on top of your existing search relevance, without scripts, ML models, or major architecture changes.</p><h2>What’s next?</h2><p>This pattern is a powerful example of how to build sophisticated relevance rules directly into your queries, ensuring speed and reliability.</p><ol><li><p><strong>Implement custom personalization faster:</strong> If you're ready to deploy and optimize this advanced cohort personalization strategy, or to tackle other complex relevance challenges, our team can help you build, tune, and operationalize your Elasticsearch solution quickly. Contact <a href="https://www.elastic.co/consulting">Elastic Services</a> for help implementing this and other advanced search techniques.</p></li><li><p><strong>Join the discussion:</strong> For general questions about advanced relevance techniques and implementation, join the <a href="https://discuss.elastic.co/">broader Elastic Stack community</a> for search discussions.</p></li></ol>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/ecommerce-search-relevance-cohort-aware-ranking-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/ecommerce-search-relevance-cohort-aware-ranking-elasticsearch</guid>
    <category><![CDATA[Relevance]]></category>
    <dc:creator><![CDATA[Alexander Marquardt]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta288b12a3e3fa2a8/6a170bda286714007393e33b/e88ddfa1b453327fe75211500b850b49ad3776f5-1172x844.png" length="0" type="image/png"/>
    <pubDate>Wed, 10 Dec 2025 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>