用户说“搜不到”,你打开后台一搜就有;用户说“排得不对”,你看排序也说得过去。这类争论靠演示解决不了,因为双方拿的查询、期望和样本都不一样。相关性工程不是把排序调到所有人满意,而是把它变成可归因、可度量、可回归的机制——否则每次改权重都只是赌一次。
1. 归因四分:先确定问题在哪一层
“搜得准不准”混了四种故障。不做归因就调权重,等于把排序层的补丁打在索引层的病上。
| 归因 | 现象 | 取证手段 | 责任层 |
|---|---|---|---|
| 没索引到 | 全站搜该词无结果,库里确实有 | 直查存储层文档数 vs 索引文档数 | 采集/索引链路 |
| 词没匹配上 | 文档在索引里,但查不到 | _analyze 对比查询与文档字段的切词 |
分析器/查询构造 |
| 匹配了但靠后 | 首屏没有,往后翻能看到 | 取该 query 的完整结果序列定位排名 | 打分/排序 |
| 排序对了没展示 | 引擎返回正确,页面看不到 | 对比接口响应与页面渲染 | 应用层过滤/前端 |
# 逐层取证模板(参数名按所部署引擎版本核对)
curl -s "HOST/INDEX/_analyze" -H 'Content-Type: application/json' \
-d '{"field":"title","text":"QUERY_TERM"}'
curl -s "HOST/INDEX/_count" -d '{"query":{"term":{"title.keyword":"QUERY_TERM"}}}'
curl -s "HOST/INDEX/_search?explain=true" -d '{"query":{"match":{"title":"QUERY_TERM"}}}'
四分法把责任分给四个团队:采集、检索配置、排序、前端。省掉归因,所有投诉都落到排序身上,而排序最难自证清白。
2. 指标设计:哪些数能用,哪些数在骗人
相关性没有绝对分数,只有相对变化。指标只用来发现退化。
| 指标 | 定义 | 常见误导 |
|---|---|---|
| 点击率 CTR | 有点击的会话 / 有结果的会话 | 展示位置越好点击越高,与相关性无关 |
| 首条点击率 | 位置 1 被点击占比 | 用户习惯性点第一条,指标虚高 |
| 无结果率 | 零结果查询数 / 总查询数 | 可被“兜底返回一堆不相关”刷低 |
| 改查率 | 同一会话内连续改写的比例 | 用户补充条件是正常行为,不全是差评 |
{"event":"search","query_raw":"小刀能带上飞机吗","query_norm":"小刀 飞机 携带",
"result_count":12,"clicked_rank":3,"dwell_ms":8400,"reformulated":false,"trace_id":"T"}
三条纪律:分母写清楚(无结果率的分母是查询数还是会话数);按查询分层看(头部与长尾混着平均等于没说);单一指标不决策(只盯 CTR 会让人把吸睛但无关的结果往上顶)。
3. 离线评测集:从日志采样到分层
没有标注集只能凭感觉上线,感觉的方差常大于改动的收益。评测集不求大,求代表性。
| 采样来源 | 典型占比 | 覆盖什么 |
|---|---|---|
| 头部高频查询 | 小 | 回归安全网,改动最先暴露在这里 |
| 长尾随机查询 | 大 | 真实分布,最容易漏掉的部分 |
| 零结果查询 | 小 | 召回缺口,改分析器/同义词的靶子 |
| 业务指定查询 | 小 | 高价值场景,不允许退化 |
| 人工构造对抗查询 | 极小 | 拼写错误、混合中英、歧义词 |
-- 分层采样占位:先按频次分桶再抽,避免只抽到头部
SELECT bucket, query_norm FROM (
SELECT query_norm, CASE WHEN cnt >= HEAD_T THEN 'head'
WHEN cnt >= MID_T THEN 'mid' ELSE 'tail' END AS bucket,
ROW_NUMBER() OVER (PARTITION BY bucket ORDER BY RAND()) rn
FROM query_freq_daily
) t WHERE rn <= QUOTA_PER_BUCKET;
分层采样要分层报告:整体涨 2% 而长尾跌 8% 的改动通常不该上线。评测集建立后冻结版本,只追加,不在改动同时换样本。
4. 相关性标注:分级与一致性
二值标注不够用:它无法表达“能答但不完全对”。常用三级:
| 等级 | 含义 | 判定口径 |
|---|---|---|
| 0 不相关 | 答非所问 | 主题不同,或只擦边命中关键词 |
| 1 部分相关 | 沾边、可参考 | 主题相关但未回答核心意图 |
| 2 完全相关 | 直接回答 | 首段即可满足查询意图 |
一致性不是可选项:两人一致率七成时,任何 2% 的指标变化都是噪声。
# 一致率与 kappa(占位实现,用于标注培训后自检)
def kappa(a, b):
n = len(a); po = sum(x == y for x, y in zip(a, b)) / n
pe = sum((a.count(l) / n) * (b.count(l) / n) for l in set(a) | set(b))
return (po - pe) / (1 - pe) if pe < 1 else 1.0
顺序是:写标注手册 → 两人标同一小批 → 算 kappa → 对齐分歧 → 再批量标。跳过培训直接标,标注集本身就是错的。
5. BM25 之外:加信号的收益与侵蚀
BM25 只回答“文本像不像”,业务要的是“该给谁”。加信号能改善头部效果,代价是可解释性下降。
| 信号 | 收益 | 侵蚀风险 | 施加位置 |
|---|---|---|---|
| 时效衰减 | 新闻/公告明显 | 常青内容被压死 | 查询期打分 |
| 热度 | 优质内容上浮 | 强者更强,新内容永无曝光 | 查询期或离线分 |
| 用户画像 | 个性化明显 | 同查询不同人不同结果,难复现 | 应放在重排层 |
| 业务权重 | 商业目标可调 | 堆叠后无人说得清排序理由 | 配置化+版本化 |
{
"function_score": {
"query": { "match": { "title": "QUERY_TERM" } },
"functions": [
{ "gauss": { "publish_at": { "scale": "30d", "decay": 0.5 } }, "weight": W_TIME },
{ "field_value_factor": { "field": "hot_score", "factor": 1.0 }, "weight": W_HOT }
],
"score_mode": "sum",
"boost_mode": "multiply"
}
}
判断信号该不该留,问三句:它解决哪些具体 badcase?去掉退回什么状态?权重谁维护? 答不上来的,两次迭代后没人敢动。
6. 查询理解:收益排序
同义词、纠错、改写、意图分类不是并列选项:收益从高到低,代价递增。按顺序做能用最小改动拿到大部分提升。
| 手段 | 典型收益面 | 主要代价 | 建议顺序 |
|---|---|---|---|
| 同义词/别名词典 | 高:术语、缩写、俗称 | 词典维护成本,易过宽 | 1 |
| 拼写纠错 | 高:错字、拼音混输 | 误纠会把正确词改坏 | 2 |
| 查询改写 | 中:口语转化为检索式 | 需要日志与规则/模型 | 3 |
| 意图分类 | 中:路由到不同索引 | 分类错则整块结果错 | 4 |
| 向量召回兜底 | 中:语义相近表达 | 成本与噪声,需要阈值 | 5 |
# 同义词(格式按所部署引擎版本核对):=> 表示单向展开
synonyms: ["服务器, 主机, server", "报错, 异常, error => 错误"]
同义词最容易犯的错是双向过宽:苹果与水果互相展开,查“苹果手机”时水果内容全被拉进来。规则应单向、带条件、可回滚。
7. badcase 收集与回归
badcase 是相关性工作的燃料,收集入口要低摩擦。
badcase 最小字段
query 原始查询 expect 期望命中的文档标识
got 实际前 3 条 note 为什么判错(对应第 1 节四分)
by/date 谁何时报的 status open / fixed / wontfix
回归纪律:每次改动跑同一集合,报全量结果而非“已修复”。输出用固定列表格,方便 diff:
python eval.py --set EVAL_SET.json --endpoint HOST --out result.csv
diff -u baseline.csv result.csv | head -50 # 只看三类:通过/回归/提升
健康的 badcase 库是“陈旧条目多、新增也有”。长期只增不消化,说明收集与修复脱节。
8. AB 实验在搜索场景的特殊性
搜索 AB 比推荐、广告都难,原因在分布:
| 特殊性 | 后果 | 应对 |
|---|---|---|
| 查询分布长尾 | 单 query 样本量小,方差大 | 按查询聚类分组后再比 |
| 指标滞后 | 点击在会话中后段才发生 | 观察完整会话窗口,不切流即比 |
| 会话内污染 | 同用户跨组体验不一致 | 按用户/会话哈希分流 |
| 冷启动样本 | 新 query 无历史基线 | 单独看,不混入总体 |
按请求分流是最常见的错误:同一用户两次搜索命中不同版本,归因就乱。分流单位不小于观测单位(会话级观测 → 用户级分流)。
9. 冷启动与长尾查询
长尾查询的共性是每个词样本都不足。对策不是提升排序精度,而是减少空结果:
- 降级策略:无结果时依次退到 AND 放宽为 OR、字段放宽、同义词扩展,每次降级记日志,用于建定向词典;
- 兜底展示:明说“没有完全匹配,以下是相近内容”,而不是静默返回无关项;
- 长尾单看:长尾改善常不体现在总 CTR 上,只看总指标会把长尾优化判为无效。
10. 排错:相关性下降的常见原因
| 现象 | 可能原因 | 核查点 |
|---|---|---|
| 某天起整体变差 | 索引重建/分析器改动 | mapping 与分析器是否同步变更 |
| 只劣化部分词 | 权重堆叠或同义词过宽 | 最近一次配置 diff |
| 首屏正常翻页异常 | 深分页/重排截断 | 重排窗口是否覆盖该位次 |
| 结果数量骤减 | 过滤器条件收紧 | 查询模板里的 filter 是否含新增条件 |
| 结果重复 | 多索引/别名叠加 | 别名是否指向了同一份数据的多个索引 |
| 用户搜不到你搜得到 | 权限或登录态过滤 | 同身份复现,查过滤链 |
核查顺序固定:索引与文档数 → 分析器 → 打分。 反过来查,常在权重里绕很久才发现少了个字段。
11. 边界:什么情况本就不该做相关性工程
| 场景 | 判断 | 替代动作 |
|---|---|---|
| 查询量太小 | 没有统计意义,指标不可读 | 只做 badcase 清单人工看 |
| 强结构化查询(ID/编号) | 精确匹配足够 | 不引入打分与改写 |
| 内容本身质量差 | 排序救不回来 | 先治理内容与采集覆盖 |
| 结果集很小且检索是唯一入口 | 排序收益低于展示改造 | 优化筛选与分类导航 |
| 团队无人维护词典与标注 | 机制会腐化 | 明确负责人再启动 |
天花板由内容覆盖与评测机制决定:内容缺的排不上去,没有评测的改了也不知道。
12. 自测
- 用户投诉“搜不到”,你的第一条取证命令查什么?
- 无结果率下降了但用户投诉变多,最可能是哪种掩盖?
- 标注一致性只有七成时,2% 的指标提升能不能作为上线依据?
- 同义词为什么建议单向配置?
- 搜索 AB 实验为什么不该按请求分流?
- 长尾查询改善不明显体现在总 CTR 上,你应该另看什么?