高级 11 分钟优化

混合检索:BM25 + vectoriel

直接回答

混合检索针对同一问题,并行执行关键词检索(BM25)和向量检索,然后融合两个排名,最常用的方法是倒数排名融合(Reciprocal Rank Fusion,RRF)。它弥补了嵌入检索失败的情况(标识符、专有名词、罕见术语),同时保留对不同措辞表达同一意思的理解能力。Qdrant、Weaviate 和 Elasticsearch 原生支持混合检索;使用 ChromaDB 时,可以用三十行 Python 代码构建这一功能。

向量数据库按语义检索,而非按字面匹配:像“RG/2024-117”这样的编号或工单号,它就可能找不到。反过来,关键词搜索无法理解“automobile”和“voiture”指的是同一种东西。本指南介绍如何结合这两种搜索、如何选择融合方法、Qdrant 和 Weaviate 各自能做什么,以及一个会在不知不觉中破坏 BM25 部分的法语分词陷阱。

作者: Mohamed Meguedmi·更新于 2026-09-30·已在 Windows、macOS 和 Linux 上测试

#为什么纯向量检索会在某些问题上失效

向量搜索将每段文本转换为一个概括其整体含义的向量,然后返回向量与问题向量最接近的段落。这种压缩方式很擅长处理同义改写,却不擅长查找必须逐字匹配的内容:标识符、错误码、人名或内部缩写。在嵌入表示中,“PROD-4817”和“PROD-4871”很相似,因此无关工单可能排在正确工单之前。混合搜索弥补了这一缺陷:它增加一个基于词语精确出现情况的独立排序,再融合两种排序,让两种方法互相弥补盲点。

标识符与编号
工单号、档案编号、合同号、SKU、错误码:这些字符串无法可靠地保留在嵌入向量中,而只要它们出现在文本片段里,BM25 就能检索到。
专业领域罕见词汇
不常见的医学、法律或技术术语:罕见词在 BM25 中的权重很高,而其嵌入向量表达的含义可能较模糊。
非常短的查询
像“发票 2024”这样的两个词,能为生成嵌入提供的信息很少;关键词则直接按原样进行比较。
改写与重述
相反的情况是:“contrat à durée déterminée”(固定期限合同)与“CDD”,或“voiture”与“automobile”(两者均意为汽车),不包含任何共同词语;只有向量搜索才能将它们关联起来。
i
一个典型场景
基于工单的检索中,以纯向量方式查询「PROD-4817」会返回内容相近的工单,而基于关键词的搜索则会立即定位到唯一包含该编号的工单。

#BM25:关键词搜索能做什么

本地 RAG 工具包

你的文档,你的 AI:基于你的 PDF、笔记和邮件的可靠本地 RAG——无需向云端发送任何内容。

  • 在线空间,终身可用
  • PDF + 文件
  • 30 天内退款

BM25 是 Lucene、Elasticsearch 和 OpenSearch 等系统中使用的词法排序函数,SQLite 也在其 FTS5 模块中提供,其文档将 bm25() 函数描述为返回一个表示行与查询匹配质量的值。该函数基于三个原则:稀有词的权重高于常见词,当某个词出现次数超过一定阈值后,其重复出现的权重会逐渐降低,且较长的文本段相对于短段会受到轻微惩罚。参数 k1 用于控制这种饱和效应:根据 Elastic 的说明,它限制了查询中单个术语对文档得分的影响程度。

优势
罕见术语、标识符、短查询、专业词汇,无需加载或训练任何模型,索引紧凑,结果可解释(可以知道是哪个词让该段落出现在检索结果中)。
弱点
同义词、换一种说法、释义、拼写错误;如果不进行词形还原,“signé”(已签署)和“signature”(签名)会被视为两个不同的词。
前置条件
细致的分词处理:转为小写、去除重音符号,必要时进行词干提取。大多数自行编写的实现都在这一环节出错,详见下文。

#混合检索:基本原理

针对同一个问题执行两种检索,每种检索各自返回一份候选列表(20 至 50 个文本片段),然后将两份列表合并为一个统一的排名。效果提升源于一个简单的观察:同时出现在两份列表中的文本片段几乎总是相关的,而且每种方法还会找出另一种方法遗漏的片段。Weaviate 对混合检索的定义是:通过融合向量检索和关键词检索的两个结果集,将两者的结果结合起来,融合方法和相对权重均可配置。具体提升幅度取决于语料库和问题:任何通用数字都不可靠,必须在您自己的文档上进行测量(见下文)。

#无需归一化的融合:倒数排名融合(Reciprocal Rank Fusion)

常见误区是直接把原始分数相加。BM25分数是没有上限的正数;向量相似度分数则是距离或余弦值,处于有界区间内:这两种评分尺度无法直接比较,语料库哪怕发生微小变化,也会使它们偏移。倒数排名融合(Reciprocal Rank Fusion)只使用排名,从而绕过这一问题。Elasticsearch文档将其描述为一种无需调参、且各相关性指标之间无需存在关联的方法。

RRF 公式
score(doc) = somme, sur chaque liste i où le doc apparaît, de 1 / (k + rang_i(doc))

k = 60 par défaut (valeur par défaut d'Elasticsearch)
rang_i = 1 pour le premier de la liste, 2 pour le deuxième, etc.
Un doc absent d'une liste n'ajoute rien pour cette liste.

一个在 BM25 检索中排名第一、在向量检索中排名第五的文本段落,得分为 1/61 + 1/65,约为 0.0318;一个在两个列表中都排名第十五的段落,得分为 2/75,约为 0.0267。常数 k 会减弱第一名的优势:k 越大,靠后排名的相对影响就越大。Elasticsearch 文档将这个常数称为 rank_constant,默认值为 60;窗口大小 rank_window_size 则决定融合前每个列表的长度。文档指出,更大的窗口会提高相关性,但会牺牲性能。

#那么,基于分数的融合呢?

某些检索引擎提供另一种方案:先对每个列表的分数进行归一化,再按权重组合。Weaviate 文档介绍了两种方法:按排名排序和按相对分数融合,后者自 1.24 版本起成为默认方法;使用混合运算符的 autocut 功能时必须采用该方法。Qdrant 提供 RRF 和 DBSF,后者保留原始分数,但在组合前对其分布(均值和标准差)进行归一化。不了解分数分布时,按排名融合更稳健;能够进行测量时,按分数融合则支持更精细的调节。

#工具选择:原生支持混合检索的引擎

根据工具实现的混合检索(官方文档,2026年9月)
工具原生混合模式融合权重调整
Qdrant是的,可通过 Query API 实现(自 1.10 版本起可用)RRF 或 DBSF较新版本支持按查询调整权重和常数 k
Weaviate是,使用 hybrid 运算符排名或相对得分(自 1.24 版本起为默认方式)参数alpha:1表示纯向量,0表示纯关键词
Elasticsearch是,使用 rrf 检索器RRFrank_constant(默认值为60)和 rank_window_size
SQLite FTS5 + 向量扩展待组装需要编写由您决定
ChromaDB + rank_bm25需用 Python 自行组合实现需自行编写(用 6 行代码实现 RRF)由您决定

根本区别不在于融合本身——这只需几行代码就能实现——而在于索引:原生引擎会同步维护两个索引,使其保持最新,而自建方案将 BM25 索引保存在内存中,每次添加文档时都必须重建。对于只有几千个片段且很少变化的语料库,自建方案完全够用。规模再大一些,或者只要文档每天都在变化,原生引擎就能避免两个索引之间出现不一致。关于 Weaviate 的指南详细介绍了这个工具。

#自研实现:ChromaDB、rank_bm25 和 RRF

以下代码将三个模块组合起来。它修正了示例中一个常见的问题:规范化函数必须先去除变音符号,再进行过滤,否则每个带重音的字母都会把一个单词截成两部分。

使用 RRF 的 BM25 + ChromaDB 混合检索
import re, unicodedata
import chromadb
from rank_bm25 import BM25Okapi

def normalize(txt):
    txt = unicodedata.normalize("NFKD", txt.lower())
    txt = "".join(c for c in txt if not unicodedata.combining(c))  # retire les accents
    return re.findall(r"[a-z0-9]+", txt)

# Indexation BM25 (en mémoire) : l'indice de la liste = l'identifiant du passage
all_docs = [d["text"] for d in load_docs()]
bm25 = BM25Okapi([normalize(d) for d in all_docs])

# ChromaDB : les ids doivent être les mêmes, sous forme de chaînes "0", "1", ...
coll = chromadb.PersistentClient("./chroma_db").get_collection("docs")

def rrf_fuse(rankings, weights=None, k=60):
    weights = weights or [1.0] * len(rankings)
    scores = {}
    for ranking, w in zip(rankings, weights):
        for rank, doc_id in enumerate(ranking, start=1):
            scores[doc_id] = scores.get(doc_id, 0) + w / (k + rank)
    return sorted(scores, key=scores.get, reverse=True)

def hybrid_search(question, top_k=5, n_candidates=20):
    s = bm25.get_scores(normalize(question))
    bm25_top = sorted(range(len(all_docs)), key=lambda i: -s[i])[:n_candidates]
    bm25_ranking = [str(i) for i in bm25_top]
    vec_ranking = coll.query(query_texts=[question], n_results=n_candidates)["ids"][0]
    fused = rrf_fuse([bm25_ranking, vec_ranking])
    return [all_docs[int(i)] for i in fused[:top_k]]
!
法语分词陷阱
采用常见的归一化流程“先进行 NFKD 分解,再将所有非 a-z 字母或数字的字符替换为空格”时,“référence”会变成“re fe rence”:每个重音都会留下一个组合符号,随后被替换为空格。BM25 部分仍能正常匹配标识符,却会漏掉所有带重音的单词。建立索引前,请用三句话测试 normalize。

有两点实用注意事项。首先,两个索引中的标识符必须一致:这里将列表下标转换为字符串,用作 Chroma 的标识符。其次,内存中的 BM25 索引会在程序停止时消失:请在启动时重新构建,处理数万段文本只需几秒钟;或者将文档列表保存在数据库旁边。

#调节 BM25 与向量检索之间的平衡

默认情况下,RRF 对两份列表赋予相同权重。如果您的语料库包含大量参考资料(判例、工单、目录),请提高 BM25 的权重;如果问题偏向对话式,则保持权重平衡或偏重向量检索。在前述函数中,只需传入 weights=[0.6, 0.4],就能为 BM25 分配 60% 的权重。Qdrant 也支持同样的设置:其文档指出,每条查询的默认权重为 1,此时得到的就是原始 RRF 公式;较新版本还允许调整常数 k。在 Weaviate 中,调整的是 alpha:1 表示纯向量检索,0 表示纯关键词检索。

根据语料库类型确定起点
语料库与问题BM25/向量初始权重您所监控的内容
参考信息、编号、专有名称(法律、工单、目录)60 / 40按标识查询的问题是否会优先返回?
文字形式的文档,以自然语言提出的问题40 / 60换一种说法提问,能否检索到正确的段落?
混合或未知语料库50 / 50在 30 至 50 个真实问题上评估召回率@5
专业术语和缩略语55 / 45,并在 BM25 检索中使用同义词词典使用缩写和完整名称,能否检索到同一段内容?
→
调整前需测量
这些值只是起点,并非实测结果。请整理 30 至 50 个真实问题,并为每个问题配上预期检索到的段落;分别计算仅用 BM25、仅用向量检索以及混合检索时前 5 个结果的召回率,只有当混合检索在您的文档上表现更好时才保留它。

#融合之后:添加重排序器

融合得到的候选集比任何一种方法单独得到的候选集都更多样。重排序器随后可以结合问题阅读每个段落,对这个候选集进行排序:这两种技术可以叠加使用。通常的顺序是先进行混合检索,再融合,然后对前 20 至 50 个结果进行重排序,最后将最好的 3 至 5 个段落放入提示词中。重排序器指南详细介绍了最后这一步,分块指南则解释了为什么段落大小对 BM25 和嵌入的影响都如此之大。

#关于混合搜索的常见问题

FAQ
RAG 中的混合检索是什么?+
这是将针对同一问题的两种检索结合起来:向量检索用于找出语义相近的段落,关键词检索(BM25)用于找出包含相同词语的段落。两份排名合并为一份,通常采用倒数排名融合(Reciprocal Rank Fusion),再将最相关的段落传递给模型。
在将 BM25 和向量得分相加前,需要对它们进行归一化吗?+
使用 RRF 时无需归一化,因为它只使用排名,这也正是采用它的目的:两种分数的尺度无法直接比较。如果您更倾向于合并分数,就必须先将它们归一化,Weaviate 的相对分数融合和 Qdrant 的 DBSF 就是这样做的;还需要检查语料库变化后,结果是否仍然稳定。
在 RRF 中应选择多大的 k 值?+
如果没有理由调整,就保持 Elasticsearch 的默认值 60。较小的值更偏重排名最靠前的结果,较大的值则让排名靠后的结果拥有更大影响。相比于分词质量和各列表的候选数量,这项设置的影响较小。
可以使用 ChromaDB 实现混合检索吗?+
可以,通过组合以下组件实现:Chroma 的向量检索、Python 中的 BM25 索引(rank_bm25 库),以及用几行代码实现的 RRF 融合。请确保两个索引使用相同的标识符,并在分词时去除重音符号。对于经常变化的语料库,使用 Qdrant 或 Weaviate 等原生支持混合检索的引擎,可以避免维护两个索引。
混合搜索是否会显著降低查询速度?+
通常只会慢一点:在内存索引上运行 BM25 非常快,而且向量检索本来就在进行。真正的开销来自之后可能使用的重排序器,以及词法索引占用的内存。请测量实际查询的端到端耗时,而不是各个步骤单独的耗时。
如何判断混合检索对我的文档是否值得采用?+
选取30到50个真实问题,并确保您知道每个问题应检索到的文本段落,然后比较单独使用BM25、单独使用向量检索和混合检索时前5个结果的召回率。如果混合检索没有带来提升,您的语料库可能完全由对话内容构成,向量检索就足够了。如果提升主要体现在标识符检索上,请增加BM25的权重。
这份指南对您有帮助吗?

有反馈、发现了错误,或想补充说明?请告诉我们,让这份指南对每个人都更有帮助。