多路召回学习笔记:BM25、Embedding、SPLADE 和 Reranker 各在解决什么

从搜索系统的召回与精排出发,整理 BM25、Embedding、SPLADE、Reranker 的直觉、适用场景和组合方式

最近在补 RAG 和检索系统的基础。刚开始看资料时,最容易混在一起的几个词就是:BM25、Embedding、SPLADE、Reranker,再加一个听起来很工程的词:多路召回。

单独看定义不难,难点在于把它们放进同一条检索链路里。复习时重点回答这些问题:

  • 哪些方法负责“先找一批候选”?
  • 哪些方法负责“把候选重新排好”?
  • BM25 和 Embedding 明明都能搜,为什么还要一起用?
  • Reranker 听起来更准,为什么不直接全库跑?
  • SPLADE 到底是关键词检索,还是语义检索?

这篇笔记不铺满公式,重点讲清每种方法在检索系统里的位置、优势和边界。


速读版

先给一个压缩版结论:

方法系统位置主要解决优点短板
BM25关键词搜索字面精确命中快、可解释、对术语敏感不懂同义表达
Embedding语义近邻搜索意思接近但字面不同泛化强,适合自然语言问题细粒度容易混
SPLADE神经稀疏搜索语义扩展后的关键词匹配兼顾稀疏结构和语义扩展模型和索引更复杂
Reranker精排裁判少量候选谁最相关判断细,通常更准慢,不适合全库扫描

一条典型链路大概是:

1
2
3
4
5
用户 query
    -> BM25 / Embedding / SPLADE 多路召回
    -> 候选合并、去重、分数融合
    -> Reranker 精排
    -> 返回 top-k 或交给下游生成模型

多路召回要解决的是:不要让单一检索方法承担所有召回责任,而是让不同方法互相补盲区。


一、先分清召回和精排

搜索系统一般不会一开始就让最复杂的模型扫描整个文档库。文档量一大,成本会很高。

所以它会分成两步:

1
2
3
4
5
Recall 召回
    从大量文档里快速捞出一批“可能相关”的候选。

Ranking / Reranking 排序或精排
    在候选集合变小之后,再认真比较谁更相关。

召回阶段的目标是“尽量别漏掉相关候选”;精排阶段的目标是“在候选里排得更准”。

BM25、Embedding、SPLADE 通常更适合放在召回阶段;Reranker 通常更适合放在精排阶段。


二、BM25:最经典的关键词检索

BM25 是传统搜索里很经典的方法。它不理解深层语义,也不做推理,主要看一个问题:

query 里的词,在文档里有没有出现?出现的词有没有区分度?

比如用户搜:

1
酒店住宿报销

如果一篇文档里出现了“酒店”“住宿”“报销”,它就应该比完全没出现这些词的文档更相关。

BM25 可以先记成:

1
BM25 分数 ≈ 词频 TF × 逆文档频率 IDF × 文档长度修正

复习时先理解这三个部分,再回到完整公式。

2.1 词频:命中 query 词,说明可能相关

如果 query 里有“住宿”,文档 A 出现了“住宿”,文档 B 没出现,那文档 A 大概率更相关。

但 BM25 不会让词频无限加分。一个词重复出现很多次,并不代表相关性可以无限升高。所以 BM25 会对词频做饱和处理。

2.2 IDF:越少见的词,越有区分度

有些词太常见:

1
申请、说明、费用、管理、流程

它们出现了当然有一点信息,但区分度不强。

有些词更具体:

1
住宿、发票、差旅、押金、手续费

这些词更能帮助判断文档主题。

这就是 IDF 的直觉:一个词在全库里越少见,它越能区分文档。

2.3 文档长度修正:长文档不能天然占便宜

长文档词多,命中 query 词的概率天然更高。如果不修正,长文档会占便宜。

BM25 会考虑文档长度,让“短但精准命中”的文档也有机会排在前面。

2.4 BM25 的位置

BM25 特别适合这些场景:

  • 搜索专有名词
  • 搜索编号、代码、实体名
  • 搜索业务词或固定术语
  • 需要可解释的检索结果

比如:

1
增值税专用发票

如果文档里明确出现这个词,BM25 的表现通常很稳。

它的短板也很明显:不太懂同义词。

1
2
3
住酒店
住宿费
差旅住宿支出

这些表达意思接近,但字面不完全一样。如果没有共同词,BM25 可能就会漏。

复习锚点:BM25 适合作为基础召回,尤其适合术语、编号、实体名;短板是同义表达和口语化改写。


三、Embedding:把文本放进语义空间

Embedding 的思路和 BM25 不同。BM25 看词有没有命中;Embedding 把文本编码成向量。

语义相近的文本,在向量空间里应该更接近。

比如:

1
2
3
员工出差住酒店
差旅住宿费用
酒店房费报销

这些句子字面不同,但语义接近。好的 Embedding 模型会尽量把它们编码到相近的位置。

检索时通常是这样:

1
2
3
4
文档库里的文档 -> 提前编码成向量
用户 query -> 编码成向量
计算 query 向量和文档向量的相似度
返回最相似的 top-k 文档

最常见的相似度是余弦相似度:

1
cosine similarity = 两个向量方向的接近程度

它关心的是方向,而不是文本长度。两个文本语义越接近,向量夹角通常越小,相似度越高。

3.1 Embedding 解决了 BM25 的一部分盲区

用户的问题往往很口语化:

1
出差住酒店怎么报销

文档标题可能是:

1
差旅住宿费用管理办法

BM25 如果没有命中关键分词,可能不够稳;Embedding 更可能从整体意思上判断它们相关。

Embedding 适合:

  • 同义表达
  • 口语化 query
  • 标题和问题表达方式不一致
  • 需要整体语义理解的场景

3.2 Embedding 也会犯“语义太近”的错

Embedding 的问题是,它有时会太模糊。

比如:

1
2
3
4
技术服务费
咨询服务费
软件服务费
会议服务费

从语义上看都和“服务”有关,向量空间里可能靠得很近。但在实际业务里,它们可能需要被分到不同类别。

复习锚点:Embedding 擅长找“语义相近”的内容,但在术语边界、细分类别、编号实体上可能不够精确。因此它常和 BM25 搭配使用。


四、为什么需要多路召回

BM25 和 Embedding 的优缺点互补。BM25 负责词面命中,Embedding 负责语义相近;SPLADE 可以补充神经稀疏召回。多路召回就是同时跑多种召回方法,再合并候选:

1
2
3
4
5
6
BM25 召回一批
Embedding 召回一批
SPLADE 也可以召回一批
    -> 合并候选
    -> 去重
    -> 融合或重排

举个简单例子。

1
query: 出差住酒店怎么报销

BM25 可能找到:

1
2
酒店发票填写要求
住宿费报销单填写说明

Embedding 可能找到:

1
2
差旅费用管理办法
员工出差报销流程

两路结果合起来,比单独用一路更稳。

4.1 分数不能直接相加

多路召回带来一个麻烦:不同方法的分数不是一个尺度。

BM25 可能返回:

1
12.7, 8.3, 3.1

Embedding 的余弦相似度可能是:

1
0.82, 0.77, 0.65

这些分数不能直接相加。

常见处理方法有两类。

第一类是分数归一化,例如 min-max normalization,把不同来源的分数压到相近区间,再加权融合:

1
final_score = 0.5 * norm(BM25) + 0.5 * norm(Embedding)

第二类是只看排名,比如 Reciprocal Rank Fusion。它不太关心原始分数,而是看一个文档在每一路里排第几:

1
RRF(d) = sum(1 / (k + rank_i(d)))

如果一个文档在多路召回里都排得靠前,它的融合分数就会更高。

RRF 的复习锚点:多路召回都排得靠前的文档,融合后应该获得更高优先级。


五、Reranker:候选少了以后,再认真判断

如果 BM25、Embedding、SPLADE 负责先找候选,Reranker 负责在候选变少后做细粒度比较。

Embedding 通常是 bi-encoder:

1
2
3
query 单独编码
doc 单独编码
然后算向量相似度

这种方式快,因为文档向量可以提前算好。

Reranker 通常接近 cross-encoder 思路:

1
2
把 query 和 doc 放在一起
让模型直接判断相关性

例如:

1
2
3
query: 出差住酒店怎么报销
doc: 差旅住宿费用管理办法
score: 0.93

模型可以同时看到 query 和 doc,细粒度判断它们是否匹配。

5.1 为什么 Reranker 不直接全库跑

Reranker 通常更准,但也更慢。

如果文档库有 100 万篇,让 Reranker 对每篇都判断一次:

1
2
3
4
query-doc1
query-doc2
...
query-doc1000000

成本太高。

所以它一般放在召回之后:

1
2
先用 BM25 / Embedding / SPLADE 召回 top50
再用 Reranker 对这 50 个候选重新排序

复习锚点:召回阶段追求快和覆盖,Reranker 阶段追求细粒度相关性判断。

5.2 Reranker 适合处理候选之间的细微差别

比如 query 是:

1
酒店发票抬头填错了怎么办

召回阶段可能找出:

1
2
3
酒店发票填写要求
发票作废与重开流程
差旅住宿费报销规则

这些都相关,但最该排前面的可能是“发票作废与重开流程”。Reranker 就是在这种时候发挥价值。


六、SPLADE:神经网络版的关键词检索

SPLADE 是这几个方法里最容易混淆的一块,可以先这样理解:

SPLADE 试图把 BM25 的稀疏可解释性,和神经模型的语义扩展能力接起来。

BM25 在词项空间工作。文档里有哪些词,就在哪些词上有统计权重。

Embedding 在稠密向量空间工作。每个维度不一定有明确含义,但整体能表达语义。

SPLADE 也在词表空间里工作。假设模型词表有 30,000 个 token,那么一段文本会被表示成一个 30,000 维向量。

不同的是,这个向量很稀疏:

1
2
大部分维度是 0
少数相关 token 维度有正权重

比如用户输入:

1
住酒店

SPLADE 可能不只激活“酒店”,还会激活:

1
住宿、旅馆、房费、差旅

这相当于模型自动帮 query 做语义扩展,但结果仍然落在稀疏词项空间里。

6.1 SPLADE 的激活直觉

很多 SPLADE 实现会对模型输出做类似这样的变换:

1
log(1 + ReLU(x))

拆开看:

  • ReLU(x):负数清零,只保留正向激活。
  • log(1 + x):压缩过大的权重,让数值更稳定。

这样得到的是非负稀疏权重。

检索时可以直接做点积:

1
score(query, doc) = sparse_query_vector · sparse_doc_vector

如果 query 和 doc 在相同或相关 token 上都有权重,分数就会变高。

6.2 SPLADE 放在哪个位置

可以把它放在 BM25 和 Embedding 中间理解:

1
2
3
BM25      :词面匹配,稀疏,可解释
Embedding :语义匹配,稠密,不太可解释
SPLADE    :神经稀疏匹配,有语义扩展,也保留词项空间

SPLADE 吸引人的地方在于,它不像普通 Embedding 那样完全进入黑盒稠密空间,也不像 BM25 那样只依赖原始词面。它可以学习到一些扩展关系。

它的成本也更高:需要专门模型,向量维度很高,索引和部署比 BM25 更复杂。


七、一个完整检索链路可以怎么想

假设用户问:

1
差旅住宿发票丢了还能报销吗

一条比较完整的检索链路可以这样组织:

1
2
3
4
5
6
7
1. BM25 找到包含“差旅”“住宿”“发票”“报销”的文档
2. Embedding 找到语义上接近“发票遗失”“报销材料补充”的文档
3. SPLADE 如果可用,再补一批神经稀疏召回结果
4. 合并候选,去掉重复文档
5. 用归一化加权或 RRF 做候选融合
6. 用 Reranker 对候选重新排序
7. 把 top 文档交给下游问答模型或直接返回

每一步对应一个具体问题:

  • BM25:有没有明确命中关键词?
  • Embedding:语义上像不像?
  • SPLADE:能不能做更聪明的词项扩展?
  • Reranker:候选里谁最贴合用户问题?

八、选型建议

如果只是做一个小型知识库,最简单的起点通常是:

1
Embedding 召回 + Reranker 精排

如果业务词很强,比如法规条款、财务科目、商品型号、错误代码,可以加上 BM25:

1
BM25 + Embedding 多路召回 + Reranker 精排

如果发现 BM25 词面太死、Embedding 又容易把细分类别混在一起,再考虑 SPLADE:

1
BM25 + Embedding + SPLADE 多路召回 + Reranker 精排

不要一开始就把所有东西都加上。检索系统越堆越复杂,调试成本也越高。更稳的做法是先跑通 baseline,再根据错误样本决定补哪一路。


小结

多路召回不是一个单独算法,而是一种系统设计思路:让不同检索方法从不同角度找候选,再用排序模型做最终筛选。

BM25 解决精确词面匹配,Embedding 解决语义泛化,SPLADE 尝试在稀疏词项空间里加入神经语义扩展,Reranker 负责最后的细粒度判断。

以后复习或设计检索链路时,优先问这些问题:

1
2
3
4
5
6
它用了哪些召回路?
每一路分别解决什么盲区?
候选是取并集,还是只在某一路结果里重打分?
不同分数怎么融合?
Reranker 放在多少候选之后?
最终阈值有没有单独校准?

这些问题比单独背概念更有用。检索效果通常不是由某个单点算法决定的,而是由整条链路的召回覆盖、候选融合、精排质量和阈值校准共同决定的。

使用 Hugo 构建
主题 Stack 由 Jimmy 设计