Lesson 0002 · Relevance & Scoring
Where _score actually comes from — three intuitions, one formula, two knobs.
Before we start: which two bool clauses produce a _score?
Answer from memory, then check.
(Lesson 0001). — It's must and
should (query context). Everything below is what those clauses compute.
In Lesson 0001 you learned whether a clause is scored. Now: how. Since Elasticsearch 5.0, the default similarity is BM25 (“Best Match 25”), which replaced classic TF/IDF. — Connelly, Practical BM25 (Part 2) Don't memorize the formula cold — understand its three moving parts, and the formula reads itself.
A match on elasticsearch tells you more than a match on the.
Inverse Document Frequency weights each query term by how rare it is across
the collection: the fewer documents contain a term, the higher its weight.
A document mentioning tuning five times is more about tuning than one mentioning
it once — but not five times more. BM25 lets term frequency raise the score while
saturating: each extra occurrence adds less than the last. This is the key
thing BM25 fixed about raw TF/IDF, where frequency grew without limit.
A term in a 4-word title is a stronger signal than the same term buried in a 900-word body. BM25 compares each field's length to the average length for that field and discounts longer-than-average fields.
Put the three together. For a query term t in document D:
Sum that over every query term to get the document's score. The pieces:
f(t,D) — how many times t appears in the field (term frequency).|D| — the field's length in terms; avgdl — average field length across docs.IDF(t) = ln(1 + (N − n + 0.5) / (n + 0.5)), with N = total docs, n = docs containing t.As f(t,D) grows very large, the fraction approaches (k1 + 1) —
a hard ceiling. So one term's contribution can never exceed IDF · (k1+1),
no matter how many times it repeats. That ceiling is saturation.
k1 = 0 → term frequency ignored
entirely (presence/absence only).b = 0 → field length ignored; b = 1 → full normalization (long
fields penalized hardest).Defaults and tuning guidance: Connelly, Practical BM25 (Part 3) · Similarity settings
N and n in IDF are counted per shard, not cluster-wide.
So the same document can score slightly differently depending on which shard it lands on —
very visible with few documents. Force globally-consistent stats with the search type
dfs_query_then_fetch (computes distributed term stats first).
— Practical BM25 (Part 1)
It has no upper bound and no absolute meaning. Scores are only comparable within one
query against one index — never treat a raw _score as a confidence or
compare it across different queries.
All of this runs only in query context. A filter clause, or a
constant_score, computes none of it — that's exactly why filters are cheaper.
From memory — effortful recall is the point. Feedback is immediate.
In BM25, a term appearing in very few documents receives:
ln(1 + (N−n+0.5)/(n+0.5)).The k1 parameter controls:
k1 sets how fast repeated terms saturate. Length normalization is b; IDF has no knob.Setting b = 0 means:
b = 0 the |D|/avgdl term drops out, so field length no longer affects the score.Doubling a term's frequency in a document:
IDF·(k1+1).Two shards can score the same match differently because:
N and n, counted per shard. TF is per-document; boost is query-defined. Use dfs_query_then_fetch for global stats.The _explain API prints the exact BM25 arithmetic for one document. Reuse the
lab_articles index from Lesson 0001
(re-run its seed if you deleted it).
GET /lab_articles/_explain/3
{ "query": { "match": { "title": "tuning" } } } // doc 3: "postgres tuning"
In the response, find the idf and tf sub-explanations. You'll see the
literal inputs: n (docs with "tuning"), N (total docs), freq,
k1=1.2, b=0.75, and the field's avgdl.
GET /lab_articles/_explain/2
{ "query": { "match": { "title": "tuning" } } } // doc 2: "...tuning tuning" (freq=2)
Doc 2's tf component is higher than doc 3's, but not
double — that's saturation in the raw numbers. Its idf is identical to
doc 3's (same term, same collection). And doc 2's field is longer, so length normalization
pushes back a little. Three intuitions, one number.
PUT /lab_bm25_b0
{ "settings": { "index": { "similarity": {
"default": { "type": "BM25", "b": 0 } // length normalization OFF
}}}}
Re-index the same docs into lab_bm25_b0 and compare a query's scores against
lab_articles. Predict first: which docs should rise when long-field
penalty is removed? Then DELETE /lab_articles /lab_bm25_b0 to clean up, and bring
any surprising numbers to your teacher.
Shane Connelly — Practical BM25, Part 2: The BM25 Algorithm and its Variables.
The clearest plain-English derivation of this exact formula. Parts 1 and 3 cover shards and
tuning b/k1 respectively.