介绍
BM25(Okapi BM25)是工业界最主流的 全文检索相关性打分算法,核心是对 TF‑IDF 做词频饱和+长度归一化修正,让短文档更占优、高频词不过度权重、长文档不天然占便宜。下面从原理、公式、参数、流程、优缺点、与 TF‑IDF 对比、工程实现、调优与应用场景,一次性讲透。
BM25 是什么
- 全称:Okapi BM25(Best Matching 25),1970s 由 Robertson 等人提出,因是第25版且效果最佳而得名。
- 定位:概率检索模型下的词袋(Bag‑of‑Words)排序算法,只看词是否出现、不看语序。
- 用途:计算查询 Q 与文档 D 的相关性分数,分数越高排越前;是 Elasticsearch、Solr、OpenSearch 的默认相关性算法。
应用场景
- 传统搜索引擎:ES、Solr、OpenSearch 默认排序
- RAG 检索:大模型知识库检索(先 BM25 召回,再重排)
- 站内搜索:电商商品、新闻、博客、知识库搜索
- 短文本匹配:标题、摘要、关键词检索
- 混合检索:BM25(关键词)+ 向量检索(语义),兼顾精准与语义
公式
全文检索完整流程
建索引阶段
文档分词 → 词项列表
统计:总文档数 N、各词 n(q_i)、文档长度 |D|、平均长度 {avgdl}
- 计算每个词的 IDF 并存入索引
检索阶段
查询分词 → (q1 \ldots qn)
对每个查询词:
- 查倒排索引,拿到包含该词的文档列表
- 计算该词在文档中的 TF
- 代入公式计算单词语分
- 所有词分数求和 → 文档最终 BM25 分
- 按分数降序排序,返回 Top‑K
BM25 vs TF‑IDF
| 维度 | TF‑IDF | BM25 |
|---|---|---|
| 词频 TF | 线性增长,无上限 | 饱和增长,有天花板((k_1+1)) |
| 文档长度 | 长文档天然占优 | 归一化,短文档更占优 |
| IDF 计算 | 无平滑,易无穷大 | +0.5 平滑,稳定有界 |
| 排序效果 | 易被关键词堆砌作弊 | 抗作弊,相关性更准 |
| 工业应用 | 早期搜索引擎 | ES/Solr 默认,RAG 检索首选 |
优缺点总结
优点
- 相关性精准:解决 TF‑IDF 两大缺陷,排序更符合人类直觉
- 轻量高效:纯统计、无训练、可解释、速度快(适合海量数据)
- 鲁棒性强:IDF 平滑、词频饱和,抗噪声、抗作弊
- 易调优:仅 2 个核心参数,场景适配简单
缺点
- 词袋假设:不考虑语序、语义、上下文(如“我吃苹果”≠“苹果吃我”)
- 无法理解语义:同义词、近义词、多义词处理弱(需结合词向量/大模型)
- 依赖分词质量:中文需精准分词,否则效果差
工程实现
Python 极简版
import math
from collections import defaultdict
# 1. 计算 IDF
def compute_idf(documents):
N = len(documents)
idf = defaultdict(int)
for doc in documents:
unique_words = set(doc)
for word in unique_words:
idf[word] += 1
# BM25 平滑 IDF
for word in idf:
idf[word] = math.log((N - idf[word] + 0.5) / (idf[word] + 0.5))
return idf
# 2. 计算单文档 BM25 分数
def bm25_score(query, doc, idf, k1=1.2, b=0.75, avgdl=100):
doc_len = len(doc)
score = 0.0
for word in query:
if word not in doc:
continue
tf = doc.count(word)
idf_val = idf[word]
# BM25 公式
numerator = tf * (k1 + 1)
denominator = tf + k1 * (1 - b + b * doc_len / avgdl)
score += idf_val * numerator / denominator
return score
# 3. 批量检索排序
def bm25_search(query, documents, top_k=3):
idf = compute_idf(documents)
avgdl = sum(len(doc) for doc in documents) / len(documents)
# 计算所有文档分数
scores = []
for i, doc in enumerate(documents):
s = bm25_score(query, doc, idf, avgdl=avgdl)
scores.append((i, s))
# 排序取 Top-K
scores.sort(key=lambda x: x[1], reverse=True)
return [documents[i] for i, s in scores[:top_k]]
进阶:BM25 与向量检索结合(RAG 最佳实践)
- BM25 强项:关键词精准匹配、速度快、可解释
- 向量检索强项:语义理解、同义词、上下文
- 最佳组合:BM25 召回 Top‑50 + 向量重排 Top‑10,兼顾速度、精准、语义

