TECH ARTICLES
搜索召回 向量检索 推荐系统

搜索召回的四大主流算法:从倒排索引到多路召回,一篇讲透

Jackie Zhan 2026-06-27
目录
一、召回到底在解一道什么题? 二、倒排索引与 BM25:搜索的地基 三、向量检索:让机器读懂"意思" 四、双塔模型:召回的深度学习答案 五、多路召回:没有银弹,只有合奏 六、写在最后:召回的本质

10 毫秒。

这是淘宝、抖音、Google 这类系统,留给"召回"这一步的全部时间预算。

在这 10 毫秒里,它要做一件听起来不可能的事:从几亿甚至上百亿条候选里,挑出几百到几千条"可能相关"的结果,交给后面的排序模型去精挑细选。

我换个比方你就懂了。这就好比让你在 10 毫秒内,从中国 14 亿人里圈出 500 个"最可能买你这款产品"的人。你不可能挨个看一遍——光是遍历一次就要好几秒。所以召回这一步,从来就不是"找得准",而是"找得快、还不能漏"。

很多人对搜索的理解停在"关键词匹配"。但你有没有想过:为什么你搜"番茄炒蛋",能搜到标题写着"西红柿炒鸡蛋"的菜谱?为什么你刚看完一个露营视频,首页立刻给你推帐篷——可你压根没搜过"帐篷"这个词?

这些"魔法",全发生在召回这一层。今天我就带你把召回阶段的四大主流算法一次拆透:倒排索引与 BM25、向量检索(ANN/HNSW/FAISS)、双塔模型、以及把它们串起来的多路召回。读完你会发现,现代搜索的强大,不是靠某一个绝招,而是靠一场精心编排的合奏。


一、召回到底在解一道什么题?

在拆算法之前,我们得先把题目读懂。否则你会困惑:为什么不直接上最聪明的模型,一步到位排出最好的结果?

答案是:算力不允许。

一个成熟的搜索/推荐系统,通常分成三步走:召回 → 粗排 → 精排。这就像企业招聘——简历来了一万份,你不可能让 CTO 每一份都亲自面。先用关键词海选筛到 500 份(召回),再用初级面试官刷到 50 份(粗排),最后才轮到 CTO 终面(精排)。

每一层的候选越来越少,但用的模型越来越重、越来越准。召回是漏斗最宽的入口,它的任务不是排序,而是"圈定范围"。

关键区别
召回和排序,追求的是两个完全不同的指标。召回关心的是"召回率"——该找回来的有没有漏掉(宁可错抓,不可放过);排序关心的是"精准度"——排在前面的够不够好。所以评价召回好坏,你不能问"第一条准不准",而要问"正确答案,有没有落在我圈的这 500 条里"。

理解了这一点,后面所有算法的设计取舍就都顺了。召回算法的世界里,有一条铁律:能离线算好的,绝不留到线上算。因为线上只有 10 毫秒,而离线你有一整晚。

记住这句话,它是理解倒排索引、向量索引、双塔模型为什么长成这个样子的总钥匙。现在,我们从最古老、也最不该被低估的那一个开始。

二、倒排索引与 BM25:搜索的地基

先问你一个问题:一本书后面的"索引页",是干嘛用的?

你想查"量子纠缠"在哪几页讲过,不会从第一页翻到最后一页,而是翻到书末的索引,找到"量子纠缠 …… 见 87、142、203 页"。直接跳过去就行。

倒排索引(Inverted Index),干的就是这件事,只不过对象是几亿篇文档。

正排 vs 倒排:一次方向的翻转

正常存文档,是"文档 → 包含哪些词":第 1 篇文章包含 [搜索, 召回, 算法]。这叫正排。但用户查询时,给的是词,要的是文档。如果用正排,你得遍历每一篇文章看它含不含这个词——几亿次,慢到没法用。

倒排,就是把这个映射反过来:从"词 → 包含它的文档列表"。

词项 (Term) 倒排链 (Posting List) "召回" doc1 doc7 doc23 "算法" doc1 doc23 doc88 查 "召回 算法" → 两条链求交集 → doc1、doc23
倒排索引:词项指向文档链,查询即对多条链做交/并集

用户搜"召回 算法",系统就取出"召回"和"算法"两条倒排链,做个交集,瞬间得到 doc1、doc23。几亿篇文档的检索,被压缩成了几次链表的合并运算。这就是搜索引擎能"秒回"的物理基础。Elasticsearch、Lucene 这些你耳熟能详的工具,内核全是倒排索引。

BM25:命中之后,谁排前面?

但光"命中"还不够。一篇文章提了"召回"一次,另一篇提了二十次,显然后者更相关。怎么量化?这就是 BM25 出场的地方——它是过去二三十年里,文本相关性打分事实上的工业标准。

BM25 的打分逻辑,你不用记公式,记住三个直觉就够了:

打个比方
BM25 就像一个老练的 HR 看简历。某个关键词反复出现会加分,但刷十遍也没用(词频饱和);越是别人简历里没有的稀缺技能越值钱(IDF);同样一句话,写在一页纸的简历里比塞在十页流水账里更醒目(长度归一化)。

BM25 的优点是:极快、可解释、零训练成本。新上线一个搜索,不用任何数据,BM25 当天就能跑出像样的效果。直到今天,它依然是绝大多数搜索系统的默认基线,也是 RAG 系统里最稳的那一路召回。

但它有一个致命的盲区。你搜"番茄炒蛋",它永远找不到只写了"西红柿炒鸡蛋"的菜谱——因为这两个词,字面上一个字都不沾。BM25 只认字,不认意思。

这道横在"字面"和"语义"之间的鸿沟,我们叫它词汇鸿沟(Vocabulary Mismatch)。要跨过它,搜索必须学会一件全新的事:理解意思。这就要轮到向量检索登场了。

三、向量检索:让机器读懂"意思"

怎么让机器知道"番茄"约等于"西红柿"?

办法是:把每一段文字,通过一个模型(Embedding 模型),映射成一个几百维的数字向量。这个映射有个神奇的性质——意思相近的文本,向量在空间里的位置也相近。"番茄"和"西红柿"的向量几乎重叠,而它俩离"汽车"则隔着十万八千里。

于是,"找语义相关的文档"这个模糊问题,被翻译成了一个无比清晰的数学问题:给定一个查询向量,在几亿个文档向量里,找出离它最近的 K 个。这就是最近邻搜索(Nearest Neighbor Search)。

从"精确"到"近似":ANN 的妥协智慧

问题来了:最朴素的办法,是把查询向量和几亿个文档向量逐个算距离,取最近的——这叫暴力 KNN。准是绝对准,但慢得发指,几亿次浮点运算,根本塞不进 10 毫秒。

所以工业界集体做了一个妥协,叫 ANN(Approximate Nearest Neighbor,近似最近邻)。注意那个"近似"——我们主动放弃"找到绝对最近的"这个执念,只要求"大概率找到很近的那几个"。用一点点精度,换回几百上千倍的速度。

这个取舍,正是召回哲学的完美体现:召回要的从来不是完美,而是又快又不漏。那 ANN 具体怎么做到"既快又准"?最主流的答案,是一种叫 HNSW 的图结构。

HNSW:给向量世界修一套"高速公路网"

HNSW(Hierarchical Navigable Small World,分层可导航小世界)的思路,我用一个你天天在用的东西类比:跳表,或者说地图导航的分层。

想象你要从北京的一个胡同,导航到上海的一条小街。导航不会让你一格一格挪。它分层:

第2层 · 高速 第1层 · 国道 第0层 · 街道 入口 目标
HNSW 分层图:从稀疏的高层"大跳",逐层下沉到稠密底层"精定位"

HNSW 检索时,就是从最高层的入口点出发,贪心地往离目标更近的邻居走,走到头就下沉一层,继续逼近,直到最底层锁定结果。原本要比对几亿次的搜索,被压缩到几百次跳转。这就是它能在毫秒级返回的奥秘。

HNSW 有三个你迟早会调的参数,顺手记一下:M(每个节点连多少邻居,越大越准也越占内存)、efConstruction(建图时的搜索宽度,影响索引质量)、efSearch(查询时的搜索宽度,这是线上调"精度/速度"最常用的旋钮——调大更准更慢,调小更快但可能漏)。

FAISS:把这一切变成工程现实

算法再漂亮,也得有趁手的工具落地。FAISS(Facebook AI Similarity Search)就是这个领域最知名的开源库,Meta 出品,几乎是向量检索的工业默认选项。

FAISS 厉害在它不是一个算法,而是一整套"积木"。除了 HNSW,它还提供另一条主流路线 IVF(倒排文件):先用 k-means 把所有向量聚成几千个簇,查询时只在最近的几个簇里搜(参数 nprobe 控制搜几个簇)——这又是一次"缩小范围"的经典操作。再配上 PQ(乘积量化),把高维向量压缩存储,能让上亿向量塞进一台机器的内存。

踩坑记录
别一上来就认为"HNSW 一定最好"。经验法则是:万级到千万级、内存够、追求高召回率,选 HNSW;数据量极大、内存吃紧、能接受召回率在 85%~90% 封顶,选 IVF-PQ。还有个容易翻车的点——IVF 的簇中心是训练出来的,新向量插入到一定量后,簇会"漂移",召回率悄悄下降,需要定期重建索引;而 HNSW 支持增量插入,运维上更省心。这些都是上线后才会咬你一口的细节。

讲到这,向量检索解决了"理解意思"和"快速找近邻"两件事。但有个根本问题我们一直绕过去了:那个把文本变成"好向量"的模型,到底长什么样、怎么训出来的?这就是双塔模型的主场。

四、双塔模型:召回的深度学习答案

向量检索能成立,前提是有人先把 query 和 document 都变成"靠谱的向量"。在搜索和推荐里,产出这些向量的主力模型,就是双塔模型(Two-Tower Model)

它的结构正如其名,两根柱子:一根是用户塔/查询塔,吃进用户特征或查询词,吐出一个用户向量;另一根是物品塔/文档塔,吃进物品特征,吐出一个物品向量。两个向量算个内积(或余弦相似度),分数高就代表"匹配"。

用户 / 查询塔 用户/Query 特征 DNN 编码层 用户向量 u 物品 / 文档塔 物品/文档 特征 DNN 编码层 物品向量 v 相似度 u · v
双塔模型:两塔独立编码,顶层只用一次内积交互

为什么是"两座分开的塔"?这是天才设计,不是偷懒

你可能会问:让 query 和 document 早点交互、充分纠缠,模型不是更准吗?没错,那种"交叉模型"(Cross-Encoder)确实更准。但它有个致命问题:必须拿到 query 才能算分,因为它的每一层都把 query 和 doc 揉在一起算。

双塔的精髓,恰恰在于它故意不让两塔在中间交互,只在最顶层碰一次内积。这个"克制",换来了一个杀手锏能力:

打个比方
双塔模型像一场"提前命题"的相亲。所有物品的"自我介绍"(物品向量)可以提前一晚全部写好、装进档案库(ANN 索引)。用户一来,只需现场写一句话介绍(算一次用户向量),再去档案库里秒搜最匹配的几百个。而交叉模型像"必须见面才能聊"——用户每来一次,都得和上亿个物品逐一面谈,线上根本算不过来。

看懂这一层,你就理解了整个召回-排序架构的分工逻辑:双塔因为能离线预计算物品向量,所以适合做召回(海量、要快);交叉模型因为更准但算不快,所以留给精排(少量、要准)。还记得第一章那句铁律吗——"能离线算好的,绝不留到线上算"。双塔模型就是这句话的最佳代言人。

训练的胜负手:负样本怎么选

双塔模型怎么学?正样本好找——用户点过、买过的就是正例。难的是负样本:你不能只拿"用户看了没点"的当负例,那些样本太"接近正例",学不出区分度。

工业界最经典的解法叫 In-Batch Negative(批内负采样):在一个训练批次里,把别的样本对应的正例物品,当成当前用户的负例。一个 batch 有 256 个样本,就等于白嫖了 255 个负例,几乎零额外成本。这是 Google 2019 年那篇双塔论文带火的做法,至今是默认配置。

但它有个偏差:越热门的物品越容易出现在 batch 里,就越容易被当成负例,导致模型"打压热门"。于是后来又有了 Mixed Negative Sampling(混合负采样)——批内负例之外,再从全库均匀采一批负例补进来,缓解这个偏差,Google Play 的线上实验证明确有提升。负样本工程,某种意义上比模型结构更决定双塔的成败。

到这儿,四大主角里的三个——倒排/BM25、向量检索、双塔——都登场了。但真实的系统从不只用一个。它们怎么协同?这就是召回的最后一块拼图。

五、多路召回:没有银弹,只有合奏

讲了这么多算法,现在我要泼一盆冷水:没有任何一路召回,能搞定所有情况。

每一种召回都有天生的盲区:

既然各有所长又各有盲区,那答案就很自然了:全都要。让多路召回并行跑,各自捞出一批候选,再汇到一起去重、合并,统一交给粗排。这就是多路召回(Multi-way Recall)

用户请求 Query/User 倒排召回 (BM25) 语义召回 (双塔向量) 协同过滤召回 内容/标签召回 融合 去重 + 合并 → 送入粗排
多路召回:多条通路并行,各取 TopK,汇聚去重后统一送往下游

三路最常见的召回,各管一摊

语义召回,我们前两章已经讲透了——就是用双塔/向量检索那一套,负责"懂意思",跨越词汇鸿沟。它是这几年增长最快的一路。

内容召回,是最朴素也最可靠的一路。基于物品自身的属性、标签、分类来匹配:你看了篇讲"露营"的文章,我就用"露营""户外"这些标签,去倒排索引里捞同标签的内容。它的好处是可解释、对新物品友好(只要打了标签就能被召回)。

协同过滤(Collaborative Filtering),是推荐系统的灵魂一路,它的哲学一句话就能说清:"和你口味相似的人喜欢的,你大概也喜欢",或者反过来,"经常被一起购买的东西,值得一起推荐"。它完全不看物品内容是什么,只看用户行为的共现模式。你刚看完露营视频就被推帐篷,八成就是它干的——因为"看露营的人,后来很多都买了帐篷"。

关键区别
内容召回和协同过滤,常被搞混,但思路截然相反。内容召回看的是"物品像不像"(帐篷和睡袋都属于户外用品);协同过滤看的是"行为像不像"(买帐篷的人也常买防晒霜——哪怕这俩东西八竿子打不着)。前者靠物品属性,后者靠群体智慧。正因为视角不同,它们才能互补补位。

合奏的艺术:召回多路,怎么融合?

多路召回最后要汇到一起。但每一路给的"分数"含义不同——BM25 的分和向量内积的分,根本不在一个量纲上,没法直接比大小。

实务中常见两种做法。一种是配额制:不比分数,给每一路分配固定名额(比如语义路取 300 个、协同路取 200 个、内容路取 100 个),各取各的 TopK,去重后合并。简单、可控、好排查。另一种是统一打分:用 RRF(倒数排名融合)这类方法,只看每个候选在各路里的"排名",而不看原始分数,从而把不同量纲的结果公平地揉到一起。

踩坑记录
多路召回不是"路数越多越好"。每加一路,都意味着多一份计算开销、多一份维护成本,还会让下游粗排的候选池更脏(更多噪声)。我见过团队堆到十几路召回,结果线上效果还不如精简到四路——因为大量重复、低质的候选,反而稀释了真正优质的结果。召回设计的功夫,一半在"召得回",另一半在"舍得砍"。

说到底,多路召回的本质,是一种承认局限的智慧。它不指望某个绝顶聪明的单一算法包打天下,而是让一群各有专长、各有偏见的"专家"分头去找,再用一套机制把它们的意见融合起来。这,才是现代搜索真正的样子。

六、写在最后:召回的本质

我们从 10 毫秒的极限约束出发,走完了召回阶段的四大主流算法。回头看,它们其实是在用四种不同的方式,回答同一个问题:怎么在海量候选里,又快又不漏地圈出对的那一批。

如果要我把整篇浓缩成一句话:召回的进化史,就是搜索从"认字"到"懂意思"再到"懂你"的进化史。倒排认字,向量懂意思,协同过滤懂你。

而真正的高手,从不在这四者里搞"路线之争",非要分出谁取代谁。BM25 没有死,向量也没有杀死它——它们今天就肩并肩跑在同一套多路召回里。承认每个算法的边界,再用工程把它们的优势拼起来,这才是召回工程的成熟。

所以今天给你留一个能动手的挑战:

用 50 行左右的 Python,搭一个最小的"双路召回"玩具。一路用 rank-bm25 库做关键词召回,一路用 sentence-transformers 把文本编码成向量、再用 faiss 建一个 HNSW 索引做语义召回。准备十几条短文本当语料,然后构造两个查询:一个用"近义词不同字"(比如语料里写"自行车",你搜"单车"),一个用"精确术语"(比如某个型号编号)。

跑一遍你会亲眼看到:语义那路能召回"单车→自行车",BM25 那路漏了;而精确术语那路,BM25 稳稳命中,向量反而给你返回一堆"差不多"的噪声。

那一刻,这篇文章里所有的道理,会变成你指尖上的肌肉记忆。看懂多路召回为什么存在,最快的方式,就是亲手让其中一路漏掉一次。