BM25 — TF saturation × IDF × length norm
BM25 sums per-term IDF × saturating-TF / length-norm — rare terms outweigh common ones, the 100th match barely beats the 10th, and long docs are penalized.
Phase 1 of every search asked each shard to RANK its local matches. The function each shard runs is BM25 — and its three constants (k1, b, IDF) decide whose match wins.
Scene 09
BM25 — TF saturation × IDF × length-norm
- Watch
- Try it
- Predict
- Capture
Score the query 'distributed systems' against the five-book catalog. IDF is computed across the corpus (rare terms win); TF saturates so the 100th match barely beats the 10th; length-norm pushes long docs down. Books 1, 4, and 5 don't contain either query term — they sit at score 0.
Highlighted lines are the ones running in the diagram right now.
def score(doc, query):s = 0.0for term in query.terms:tf = doc.term_freq(term)if tf == 0: continuedf = corpus.doc_freq(term)s += idf(term, N=corpus.size, df=df)* saturating_tf(tf, k1, b,doc_len=doc.length, avgdl=corpus.avgdl)return s
def idf(term, N, df):return ln((N - df + 0.5) / (df + 0.5) + 1)
def saturating_tf(tf, k1, b, doc_len, avgdl):len_norm = (1 - b) + b * (doc_len / avgdl)return tf * (k1 + 1) / (tf + k1 * len_norm)
Where this sits in Build a distributed search engine (Elasticsearch / OpenSearch style)
Scene 08 of 12. BM25 ranks each match by IDF × saturating-TF / length-norm — and the saturation knob k1 is exactly what stops a keyword-stuffed doc from winning the page.
Up next. BM25 needs IDF, and IDF asks 'how many docs in the corpus contain this term?'. Each shard knows only its own corpus — so the same document scores differently depending on where it lives.
All 12 scenes in Build a distributed search engine (Elasticsearch / OpenSearch style) · Every curriculum