TF-IDF › BM25 as a TF-IDF Extension
Language AI HandbookAll chapters TF-IDF › BM25 as a TF-IDF ExtensionOne section · Sections in this chapter9 / 13 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
TF-IDF is powerful, but it has a known weakness: term frequency saturation. In the basic formula, a document where "neural" appears 50 times scores 50 times higher on that term than a document where it appears once, even though the information gain from each additional repetition diminishes rapidly. If a document mentions "deep learning" twice in a focused 200-word post, and another document mentions it 40 times in a rambling 5,000-word discussion, TF-IDF would heavily favor the longer document even though the shorter one might be more focused and relevant.
BM25 (Best Match 25), developed in the 1990s by Stephen Robertson and colleagues at City University London, addresses this through a more principled probabilistic framework. It builds on TF-IDF's core intuition but adds two structural improvements: a saturation function that limits the TF contribution, and explicit length normalization that penalizes long documents proportionally to their length.
How BM25 Differs from TF-IDF
BM25 replaces the simple multiplication with a more complex scoring function:
where:
- : raw frequency of term in document
- : saturation parameter, typically 1.2 to 2.0, controlling how quickly the TF contribution saturates. Lower values saturate faster.
- : length normalization parameter, typically 0.75, controlling how much document length affects scoring. Setting disables length normalization entirely; applies full normalization proportional to document length.
- : number of tokens in document
- : average document length across the corpus
- : IDF component, often using the BM25-specific formula
The saturation effect works through the denominator. As grows, the TF contribution approaches an asymptotic ceiling. Specifically, in the limit where and holding the length normalization term constant at some value :
This means the TF contribution can never exceed regardless of how many times the term appears. With , the ceiling is 2.5. A term appearing 3 times and a term appearing 100 times both contribute at most 2.5 to the score (and the difference between them is small). This prevents keyword stuffing from artificially inflating scores.
The length normalization term is 1 when the document is exactly average length, more than 1 for longer documents, and less than 1 for shorter documents. Adding this to the denominator means longer documents have a larger denominator, which reduces their TF contribution proportionally. A document twice the average length gets its TF contributions halved (approximately), correcting for the inflated raw counts that longer documents naturally accumulate.
TF Saturation Visualization
The saturation effect is the most important conceptual difference between TF-IDF and BM25.

The saturation curves make the key difference viscerally clear. Under TF-IDF, a term appearing 30 times scores 30 times higher than a term appearing once. Under BM25 with , the ratio is only about 1.9:1. This is the saturation property in action: the first occurrence of a term provides the most information, and each subsequent occurrence provides less. BM25's curve shape captures this diminishing-returns intuition directly in the formula.
Implementing BM25
class BM25:
"""Minimal BM25 implementation for illustration."""
def __init__(self, k1=1.5, b=0.75):
self.k1 = k1
self.b = b
def fit(self, tokenized_corpus):
self.corpus = tokenized_corpus
self.N = len(tokenized_corpus)
self.avgdl = np.mean([len(doc) for doc in tokenized_corpus])
# Document frequency
self.df = Counter()
for doc in tokenized_corpus:
self.df.update(set(doc))
# BM25 IDF
self.idf = {}
for term, df_t in self.df.items():
self.idf[term] = np.log((self.N - df_t + 0.5) / (df_t + 0.5) + 1)
return self
def score(self, query_tokens, doc_tokens):
"""BM25 score for a query against a single document."""
tf = Counter(doc_tokens)
dl = len(doc_tokens)
score = 0.0
for term in query_tokens:
if term not in self.idf:
continue
f = tf.get(term, 0)
idf_t = self.idf[term]
numerator = f * (self.k1 + 1)
denominator = f + self.k1 * (1 - self.b + self.b * dl / self.avgdl)
score += idf_t * (numerator / denominator)
return score
def rank(self, query_tokens):
"""Rank all documents in the corpus for a query."""
scores = [self.score(query_tokens, doc) for doc in self.corpus]
return sorted(enumerate(scores), key=lambda x: x[1], reverse=True)
bm25 = BM25(k1=1.5, b=0.75)
bm25.fit(tokenized_docs)Query: 'deep neural networks for image tasks' BM25 ranking: 1. Doc 2 (score=5.0219): Deep learning uses neural networks. Neural networks lea... 2. Doc 4 (score=2.3763): Computer vision analyzes images. Image recognition uses... 3. Doc 3 (score=1.2928): Natural language processing extracts meaning from text.... 4. Doc 1 (score=0.0000): Machine learning algorithms learn patterns from data. L... 5. Doc 5 (score=0.0000): Reinforcement learning agents learn through rewards. Le...
BM25 is the default retrieval algorithm in Elasticsearch, OpenSearch, Apache Solr, and many other production search systems. For most English retrieval tasks, the default parameters (, ) work well without tuning. The next chapter covers BM25 in full depth, including its probabilistic derivation and the systematic approach to tuning and for specific corpora.