Share

外观
风格

搜索不是一个模型的事:BM25、向量和重排

2026年6月3日 · 专栏

封面图

有人在知识库里搜 部署配置。文档里写的是“Nginx 反向代理”,词对不上,什么都没返回。

他换了向量搜索。这下倒是出结果了,但前几条全是似是而非的段落——讲“配置文件最佳实践”“环境变量管理”,就是没讲他要找的那个。

试到第三次,他换了个更接近文档原话的关键词,才终于翻到那一段。

到这一步,大多数人的结论是:embedding 模型不够好,得换个更强的。

但问题往往不在模型。问题在于,他整套搜索只有一层。一个能用的知识库搜索,通常不是靠一个模型解决所有问题,而是分了几道工序:关键词搜索负责不漏精确词,向量搜索负责不漏相近意思,重排负责把最相关的几条挑出来,最后才轮到生成回答。

本文讲清楚这几道工序各自在干什么。至于如何把本地 embedding 模型做成标准 API、接进具体工具,留到下一篇。

关键词搜索很可靠,但它猜不到同义词

最传统的搜索是关键词搜索,典型算法是 BM25。

BM25 不理解句子的意思。它只看几件事:词有没有出现、出现了几次、这个词在整个语料里是不是足够稀有。

正因为它不猜意思,所以特别可靠。搜产品编号、函数名、错误码、文件路径这类精确符号,它通常比任何 AI 模型都稳。

但反过来,前面那个 部署配置 的空振,根子就在这里。文档写“Nginx 反向代理”,你搜“部署配置”,两个词长得不像,BM25 就当它们没关系。它命中的是字面,不是意思。

向量搜索懂意思,却抓不住精确符号

要让搜索“懂意思”,就得引入 embedding。

Embedding 做的事只有一件:把文本变成向量。向量可以理解成一串数字,语义相近的文本,向量距离也相近。

建库和查询,是两条对称的流程:

建库:文档  -> embedding 模型 -> 文档向量 -> 向量库
查询:query -> embedding 模型 -> query 向量 -> 查向量库

这里有个容易搞混的点:embedding 模型不负责“查”。它只负责把文本变成向量。真正的查询,是拿 query 向量去向量库里找距离最近的文档向量——这一步由向量库完成,可以是 SQLite 扩展、Faiss、Milvus、Qdrant,也可以是一个简单的本地索引。

向量搜索补上了 BM25 的盲区:词不一样也能命中。但它有自己的盲区。模型文件名、服务名、API 路径、错误码这类东西更像精确符号,向量搜索对它们不一定稳。前面那个例子里,向量搜索能找回一批“语义相近”的段落,可它分不清哪一条是真正回答了问题——它只保证“沾边”,不保证“对”。

重排不负责召回,只在小候选集里做精判

到这里就轮到第三道工序:reranker,重排模型。

它跟前两者是不同的东西。它不是数据库,也不是向量库,通常没有预先存储的状态。它的输入是 query 加上一批候选文档,输出是每篇文档对这个 query 的相关性分数:

query + 候选文档 1 -> reranker -> 0.91
query + 候选文档 2 -> reranker -> 0.74
query + 候选文档 3 -> reranker -> 0.22

系统再按分数重排。

它和 embedding 的分工很清楚:embedding 适合先把全库变成向量、查询时快速召回一批候选;reranker 适合在候选已经很少时,逐对慢慢判断哪几条最相关。一个负责“找得全”,一个负责“排得准”。这套“先粗召回、再精排”的两阶段思路,正是 2021 年 RocketQAv2 那类工作反复验证的:passage retrieval 和 passage re-ranking 会各自独立地影响最终效果,缺一不可。

三道工序,谁都替代不了谁

开头那个人的反应很典型:搜不准,就想换个更强的模型。这背后是个没说出口的假设——总有一道工序是“最强”的,配齐它就够了。

但把三种“只用一道”摆在一起看,就知道这个假设不成立:每一道单独拎出来,都会露出自己的盲区。

只用 会怎样
纯 BM25 要求你猜中词。文档说“重排”、你搜“reranker”能命中;文档说“候选排序”、你搜“rerank”就不一定。
纯向量 对编号、短代码、精确路径不稳。这些是符号,不是语义。
纯 reranker 扫全库 太贵。它要拿 query 跟每篇文档配对算一遍,十万段文本就是十万次推理,延迟和成本都扛不住。

三道工序的关系不是“择优”,而是“补盲”:BM25 和向量各自堵住对方的盲区,reranker 则只在前两者已经收窄到几十上百条之后,才上场做那笔“贵但值”的精排。

把三道工序串成一个实用流程

把上面的分工落到一条可执行的链路上,比较稳妥的知识库搜索是这样:

query
  -> BM25 取 top 50-100         (不漏精确词)
  -> 向量搜索取 top 50-100       (不漏相近意思)
  -> RRF 合并排序               (把两份名单融成一份)
  -> reranker 重排 top 20-40    (精判)
  -> 取 top 5-10 给回答模型      (只把最靠谱的喂下去)

中间的 RRF 是 Reciprocal Rank Fusion,作用是把多个排序列表合并成一个。它是 2009 年提出的老方法,简单到几行代码就能写完,却一直是混合检索里最常用的融合基线。

这里有个坑值得专门点出来:不要直接把 BM25 分数和向量相似度相加。两个分数根本不是同一种东西,尺度也不一样,相加等于拿苹果加橙子。RRF 之所以稳,是因为它只看名次、不直接相信原始分数——一篇文档在 BM25 里排第 3、在向量里排第 5,它就按这两个名次融合,不管两边的原始分差了几个数量级。

一句话收束:BM25 保证不漏精确词,向量搜索保证不漏相近意思,RRF 把两份结果对齐,reranker 保证最前面那几条真的靠谱。回到开头那个搜了三次的人——他要的不是更强的模型,而是这条少了两道工序的链路。

下一篇:让本地 embedding 模型变成别人能调的 API

这套链路里,向量搜索那一环要跑一个 embedding 模型。下一篇就解决一个很具体的问题:本地已经跑着一个 embedding 模型,怎么把它包装成标准的 /v1/embeddings API,让其它应用直接调用。

会涉及三件事:本地模型怎么起服务、怎么让它在网络上可访问、怎么做访问控制——以及一个常被忽略的前提,客户端到底支不支持远程 embedding backend。

十一、参考链接

(完)