Using the Explain API for Systematic Scoring Debugging
AI generated
_doc
_index
Elasticsearch · Relevance · Debugging
The Explain API for Scoring Debugging
Why does exactly this document rank exactly this way?

"Why does product B rank above product A, even though A is the better match?" is one of the most common questions in day-to-day relevance work, and without a systematic tool the answer remains guesswork. The Explain API exposes exactly which individual terms, weights and boosts make up a BM25 score, turning scoring debugging from guessing into analysis.

16 min read _explain · explain:true · BM25 · Score Breakdown Elasticsearch 8.x · OpenSearch 2.x

1. Why scoring debugging becomes indispensable in practice

Every Elasticsearch search does not only return hits, it also returns an implicit ranking of those hits through the relevance score. This score decides which document lands in position one and which one vanishes on page three, making it directly business-critical, for instance in product search on an online shop. As soon as users or product owners ask why a certain document ranks above a seemingly better match, looking at the query alone is not enough. You need insight into the actual score calculation.

This is exactly where scoring debugging comes in: instead of guessing whether a boost is weighted too strongly or a term frequency effect tips the scale, the Explain API shows the complete calculation that makes up the final score. This is especially valuable for complex queries with multiple should clauses, function score, boosting and multi-match fields, where the overall effect is hard to predict from reading the query DSL alone.

A central principle for effective scoring debugging: the Explain API delivers facts, not judgment. It shows how the score came about, not whether that score is "right" or "wrong". Interpreting whether a weighting is appropriate remains the job of the person who understands the domain and user intent. The following sections show how to move systematically from the symptom level to the root cause in scoring using the Explain API.

2. The _explain endpoint: structure and usage

The dedicated _explain endpoint answers a very concrete question: why did exactly this one document receive this score for this query? The call is made against a known index, a known document ID and the same query used in the search. The response contains an explanation object with the final score as the root and a nested tree of sub-calculations underneath.

The _explain endpoint is deliberately focused on a single document, because a full explanation for every hit in a large result list would create too much overhead. In practice, this endpoint is typically used after the suspicious document ID has already been identified in the regular search, for example because it appears unexpectedly high or low in the result list.


GET /products/_explain/4711
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title": "hiking boots men" } }
      ],
      "should": [
        { "term": { "brand.keyword": "TrailPro" } }
      ]
    }
  }
}

# Response (shortened):
# {
#   "matched": true,
#   "explanation": {
#     "value": 8.42,
#     "description": "sum of:",
#     "details": [
#       { "value": 6.10, "description": "weight(title:hiking ...) ..." },
#       { "value": 2.32, "description": "weight(brand.keyword:TrailPro ...) ..." }
#     ]
#   }
# }

The matched field is important: if it is false, the document did not satisfy the query at all and consequently does not appear in the search results, regardless of the score calculation below it. That is the first diagnostic step for the question "why can't I find this document?" before any score analysis.

3. Reading the score breakdown tree systematically

The explanation structure is recursive: every node has a value, a textual description, and optionally a list of details, which themselves have the same structure. A node with the description sum of: adds up the values of its children, a node with max of: takes over the highest value of its children, typical for dis_max queries and multi-match with best_fields. Reading this structure systematically from the root downward is the key to understanding a complex score.

When reading it, it helps to first look only at the top level of the details and identify which subtree carries the largest share of the total score. Only then is it worth descending further into that dominant subtree. Anyone who instead tries to grasp the whole nested structure at once quickly loses track, especially for queries with multiple should clauses and nested bool queries.


# Typical tree structure of a bool query with two must clauses
{
  "value": 12.87,
  "description": "sum of:",
  "details": [
    {
      "value": 9.55,
      "description": "weight(title:hiking in 42) [PerFieldSimilarity], result of:",
      "details": [
        {
          "value": 9.55,
          "description": "score(freq=2.0), computed as boost * idf * tf from:",
          "details": [
            { "value": 2.2, "description": "boost" },
            { "value": 3.8, "description": "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5))" },
            { "value": 1.14, "description": "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl))" }
          ]
        }
      ]
    },
    {
      "value": 3.32,
      "description": "weight(brand.keyword:TrailPro in 42) [PerFieldSimilarity], result of:",
      "details": [ ]
    }
  ]
}

The identifiers in square brackets like [PerFieldSimilarity] show which similarity implementation is configured for that field. If this deviates from the expected BM25, for instance because a boolean similarity without term frequency weighting is active by accident, it often explains why a score behaves unexpectedly.

4. Understanding BM25: what the explain output really shows

Since Elasticsearch 5, BM25 has been the default similarity function, and the explain output consistently follows its formula. Three components dominate every score node: boost, a multiplier assigned manually or through the query structure, idf (inverse document frequency), which weights rare terms higher than common ones, and tf (term frequency), which accounts for how often a term appears in a document, but unlike classic TF-IDF is saturated with the parameters k1 and b, so repeated occurrences have diminishing marginal value.

The b parameter controls length normalization: a long document with many occurrences of a term does not automatically get a proportionally higher score than a short, focused document. This is why short, precise product titles often outperform long body text descriptions with the same term under BM25, even if the term occurs equally often in both. The explain output shows dl (document length) and avgdl (average document length) explicitly, so this effect can be traced directly.


GET /products/_explain/4711
{
  "query": { "match": { "description": "waterproof" } }
}

# Excerpt of the explanation with BM25 detail values:
# "description": "score(freq=1.0), computed as boost * idf * tf from:",
# "details": [
#   { "value": 2.2, "description": "boost" },
#   { "value": 4.1, "description": "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from: n, N" },
#   {
#     "value": 0.87,
#     "description": "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:",
#     "details": [
#       { "value": 1.0, "description": "freq, occurrences of term within document" },
#       { "value": 1.2, "description": "k1, term saturation parameter" },
#       { "value": 0.75, "description": "b, length normalization parameter" },
#       { "value": 42.0, "description": "dl, length of field" },
#       { "value": 38.5, "description": "avgdl, average length of field" }
#     ]
#   }
# ]

Comparing idf values between two documents immediately reveals whether a term is rare in the overall index (high idf, strong influence) or common (low idf, weak influence). This often explains why a seemingly central search term only contributes little to the score, simply because it is too widespread in the index to still act as a distinguishing feature.

5. explain:true in _search instead of single-document explain

The _explain endpoint works well when the suspicious document ID is already known. For analyzing several hits at once, the parameter "explain": true directly in the _search request is more efficient: it returns the explanation for every returned hit in a single request, allowing the score composition of multiple documents to be compared side by side directly.

This mode is especially useful for understanding the order of the top hits: why does document A rank before document B, even though B would intuitively be the better match? With explain:true, the two explanation trees can be compared side by side and the decisive difference, for instance a missing boost or a shorter field with better length normalization, becomes directly visible.


GET /products/_search
{
  "explain": true,
  "size": 5,
  "query": {
    "multi_match": {
      "query": "hiking boots waterproof",
      "fields": ["title^3", "description"]
    }
  }
}

# Every hit in the response array additionally contains:
# "_explanation": {
#   "value": 15.02,
#   "description": "max of:",
#   "details": [ ... ]
# }

A note on production usage: explain: true noticeably increases response size and compute time per query, because the full calculation is delivered for every hit. This parameter belongs in debugging sessions and automated relevance tests, not in live queries in production.

6. Detecting and diagnosing typical score anomalies

One of the most common score anomalies is a document that, despite an exact match in the most important field, ranks lower than a document with only a partial match. In such cases, the explain output frequently shows that the first document has a very long field, causing BM25 length normalization to dampen the tf component significantly despite the exact match. The solution is usually to set up an additional, shorter field such as title.exact for boosting on exact matches, instead of relying solely on the long full-text field.

A second common anomaly arises from unexpectedly low idf values: a search term that is subjectively perceived as rare and relevant actually occurs very frequently in the index, for instance because it is part of boilerplate text present in every document. The explain output immediately reveals this through a low idf value. A third anomaly shows up in function score queries, when an additive or multiplicative function completely overrides the textual relevance score, so that in the end only the function, not the actual text match, determines the ranking.


# Diagnosis: why does document 99 rank above document 42, even though 42 is the more exact match?
GET /products/_search
{
  "explain": true,
  "query": {
    "ids": { "values": ["42", "99"] }
  }
}
# Comparing the two _explanation trees frequently shows:
# - Document 42: high dl (document length) -> tf dampened despite freq=3
# - Document 99: low dl -> tf closer to 1.0 despite freq=1

7. Combining explain with function score and boosting

Function score queries multiply or add extra signals such as sales figures, ratings or recency onto the textual relevance score. For these queries, the explain output shows an additional node with the description function score, product of: or sum of:, depending on the chosen score_mode, and lists every individual function below it with its own contribution. This makes it visible whether a single function, for instance a field_value_factor on sales figures, dominates the textual score.

Comparing the _score before and after applying the function score is especially insightful. If the function's contribution exceeds the textual relevance share by a large margin, that is a strong indicator that the function's weighting should be recalibrated, for instance through normalizing input values or lowering the weight setting.


GET /products/_explain/4711
{
  "query": {
    "function_score": {
      "query": { "match": { "title": "hiking boots" } },
      "functions": [
        { "field_value_factor": { "field": "sales_count", "modifier": "log1p", "factor": 0.3 } }
      ],
      "boost_mode": "sum"
    }
  }
}

# Explanation shows separately:
# 1. Textual BM25 score from the match
# 2. function score, computed with field_value_factor
# 3. sum of: (final result from boost_mode "sum")

8. Explain API and aggregations: the limits of the tool

The Explain API exclusively answers questions about the relevance score of a query, not about aggregations. Anyone who wants to know why a specific aggregation bucket contains a specific number of documents, or why a metric aggregation returns a specific value, will find no answer in the Explain API, because aggregations run completely independently of score calculation. For such questions, targeted filter queries help instead, to manually reconstruct the base set of an aggregation.

Another limit concerns constant_score and pure filter contexts: documents matched only through a filter uniformly receive a score of 1.0 or the configured boost value, regardless of term frequency or idf. The explain output correspondingly shows little depth here, because no fine-grained score calculation happens at all in a filter context. This is not a shortcoming of the Explain API, it accurately reflects that filter contexts deliberately forgo scoring to gain performance.

9. From explain to production: the Profile API as a complement

While the Explain API answers the content question "why this score?", the related _search Profile API answers the performance question "why does this query take so long?". Both tools complement each other: a score problem caused by an inefficient query structure with many expensive should clauses shows up in the explain output as a complex tree and in the profile output as high execution time for exactly those clauses.

In practice, it is advisable not to treat scoring debugging as a one-off action, but as a fixed part of relevance maintenance. A small set of "golden queries" with expected result order, run regularly with explain:true against the current index, catches relevance regressions before users notice them in the form of worse search results.

Tool Answers Typical use
_explain/id Why did this one document get this score? Suspicious ID already known
explain:true in _search How do multiple hits differ in score? Comparing ranking order
Profile API Why does the query take so long? Performance analysis of individual clauses
Aggregation filters Why does a bucket contain these documents? Aggregation debugging without score reference

No single tool delivers the complete picture. Only the combination of the Explain API for the content question about the score and the Profile API for the performance question enables a complete diagnosis when a search ranks incorrectly and is slow at the same time.

Mironsoft

Elasticsearch relevance, scoring analysis and search quality

Ranking that no one can explain?

We analyze your search results with the Explain API, systematically uncover score anomalies and calibrate boosts and function score weights using real query examples.

Score Analysis

Systematically evaluate explain trees for suspicious hits

Relevance Tuning

Adjust boosts and function score weights based on real data

Golden Queries

Build automated relevance tests against regressions

10. Summary

The Explain API turns scoring debugging from a guess into a traceable analysis. The _explain endpoint delivers the complete score calculation for a single document as a nested tree, while explain:true in the _search request provides the same insight for multiple hits at once and allows a direct comparison of two rankings. The BM25 formula with its components boost, idf and tf can be traced step by step in the explain output, including the length normalization that favors short, precise documents over long body text.

Typical score anomalies, such as a tf value dampened by document length or a text score overridden by a function score, can be diagnosed with the Explain API in a targeted way. It is important to know the limits of the tool: aggregations and pure filter contexts lie outside score explanation. Combined with the Profile API for performance questions, the Explain API covers the vast majority of practical scoring debugging cases.

Explain API for scoring debugging, the essentials at a glance

_explain endpoint

Delivers the complete score calculation for a single known document as a tree structure.

explain:true in _search

Explanation for multiple hits at once, ideal for a direct ranking comparison.

Reading BM25

Trace boost, idf and tf with length normalization via dl and avgdl in detail.

Know the limits

Aggregations and filter contexts lie outside score explanation, use the Profile API for performance.

11. FAQ: Explain API for Scoring Debugging

1What does the Explain API do?
Delivers a complete, traceable score calculation for a document as a tree structure.
2What does matched:false mean?
The document does not satisfy the query and therefore does not appear in the search result.
3sum of: vs. max of:?
sum of: adds child values, max of: takes only the highest value, typical for dis_max.
4What do boost, idf, tf show?
boost is a manual multiplier, idf weights rare terms higher, tf accounts for frequency with saturation.
5explain:true or _explain/id?
explain:true to compare multiple hits, _explain/id when the suspicious ID is already known.
6Why does shorter rank higher?
BM25 length normalization via dl and avgdl dampens tf for long documents.
7Explain for aggregations?
No, the Explain API only covers the relevance score, aggregations need separate analysis.
8Why little detail on filters?
Filter contexts deliberately skip fine-grained scoring to gain performance.
9Explain vs. Profile API?
Explain clarifies the score, Profile clarifies execution time. Both tools complement each other.
10explain:true in production?
No, only for debugging and tests, due to increased response size and compute time per query.