BM25 详解:是什么、怎么用、适用场景
BM25 详解 - FutureAI-Zeng
BM25 是搜索引擎中计算查询与文档相关性的经典排序算法,作为 TF-IDF 的升级版,它通过词频饱和、文档长度归一化和查询词频加权解决了词频过高时分数不合理增长的问题。
BM25 详解:是什么、怎么用、适用场景
一、BM25 是什么
BM25 是一种用来计算"查询语句和文档之间相关性"的排序算法。
你在搜索引擎里搜一个关键词,它要从几百万篇文章里找出最相关的排在前面,背后最经典的算法就是 BM25。
你搜:"如何学习编程"
↓
搜索引擎拿着这句话,跟每一篇文章算一个相似度分数
↓
分数高的排前面,分数低的排后面
↓
你看到的搜索结果
BM = Best Matching(最佳匹配),25 是第 25 个版本。
它是 TF-IDF 的升级版,解决了 TF-IDF 的一些问题(比如词频过高时分数不合理增长)。
二、和 TF-IDF 有什么区别
先快速回顾 TF-IDF:
- TF(词频):这个词在文档里出现了多少次 → 出现越多越重要
- IDF(逆文档频率):这个词在多少篇文档里出现 → 越罕见的词越重要
- TF × IDF = 这个词对这篇文档的权重
BM25 比 TF-IDF 多考虑了三件事:
| 改进点 | TF-IDF 的问题 | BM25 的做法 |
|---|---|---|
| 词频饱和 | 词出现 100 次的权重是 10 次的 10 倍?不合理 | 词频达到一定程度后增速变慢,趋近一个上限 |
| 文档长度 | 长文档天然词多,占便宜 | 对长文档做惩罚,公平比较 |
| 查询词频 | 不考虑查询里的词频 | 查询中出现多次的词也有加权 |
三、BM25 公式(用大白话讲)
公式长这样:
score(Q, D) = Σ IDF(qi) × [ f(qi,D) × (k1 + 1) ] / [ f(qi,D) + k1 × (1 - b + b × |D| / avgdl) ]
别怕,拆开看就三部分:
1. IDF 部分(和 TF-IDF 差不多)
IDF(qi) = ln( (N - n(qi) + 0.5) / (n(qi) + 0.5) + 1 )
N= 总文档数n(qi)= 包含这个词的文档数- 词越罕见,IDF 越大,权重越高
2. 词频饱和部分(核心改进)
f(qi,D) × (k1 + 1) / (f(qi,D) + k1)
这部分的意思是:词频越高分数越高,但不会无限涨,会慢慢趋近一个上限。
- 词频 1 → 分数约 0.6
- 词频 3 → 分数约 0.9
- 词频 10 → 分数约 0.97
- 词频 100 → 分数几乎就是 1 了
就像你吃包子,第一个最香,第十个就没感觉了。
k1 是调节参数,通常取 1.2 ~ 2.0,控制饱和速度。
3. 文档长度归一化
k1 × (1 - b + b × |D| / avgdl)
|D|= 当前文档的长度avgdl= 所有文档的平均长度b= 调节参数,通常取 0.75
文档越长,分母越大,分数越低。因为长文档本来就容易包含更多词,不能让它占便宜。
b=0 就是不考虑长度,b=1 就是完全按长度归一化。
四、有什么用
| 场景 | 说明 |
|---|---|
| 搜索引擎 | 网页搜索、站内搜索,给搜索结果排序 |
| 推荐系统 | 找相似内容、相似商品 |
| 问答系统 | 从知识库中找最相关的答案 |
| 文本匹配 | 查重、相似文本检测 |
| RAG 检索 | 大模型 RAG 中,从知识库中召回相关片段(和向量检索混合使用) |
| 文档分类聚类 | 计算文档相似度 |
基本上只要是"给我一段话,从一堆文本里找出最相关的"这类问题,都能用 BM25。
五、什么时候用 BM25
适合用 BM25 的
- 关键词精确匹配:用户搜的是具体的词,比如产品型号、人名
- 专业领域搜索:法律、医疗、技术文档,用词准确
- 小数据集:几千到几十万篇文档,BM25 又快又准
- 需要可解释性:为什么这篇排前面?因为它包含了"xxx"这个关键词
- 冷启动:不需要训练数据,有文档就能用
不适合的(或者说需要向量检索的)
- 语义匹配:用户搜"怎么减肥",要能匹配到"瘦身方法"(不同的词,同样的意思)
- 模糊查询:表达方式差异大
- 多语言混合:中英混合搜索
现在工业界的主流做法是 BM25 + 向量检索 混合召回,BM25 抓精确匹配,向量抓语义匹配,效果最好。
六、怎么用:代码示例
Python 版本(rank_bm25 库)
最常用的库,简单直接。
pip install rank_bm25 jieba
示例:对文章做搜索
import jieba
from rank_bm25 import BM25Okapi
# 1. 准备文档
documents = [
"Python 是一种简单易学的编程语言",
"Java 是企业级开发最常用的语言",
"机器学习是人工智能的核心技术",
"Python 广泛应用于数据分析和机器学习",
"前端开发主要使用 HTML、CSS 和 JavaScript",
]
# 2. 分词(中文要先分词,英文直接按空格切)
corpus = []
for doc in documents:
tokens = jieba.lcut(doc) # 中文分词
corpus.append(tokens)
# 3. 构建 BM25 索引
bm25 = BM25Okapi(corpus)
# 4. 查询
query = "Python 机器学习"
query_tokens = jieba.lcut(query)
# 获取分数
scores = bm25.get_scores(query_tokens)
print("各文档分数:", scores)
# 获取 Top 3 结果
top_docs = bm25.get_top_n(query_tokens, documents, n=3)
print("\n最相关的 3 篇文档:")
for i, doc in enumerate(top_docs, 1):
print(f"{i}. {doc}")
输出:
各文档分数: [0. 0. 0.47000363 1.67961712 0. ]
最相关的 3 篇文档:
1. Python 广泛应用于数据分析和机器学习
2. 机器学习是人工智能的核心技术
3. Python 是一种简单易学的编程语言
完美符合预期:第 4 篇同时有 "Python" 和 "机器学习",分数最高。
调整参数
bm25 = BM25Okapi(
corpus,
k1=1.5, # 默认 1.5,越大词频权重越高
b=0.75 # 默认 0.75,越大文档长度惩罚越重
)
k1小 → 词频饱和快,出现 2 次和 20 次差不多k1大 → 词频饱和慢,出现次数越多权重越高b = 0→ 不考虑文档长度b = 1→ 完全按文档长度归一化
Elasticsearch 中使用
ES 里默认的相似度算法就是 BM25,不用你自己实现。
// 创建索引时指定 BM25(其实默认就是,可以不写)
PUT /my_index
{
"settings": {
"similarity": {
"my_bm25": {
"type": "BM25",
"k1": 1.2,
"b": 0.75
}
}
},
"mappings": {
"properties": {
"title": {
"type": "text",
"similarity": "my_bm25"
},
"content": {
"type": "text",
"similarity": "my_bm25"
}
}
}
}
正常查询就行,ES 自动用 BM25 算分排序:
POST /my_index/_search
{
"query": {
"match": {
"content": "Python 机器学习"
}
}
}
七、一个完整的 RAG 检索示例
现在最火的用法是 RAG(检索增强生成)里做召回。
from rank_bm25 import BM25Okapi
import jieba
# 知识库
knowledge_base = [
{"id": 1, "content": "BM25 是一种基于词频的文本检索算法"},
{"id": 2, "content": "向量检索通过语义相似度匹配文档"},
{"id": 3, "content": "RAG 系统结合了检索和生成两部分"},
{"id": 4, "content": "混合检索通常指 BM25 和向量检索结合使用"},
{"id": 5, "content": "LangChain 是一个大模型应用开发框架"},
]
# 构建 BM25 索引
corpus = [jieba.lcut(doc["content"]) for doc in knowledge_base]
bm25 = BM25Okapi(corpus)
def bm25_search(query, top_k=3):
query_tokens = jieba.lcut(query)
scores = bm25.get_scores(query_tokens)
# 按分数排序,返回 top_k
ranked = sorted(
zip(knowledge_base, scores),
key=lambda x: x[1],
reverse=True
)
return [(doc, score) for doc, score in ranked[:top_k] if score > 0]
# 用户提问
query = "混合检索是什么意思"
results = bm25_search(query)
print(f"用户问题: {query}\n")
print("BM25 召回结果:")
for doc, score in results:
print(f" 分数={score:.4f} 内容: {doc['content']}")
输出:
用户问题: 混合检索是什么意思
BM25 召回结果:
分数=3.0150 内容: 混合检索通常指 BM25 和向量检索结合使用
分数=0.7714 内容: 向量检索通过语义相似度匹配文档
分数=0.7714 内容: BM25 是一种基于词频的文本检索算法
然后把召回的文档丢给大模型,让它基于这些内容回答用户的问题,这就是 RAG。
八、优缺点
优点
- 快:倒排索引,百万级文档毫秒级返回
- 简单:原理易懂,调参就两个(k1、b)
- 效果好:关键词匹配场景下非常准
- 可解释:知道为什么这篇排前面
- 无需训练:有文档就能用,零成本冷启动
缺点
- 不理解语义:"电脑"和"计算机"匹配不上
- 依赖分词:中文分词效果直接影响结果
- 同义词问题:不同的词表达同样的意思,BM25 不知道
- 拼写容错差:打错字就搜不到
九、调参建议
| 场景 | k1 建议 | b 建议 | 原因 |
|---|---|---|---|
| 普通文本搜索 | 1.2 ~ 1.5 | 0.75 | 经典默认值 |
| 标题搜索 | 1.2 ~ 2.0 | 0.5 | 标题短,词频差异大,减轻长度惩罚 |
| 长文档搜索 | 1.2 | 0.75 ~ 0.9 | 长文档多,加强长度归一化 |
| 短文本(对话、query) | 2.0+ | 0.5 | 文本短,词频信息更珍贵 |
调参没有标准答案,最好用评估集(比如标注好的 query-文档对)测一下,看哪个参数的 NDCG、Recall 最高。
一句话总结:BM25 是关键词搜索领域的经典算法,通过 IDF + 饱和词频 + 文档长度归一化给文档打分排序,快、准、好解释,是搜索引擎和 RAG 召回的标配。
来源:博客园 · cnblogs.com