GO TO A SECTION

↑ ↓ to navigate · Enter to openExplore all sections →
FDEInterviews logo
← 🤖 Retrieval & Agents
Foundational

TF-IDF and BM25

BM25 is the lexical scoring function that still beats a lot of fancier setups out of the box. It scores a document by how often the query's terms appear (term frequency), discounted by how common those terms are across the corpus (inverse document frequency), with two refinements: frequency saturation so a term repeated 50 times does not score 50x, and length normalization so long documents do not win by sheer size. It is the backbone of the lexical half of hybrid search.

WATCH THE EXPLAINER3:09
Transcript

Your vector search underperforms on internal docs. Why? The answer starts with a scoring function that needs no training, no embeddings and no GPU, and still beats a lot of fancier setups out of the box. It's called BM25.

It starts from TF-IDF, which is two competing forces. Term frequency: a document that mentions idempotency five times is probably more about idempotency than one that mentions it once. Inverse document frequency: the word the is in everything, idempotency is in a handful, so matching the rare term counts for far more. And TF-IDF multiplies the two.

Put numbers on it. A corpus of a million documents, and the query idempotency key. Idempotency appears in a thousand of them, so its IDF is the log of a thousand, about 6.9. Key appears in two hundred thousand, which gives the log of five, about 1.6, so in this query the rare term weighs over four times as much as the common one.

Raw TF is linear, which is its first flaw. Fifty mentions score fifty times one mention. Absurd. BM25 runs term frequency through a saturating curve. One occurrence of idempotency scores 6.91, the second adds about three points, the tenth about 0.4, and the fiftieth almost nothing. No amount of repetition passes the ceiling of 17.3.

The second flaw is length, because long documents contain more words and pile up matches by accident, so BM25 penalizes any document longer than the corpus average, with a dial called b. Take the same three mentions at four lengths, against an average of three hundred words. Three mentions in a document four times the average length count about as much as one mention in an average one. And a short FAQ entry gets a boost.

Here's the whole thing for one term. The IDF, times a saturated, length-normalized term frequency, with k1 at one and a half and b at three quarters. Run it for a document of exactly average length, with idempotency three times and key twice, and the score comes out at 13.81. Nearly all of it is idempotency.

So where does it win? Exact part numbers, function names, error codes and rare jargon. Search for an error code, and BM25 returns the doc containing that exact string, while a vector model maps it into a fuzzy neighborhood. Its blind spot is the mirror image. How do I cancel my plan won't match a passage titled ending your subscription, because they share no terms. So modern retrieval runs both side by side, and fuses the results.

So don't reach for an embedding model first. Start with BM25 as the baseline, measure recall, and add vectors where paraphrase actually hurts. Every number here is on fdeinterviews.com.

TL;DR: BM25 ranks documents by counting query-term matches (term frequency), weighting rarer terms more (inverse document frequency), then applying two corrections that make it work in practice: it saturates repeated terms so the 20th occurrence barely helps, and it normalizes for document length so long docs do not win automatically. It needs no training, runs in milliseconds, and is the strong lexical baseline that hybrid search pairs with vectors.

Start with TF-IDF

The core intuition is two competing forces.

Term frequency (TF): a document that mentions "idempotency" five times is probably more about idempotency than one that mentions it once. More matches, higher score.

Inverse document frequency (IDF): but if a word appears in almost every document, matching it tells you nothing. "The" is in everything; "idempotency" is in a handful. IDF weights each term by how rare it is across the corpus, so matching a rare term counts far more than matching a common one. Roughly, idf(t) = log(N / df(t)), where N is the number of documents and df(t) is how many contain term t.

Classic TF-IDF multiplies these: tf(t, d) * idf(t), summed over query terms. It works, but it has two flaws that BM25 fixes.

What BM25 adds

How BM25 scores one document 1 A query term one at a time 2 Count it term frequency in this doc 3 Saturate the count the 20th match barely helps 4 Normalize for length long docs stop winning free 5 Weight by rarity inverse document frequency 6 Sum across terms that is the score 7 Rank highest score first Raw term frequency is linear, so fifty mentions would score fifty times one occurrence. A document that says GPU fifty times is not fifty times more about GPUs. Without it a long document accumulates matches simply by being long, and the corpus starts ranking by size rather than by relevance. idf(t) = log( N / df(t) ) Matching a rare term counts far more than matching a common one, which is why "the" contributes nothing. No training, answers in milliseconds, and unbeatable on exact identifiers and rare tokens. This is the leg hybrid search pairs with vectors.

The spine above scores a single document end to end, with saturation and length normalization drawn as the two steps that separate BM25 from raw counting.

BM25 (the "Okapi" weighting scheme) keeps TF and IDF and corrects raw TF in two ways.

Saturation. Raw TF is linear: a term appearing 50 times scores 50x a single occurrence, which is absurd. A document that says "GPU" 50 times is not 50 times more relevant than one that says it twice. BM25 runs TF through a saturating function controlled by k1 (typically 1.2 to 2.0), so the score rises fast for the first few occurrences then flattens.

Computed with the worked example's constants below (k1 = 1.5, b = 0.75, a document of average length, the term "idempotency" with idf = 6.91), the contribution of that one term as it repeats:

OccurrencesTerm contributionRaw TF-IDF would give
16.916.91
29.8713.82
311.5120.73
1015.0269.1
5016.77345.4

The second occurrence adds about 3 points, the tenth about 0.4, and the fiftieth almost nothing. The ceiling is idf * (k1 + 1), here 17.3, and no amount of repetition passes it. Raw TF-IDF at fifty occurrences scores twenty times higher than BM25, which is the "GPU fifty times" document winning on volume.

Length normalization. Long documents contain more words, so they accidentally accumulate more matches. BM25 penalizes documents longer than the corpus average, controlled by b (typically 0.75, where b=0 disables it and b=1 applies it fully).

The same term appearing three times, computed at four document lengths against an average of 300 words:

Document lengthContribution
150 words13.16
300 words (average)11.51
600 words9.21
1,200 words6.58

Three mentions in a document four times the average length count about as much as one mention in an average one. The effect runs both ways: a document shorter than average gets a boost, which is why a two-line FAQ entry that names the term can outrank a long chapter that also names it. That is usually what you want, and b is the dial when it is not.

The full score for a document d against query Q:

Code
score(d, Q) = sum over t in Q of:
    idf(t) * ( f(t,d) * (k1 + 1) ) / ( f(t,d) + k1 * (1 - b + b * |d| / avgdl) )

where f(t,d) is the term frequency in d, |d| is the document length in words, and avgdl is the average document length in the corpus.

The idf(t) used on this page is the plain TF-IDF form, log(N / df(t)), which is enough to see the mechanism. Lucene and Elasticsearch, where most people meet BM25, use log(1 + (N - df + 0.5) / (df + 0.5)), a smoothed version that stays positive even for a term in nearly every document. The rankings it produces follow the same intuition; the numbers differ slightly.

A short worked example

Corpus of N = 1,000,000 documents, avgdl = 300 words. Query: idempotency key. Constants k1 = 1.5, b = 0.75.

  • "idempotency" appears in df = 1,000 docs, so idf ≈ log(1e6 / 1e3) = log(1000) ≈ 6.9.
  • "key" appears in df = 200,000 docs, so idf ≈ log(1e6 / 2e5) = log(5) ≈ 1.6.

A candidate document of length |d| = 300 (exactly average, so the length factor is 1) with "idempotency" appearing 3 times and "key" twice:

python
import math

def bm25_term(f, df, N, dl, avgdl, k1=1.5, b=0.75):
    idf = math.log(N / df)
    norm = f * (k1 + 1) / (f + k1 * (1 - b + b * dl / avgdl))
    return idf * norm

N, avgdl, dl = 1_000_000, 300, 300
score  = bm25_term(f=3, df=1_000,   N=N, dl=dl, avgdl=avgdl)   # "idempotency"
score += bm25_term(f=2, df=200_000, N=N, dl=dl, avgdl=avgdl)   # "key"
print(round(score, 2))   # ~13.81

The rare term "idempotency" carries almost all the weight (its IDF is ~4x higher), and its third occurrence adds far less than its first thanks to saturation. That is BM25 doing exactly what you want: reward rare, specific matches, ignore filler, and refuse to be gamed by repetition.

Why it is still a strong baseline

BM25 needs no training, no embeddings, no GPU, and no corpus-specific tuning beyond two constants. It runs in milliseconds over millions of documents via an inverted index, and it nails the cases dense vectors fumble: exact part numbers, function names, error codes, SKUs, legal clause numbers, and rare jargon. When someone searches Error 0x80070643, BM25 returns the doc containing that exact string; a vector model maps it into a fuzzy neighborhood and may return a different error that "feels" similar.

Its blind spot is the mirror image: it scores on shared words, so it misses paraphrase. "How do I cancel my plan" will not match a passage titled "ending your subscription" if they share no terms. That is exactly why modern retrieval runs BM25 and dense vector search side by side and fuses the results: each catches what the other drops. See hybrid-search and retrieval-augmented-generation.

Why interviewers probe this

Reaching for an embedding model first on every retrieval problem is a tell that someone has read one tutorial and shipped nothing. The strong move is to start with BM25 as a baseline, measure recall, and only add a vector-databases layer where paraphrase actually hurts. The follow-up they hold back: "your vector search underperforms on internal docs, why?" The answer names BM25's strength on the codes and acronyms that dense models blur, and proposes hybrid rather than swapping one for the other.

Common misconceptions

  • "BM25 is obsolete now that we have embeddings." It is the lexical half of hybrid search and often beats vectors alone on identifier-heavy corpora.
  • "More matches always means a higher score." Saturation flattens repeated terms, and IDF can make one rare match outweigh many common ones.
  • "It ignores document length." The b parameter normalizes for length so long documents do not win by volume.
  • "TF-IDF and BM25 are the same." BM25 adds frequency saturation and length normalization, which is why it works in production where raw TF-IDF struggles.

Key takeaways

  • BM25 = term frequency, weighted by inverse document frequency, with saturation (k1) and length normalization (b).
  • Rare terms dominate the score via IDF; repeated terms saturate so repetition cannot game the ranking.
  • No training, no GPU, milliseconds over millions of docs, and unbeatable on exact codes, IDs, and rare jargon.
  • Its weakness is paraphrase, which is why it pairs with dense vectors in hybrid search rather than being replaced by them.

Related concepts

Practise this in questions