掌握BM25算法,能够显著提升信息检索与问答系统的性能,是构建高效RAG框架的关键技术之一。
核心内容:
1. BM25算法原理及其在RAG框架中的实际应用场景
2. 使用Python实现BM25算法的完整操作步骤
3. 借助rank_bm25库进行文档与查询之间的相关性计算与排序

BM25算法被众多RAG开源框架及向量库的向量查询功能所集成或引用。本文将系统介绍BM25算法的核心原理,并通过一个可直接运行的代码示例,演示如何实际运用BM25算法完成文档检索任务。
BM25(Best Match 25)是一种基于概率模型的信息检索算法,广泛应用于搜索引擎与问答系统。在RAG(检索增强生成)框架中,BM25负责从大规模文档库中快速找出与用户查询最相关的文档。它的核心机制依赖于词频(TF)和逆文档频率(IDF),并引入文档长度归一化因子,能够有效应对不同篇幅的文档——这一特性在真实业务场景中尤为重要。
BM25的基本使用
下面通过一个完整的Python示例,演示BM25算法的实际应用。我们将采用rank_bm25库,这是目前常用的BM25实现工具,能够方便地计算文档与查询之间的相关性得分。
安装依赖库
pip install rank-bm25编写测试代码
代码的核心逻辑如下:
(1)对每个文档和用户查询分别进行分词处理
(2)使用分词后的文档列表初始化BM25模型
(3)计算查询(query)与每个文档分词列表的相似度得分
(4)从文档列表中找出得分最高的文档,即为与查询最匹配的结果。
具体的代码实现如下:
from rank_bm25 import BM25Okapi
import jieba # 用于中文分词
# 示例文档库
documents = [
"猫是一种可爱的动物,喜欢抓老鼠。",
"狗是人类的好朋友,喜欢追猫。",
"老鼠是一种小型啮齿动物,猫喜欢抓它们。"
]
print(documents)
# 用户查询
query = "猫喜欢抓什么动物?"
print("问题: "+ query)
# 对文档和查询进行分词
def tokenize(text):
return list(jieba.cut(text))
# 分词后的文档库
tokenized_documents = [tokenize(doc) for doc in documents]
# 分词后的查询
tokenized_query = tokenize(query)
# 初始化BM25模型
bm25 = BM25Okapi(tokenized_documents)
# 计算查询与文档的相关性得分
doc_scores = bm25.get_scores(tokenized_query)
# 打印每个文档的得分
for i, score in enumerate(doc_scores):
print(f"文档{i+1} 的 BM25 得分: {score}")
# 找到最相关的文档
most_relevant_doc_index = doc_scores.argmax()
print(f"n最相关的文档是文档{most_relevant_doc_index + 1}: {documents[most_relevant_doc_index]}")代码说明
分词处理:借助
jieba分词库对中文文档和查询进行切分。例如,文档1的分词结果为:['猫', '是', '一种', '可爱', '的', '动物', '喜欢', '抓', '老鼠', '。']初始化BM25模型:使用
BM25Okapi类将分词后的文档库传入,完成模型初始化。计算相关性得分:通过
get_scores方法获得查询与每个文档间的相关性评分。排序与输出:依据得分从高到低排序,输出匹配度最高的文档内容。
结果输出
['猫是一种可爱的动物,喜欢抓老鼠。', '狗是人类的好朋友,喜欢追猫。', '老鼠是一种小型啮齿动物,猫喜欢抓它们。']
问题: 猫喜欢抓什么动物?
Building prefix dict from the default dictionary ...
Loading model from cache /tmp/jieba.cache
Loading model cost 0.705 seconds.
Prefix dict has been built successfully.
文档1 的 BM25 得分: 0.3001762708496166
文档2 的 BM25 得分: -0.07080064278072501
文档3 的 BM25 得分: -0.2035654844820229
最相关的文档是文档1: 猫是一种可爱的动物,喜欢抓老鼠。BM25在RAG中的应用
在RAG(检索增强生成)框架中,将基于嵌入向量的语义检索与基于BM25的关键词检索相结合,形成混合检索策略,能够显著提升查询结果的精确度。这种混合检索的核心思路是充分利用两种检索方式的互补优势,从而更全面地捕捉查询与文档之间的相关性。
向量检索和BM25检索的优缺点
向量检索(基于嵌入的相似性搜索)
优点:擅长捕捉语义相似性,即使查询与文档之间没有完全匹配的关键词,也能通过语义理解找到相关内容。适用于处理同义词、上下文关联等复杂语义关系。
缺点:对领域外词汇或罕见术语的识别能力较弱。依赖高质量的嵌入模型(如BERT、Sentence-BERT),且需要较高的计算资源开销。
BM25关键词检索
优点:对精确关键词匹配效果极佳,特别适合处理明确术语和短查询。计算效率高,可轻松应对大规模文档库。
缺点:无法理解语义相似性,对同义词、上下文关联不敏感。在处理长查询或复杂查询时表现欠佳。
混合检索的原理
混合检索的核心思想是综合向量检索与BM25检索各自的优势,通过加权融合或排序整合的方式,生成最终检索结果。具体流程如下:
(1)分别计算向量检索和BM25检索的得分
- 向量检索:使用嵌入模型(如BERT)将查询和文档转换为向量,再通过余弦相似度计算得分。
- BM25检索:利用BM25算法计算查询与文档之间的关键词匹配得分。
(2)得分归一化:由于两种检索的得分尺度不同,需要对得分进行归一化处理,使其处于同一可比较的量纲下。
(3)加权融合:将两种得分按权重进行组合,得到最终检索得分。例如:Final Score = α·BM25 Score + (1-α)·Vector Score
其中,α为权重参数,用于调节BM25与向量检索的贡献比例。
(4)排序与返回:依最终得分对文档降序排序,返回最相关的若干文档。
实际应用中的混合检索策略
在实际项目中,混合检索可以通过以下方式实现:
(1)加权求和:将向量检索得分与BM25得分按一定权重直接相加,得出最终得分。例如:Final Score = 0.5·BM25 Score + 0.5·Vector Score
(2)排序融合:分别对向量检索结果和BM25检索结果进行排序,再通过加权或插值方法合并排序列表。例如,取向量检索的前10个结果与BM25检索的前10个结果,合并后重新排序。
(3)阈值过滤:先使用BM25检索快速筛选出关键词匹配的文档集合,再对该集合应用向量检索进行语义排序,提升整体效率。
混合检索的代码实现
通过本地部署的ollama嵌入模型,可以生成文档的嵌入向量。下面的代码演示了如何嵌入模型计算向量,并基于混合策略进行融合排序。
from rank_bm25 import BM25Okapi
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
import jieba
import requests # 用于发送HTTP请求
# 示例文档库
documents = [
"猫是一种可爱的动物,喜欢抓老鼠。",
"狗是人类的好朋友,喜欢追猫。",
"老鼠是一种小型啮齿动物,猫喜欢抓它们。"
]
# 用户查询
query = "猫喜欢抓什么动物?"
# 分词函数
def tokenize(text):
return list(jieba.cut(text))
# 分词后的文档库
tokenized_documents = [tokenize(doc) for doc in documents]
# 初始化BM25模型
bm25 = BM25Okapi(tokenized_documents)
# 计算BM25得分
bm25_scores = bm25.get_scores(tokenize(query))
# 嵌入模型API地址
EMBEDDING_MODEL_URL = "http://172.16.1.54:11434/api/embeddings"
# 获取嵌入向量的函数
def get_embedding(text):
payload = {
"model": "unclemusclez/jina-embeddings-v2-base-code:latest",
"prompt": text
}
response = requests.post(EMBEDDING_MODEL_URL, json=payload)
if response.status_code == 200:
embedding = response.json().get("embedding")
return np.array(embedding)
else:
raise Exception(f"Failed to get embedding: {response.status_code} - {response.text}")
# 获取查询和文档的嵌入向量
query_embedding = get_embedding(query)
document_embeddings = [get_embedding(doc) for doc in documents]
# 计算余弦相似度
vector_scores = cosine_similarity([query_embedding], document_embeddings)[0]
# 归一化得分
bm25_scores_normalized = (bm25_scores - np.min(bm25_scores)) / (np.max(bm25_scores) - np.min(bm25_scores))
vector_scores_normalized = (vector_scores - np.min(vector_scores)) / (np.max(vector_scores) - np.min(vector_scores))
# 加权融合
alpha = 0.5
final_scores = alpha * bm25_scores_normalized + (1 - alpha) * vector_scores_normalized
# 排序并输出结果
sorted_indices = np.argsort(final_scores)[::-1]
for i in sorted_indices:
print(f"文档{i+1} 的混合得分: {final_scores[i]} - {documents[i]}")输出:
Building prefix dict from the default dictionary ...
Loading model from cache /tmp/jieba.cache
Loading model cost 0.715 seconds.
Prefix dict has been built successfully.
文档1 的混合得分: 1.0 - 猫是一种可爱的动物,喜欢抓老鼠。
文档2 的混合得分: 0.5282365628163656 - 狗是人类的好朋友,喜欢追猫。
文档3 的混合得分: 0.0 - 老鼠是一种小型啮齿动物,猫喜欢抓它们。说明:这里的得分是基于当前使用的嵌入模型得到的,不同模型结果可能有所差异。
总结
通过融合向量检索与BM25检索,混合检索方法能够同时兼顾语义理解与关键词匹配,从而有效提升RAG框架的查询准确率。该策略在处理多样化查询、减少漏检与误检方面表现突出,是优化检索增强生成系统性能的重要手段。
