本文是 搜推算法笔记 第 2 篇。索引见 搜推算法 · 知识索引。
「搜不到」在工程上常有两类根因:Query 层(输入脏、错、歧义)和召回层(覆盖不够、路数缺失)。例如用户输入「蓝芽耳机」——Query 有错别字;搜「纯奶」但标题写「纯牛奶」——字面召回覆盖不足。两者表象相同,排查路径不同。
Query 理解和召回是搜索链路的上下游:前者把脏输入变成干净、可扩展的信号;后者用这些信号从全库捞出候选池。本文将两层合并讲解,因其在工程上总是紧挨着发生。
一、Query 理解:把脏输入变干净
1.1 用户输入的常见问题
| 问题 | 例子 | 不处理的后果 |
|---|---|---|
| 错别字 | 蓝芽耳机 | 召回失败/偏移 |
| 同义表达 | 纯奶 / 纯牛奶 | 漏召 |
| 过短/过宽 | 「奶」「手机」 | 结果太散 |
| 过长口语 | 「我想买个送女朋友的红色围巾」 | 噪声词干扰 |
| 类目混淆 | 「苹果」水果还是手机? | 错类目硬过滤会空 |
1.2 标准流水线
1 | 原始 Query |
五条设计原则:
- 先确定性、后统计/模型(清洗永远先做)
- 先纠错、后改写(避免错词扩散)
- 低置信不改(误伤成本 > 漏纠)
- 改写要有上限(防延迟与噪声爆炸)
- 类目慎用硬过滤(预测错就空结果)
1.3 文本清洗
清洗是规则系统,追求稳定、可解释、零误伤:
| 步骤 | 例子 | 说明 |
|---|---|---|
| Trim / 合并空白 | " 红 牛 " → "红牛" | 视业务是否保留中间空格 |
| 大小写统一 | iPhone → iphone | 英文检索常用 |
| 全角转半角 | A → A | 中文输入法常见 |
| 标点处理 | 去掉无意义符号 | 保留型号里的 -、+ |
| 长度截断 | 最长 30~64 字 | 防恶意超长 |
电商 SKU 字段里型号常含 -、+,清洗时不能一刀切去掉。
1.4 纠错
噪声信道模型:(q^* = \arg\max_{c} P(c) \cdot P(q \mid c))
| 方法 | 中文场景 | 英文场景 | 局限 |
|---|---|---|---|
| 编辑距离 | 通用 | 通用 | 候选爆炸 |
| 拼音纠错 | 核心(蓝芽→蓝牙) | 不适用 | 多音字 |
| 键盘距离 | 弱 | 核心(teh→the) | 移动端差异 |
| 语言模型 | 整句纠错 | 整句纠错 | 计算贵 |
| 日志共现 | 补充信号 | 补充信号 | 冷启动无日志 |
工程流水线:检测 → 候选生成 → 多信号打分 → 阈值决策。误纠通常比漏纠更伤——品牌、型号、SKU 词进保护表。
1.5 改写
用户搜「纯奶」,标题可能是「新鲜纯牛奶 200ml」。改写产出 ["纯奶", "纯牛奶", "牛奶"],供下游多 Query 召回。
| 来源 | 覆盖率 | 噪声风险 | 适用阶段 |
|---|---|---|---|
| 同义词典 | 低 | 低 | 冷启动、核心品类 |
| 删除改写 | 中 | 低 | 长尾口语 Query |
| 日志挖掘 | 高 | 中 | 有流量后 |
| 向量近邻 | 高 | 高 | 需人工审核 |
| LLM 生成 | 很高 | 很高 | 辅助 + 人工审核 |
LLM 改写是 2024–2026 的新选项,工业界很少全自动——「牛奶」扩成「奶粉、奶茶、奶牛」的噪声太常见。
1.6 意图与类目
| 任务 | 输出 | 用途 |
|---|---|---|
| 意图分类 | 找商品/找店铺/找活动 | 路由 |
| 类目预测 | 一级/叶子类目 | 召回约束、特征 |
| 实体识别 | 品牌、型号、属性 | 结构化召回 |
硬过滤 vs 软约束:
| 策略 | 做法 | 风险 |
|---|---|---|
| 硬过滤 | 只保留预测类目商品 | 类目预测错 → 空结果 |
| 软约束 | 不删候选,调分或当 LTR 特征 | 可能混入跨类目 |
类目模型不够准时,优先软约束。「苹果」消歧靠用户历史、多意图召回 + 精排。
1.7 端到端例子
输入: 蓝芽耳机性价比高
1 | ① 清洗 → "蓝芽耳机性价比高" |
二、召回层:把候选捞进池子
召回决定上限——候选池里没有的东西,精排再强也排不出来。
2.1 召回层目标
[
R(q,u,c) \subset I,\quad |R| = K \ll |I|
]
| 目标 | 含义 |
|---|---|
| 覆盖好物品 | 相关/会转化的进池(Recall@K) |
| 控制噪声 | 池子别太脏 |
| 快 | P99 在延迟预算内 |
| 容错 | 一路挂了不至于全空 |
2.2 倒排召回与 BM25
1 | 倒排:term → [(doc_id, tf), ...] |
[
\mathrm{BM25}(q,d)=\sum_{t\in q}
\mathrm{IDF}(t)\cdot
\frac{f(t,d)\cdot(k_1+1)}{f(t,d)+k_1\cdot\bigl(1-b+b\cdot\frac{|d|}{\mathrm{avgdl}}\bigr)}
]
词越稀有 IDF 越大;词频加分但饱和;长文档要惩罚。ES 默认就是 BM25,参数调优见 ES 相关度评分。
IDF 从 TF-IDF 到 BM25
经典 TF-IDF 的 IDF:
[
\mathrm{IDF}_{\mathrm{TF-IDF}}(t)=\log\frac{N}{n_t}
]
BM25 改用带平滑的 IDF(Lucene 实现):
[
\mathrm{IDF}_{\mathrm{BM25}}(t)=\log\frac{N-n_t+0.5}{n_t+0.5}
]
差异直觉:
- (n_t \to N)(词出现在几乎所有文档):BM25 IDF → 0,几乎不贡献分——合理,因为无区分度
- (n_t) 很小时:BM25 IDF 更大,更强调稀有词
+0.5平滑:避免 (n_t=0) 或 (n_t=N) 时分母/分子为 0
倒排查询:Boolean 合并
多 term Query 在倒排层先做集合运算,再对交集/并集候选算 BM25:
1 | Query: "蓝牙 耳机" |
工程含义: AND 召回更准但易空;OR 覆盖广但噪声大。中文短 Query 常用 should + minimum_should_match 折中。
BM25 参数调优直觉
| 参数 | 调大 | 调小 | 典型症状 |
|---|---|---|---|
| (k_1) | 词频重复命中加分更多 | 词频饱和更快 | 标题堆砌词霸榜 → 降 (k_1) |
| (b) | 长文档惩罚更重 | 长度影响更小 | 长标题总排前面 → 升 (b) |
ES 默认 (k_1=1.2, b=0.75)。短标题场景(商品名)有时适当降低 (b);长文本场景(内容搜索)可升高 (b)。
编辑距离纠错原理
Levenshtein 距离:最少几次「插入 / 删除 / 替换」把字符串 (a) 变成 (b)。
1 | 蓝芽 → 蓝牙:替换「芽」→「牙」,距离 = 1 |
纠错候选生成:对 Query 每个词,在词典/索引词表中找编辑距离 ≤ 2 的候选,再用语言模型或共现频率排序:
[
c^*=\arg\max_c ; P(c)\cdot P(q\mid c)
]
(P(c)) 是候选词先验(词频/热度),(P(q\mid c)) 是「用户本想说 (c) 却打成 (q)」的噪声概率。拼音纠错把「编辑」换成「拼音编辑距离」——「蓝芽」与「蓝牙」拼音相同,距离为 0。
2.3 各路召回算法对比
| 算法 | 个性化 | 冷启动 | 延迟 | 可解释 | 搜索主路 | 推荐主路 |
|---|---|---|---|---|---|---|
| BM25 倒排 | 弱 | 强(文本) | 低 | 高 | 是 | 辅助 |
| 向量语义 | 弱~中 | 中 | 中 | 低 | 补召 | 是 |
| 热门/类目热门 | 弱 | 强 | 极低 | 高 | 兜底 | 兜底 |
| ItemCF | 强 | 差 | 低 | 中 | 很少 | 是 |
| 多 Query | 弱 | 强 | 中 | 中 | 是 | 辅助 |
| 双塔兴趣 | 强 | 差 | 中 | 低 | 辅助 | 是 |
2.4 BM25 vs 向量:互补而非替代
| 维度 | BM25 | 向量 |
|---|---|---|
| 同义词 | 弱 | 强 |
| 专名/型号 | 强 | 弱 |
| 描述性 Query | 弱 | 强 |
| 延迟 | 低 | 中 |
| 可解释性 | 高 | 低 |
最佳实践:两路互补,RRF 融合。 字面召回(BM25)保精确,语义召回(向量)补覆盖。向量细节见 向量检索与 ANN。
2.5 UserCF vs ItemCF
| 维度 | UserCF | ItemCF |
|---|---|---|
| 相似度 | 用户 ↔ 用户 | 物品 ↔ 物品 |
| 缓存 | 难 | 易(离线预计算) |
| 电商场景 | 较少 | 更常见 |
| 搜索场景 | 很少作主路 | 相关推荐、兜底 |
ItemCF 计算示例:
用户 (u) 点击过 ({A, B}),物品相似表 (\mathrm{sim}(A,C)=0.8, \mathrm{sim}(B,C)=0.5),则:
[
\mathrm{score}(u,C)=0.8+0.5=1.3
]
离线可预计算「物品 → TopK 相似物品」表,在线 (O(|I_u|)) 查表累加——适合「看了又看」「买了又买」。
Swing 等改进: 惩罚两个物品被同一批「大众用户」共现带来的虚高相似度,缓解热门物品垄断。
2.6 多 Query 召回
Query 理解产出 (Q_{\mathrm{list}}),对每个 (q_i) 做倒排或向量 TopK,合并去重。价值:扩覆盖——改写词能召到原词漏掉的商品。
成本控制:限制改写数 m、每路 TopK、合并后截断。
三、多路架构与融合
3.1 架构
1 | ┌─ A 倒排/向量(相关) |
并发耗时 (T_{\mathrm{parallel}}\approx\max T_i)。单路超时返回空,其它路继续;全部空走全局兜底。Python 实现见 Python 搜推技术栈。
3.2 RRF 融合
各路分数不可比(BM25 0~几十、余弦 0~1、销量 0~10 万),RRF 只看排名:
[
\mathrm{RRF}(d)=\sum_{i\in\text{lanes}}\frac{1}{k+\mathrm{rank}_i(d)}
]
(k) 常取 60。手算例(k=60):
| 商品 | 路A名次 | 路B名次 | RRF | 排序 |
|---|---|---|---|---|
| d1 | 1 | 5 | 0.03177 | 2 |
| d2 | 2 | 1 | 0.03252 | 1 |
| d3 | 3 | — | 0.01587 | 3 |
为什么 RRF 有效?
- 尺度无关:BM25 分可能是 12.3,向量余弦是 0.87——直接加权需要归一化且对分布敏感;RRF 只看 rank
- 鲁棒:某路分数整体偏高不会 dominate 融合结果
- 简单:无需离线标定各路权重(但 LTR 精排仍可进一步学习最优融合)
(k) 的作用: 分母中的 (k) 控制「排名靠后」的衰减速度。(k) 小 → 前几名权重差距大;(k) 大 → 各路排名 10 和 20 差别变小。常用 60 是经验值,可在 20~100 间 AB 调优。
某文档只在一路出现、排名为 (r) 时,RRF 贡献为 (\frac{1}{k+r})。两路都进 Top10 的文档通常融合分更高——多路共识信号。
3.3 融合策略对比
| 策略 | 优点 | 缺点 | 适用 |
|---|---|---|---|
| RRF | 不依赖分数尺度 | 丢掉分差 | 异构多路首选 |
| Min-Max + 加权 | 可表达路权重 | 需调参 | 同构路 |
| 级联 | 延迟低 | 前面路错则全错 | 粗筛 + 精筛 |
权重需精细控制时,RRF 之后对 Top 候选再做 LTR 精排。
四、典型故障模式与排查
4.1 Query 层:分词与清洗
现象: 索引中有文档,但搜索无结果或结果偏移。
常见原因:
| 原因 | 说明 | 排查方向 |
|---|---|---|
| 分析器不一致 | 索引与查询分词策略不同 | 对比 index/search analyzer |
| 品牌词切坏 | 「华为手机」被切成单字 | 自定义词典 |
| 清洗过度 | 型号中 -、+ 被去掉 | 检查 normalize 规则 |
| 纠错误伤 | 正确 SKU 被改成热门词 | 检查保护表、纠错阈值 |
解法: 重建索引并统一 analyzer;品牌/SKU 进自定义词典;清洗规则保留型号特殊字符。
4.2 召回层:覆盖不足
现象: Query 理解输出正常,但候选池为空或缺少明显相关物品。
常见原因:
| 原因 | 说明 | 排查方向 |
|---|---|---|
| 仅字面召回 | 同义/描述性 Query 漏召 | 加向量语义路 |
| 改写未启用 | 用户表述与标题用词不同 | 加多 Query 召回 |
| 类目硬过滤 | 预测错导致全空 | 改软约束 |
| 单路超时 | 某路挂掉且无兜底 | 检查超时与降级 |
解法: BM25 + 向量多路互补;无结果时触发语义补召 + 类目热门兜底;RRF 融合后 LTR 精排压噪。
4.3 快速定位:看哪一层
1 | 有结果但不相关 → 精排 / 机制层 |
五、怎么选召回路
| 场景问题 | 优先加哪路 |
|---|---|
| 关键词搜不准 | BM25 调权、同义词 |
| 同义/描述性差 | 向量语义 |
| 措辞多变漏召 | 多 Query |
| 空结果多 | 类目热门补召 |
| 要个性化 | CF / 双塔 |
| 新物品多 | 内容/向量,而非纯 CF |
设计口诀:每路必须能回答「本路解决哪种失败」。


