Relevance and scoring explainer
See why documents rank where they do: BM25 term by term like Elasticsearch's _explain, with k1 and b sliders that re-rank live, classic TF-IDF side by side, multi-field strategies and boosts, function_score (field_value_factor, decay curves, a recency script), and hybrid lexical + vector ranking with reciprocal rank fusion or a normalised linear blend.
Added
Works with
- System design interviews Interview prepwhere scoring sits in a web-scale search engine — cheap retrieval, a heavier ranker, then re-ranking — as an interview design
How to use it
How to use it
Choose a document set — a product catalogue, articles about search, or short vs long fields (made to show length normalization and saturation) — or edit your own as JSON. Type a query; drag k1 and b. Everything re-ranks as you go.
BM25 explained. Pick a field and OR/AND. The query terms appear with their document frequency
and idf; the table ranks the matches with their BM25 score, how far each moved from the defaults
(k1 = 1.2, b = 0.75), and its rank and score under classic TF-IDF. Click a row for the per-term
breakdown (tf, df/N, idf, dl, avgdl, tf/(tf + K), contribution) and the explanation tree in the
shape _explain returns.
Multi-field. Tick fields and set boosts (title^3), choose best_fields, most_fields or
cross_fields, a tie_breaker, and an optional phrase boost (a should match_phrase). The
generated query, its Lucene form and the ranking under all three types are shown side by side.
Function score. Combine the text score with field_value_factor (any modifier: log1p, sqrt,
ln2p…), a gauss/exp/linear decay on a number or date (origin, scale, offset, decay), and a
recency script_score (half / (half + age in days)). The charts draw the decay shapes with every
document placed on the curve, and the modifier curve; the table shows text score, function value,
final score and rank change. "Now" is fixed at 2026-10-15 so results are reproducible.
Hybrid (RRF). The lexical query (BM25) and a vector query (toy 8-concept embeddings, so
"earbuds for the gym" finds sports earbuds without the word) produce two ranked lists, fused by
reciprocal rank fusion (Σ 1/(k + rank), Cormack, Clarke and Büttcher, SIGIR 2009) or by a
linear blend of min–max normalised scores. Adjust k, the window and α; the equivalent
Elasticsearch retriever JSON is shown.
Curves. Term-frequency saturation for several k1 against classic √tf; the length-normalization factor against dl/avgdl for several b; and BM25’s idf against classic idf.
The formulas
BM25 (Lucene ≥ 8) |
score = boost · idf · tf / (tf + k1 · (1 − b + b · dl / avgdl)); idf = ln(1 + (N − n + 0.5) / (n + 0.5)). Lucene’s |
BM25 (Elasticsearch) |
Elasticsearch 7 and 8 use |
Field length |
dl is stored in one byte per document (Lucene |
Classic TF-IDF |
Lucene |
Decay |
gauss: exp(−d² / 2σ²) with σ² = −scale² / (2 ln decay); exp: e^(λd), λ = ln(decay)/scale; linear: max(0, (s − d)/s), s = scale/(1 − decay); d = max(0, |value − origin| − offset) — the Elasticsearch function_score reference. |
RRF |
score(d) = Σ 1/(k + rank_i(d)), ranks from 1, k = rank_constant (default 60). |
Limits
-
One shard, no deletions: document frequencies are exact. In a real cluster idf is per shard unless you use
dfs_query_then_fetch, which can reorder close scores. -
Scores are computed in float32 like Lucene; sums can differ from Elasticsearch in the last digit.
-
cross_fieldsblends document frequencies as the largest across the fields (an approximation of Elasticsearch’sBlendedTermQuery). -
The vectors are a hand-made 8-dimension toy for teaching, not a language model; real hybrid search uses a trained embedding model and an approximate (HNSW) index.
-
Only the script forms shown are supported (a small Painless subset).
Linking to an example
Links can be written by hand. Add the tab after #: explain, fields, functions, hybrid or curves.
| Parameter | Meaning | Example |
|---|---|---|
|
Document set: |
|
|
The query |
|
|
The field on the BM25 tab |
|
|
BM25 k1 (0–3) |
|
|
BM25 b (0–1) |
|
|
Score variant: |
|
|
Operator on the BM25 tab: |
|
|
One of the presets below; parameters given with it override it |
|
example= |
Shows |
|---|---|
|
Term-frequency saturation: "Lucene" five times barely beats once (open) |
|
b = 1: full length normalization, the one-word title wins (open) |
|
b = 0: length ignored (open) |
|
k1 = 3: repeated terms keep paying off for longer (open) |
|
best_fields vs cross_fields for a query spread over title and body (open) |
|
A should match_phrase adds a bonus when the words are adjacent (open) |
|
field_value_factor: log1p(reviews_count) multiplies the text score (open) |
|
A gauss decay on the publish date: newer articles rise (open) |
|
Lexical BM25 + toy vectors fused with RRF, k = 60 (open) |
|
The same with a min–max linear combination, α = 0.3 (open) |
Feedback is a gift. Something to improve, a feature to add, or a bug? Tell me.
Runs entirely in your browser; nothing you type leaves this page.