BM25 详解

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 库)

最常用的库,简单直接。

bash 复制代码
pip install rank_bm25 jieba

示例:对文章做搜索

python 复制代码
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" 和 "机器学习",分数最高。

调整参数

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,不用你自己实现。

json 复制代码
// 创建索引时指定 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 算分排序:

json 复制代码
POST /my_index/_search
{
  "query": {
    "match": {
      "content": "Python 机器学习"
    }
  }
}

七、一个完整的 RAG 检索示例

现在最火的用法是 RAG(检索增强生成)里做召回。

python 复制代码
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 召回的标配。

相关推荐
神也畏惧菠萝2 小时前
Coze 工作流实战 Day05:RAG 检索机制、多模态 AIGC 与智能体编排
aigc·音频处理·rag·视频生成·智能体·coze·图形处理
星野云联AIoT技术洞察7 小时前
设备手册离线 RAG:先管住版本、权限和引文,再谈本地大模型
llama·rag·设备告警·rag知识库·设备运维·设备离线·设备手册
张张123y1 天前
后端第十二课:Agent 开发(让大模型从“会答“升级到“会做“)
前端·大模型·llm·agent·rag
云水初1 天前
【agent篇】RAG Agent避坑指南(附高级RAG Agent汇总)
开发语言·笔记·python·学习·langchain·agent·rag
张张123y1 天前
后端第十一课:RAG 落地实战(从 0 到 1 接上私有知识库,并设计一个好用的 RAG)
开发语言·人工智能·python·机器学习·fastapi·rag
今年下半年2 天前
AI智能体项目实战记录(06):RAG项目实战之文件上传及文档读取解析
人工智能·rag·文档解析
for_ever_love__3 天前
知识图谱构建流水线实战:实体关系抽取的提示词、质量抽检与增量维护
知识图谱·实体·文档·构建·rag·关系·抽取
for_ever_love__3 天前
Map-Reduce 式全文问答:当文档超出上下文时的分而治之
人工智能·map·汇总·分块·rag·reduce·长文
code2cat3 天前
【随笔】RAG重排怎样挑证据:先扩大候选,再把相关片段排到前面
人工智能·python·大语言模型·rag