2926 字
约 9 分钟
19
S13-A09 · 搜索相关性调优:评分、召回与业务排序

"搜出来的东西用户不点",最容易被读成"分数给低了"。可能坏在三层完全不同的地方:库里没有那篇文档、有但排在第十七条、排第一但摘要看不懂。把三层混成一个"相关性分数"去调,是调优事故的起点。

只给判据:坏在哪层、每个杠杆的代价、何时停手。


1. 三层分解:先定位,再动手

层 用户原话 观测信号 便宜的手段
召回 "搜不到""没这个东西" 零结果率、目标文档是否在前 N 同义词、词典、补字段入索引
排序 "排太后面""第一条不对" 目标文档平均名次、首条点击率 字段权重、BM25、rescore
展示 "点进去不是我要的" 点击后跳出、二次改写率 摘要、标题、高亮

一次只动一层:同时改同义词和 boost,涨了不知是谁的功劳,跌了不知回滚哪个。判定顺序写死:

1 零结果率高      -> 先查 filter、mapping、分词(九成是过滤太严)
2 前 50 条命中率低 -> 查同义词、查询改写、字段是否入索引
3 平均名次低      -> 动 boost / BM25 / rescore
4 首条点击低      -> 改摘要、标题、结果条数

没有候选时调打分是纯粹的浪费。


2. 召回判据:用日志抽样,而不是感觉

判据不是"召回率高",而是在固定的一份抽样集里,已知目标文档是否出现在前 N 条。没有这份集合,"召回不够"只是一句感觉。

抽样方式 覆盖 主要用途
按频次分层(头/腰/长尾) 高频与长尾兼顾 判断漏召回是否系统性
零结果查询全量 最痛的一批 分词与过滤问题富矿
有点击的查询 隐性失败 排序与展示
运营指定查询 业务底线 上线回归集

规模按"每层数十到数百条"起步,按查询总量与人力核对。要害在固定:每次调完换一批新查询,等于每次做新题。

-- 分层抽样(口径示例,按实际日志表核对)
WITH q AS (SELECT query, COUNT(*) cnt FROM search_log
           WHERE dt >= CURRENT_DATE - 7 AND query <> '' GROUP BY query),
     r AS (SELECT *, NTILE(3) OVER (ORDER BY cnt DESC) tier FROM q)
SELECT tier, query FROM r ORDER BY random() LIMIT 300;

标注只回答一件事:目标文档在不在前 N、在第几位,且要区分"库里没有"与"有但排得低"。这栏标错,后面全错。


3. _score 的构成与四类加权手段

手段 改什么 适用 代价
boost 乘在子查询分数上 快速验证字段重要性 分数不可比,阈值与断点失效
function_score 字段值、衰减、脚本 距离、时效、热度 boost_mode 选错覆盖相关性;脚本慢难解释
rank_feature 数值特征并入打分 点击率、销量 字段类型特殊,改动要重建索引
rescore 前 N 条二次打分 精排窗口 窗口外一条不动

铁律:业务权重不进 _score。破坏的是评估——销量乘进分数后,NDCG 不再衡量相关性,你失去唯一标尺。

{"query": {"function_score": {
  "query": {"multi_match": {"query": "退款", "fields": ["title^3", "body"]}},
  "functions": [
    {"filter": {"term": {"status": "active"}}, "weight": 1.2},
    {"gauss": {"publish_at": {"origin": "now", "scale": "30d", "decay": 0.5}}}],
  "score_mode": "multiply", "boost_mode": "multiply"}}, "size": 50, "explain": true}

读 explain 分两层:query 原始分与 function 乘数。混着看的典型误判是"字段权重没生效",实际是 score_mode 把乘数叠成常数。


4. BM25 的 k1 与 b 改变什么

参数 管什么 调小 调大
k1 词频饱和速度 重复词更快失增益 长文靠重复堆分
b 长度归一化强度 长文档占便宜(b=0 关闭) 强烈惩罚长文

默认 k1=1.2、b=0.75(BM25Similarity 默认值,按版本核对)。站内搜索"标题短、正文长、查询短",这套默认通常够用:

PUT /docs
{"settings": {"similarity": {"bm": {"type": "BM25", "k1": 1.0, "b": 0.8}}},
 "mappings": {"properties": {"title": {"type": "text", "similarity": "bm"}}}}

字段级 similarity 建索引时绑定,改它要重建;全局 index.similarity 要重启节点。先用 explain 确认长度归一化占了多少分,再决定是否值得重建。为调 b 重建三次索引,是把可观测问题做成运维事故。


5. 字段权重与 multi_match 的四种 type

type 打分 适用 代价
best_fields(默认) 取单字段最高分 一个字段够回答 多字段各命中一半时丢信息
most_fields 各字段分相加 同义/多语言副本 同词重复计分,长文虚高
cross_fields 多字段当一字段 结构化拆分(省/市) 要求 analyzer 一致,minimum_should_match 语义变
phrase_prefix 短语前缀 搜索框补全 末词扫描,长前缀慢
{"query": {"multi_match": {"query": "杭州 退款", "type": "cross_fields",
  "fields": ["city", "title^2", "body"], "operator": "and"}}}

判据:概念拆进多字段 → cross_fields;概念有副本 → most_fields;只关心最强字段 → best_fields;用户还在打字 → phrase_prefix。 字段权重(title^3)对所有查询一视同仁,解决不了"意图判错"。


6. 同义词与查询扩展的双刃

扩召回与降精度是同一枚硬币:"退货"映射到"退款"能救一批查询,也会让"退款失败"命中"退款成功"。

做法 收益 代价
索引期同义词 查询期零开销 改词表要重建索引
搜索期同义词 词表可热更 每次展开,分数分布变宽
单向东扩 A => B 不反向污染 维护成本高
PUT /docs/_settings
{"analysis": {"filter": {"syn": {"type": "synonym_graph",
   "synonyms": ["退货 => 退款", "七天无理由 => 7天无理由"]}},
 "analyzer": {"search_syn": {"type": "custom", "tokenizer": "standard",
   "filter": ["lowercase", "syn"]}}}}

挂 search_analyzer 而非 index_analyzer,索引 term 保持原样、词表可热更。副作用两点:展开后 term 变多,operator: and 会严到零结果;minimum_should_match 不调则每 term 分摊变薄,本该第一的掉后。先 _analyze 看展开,再 explain 看分项。


7. 分层:先算相关,再叠业务,最后去重

阶段 输入 输出 不该做
粗排 查询、候选 相关序 top 100 掺业务权重
业务加权 相关序 + 业务字段 重排 top 100 改相关性分数
去重打散 重排结果 分页序列 重打分
去重与打散的幂等性
- 同 SPU 不同 SKU:按 SPU 去重,留分数最高一条
- 打散:第 N 页只由前 N*pageSize 候选决定
- 反例:按已展示集合动态打散 -> 翻页漂移

业务加权放第二段的好处:相关性评估只跑第一段,活动加权改版不触发相关性回归;写进 boost 则每次大促重校基线。


8. 个性化与 LTR 的门槛

阶段 触发条件 最小实现 失效信号
规则个性化 有可靠分层字段 按用户组套权重 分层假设与行为不符
反馈特征 稳定用户 ID + 足够曝光 点击率类特征入 rank_feature 冷启动用户全排后
LTR 标注够 + 特征稳 + 有评估 ES 8.4 起内置,旧版靠插件 离线涨、线上平

LTR 的门槛不是算法,是三件事同时成立:标注量随特征维度上升,特征多标注少则学噪声;特征训练与在线同源,否则训练-服务偏差吃掉全部收益;有分流实验,否则效果无法归因。冷启动顺序是规则 → 带位置偏置校正的反馈 → 模型;先上模型后补闭环,等于用没有标签的目标函数训练。


9. 指标偏差与 A/B 最小设计

指标 衡量 主要偏差
NDCG@k 分级相关性 + 名次折扣 依赖标注分级一致性
MRR 首个正确结果名次 多答案查询失真
点击率 用户实际选择 位置偏置、标题诱导
零结果率 覆盖 不反映质量,诱导滥扩召回
A/B 四项缺一结论就不可信
样本量 按基线指标与最小可检测效应估算,量级常在每桶数千到数万次查询(按实际流量核对)
分流   按用户或查询 hash,同一人多轮同组;按请求随机会让体验抖动
指标   一个主指标 + 护栏(延迟、零结果率、跳出),只看点击会漏掉延迟回退
周期   覆盖完整周期含周末,跨大促对比结论作废

10. 误判清单

现象 更可能的原因 验证
某文档怎么都搜不到 字段没进索引(_source 有、mapping 无) 查 _mapping + _termvectors
某些用户搜不到 过滤过严(状态、时间、租户) 去掉 filter 再查
中文词搜不到 分词器与词典不匹配 _analyze 看切分
同义词不生效 挂在 index_analyzer 查 search_analyzer

11. 排错

现象 原因 处理
分数全变排序没变 全量同比例放大 看乘数是否恒定,改 boost_mode
加同义词后搜不到 term 变多、operator 变严 调 minimum_should_match
function_score 后指标降 业务权重污染分数 挪到排序第二段
rescore 无效果 window_size 小于所需窗口 核对 window 与 size
改 k1/b 分数不动 字段级 similarity 未绑定 查 mapping,必要时重建

12. 边界:什么时候规则排序就够

场景 更合适 理由
查询量很小(每日数百) 规则排序 + 人工置顶 没有统计意义
结果集小(几十条) 数据库排序 用户翻得完
后台管理检索 精确过滤 + 倒序 用户知道要什么
无标注无埋点 先做评估集 没有评估只有感觉
数据脏(标题缺失、类目错) 先修数据 调优修不了脏数据

分界在是否有稳定的"好"的定义。定义得出、能量化、能回归,才值得上模型;定义不出时,人工置顶的成本永远低于无人能解释的模型。


13. 自测

  1. 用户说"搜不到",先看哪个指标?
  2. 为什么要分"库里没有"与"有但排得低"?
  3. 业务权重写进 _score 破坏的是哪个环节?
  4. b=0 意味着什么?什么现象提示查 b?
  5. best_fields 与 most_fields 差别在什么数据下暴露?
  6. 同义词展开后结果变差,先查什么?
S13-A09 · 搜索相关性调优:评分、召回与业务排序
https://www.clxhxhhr.top/posts/6168/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。