BM25 混合搜索实战:融合向量检索,用召回率验证效果
搜索能理解语义,却漏掉产品编码或错误码?本文讲解如何将 BM25 与向量检索结合,比较 RRF 和加权得分的适用条件,并在 Elasticsearch、Weaviate、Pinecone 中配置混合搜索。通过同一组标注查询评估召回率,区分候选缺失与排序问题,同时保留可靠的精确标识符查找路径。
发布于

搜索能理解问题,却总漏掉产品编码?先给系统补上词项检索分支,再考虑更换嵌入模型。混合搜索把 BM25 等关键词检索与向量检索生成的 2 份候选列表合并,形成统一排序。可以从倒数排名融合(RRF)起步,同时明确处理精确标识符;只有标注查询集表明召回率有所提升、原本有效的查询也没有被破坏,才保留这项改动。
文档核验日期:2026 年 10 月 24 日。 以下实现建议依据 Elasticsearch、Weaviate 和 Pinecone 的现行文档。评估参数只是建议的起步方案,并非实测提升,也不是厂商基准测试结果。
什么是混合搜索?BM25 与向量检索如何配合?
混合搜索让字面匹配和语义理解共同决定检索结果。本文采用的架构中,关键词检索和向量检索各自生成一份有序列表,再由融合方法将两者合并。Weaviate 的说明介绍了这两个分支及其融合方式。
可以把它想成两位研究员在回答同一个客服问题:一位查索引,寻找准确的名称和错误字符串;另一位从语义入手,寻找相关内容,包括换一种说法表达的答案。你将两人的候选清单合并,而不是要求任何一人独自包办这两类工作。
BM25 是一种词项排序函数,根据文档包含的查询词计算得分。词频和文档长度都会影响得分,因此它比单纯统计匹配词数量更有用。Elasticsearch 默认用 BM25 计算文本相似度。Elastic 的相似度参考文档解释了这些影响因素。
向量检索通过比较嵌入向量,也就是内容的数值表示,寻找含义相近的材料。即便措辞不同,它也能将“我总是被退出登录”与介绍会话过期的文档关联起来。我们的语义搜索指南详细介绍了这条检索路径。
以示意查询 ERR_SESSION_EXPIRED after login 为例:词项分支为字面相同的错误字符串提供进入结果集的路径,语义分支则可能补充身份验证会话和续期的相关说明。融合方法决定最终顺序。这个例子描述的是预期行为,并不意味着未经配置的存储系统就会给出某种特定排名。

混合向量搜索也需要明确的精确匹配规则
有关键词分支,不代表就能保证精确命中标识符。文本分析会把字符串转换成可搜索的词元,这一过程可能改变实际索引的内容。例如,Elastic 标准分析器在文档示例中会拆分带连字符的短语。请用实际配置的分析器检查自己的 SKU 和错误码会被怎样处理。参见标准分析器文档。
如果标识符的准确写法决定了用户要找的对象,就应将它保存在专门用于精确匹配的字段中。Elastic 文档中的 keyword 字段正是用于这类结构化值。用户搜索虚构 SKU AX-BLUE 时,不应仅因描述相似,就在不作说明的情况下被推荐 AX-GREEN。参见Keyword 字段参考文档。
**决策原则:**如果产品要求唯一标识符必须对应指定记录,就将已识别的唯一标识符路由到精确查找。对于同时包含标识符和更宽泛需求的问题,例如查找该产品的故障排查说明,再使用混合排序。
RRF 与加权得分:检索结果怎么合并?
如果需要一个不必校准不同得分尺度的基线,先用倒数排名融合。当你已有标注数据可用于调节两路权重,并且有理由保留得分之间的差距时,再考虑加权得分。
RRF 依据排名位置进行融合
倒数排名融合,即 RRF,会累加文档在每份结果列表中的贡献:
RRF(文档) = 对各检索列表中的 1 / (c + rank) 求和。
排名从首位开始计数。文档未出现在某份列表中,就不从该列表获得贡献。Gordon V. Cormack、Charles L. A. Clarke 和 Stefan Büttcher 在 SIGIR 2009 论文中提出了 RRF,实验中将常数固定为 60。论文将该常数记作 k;本文改用 c,以免与 recall@k 中的结果数量混淆。
RRF 的便利之处在于,BM25 的高分与向量检索的高分不必具有相同含义,融合时只看各自排名。Pinecone 的 RRF 实现明确说明了排名从首位计数,以及文档缺席某份列表时的处理方式。
这里有个值得验证的结果。设定一个示例:文档 A 在关键词列表中排第一,但未进入向量列表;文档 B 在两份列表中都排第二。当 c = 60 时,A 得到 1/61,B 得到 2/62。B 胜出。 这是我们根据公式算出的结果,不是搜索基准测试。
加权得分保留分数之间的差距
加权得分融合先让相关性得分的尺度可比,再将它们组合起来。一种常见形式是:
score = alpha × 归一化后的向量得分 + (1 − alpha) × 归一化后的关键词得分。
权重 alpha 控制两路得分的比重。以 0.5 起步,只意味着两项系数相等,并不代表两路检索同样有用,也不代表这是最优值。应同时记录归一化和权重配置,因为改变其中任何一项都可能改变排序。
Elasticsearch 的线性检索器支持配合 minmax 等归一化方法进行加权求和。Weaviate 的相对得分融合保留各结果集内部得分的相对差距;基于排名的融合则会舍弃这些差距。
先用 RRF 建立对照,只有留出查询集的结果支持时,才保留加权融合。 不要把原始 BM25 分数和向量相似度分数直接相加,就认定总分有意义。还应记录候选深度:进入归一化列表的结果发生变化,得分范围也可能随之改变。
什么情况下,混合搜索比纯向量搜索更有效?
如果关键词分支找到了向量分支漏掉的相关材料,而融合又让这些材料排到了足够靠前、能够发挥作用的位置,混合搜索就有帮助。这是可以逐项检查的条件,不是普遍适用的准确率承诺。
以下场景最值得试验:
- **客服搜索:**错误字符串原文、函数名和账户术语,与症状描述混在同一查询中。
- **商品目录搜索:**准确产品名或 SKU,与兼容性、替换零件等需求一同出现。
- **内部知识搜索:**项目名、缩写与自然语言问题混用。
Pinecone 的混合搜索概览明确将产品编码、错误字符串和名称列为关键词信号,并结合稠密检索来处理语义。
对于纯标识符查询,先做好可靠的精确查找。如果现有向量检索已经能很好地处理语料和查询集,就先找到检索问题的证据,再增加分支。
实际限制在于候选覆盖范围。融合只能对各分支已检索文档的并集排序。如果两个分支都没找到相关段落,换融合方法也找不回来。如果段落在融合前已经出现,最终结果却没有它,就应检查排序和截断环节。
在现有存储系统中加入混合搜索
只要当前系统具备所需的检索能力,就继续使用。换数据库和换检索策略同时进行,会让你更难判断究竟是哪项改动带来了效果。
下表区分的是融合发生在哪里,并非给厂商排名。
Elasticsearch 混合搜索怎么配置?
Elasticsearch 支持文本和向量检索,可以在 rrf 检索器下放置一个 standard 文本检索器和一个 knn 向量检索器。官方 RRF 检索器参考文档介绍了这种组合及其候选窗口。
对于已有索引,将可搜索文本和对应嵌入向量保存在同一个文档 ID 下。两个分支应检索同一范围内符合条件的内容,再进行融合。租户、权限和产品版本限制也要在两个分支中保持一致。
设置好 ES_URL 和 ES_API_KEY 后,可以运行下面的请求。它假定已有一个名为 docs 的索引,其中包含文本字段 text 和稠密向量字段 embedding,并且 query-vector.json 已保存查询的实际嵌入向量数组。生成该数组时,须使用为索引文档生成嵌入向量的同一模型。命令依赖 jq 和 curl。
jq -n --slurpfile v query-vector.json \
--arg q 'ERR_SESSION_EXPIRED after login' \
'{
size: 10,
retriever: {
rrf: {
retrievers: [
{standard: {query: {match: {text: $q}}}},
{knn: {
field: "embedding",
query_vector: $v[0],
k: 50,
num_candidates: 100
}}
],
rank_window_size: 50,
rank_constant: 60
}
}
}' > hybrid-request.json
curl --fail-with-body "$ES_URL/docs/_search" \
-H "Authorization: ApiKey $ES_API_KEY" \
-H 'Content-Type: application/json' \
--data-binary @hybrid-request.json这些参数仅用于演示评估配置。size 是最终结果数量;rank_window_size 控制每个子检索器有多少条结果可以参与融合。如果应用有权限控制,请先补充访问资格约束,再使用这个未加过滤的示例。Elastic 文档说明了父级 filter:它会按照各子检索器的规范作用于它们。
**容易卡住的地方:**子检索器窗口太窄,可能恰好丢掉你希望混合检索找回的结果。调整融合方法前,先比较分支候选和最终结果。如果改用线性检索器,应将归一化和权重一起配置。
Weaviate 混合搜索怎么配置?
Weaviate 支持文本和向量检索,通过 collection.query.hybrid 提供混合搜索。它的词项检索组件是 BM25F,即支持多字段的变体。混合查询文档介绍了可搜索属性、融合方式选择,以及得分解释。
将 query_properties 指向能提供有效词项线索的字段,例如标题、错误文本和产品名。如果由应用生成嵌入向量,就直接提供查询向量;也可以使用集合已配置的向量化路径。比较融合方法时,显式设置 fusion_type;排查某条结果为何发生排名变化时,请求 explain_score。
alpha 也应显式设置:0 表示选择关键词搜索,1 表示选择向量搜索。现行文档提醒,省略它时,实际生效的权重取决于客户端。默认融合方法是 relativeScoreFusion,也可选择 rankedFusion。参见 Weaviate 的融合参考文档。
**容易卡住的地方:**调整权重无法补上缺失的可搜索字段,也修不好被错误处理的标识符词元。在认定需要更换嵌入模型之前,先检查词项分支的贡献。用权重范围两端的纯向量和纯关键词检索作为诊断基线,再评估组合配置。
Pinecone 混合搜索该选哪条路径?
检索数据库 Pinecone 在文档中区分了不同的混合方案。复制示例前,先选定方案:服务端稠密/稀疏组合和客户端 RRF 是不同实现。参见 Pinecone 的混合搜索概览。
对于 BM25 与稠密检索的组合,Documents API 路径会分别执行全文搜索和稠密向量搜索,再由应用通过 RRF 合并两份有序文档 ID 列表。Pinecone 的 RRF 指南提供了该客户端实现。
对于原生稠密与稀疏向量组合,Vectors API 可以在每条记录中保存两种表示,并在单次查询中将它们组合。其单索引指南要求使用 vector_type="dense" 和 metric="dotproduct";客户端通过缩放查询向量,设定两者的相对影响。
稀疏向量只是一种表示形式,不能据此认定它由 BM25 生成。请在架构说明中明确记录所用的词项模型或全文评分器。
**容易卡住的地方:**先做关键词过滤、再按稠密向量排序,不等于让两类信号共同影响排名。过滤器会剔除候选,无法找回已经被它排除的同义表达匹配。必须满足的条件适合用过滤器;希望任一检索分支都能贡献结果时,则应使用融合。
混合检索在 RAG 中起什么作用?
混合搜索改变的是检索增强生成(RAG)的检索阶段。在这个阶段,应用会先把检索到的源材料提供给语言模型,再让它回答。混合搜索不会替代生成阶段,也不能证明答案正确。
检索单位要保持一致。如果关键词分支返回整篇文档,向量分支返回段落,就要先确定两者的 ID 如何映射,再做合并。否则,同一来源的重复段落可能占满候选清单,挤掉其他相关来源。我们的向量数据库详解介绍了存储和检索的基础。
Agentic RAG增加了决策环节,用来判断采用哪种检索动作、是否需要再次搜索。混合检索器可以是其中一种动作。如果客服机器人漏掉了错误码原文,应先修复并评估检索器,再增加编排环节。
比较检索效果时,固定生成模型和提示词。如果精确词项查询组有所改善,但改写查询的检索仍然薄弱,就用同一批标注查询比较嵌入模型。同时改变嵌入模型、分块和融合方法,会让结果无法归因。
用 recall@k 评估改动前后的召回率
用一小组经过标注的查询,对照同一组相关答案,比较纯关键词、纯向量和混合检索。生成的回答读起来流畅,并不等于检索效果好。
Recall@k = 前 k 条结果中检索到的相关 ID 数量 ÷ 该查询所有已标注的相关 ID 数量。 对每条查询的结果取平均,就得到宏平均 recall@k。如果标注不完整,应将结果称为基于已知相关性判断的召回率;它不能代表对语料中全部相关内容的召回率。
建立小规模查询集
先准备 60 条有答案的查询:20 条精确词项查询、20 条改写查询,以及 20 条混合查询。这是建议的查询类型划分,并不保证样本量足够。尽量取材于真实搜索,并纳入当前检索失败的名称、SKU 和错误字符串。
每组留一半用于开发调优,另一半作为留出集核验。另加 6 条特意设计为无答案的查询,单独检查其表现;这些查询的相关集合为空,召回率分母为零,因此不参与召回率计算。
标注相关的检索单元
为每条查询记录 ID、所属查询组,以及所有能够确认相关的文档或段落 ID。汇集三种检索方法的候选结果,补上已知但三者都未检索到的答案;判断相关性时,不要查看候选由哪种方法提供。全程使用稳定的 ID,并统一采用文档或段落作为检索单位。
固定对比条件
使用相同的语料快照、权限、嵌入模型和分块方式。起步时,将最终 k 设为 10,混合检索的每个分支各取 50 个候选。这些参数供初步探索,并非最优值。第一轮对比先关闭重排序。
导出每种方法最终返回的有序 ID 列表。同时保存融合前的关键词和向量候选列表,以便区分候选缺失问题与排序问题。
先调优,再启用留出集
在用于开发的那一半查询上,调整融合方式和候选深度。选定配置后,再对留出的另一半评估一次。既报告总体平均值,也报告每组查询的结果,并逐条查看效果退步的查询。平均值小幅提升值得继续调查,但不能作为统计证明。

在本地运行召回率评估脚本
将以下代码保存为 evaluate_recall.py。它只使用 Python 3 标准库。qrels.jsonl 中每条查询对应一个 JSON 对象,包含 qid、slice 和 relevant 字段。各结果文件将查询 ID 映射到按返回顺序排列的 ID 列表。
例如,一条标注记录可以写成 {"qid":"q-exact","slice":"exact","relevant":["guide-login"]},结果文件则可包含 {"q-exact":["guide-reset","guide-login"]}。这些只是格式示例,不是某个经过测试的系统所返回的搜索结果。每份结果文件都必须包含所有已标注查询;某条查询未检索到任何内容时,用空列表表示。
import json
import sys
from collections import defaultdict
from statistics import mean
qrels_path, bm25_path, vector_path, hybrid_path, cutoff = sys.argv[1:]
k = int(cutoff)
if k < 1:
raise ValueError("k must be positive")
with open(qrels_path, encoding="utf-8") as f:
labels = [json.loads(line) for line in f if line.strip()]
if not labels or len({r["qid"] for r in labels}) != len(labels):
raise ValueError("Labels must be nonempty with unique query IDs")
runs = {}
for name, path in (("bm25", bm25_path), ("vector", vector_path),
("hybrid", hybrid_path)):
with open(path, encoding="utf-8") as f:
runs[name] = json.load(f)
missing = {r["qid"] for r in labels} - runs[name].keys()
if missing:
raise ValueError(f"{name} is missing query IDs: {sorted(missing)}")
groups = defaultdict(list)
skipped = 0
for row in labels:
relevant = set(row["relevant"])
if not relevant:
skipped += 1
continue
scores = {}
for name, run in runs.items():
unique_ids = list(dict.fromkeys(run[row["qid"]]))
scores[name] = len(relevant & set(unique_ids[:k])) / len(relevant)
groups["overall"].append(scores)
groups["slice:" + row["slice"]].append(scores)
if not groups:
raise ValueError("No answerable queries to score")
for group, rows in sorted(groups.items()):
values = {name: mean(r[name] for r in rows) for name in runs}
delta = values["hybrid"] - values["vector"]
report = " ".join(f"{name}={v:.3f}" for name, v in values.items())
print(f"{group} n={len(rows)} recall@{k} {report} delta={delta:+.3f}")
print(f"Excluded no-answer queries: {skipped}")对留出集的标注和导出结果运行:
python3 evaluate_recall.py qrels.jsonl bm25.json vector.json hybrid.json 10脚本会先去除重复 ID,再截取前 k 条结果。应用中也应使用相同的去重规则。它既报告混合检索相对向量检索的变化,也展示关键词基线,避免忽略原本就很强的词项检索系统。
当相关查询组的表现有所提升,且退步情况在产品要求允许的范围内时,再上线。 如果候选覆盖改善了,最终召回率却没提高,就调整融合或结果截断数量。如果最终召回率提高了,还要单独检查结果顺序、生成答案的正确性和端到端延迟。召回率无法代替这些评估。
哪些说法被夸大了,接下来该做什么?
“混合搜索兼得两者之长”这句话,掩盖了真正需要作出的工程决策。它可能扩大有效覆盖范围,但也让另一个检索分支介入最终排序。RRF 可能降低精确命中结果的排名;加权融合可能因得分尺度或归一化表现不同而偏向某一分支。两者都无法补上缺失的源内容。
**开发者可以立即动手的情况:**检查发现,与精确词项相关的文档未进入向量候选。此时可加入词项分支,保持文档标识和权限一致,再开展基于标注的对比。
**运维人员应暂缓上线的情况:**当前问题是内容过时、缺少权限,或没有相关性标注。先补齐这些前提,再比较检索策略。
**已有可靠标识符查找能力的团队,暂时无需调整:**等到需要更广泛地发现内容时,再引入新的检索方式。保留可靠的查找路径,并针对自然语言问题单独评估混合搜索。
下周一就可以这样开始:收集失败的搜索,标注预期来源,让现有向量检索器与关键词基线并排运行。看清各分支分别贡献了什么,再加入融合。最终留下在留出查询集上证明有效的配置。
常见问题
BM25 本身就是混合搜索吗?
不是。BM25 是词项排序函数。将它的结果与向量检索分支组合起来,才构成一种混合搜索。
ChatGPT 使用语义搜索吗?
仅凭生成的回答,无法判断底层采用了什么检索算法。如果你围绕语言模型搭建系统,应检查实际配置的检索路径,而不是猜测它使用了 BM25、向量检索或 RRF。
什么是语义搜索?能举个例子吗?
“我总是被退出登录”这样的查询,可以匹配到有关会话过期的说明,即使两者措辞不同。这就是基于含义的检索;但对于准确的错误字符串,可能仍然需要词项匹配。
混合搜索的实际例子是什么?
对于 ERR_SESSION_EXPIRED after login,用关键词分支检索字面匹配的错误字符串,用向量分支查找相关会话说明。合并候选后,再根据相关性标注核对预期来源是否被检索到。
混合搜索和向量搜索最主要的区别是什么?
纯向量搜索依据嵌入相似度检索。本文介绍的混合方案还会通过关键词分支检索,再将两路依据合并为同一份结果列表。
混合搜索等同于语义搜索吗?
不等同。语义检索可以是混合搜索的组成部分。混合搜索描述的是多种检索信号的组合,并不特指某一种融合算法。
Google 搜索属于语义搜索吗?
Google Search 的排名系统使用了语义理解能力。Google 的排名系统指南介绍了 BERT 和神经匹配。但这并不能证明它的整体架构就是本文描述的 BM25 加向量检索方案。
RAG 和语义搜索有什么区别?
RAG 检索源材料,并在生成答案时使用这些材料。语义搜索是一种可以提供这类材料的检索方法。RAG 应用也可以使用关键词检索或混合检索。
混合搜索是什么,具体如何工作?
在本文的架构中,先对符合条件的内容执行关键词和向量检索,按稳定 ID 合并候选,再用 RRF 或经过明确设计的加权得分方法排序。上线前,使用标注查询评估最终结果。
- 发布日期
- 分类
- Build
- 语言










