Test Elastic's leading-edge, out-of-the-box capabilities. Dive into our sample notebooks, start a free cloud trial, or try Elastic on your local machine now.
Overview
In this article, you will learn how to combine BM25 relevance with real business metrics like profit margin and popularity using Elasticsearch’s function_score query. This step-by-step guide shows how to control scaling with logarithmic boosts and allows full explainability for each ranking calculation.
Introduction
In many use cases, search results focus on lexical (keyword) and semantic (meaning-based) analysis to find the content that most accurately and authoritatively answers a user’s query. However, e-commerce search is a bit more complex.
Results must reflect the shopper’s intent and incorporate business objectives such as profit margin, product popularity, or other factors that don’t always directly align with purely lexical or semantic matching.
While text relevance ensures customer satisfaction, ranking by profitability and popularity turns search into a business optimization engine.
In order to demonstrate how business signals can be incorporated into search results, in this post we’ll explore:
- How to boost product rankings by margin (profitability 0% to 200% in the demo data below).
- How to extend that same logic to include popularity (number of sales).
Once you understand how to boost by margin and popularity, extending search to incorporate other signals is straightforward.
Setup
Below is a small dataset you can paste directly into Dev Tools to follow along.
Each document represents a product with:
- margin: profit margin (percent)
- popularity: relative sales volume (e.g. weekly average, or last week’s sum)
Ranking without margin
We can see how baseline results look by executing a simple query for “McCain chips” that does not take into consideration margin, as follows:

Which returns the following results:
As you can see from the above results, the high margin version of the chips is 3rd in the results because the ordering does not consider margin.
Ranking by margin
Without any additional context, all sizes of “McCain chips” look equally relevant — but from a business perspective, it is possible that the higher-margin items should rank higher.
| Product | Margin (%) | Description |
|---|---|---|
| McCain Home Chips 500g – High Margin | 200% | small pack |
| McCain Home Chips 1kg | 100% | mid pack |
| McCain Home Chips 1.5kg | 50% | family pack |
We’ll use Elasticsearch’s function_score query to apply a margin-based boost.
The above query results in the following, which reflect the impact of the margin boosting on the score. Notice that, as we intended, the high-margin McCain Chips have been boosted to the 1st position in the results.
Understanding the formula
The function_score query allows us to apply a smooth, interpretable boost based on margin without overwhelming BM25’s lexical relevance.
Here’s how it works:
- margin_boost = ln(1 + margin × factor)
- boost = 1 + margin_boost
- final_score = BM25 × boost
Where the query is specified with the following fields:
- field_value_factor – uses a document field to influence scoring without scripting overhead.
- modifier: “ln1p” – computes ln(1 + margin × factor)
- Note: ln1p(x) is shorthand for ln(1 + x).
- factor – controls scale; 0.0085 caps boosts near 2× at margin=200.
- weight: 1 – ensures a minimum boost of 1 for neutral items.
- score_mode: “sum” – adds constant 1 (from that standalone “weight” : 1) and the margin_boost together.
- boost_mode: “multiply” – multiplies BM25 by the computed boost.
Why was that formula chosen?
The logarithmic (ln1p) scaling behaves well across real-world data:
- It grows fast at small margins (rewarding incremental gains).
- It flattens at high margins (preventing runaway scores).
- It’s continuous and interpretable — no thresholds or discontinuities.
| Margin | ln(1 + margin × 0.0085) | Boost (≈1+ln1p) | Boost Multiplier |
|---|---|---|---|
| 5 | 0.042 | 1.04 | ×1.04 |
| 50 | 0.35 | 1.35 | ×1.35 |
| 100 | 0.63 | 1.63 | ×1.63 |
| 200 | 0.99 | 1.99 | ×1.99 |
Ranking by margin and popularity
We can extend the same logic to add a popularity boost. Here, we tune the popularity factor so that the boost increases by roughly +1.0, at a popularity of 10,000. (These thresholds depend on your dataset’s scale.)
Which returns results with the most popular product in 1st place, even though it is not the highest margin, as follows — in this case, the impact of the popularity boost has pushed up McCain Home Chips 1.5kg to 1st place.
What the resulting boosts look like
The “factors” are tuned to add +1.0 to the boost at the assumed maximums. These are calculated to satisfy the following formulas:
Then:
Each cell in the table below represents the total BM25 multiplier for various margin and popularity values.

How to read the table:
- The first column (popularity = 0) isolates the margin effect.
- Moving right, popularity increases the boost — but since its weight is 0.5, its contribution to the summed boost is halved.
- Even at extreme values (popularity = 100,000), the boost flattens due to logarithmic scaling.
Tuning
If you find popularity can spike very high (e.g., 100k+) and you don’t want boosts above some ceiling, you can:
- Lower the popularity factor further, or
- Add “max_boost”: <cap> to function_score, or
- Split weights, e.g. “weight”: 0.25 on popularity and “weight”: 1 on margin (still with score_mode: “sum”), if you want one to dominate less.
Using rank_feature for similar use cases
At first glance, rank_feature and rank_features look like a natural choice for incorporating numeric signals such as popularity, recency, or even profit margin. They are fast, compressed, and easy to operationalize — which is why many teams reach for them first.
However, they are not a good fit for this type of scoring model, for the following reasons:
1. Rank-feature contributions are strictly additive
The score takes the form:
This means the effect of the boost changes dramatically depending on the scale of the BM25 score.
- When BM25 is small, the boost dominates the ranking.
- When BM25 is large, the identical boost becomes negligible.
We need consistent, proportional behavior instead.
2. Impossible to express “percentage-based” or multiplicative logic
This article’s model requires expressing things like:
- “Popularity increases relevance by ~20%.”
- “Margin strengthens relevance but never overrides it.”
rank_feature cannot do this. It does not support multiplicative shaping of the BM25 score.
3. Combining multiple signals becomes unstable and hard to tune
If you try to combine margin, popularity, availability, or other business metrics via rank_features, each feature adds another independent additive term. These interact in opaque ways, making tuning brittle and unpredictable.
4. Bottom line
rank_feature is great for simple additive numeric boosts. It is not suitable when you need:
- stable behavior across queries
- proportional / multiplicative effects
- explainable blending of multiple signals
For this reason, the article uses function_score instead, because it provides explicit, controlled scoring that behaves consistently regardless of BM25 scale.
Wrapping up
Elastic’s function_score query makes it simple to transform search ranking from content relevance into business-aware optimization.
By combining BM25 relevance with economic signals like margin and popularity, you can:
- Align search with real business outcomes.
- Tune scaling via a single parameter (factor).
- Maintain full explainability through _
explain.
Once this foundation is in place, you can extend it to Stock levels (reduce the ranking of low-stock products), Recency (prioritize new products), or other business-critical signals that you want to take into consideration. Each new signal simply adds to the boost which is then multiplied by the base BM25 relevance score.




