想象你是一家失物招领处的主任。一位失主急匆匆跑来说:“我丢了一部手机,银色的,背面有个被咬了一口的苹果图案。”你手下的两位员工反应完全不同。老陈是个严谨的登记员,他翻开失物登记簿,只找标签上写着“银色”“苹果”“手机”的条目;小蓝是个观察力敏锐的年轻人,他扫了一眼失物架,凭“看起来像是失主描述的那种感觉”快速挑出几个候选。老陈可靠但死板,描述里只要没有登记簿上的原词就会漏;小蓝灵活但含糊,遇到一串具体型号码就发懵。如果只派一个人去找,总有一半的失物找不到。 混合搜索(Hybrid Search)就是招领处里的“组合拳”:让老陈(关键词检索 BM25)和小蓝(向量语义检索)同时出发,各自带回一队候选,再由主任(分数融合与重排序)把两路人马的结果合并成一份最终排名。这套思路正是现代搜索引擎和 RAG(检索增强生成)系统的标配。

岗位关联

对 AI 从业者来说,混合搜索是把“高召回”和“高精度”同时握在手里的核心技能。无论是做 RAG 问答、电商搜索还是企业知识库,掌握两路召回与 RRF 融合,你就能设计出真实可落地的检索系统。

目录
  1. 可视化演示
  2. 两路召回策略:让“核对标签”与“凭印象”同时上阵
  3. 向量检索原理:小蓝的“拍照归档”法
  4. BM25算法:老陈的“登记簿统计法”
  5. 分数融合方法:主任的“排名开会法”
  6. 重排序精排:主任亲自细看
  7. 协同优化:出发前问清楚,路上互相补位
  8. 架构实战:从失物招领处到城市寻物平台
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

动态演示混合搜索Hybrid Sea的核心机制,通过全局/局部/动态/静态四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

混合搜索Hybrid Sea:混合搜索 混合搜索的核心思想与双路召回架 BM25全文检索与向量语义检索 核心机制可视化 混合搜索Hybrid Sea整体架构与关键组件

两路召回策略:让“核对标签”与“凭印象”同时上阵

失主刚说完“银色、被咬了一口的苹果”,老陈已经翻开登记簿去找“银色”“苹果”这几个字了。小蓝则走到失物架前,凭整体印象扫视哪些物品“像是失主描述的东西”。两人各有一套本事,也各有看不见的死角:老陈看到“缺角的苹果”就认不出来,小蓝看到一串产品编号就发懵。聪明的主任当然两个人都派出去,而不是赌其中一个人足够靠谱。

放到搜索系统里,“老陈”就是 BM25 关键词检索,“小蓝”就是向量语义检索。BM25 擅长精确的词面匹配——产品编号、人名、法律条款号这类必须一字不差的查询;向量检索擅长理解语义——“能拍照的智能设备”也能找到“手机”。两者并行执行,各自按自己的打分取回 Top-K 篇文档,合并成一个候选池,这就是两路召回。并行查询架构保证两路互不拖累,一路慢不会让另一路干等。

两路人马带回的候选当然不会立刻答复失主,而是先合并成一份“待确认清单”,交给主任统一裁决。候选池只是第一步,真正的重头戏是后面的分数融合——如果两路结果直接堆在一起展示,排名会乱成一锅粥。

Rhybrid=RBM25(K)∪Rvec(K)\displaystyle R_{\text{hybrid}} = R_{\text{BM25}}^{(K)} \cup R_{\text{vec}}^{(K)}
这个公式说的是:混合候选集 = BM25 取前 K 篇 ∪ 向量检索取前 K 篇。注意这里只是“召回候选”,还没排序。
  1. BM25 候选集老陈按登记簿上的关键词打分,取出分数最高的 K 篇文档,形成“老陈的候选名单”。
    RBM25(K)R_{\text{BM25}}^{(K)}
  2. 向量候选集小蓝按语义相似度打分,取出前 K 篇,形成“小蓝的候选名单”。
    Rvec(K)R_{\text{vec}}^{(K)}
  3. 并集运算把两份名单合并、去掉重复的文档,得到混合候选池。
    ∪\cup
  4. 混合候选集这个池子里既有精确命中的文献,也有语义相近的文档,是后续融合排序的原料。
    RhybridR_{\text{hybrid}}

向量检索原理:小蓝的“拍照归档”法

小蓝为什么能凭印象找人?因为他平时每收进一件失物,就会拍一张“特征照片”存入档案:颜色、形状、质感、标志,全凝练在一张图上。找东西时,他不必逐一描述,只要把失主的印象和档案里的照片比对,看谁“长得像”。照片拍得越好,找得越准;档案越大,比对越慢。

计算机的“特征照片”就是向量(Embedding)。Embedding 模型把一段文本映射成一串数字,例如“手机”变成 [0.1, 0.8, ……],“智能手机”变成 [0.12, 0.79, ……]——语义相近的文本,向量距离也近;“香蕉”的向量则离得很远。检索时,把用户查询也映射成向量,然后在向量空间里找最近的文档向量。衡量“最近”最常用的指标是余弦相似度:计算两个向量夹角的余弦值。

小蓝的照片档案就是向量索引,但每次把查询向量和全部照片比对一遍太慢了,工程上会用 ANN(近似最近邻)算法,只挑一小部分“最可能像的”照片来比对,这才让向量检索能在毫秒级返回。小蓝的“快”和“懂意思”,都是建立在向量索引之上的。

sim(q⃗,d⃗)=q⃗⋅d⃗∥q⃗∥⋅∥d⃗∥=∑i=1nqidi∑i=1nqi2⋅∑i=1ndi2\displaystyle \text{sim}(\vec{q}, \vec{d}) = \frac{\vec{q} \cdot \vec{d}}{\|\vec{q}\| \cdot \|\vec{d}\|} = \frac{\sum_{i=1}^{n} q_i d_i}{\sqrt{\sum_{i=1}^{n} q_i^2} \cdot \sqrt{\sum_{i=1}^{n} d_i^2}}
余弦相似度把两个向量的“方向一致性”压缩成 -1 到 1 之间的数,越接近 1 越相似。它不关心向量的长短,只关心方向是否一致。
  1. 点积计算两个向量逐维相乘再相加。如果两个向量在各维度上都同向,点积就大,说明语义方向一致。
    q⃗⋅d⃗=∑i=1nqidi\vec{q} \cdot \vec{d} = \sum_{i=1}^{n} q_i d_i
  2. 向量模长每个向量各维平方和再开方,代表向量的“长度”。文档长、词多,向量模长通常也大。
    ∥q⃗∥=∑i=1nqi2\|\vec{q}\| = \sqrt{\sum_{i=1}^{n} q_i^2}
  3. 归一化相除点积除以两个模长,消除文档长短带来的偏差,只保留方向信息。
    q⃗⋅d⃗∥q⃗∥⋅∥d⃗∥\frac{\vec{q} \cdot \vec{d}}{\|\vec{q}\| \cdot \|\vec{d}\|}
  4. 值域与判定结果越接近 1,说明两个向量在向量空间里越“同向”,语义越相近,排序时越靠前。
    sim(q⃗,d⃗)∈[−1,1]\text{sim}(\vec{q}, \vec{d}) \in [-1, 1]

BM25算法:老陈的“登记簿统计法”

老陈的核对方式很朴素:登记簿上每件物品都记了一串词,他把失主描述拆成词,然后数每个词在每件物品的登记记录里出现了几次。他还留了一手——如果某个词在整本登记簿里很少见,比如“翻盖”,那它的分量就比“手机”这种烂大街的词重得多。这就是老陈的“字面统计逻辑”。

这套逻辑的学名就是 BM25,它是传统全文检索的经典排序算法。它给每个查询词算两笔账:词频(这个词在文档里出现多少次)和逆文档频率(这个词在全库中多稀有)。BM25 的前身是 TF-IDF,但 BM25 多了两个重要改进:词频饱和(一个词出现 3 次和 30 次,收益增长越来越小)和文档长度归一化(长文档里出现 2 次,含金量低于短文档里出现 2 次)。

老陈这套“只信字面”的打分方式在精确匹配上无懈可击,但失主说“被咬了一口的苹果”,登记簿上写“缺角的苹果”,老陈就彻底愣住了。这正是小蓝要补位的时候——但老陈的价值依然不可替代,因为真实检索里到处是需要一字不差的查询。

score(D,Q)=∑i=1nIDF(qi)⋅f(qi,D)⋅(k1+1)f(qi,D)+k1⋅(1−b+b⋅∣D∣avgdl)\displaystyle \text{score}(D, Q) = \sum_{i=1}^{n} \text{IDF}(q_i) \cdot \frac{f(q_i, D) \cdot (k_1 + 1)}{f(q_i, D) + k_1 \cdot \left(1 - b + b \cdot \frac{|D|}{\text{avgdl}}\right)}
IDF(qi)=ln⁡(N−n(qi)+0.5n(qi)+0.5+1)\displaystyle \text{IDF}(q_i) = \ln\left( \frac{N - n(q_i) + 0.5}{n(q_i) + 0.5} + 1 \right)
BM25 的分值由两部分构成:词的稀有度(IDF)乘以词频的“饱和贡献”。k1 控制饱和速度,b 控制文档长度的惩罚强度。
  1. 遍历查询词把查询拆成词后,每个词对文档的贡献累加起来,得到文档的总分。
    ∑i=1nIDF(qi)\sum_{i=1}^{n} \text{IDF}(q_i)
  2. 逆文档频率包含该词的文档数越少,IDF 越大。罕见词一旦命中,权重非常高。
    IDF(qi)=ln⁡(N−n(qi)+0.5n(qi)+0.5+1)\text{IDF}(q_i) = \ln\left( \frac{N - n(q_i) + 0.5}{n(q_i) + 0.5} + 1 \right)
  3. 词频饱和函数词频 f 越大,分子分母同时变大,分数增长趋缓,防止某个词刷屏主导排序。
    f⋅(k1+1)f+k1⋅(1−b+b⋅∣D∣avgdl)\frac{f \cdot (k_1 + 1)}{f + k_1 \cdot (1 - b + b \cdot \frac{|D|}{\text{avgdl}})}
  4. 长度归一化文档长度 |D| 除以平均长度 avgdl,比值越大,分母越大,长文档的词频被适度打折。
    ∣D∣avgdl\frac{|D|}{\text{avgdl}}

分数融合方法:主任的“排名开会法”

两路人马各带回五件候选,主任犯了难:老陈报的是“登记分”,动辄上百分;小蓝报的是“印象分”,0 到 1 之间。两本账的尺子完全不一样,直接相加等于让身高用厘米、体重用公斤的人比谁数字大,荒唐。主任想了个办法:不问分数,只看名次。老陈那里排第 1 的,小蓝那里排第 3 的,各按名次折算成分数,再相加。

这个“排名开会法”的学名是 RRF(Reciprocal Rank Fusion,倒数排名融合)。它的核心思路是:完全无视各路原始分数的大小,只利用排名次序。某文档在某路排名第 r,就贡献 1/(k+r) 分,k 是平滑常数,通常取 60。把两路的贡献加在一起就是融合分。另一种常见方案是加权线性融合:先把两路分数各自归一化到 [0,1],再按权重 α 和 1−α 加权求和。归一化公式好写,但分数分布不均匀时容易失真,RRF 则天然免疫这个问题。

主任的开会法有个妙处:两路人马不必统一评分口径,只要各自的排名是可信的,融合结果就稳健。RRF 也因此成为工程界最常用的融合方案——不挑食、不调参、鲁棒性好。融合完成,候选名单有了统一顺序,但主任还想再仔细看看前几名。

RRF(d)=∑r∈R1k+r(d)\displaystyle \text{RRF}(d) = \sum_{r \in R} \frac{1}{k + r(d)}
RRF 的输入是两路文档的排名列表,输出是每篇文档的融合分。它不需要知道“分是怎么打的”,只需要知道“排第几”。
  1. 排名列表集合R 是所有参与融合的排序列表,比如老陈的 BM25 排名表和小蓝的向量排名表。
    R={rankBM25,rankvec}R = \{ \text{rank}_{BM25}, \text{rank}_{vec} \}
  2. 取排名r(d) 表示文档 d 在某一路中的名次,第 1 名、第 2 名……依次取值。
    r(d)r(d)
  3. 倒数折算1 除以 (k + r)。名次越靠前,得分越高;k=60 让相邻名次的分差平滑,不至于第一名垄断。
    1k+r(d)\frac{1}{k + r(d)}
  4. 累加融合把文档在所有路中的倒数得分加起来,总分越高,最终排名越靠前。
    ∑r∈R1k+r(d)\sum_{r \in R} \frac{1}{k + r(d)}

重排序精排:主任亲自细看

融合后的名单排好了,但主任没有直接答复失主。他总觉得,光靠登记词和印象照片还不够稳妥——万一某件失物在两条路上都排得靠前,但其实是个“看起来像、实际不对”的冒牌货?于是主任决定:把前几名候选亲手拿起来,对着失主的描述逐项细看,确认无误后才下单答复。

这个“亲手细看”的环节,对应检索系统的双阶段架构。第一阶段是召回:BM25 和向量检索并行,快速从海量文档中捞出几百个候选,要求高召回、低延迟;第二阶段是重排序(Rerank):用交叉编码器模型,把查询和每篇候选文档拼成一个输入序列,让模型同时看到两者后输出一个相关性分数。交叉编码器比向量检索慢几个数量级,所以只能对融合后的 Top-N 个小集合执行。

主任的细看确实让最终结果更可靠,但他也知道,如果前几名里压根没有失主的手机,再仔细看也是白费功夫。所以精排的前提是召回足够好——这正是混合搜索存在的意义。流程走到这里,招领处还想继续优化效率。

sj=fθ(q,dj)\displaystyle s_j = f_{\theta}\left(q, d_j\right)
交叉编码器把查询 q 和候选文档 d_j 拼接后整体编码,输出一个标量相关性分数,用来替换融合阶段的粗略排序。
  1. 查询文本q 是用户输入,对应失主的原始描述,如“银色、被咬了一口的苹果手机”。
    qq
  2. 候选文档d_j 是融合后进入精排名单的第 j 篇文档,对应某一件失物的完整登记记录。
    djd_j
  3. 交叉编码器f_θ 是一个深度模型,把 q 和 d_j 拼接成一个序列做全交互编码,而不是像向量检索那样各自独立编码。
    fθ(q,dj)f_{\theta}(q, d_j)
  4. 精排分数s_j 是模型输出的相关性分数,按它重新排序,取最终 Top-k 交给下游(比如生成答案的 LLM)。
    sjs_j

协同优化:出发前问清楚,路上互相补位

招领处运营了一段时间,主任发现还有改进空间。失主一进门就喊“我手机丢了”,信息太模糊,老陈和小蓝都要大海捞针;登记簿按时间顺序堆放,老陈翻起来费劲;小蓝的照片库里全是相似的白手机,他很容易全队都挑成“白手机”。于是主任立了新规矩:接案时先引导失主补充特征,登记簿按物品类别排架,两路人马还要在出发前对一下各自容易漏的盲区。

工程里的对应招式有三板斧。第一是查询改写:用 LLM 把“苹果手机”扩写成“iPhone + 苹果手机 + 智能手机”,喂给两路检索,让原本只命中一方的词变成两方都能命中。第二是索引优化:BM25 依赖倒排索引(词 → 文档列表)加速词面定位,向量检索依赖 ANN 索引(如 HNSW)加速近邻查找,两条链路并行执行、互不阻塞。第三是召回多样性控制:融合结果不能全是被向量路垄断的“语义相似族”,也要保证字面不同但相关的文档进得来。

这套协同优化让老陈和小蓝配合得越来越默契:一个按字面兜底,一个按语义扩容,查询改写在出发前就把“问清楚”做掉了。整个招领处流程基本成熟,主任决定把它打包成一个全城都能用的平台。

q′=LLM(q,instruction)\displaystyle q' = \text{LLM}\left(q, \text{instruction}\right)
这个公式表示查询改写:输入原始查询和一个“请扩展同义词”的指令,大模型输出改写后的查询,再分发给两路检索。
  1. 原始查询q 是用户最先输入的模糊描述,比如“手机丢了”。
    qq
  2. 指令模板instruction 是预先写好的改写提示,比如“请扩展同义词、补充相关概念”。
    instruction\text{instruction}
  3. 大模型改写LLM 根据指令生成一个信息更丰富的查询,让两路检索都有更多线索可用。
    LLM(q,instruction)\text{LLM}(q, \text{instruction})
  4. 改写结果q' 是增强后的查询,同时喂给 BM25 和向量检索,两路召回质量和多样性随之提升。
    q′q'

架构实战:从失物招领处到城市寻物平台

流程固化后,主任把招领处升级成了“全城寻物平台”:市民在 App 里描述丢失物品,系统自动完成两路召回、RRF 融合、精排,几百毫秒内返回候选。这个平台就是 RAG 系统里最核心的检索层——用户在问答框提问,系统检索知识库,把 Top-k 文档拼接给 LLM 生成答案。但平台上线后不能只看“感觉变好了”,要有体检指标。

最重要的体检指标之一就是召回率:查询一批测试问题,人工标注每道题的“标准答案文档”,然后看系统返回的前 K 条里有多少比例包含了正确答案。同时还要盯延迟:召回阶段可以用 ANN 把向量路压到毫秒级,精排阶段只做 Top-50 控制成本。离线评估用 Recall@K,在线观察点击率和用户满意度,两者结合才能判断混合搜索调得好不好。

平台运转一段时间后,主任手里的评估数据越来越丰富:哪个查询词在 BM25 路表现好、哪类语义描述在向量路更准、精排模型在哪些 case 上误判,都成了下一轮调优的依据。这就是混合搜索的完整闭环:两路召回互补,融合与精排兜底,数据驱动持续迭代。

Recall@K=∣G∩RK∣∣G∣\displaystyle \text{Recall@}K = \frac{|G \cap R_K|}{|G|}
Recall@K 衡量“正确的 K 条结果里,有多少相关文档被找回来了”。它是检索系统召回能力的核心指标。
  1. 标准答案集合G 是人工标注出的全部相关文档,相当于失主最后确认“这几件才是我的东西”。
    GG
  2. 系统返回结果R_K 是系统返回的前 K 条候选,对应平台上展示给用户的候选列表。
    RKR_K
  3. 交集计算G ∩ R_K 是前 K 条里真正相关的那几条,交集越大,说明系统的候选越靠谱。
    G∩RKG \cap R_K
  4. 比例归一除以 |G| 得到“相关文档被找回的比例”,值越大,召回能力越强。
    ∣G∩RK∣∣G∣\frac{|G \cap R_K|}{|G|}

AI 应用场景

RAG 企业知识库问答

阶段:检索增强生成(RAG)的检索阶段

解决的问题:员工问“我们这个月加班费怎么算”,知识库里存放的是《月度加班薪酬核算办法(2024版)》,字面上只有“加班薪酬”没有“加班费”,纯 BM25 容易漏;但员工问“文件编号 HR-D-1024”时,纯向量检索又容易把编号记混。混合搜索让两类问题都稳:BM25 锁死编号,向量理解同义表达。

具体案例:某车企内部知识库接入混合检索后,先 BM25+向量双路召回,再 RRF 融合,最后用 Rerank 精排,Top-5 文档拼接 Prompt 喂给 LLM 生成回答。上线后检索命中率提升约 35%,“口语化提问”的答复质量明显改善。

电商商品搜索

阶段:搜索链路的召回与粗排阶段

解决的问题:用户搜“通勤小白鞋”,既要 BM25 精确匹配商品标题里的“小白鞋”,又要向量理解“舒适、百搭、运动鞋”这类描述;搜“iPhone 15 256G 蓝色”则必须精确匹配型号。单一检索无法同时满足两种查询意图。

具体案例:某跨境电商平台把 BM25 与向量检索并行执行,RRF 融合两路结果。结果显示:“意图模糊类”查询的点击率提升 18%,而“精确型号类”查询的点击率没有回落,长尾流量被有效激活。

医疗与法律文档检索

阶段:专业知识库的智能检索

解决的问题:医生问“糖尿病患者的用药指南”,需要语义召回“二甲双胍使用建议”;律师查询“合同法第 52 条”,必须精确匹配条款号。医疗和法律场景里,自然语言描述与规范编号并存,混合搜索是刚需。

具体案例:某法律科技公司为律所构建混合检索系统:BM25 处理条款号、判例编号,向量处理案情自然语言描述,RRF 融合后排序,显著提升了跨类别查询的检索准确率。

Python 演示

python94 行
import math # 一、语料库:每篇文档已用空格分词corpus = {    "docA": "混合 搜索 结合 关键词 检索 与 向量 检索",    "docB": "向量 检索 利用 语义 向量 计算 相似度",    "docC": "关键词 检索 精确 匹配 词频 统计",    "docD": "RRF 倒数 排名 融合 两路 检索 结果",    "docE": "重排序 模型 精排 候选 文档 提升 精度",} def tokenize(text: str) -> list:    """将文本按空格拆成词列表"""    return text.split() # 二、离线统计:文档频率 DF 与平均长度docs_tokens = {doc_id: tokenize(text) for doc_id, text in corpus.items()}N = len(docs_tokens)df = {}for tokens in docs_tokens.values():    for word in set(tokens):        df[word] = df.get(word, 0) + 1avgdl = sum(len(tokens) for tokens in docs_tokens.values()) / N def compute_idf(word: str) -> float:    """逆文档频率:越稀有权重越高"""    return math.log((N - df.get(word, 0) + 0.5) / (df.get(word, 0) + 0.5) + 1) # 三、BM25 打分(老陈的登记簿算法)def bm25_score(query_terms: list, doc_tokens: list, k1: float = 1.5, b: float = 0.75) -> float:    dl = len(doc_tokens)    tf = {}    for word in doc_tokens:        tf[word] = tf.get(word, 0) + 1    score = 0.0    for q in query_terms:        if q not in tf:            continue        idf = compute_idf(q)        f = tf[q]        score += idf * (f * (k1 + 1)) / (f + k1 * (1 - b + b * dl / avgdl))    return score # 四、向量检索:TF-IDF 词向量 + 余弦相似度(小蓝的特征照片)def tfidf_vector(tokens: list) -> dict:    vec = {}    for word in set(tokens):        vec[word] = tokens.count(word) * compute_idf(word)    return vec def cosine_similarity(vec1: dict, vec2: dict) -> float:    common = set(vec1) & set(vec2)    dot = sum(vec1[w] * vec2[w] for w in common)    norm1 = math.sqrt(sum(v * v for v in vec1.values()))    norm2 = math.sqrt(sum(v * v for v in vec2.values()))    return dot / (norm1 * norm2) if norm1 and norm2 else 0.0 # 五、RRF 融合(主任的排名开会法)def rrf_fuse(ranked_lists: list, k: int = 60) -> list:    fusion = {}    for ranked in ranked_lists:        for rank, doc_id in enumerate(ranked, start=1):            fusion[doc_id] = fusion.get(doc_id, 0.0) + 1.0 / (k + rank)    return sorted(fusion.items(), key=lambda item: item[1], reverse=True) # 六、混合主流程:两路召回 -> RRF 融合 -> 简易重排def hybrid_search(query: str, top_k: int = 3) -> list:    q_terms = tokenize(query)    bm25_rank = sorted(        docs_tokens,        key=lambda doc_id: bm25_score(q_terms, docs_tokens[doc_id]),        reverse=True,    )    q_vec = tfidf_vector(q_terms)    vec_rank = sorted(        docs_tokens,        key=lambda doc_id: cosine_similarity(q_vec, tfidf_vector(docs_tokens[doc_id])),        reverse=True,    )    fused = rrf_fuse([bm25_rank, vec_rank])    # 简易重排:用查询词在文档中的覆盖比例微调顺序,模拟 Rerank 阶段    def coverage(doc_id: str) -> float:        words = set(docs_tokens[doc_id])        return len(set(q_terms) & words) / max(len(set(q_terms)), 1)    fused.sort(key=lambda item: coverage(item[0]), reverse=True)    return fused[:top_k], bm25_rank, vec_rank if __name__ == "__main__":    query = "语义 相似 检索"    results, bm25, vec = hybrid_search(query)    print("Query:", query)    print("BM25 召回:", bm25)    print("向量召回:", vec)    print("RRF融合:", [doc for doc, _ in results])
目的:用不到 80 行代码实现一个完整的混合搜索链路——BM25 召回、向量召回(TF-IDF 向量近似)、RRF 融合和简易重排,跑通“两路取长补短”的完整流程。
  1. df[word] = df.get(word, 0) + 1统计每个词出现在多少篇文档中,这是 IDF 计算的原料
  2. return math.log((N - df.get(word, 0) + 0.5) / (df.get(word, 0) + 0.5) + 1)罕见词的 IDF 更高,权重更大
  3. score += idf * (f * (k1 + 1)) / (f + k1 * (1 - b + b * dl / avgdl))BM25 的核心累加:稀有度 × 词频饱和值
  4. vec[word] = tokens.count(word) * compute_idf(word)用 TF-IDF 构建文档的“特征照片”(真实系统会换成 Embedding 模型)
  5. return dot / (norm1 * norm2) if norm1 and norm2 else 0.0余弦相似度,衡量两个向量的方向一致性
  6. fusion[doc_id] = fusion.get(doc_id, 0.0) + 1.0 / (k + rank)RRF 的灵魂:每路排名按倒数折算后累加
  7. bm25_rank = sorted(docs_tokens, key=..., reverse=True)老陈这一路按 BM25 分数排序
  8. vec_rank = sorted(..., reverse=True)小蓝这一路按余弦相似度排序
  9. fused = rrf_fuse([bm25_rank, vec_rank])主任把两路排名合并成统一分数
  10. fused.sort(key=lambda item: coverage(item[0]), reverse=True)简易重排:查询词覆盖度越高的文档排越前

常见误区

误区把 BM25 返回的前 10 篇和向量检索返回的前 10 篇直接合并,去掉重复的就展示给用户,以为这就是混合搜索。我一开始就是这么写的,感觉候选都在了呀。

正解两路的分数尺度完全不同:BM25 的得分理论上无上限,余弦相似度只有 -1 到 1。直接合并会导致排名被分数高的一路主导,另一路即使很相关也被挤到后面看不见。正确做法是用 RRF 或归一化加权融合,把两路的排名统一打分后再排序,而不是简单取并集。

为何常见:很多人从“多路召回”的视角理解混合搜索,以为只要候选都在就万事大吉,忽略了“排序”才是检索系统的关键环节。

误区RRF 就是把 BM25 的分数乘以 0.5、向量分数乘以 0.5 再相加,权重可以自己调。反正都是融合,应该差不多。

正解RRF 根本不看原始分数,只看排名次序:某文档在某路排第 2,就贡献 1/(60+2)。它不需要归一化,也不用调权重,天然规避了两路分数不可比的问题。加权线性融合才是先归一化再按 α 加权的做法,两者是两种完全不同的思路。

为何常见:名字里都有“融合”,而且 RRF 公式里也有求和符号,容易让人误以为它和加权求和是一回事。

误区语义检索这么聪明,BM25 这种“数词频”的老办法该淘汰了。反正 Embedding 能理解意思,关键词检索没存在的必要。

正解真实场景里有大量必须精确匹配的需求:产品型号 “iPhone 15 Pro“、订单号 ”ORD-2024-001“、法律条款”第 52 条“。这些硬标识在语义空间里很容易混淆,”iPhone 14“和”iPhone 15”的向量距离极近,但价格和功能天差地别。BM25 在精确、稀有、长尾词条上的可靠性远高于向量检索,两者是互补关系而不是替代关系。

为何常见:向量检索的演示效果往往聚焦“同义改写也能搜到”,看起来很神奇,让人忽略了真实系统里大量存在的硬匹配场景。

误区召回阶段只要大概捞一批文档,最后重排序模型会把真正的答案提到前面。所以召回质量不用太在意,重排才是重点。

正解重排序只对“已经回来的候选”排序。如果 Top-K 召回里根本没有正确答案,精排模型再强也变不出来。混合搜索的意义正是在召回阶段提高“正确答案被捞进来”的概率,Rerank 是在此基础上精挑细选。工程里“召回不足”是检索系统翻车的第一大原因。

为何常见:Rerank 在公开榜单上效果惊艳,让人误以为它是万能补偿器,忽略了精排的输入天花板由召回决定。

误区给系统加上 BERT 重排、LLM 查询改写、多模型融合,效果一定更好。技术堆得越高,结果越厉害。

正解每加一个模块就多一层延迟和成本。轻量场景下“BM25 + 向量 + RRF”已经够用;只有候选量大、相关性要求高时才值得上交叉编码器。工程师要按业务指标(Recall@K、P99 延迟、机器成本)做取舍,而不是无脑堆模型。

为何常见:技术文章总在讲“更先进的方案”,容易让人误以为复杂 = 更好,忽略了工程里最重要的性价比权衡。

面试题

Q1. 混合搜索把哪两种检索方式结合起来?

单选题 · easy

  • A.关键词检索与向量检索← 正确答案
  • B.缓存检索与日志检索
  • C.正则检索与模糊检索
  • D.标签检索与统计检索

答案:A。混合搜索的核心是“词面精确匹配 + 语义向量匹配”两路互补。B、C、D 里的组合都不是工业检索的主流组件。就像招领处同时派出老陈(按登记词核对)和小蓝(凭印象扫描),而不是派两个同类型的人。

Q2. BM25 评分中 IDF 的作用是什么?

单选题 · easy

  • A.衡量词语在文档库的稀有程度← 正确答案
  • B.衡量词语在文档中的出现频率
  • C.衡量文档本身篇幅的长短
  • D.衡量查询包含词语的多少

答案:A。IDF(逆文档频率)看的是“全库有多少篇文档包含这个词”,越稀有的词权重越高。“手机”常见所以权重低,“翻盖”罕见所以权重高。B 描述的是词频 TF,C 是长度归一化,D 是查询长度,都和 IDF 无关。

Q3. 向量检索最常使用的相似度度量是什么?

单选题 · easy

  • A.余弦相似度计算← 正确答案
  • B.编辑距离计算
  • C.哈希碰撞计算
  • D.正则匹配计算

答案:A。向量检索把文本映射成向量后,用余弦相似度衡量方向一致性,值越接近 1 语义越相近。编辑距离用于字符串差异,哈希碰撞用于去重,正则匹配用于模式查找,都不是向量空间的度量。

Q4. 某文档在 BM25 路排名第 2、向量路排名第 4,RRF(k = 60)总贡献约为多少?

单选题 · extreme

  • A.约0.017
  • B.约0.032← 正确答案
  • C.约0.048
  • D.约0.065

答案:B。RRF 每路贡献 1/(k+排名)。BM25 路贡献 1/(60+2) ≈ 0.0161,向量路贡献 1/(60+4) ≈ 0.0156,相加约 0.032。这就是主任开会法的手算版本:不问分数,只看排名折算。

Q5. 加权线性融合前必须完成哪一步?

单选题 · mid

  • A.两路分数归一化到同一尺度← 正确答案
  • B.两路文档列表分别去重
  • C.查询语句进行扩展改写
  • D.融合权重进行随机初始化

答案:A。加权融合要求两路分数可比,否则 BM25 的百分制分数会碾压余弦的 0~1 分数。所以必须先归一化。去重是召回后的事,查询改写是召回前的优化,随机初始化权重是错误做法。

Q6. 重排序使用交叉编码器的核心原因是什么?

单选题 · hard

  • A.查询与文档可以深度交互建模← 正确答案
  • B.文档向量可以离线批量缓存
  • C.完全不需要标注训练数据
  • D.相比向量检索计算量更小

答案:A。交叉编码器把查询和文档拼成一个序列整体编码,让两者充分交互,精度远高于双塔式向量检索。代价是计算量大,所以只能对 Top-N 候选做精排。B 是双塔模型的优势,C 不对,D 说反了。

Q7. RRF 相比加权融合的主要优势是什么?

单选题 · mid

  • A.无需归一化各路原始分数← 正确答案
  • B.直接使用各路原始分相加
  • C.可以完全消除评分误差
  • D.只需一路召回结果即可

答案:A。RRF 只看排名次序,天然规避了各路分数尺度不同的问题。加权融合必须先归一化。B 是错误做法,C 夸大了任何融合方法的能力,D 与 RRF 需要多路输入的前提矛盾。

Q8. 一个典型混合搜索模块包含哪些组件?

多选题 · mid

  • A.BM25关键词检索模块← 正确答案
  • B.向量相似度检索模块← 正确答案
  • C.排名融合打分模块← 正确答案
  • D.用户点击日志模块

答案:ABC。混合搜索的三大件是 BM25 检索、向量检索和融合打分。点击日志是搜索系统做效果分析的数据源,但不是混合搜索模块本身的组成部分。就像招领处需要登记员、侦察员和主任,但不需要记录每天谁在门口张望。

Q9. 关于 RRF 融合,下列说法正确的是?

多选题 · hard

  • A.输入是各路候选的排名次序← 正确答案
  • B.排名越靠前贡献分值越大← 正确答案
  • C.需要先归一化各路的分数
  • D.第二名的贡献恒低于第一名← 正确答案

答案:ABD。RRF 的输入是排名列表,每路按 1/(k+rank) 折算,排名越靠前分越高;因为 k+2 > k+1,所以第二名贡献严格小于第一名,D 正确。C 是 RRf 不需要做的事,恰恰是它相对加权融合的优势。

Q10. 生产环境里,哪些做法能有效提升混合搜索效果?

多选题 · hard

  • A.查询改写补充同义近义词← 正确答案
  • B.用交叉编码器执行重排← 正确答案
  • C.关闭一路只保留词面检索
  • D.平衡两路召回候选数量← 正确答案

答案:ABD。查询改写让两路检索都能命中更多,交叉编码器精排提升精度,平衡两路 Top-K 数量避免某一路垄断。C 等于退化成单路检索,与混合搜索的初衷相悖。