2208 字
约 7 分钟
17
S13-A26 · 同分怎么排:搜索的"并列"其实不存在

一个文档库搜索"权限"二字,前三条是"权限模型""权限设计""权限校验"——三次刷新后它们的顺序变了。用户于是觉得"搜索不可靠"。技术上原因是:三条得分完全相同,而没有"兜底排序键",于是"引擎返回什么顺序,前端就显示什么顺序".

搜索的"排序"有两层:相关性得分(是否匹配)与兜底排序(同分怎么排)。前者是"搜索质量",后者是"结果稳定性"——而稳定性常被当成"不重要的细节".

1. 三种"同分"的成因

成因 说明
短语匹配(多字段同分) 三条都"标题命中"
短文档(长度归一化后接近) TF 相近
自定义打分"截断"(取整/取档) 人为并列

第三项最常被忽略:"把得分取整到 1 位小数"看起来"干净",实际上"制造大量并列".

2. "不稳定"的三种后果

后果 表现
用户"找不到刚看过的" 位置每次不同
分页"重复/漏项" 第 2 页出现第 1 页的内容
A/B 实验"看不出差异" 噪声掩盖了改动

第二项的"机制"很具体:"第 2 页"对应"offset 10",而"两次查询里'前 10 名'的同分顺序可能不同"——于是"第 2 页的第一条"可能"已经在第 1 页出现过".
第二项是"分页"的经典 bug:
"没有兜底排序"时,引擎在两次查询里可能返回"不同的同分顺序"**——**于是"第 2 页"和"第 1 页"重叠或漏掉.

"三段排序"的写法在 DSL 里就三行,而它是"最小改动":

{"query": {"match": {"title": "权限"}},
 "sort": [{"_score": "desc"},
          {"updated_at": {"order": "desc"}},
          {"id": {"order": "asc"}}]}
// 判据:同分时"新的在前",再同则"ID 小的在前"——顺序完全确定

3. "兜底排序"的三种选择

键 特点
更新时间(倒序) 体验自然(新内容优先)
文档 ID(升序) 绝对稳定(但"无意义")
热度/点击 有业务含义(但"变化快")

第一项是"体验与稳定的折中":"同分时新的在前"第一项的"附加好处"是"业务友好":"同等相关时,用户通常更想看新的"——于是"稳定"与"合理"用同一个键就解决了.
——而它"不变"的前提是"更新时间不变".
第三项的"判据"是"这个键会不会'在两次查询之间变化'":
"更新时间"只在新文档出现时变,"热度"每次点击都变
——于是后者只能"进得分",不能"当兜底".
第三项要小心:
"热度"会变,于是"同分组的顺序"仍会变**——**于是"热度"必须"作为得分的一部分",而不是"兜底键".

"游标分页"比"偏移分页"稳,而它要"把排序键带回":

// 第一页:取 10 条,记住最后一条的 sort 值
{"size": 10, "sort": [{"_score":"desc"},{"updated_at":"desc"},{"id":"asc"}]}
// 第二页:用 search_after
{"size": 10, "search_after": [0.5, "2026-11-01T00:00:00Z", "doc-128"],
 "sort": [{"_score":"desc"},{"updated_at":"desc"},{"id":"asc"}]}
// 判据:翻页"不重复、不漏",且"深分页"仍快

4. "分页稳定"的三个要求

要求 做法
排序键"全序"(能唯一确定顺序) 得分 + 时间 + ID
翻页用"游标"而非"偏移" search_after
快照一致(可选) 固定 preference/时间点

第一项是"根本":"得分 + 更新时间 + ID"三者组合,几乎不可能再并列第一项的"数学"很朴素:"ID 唯一 → 全序成立"——**于是"不管得分怎么并列,最后一步总能'分开'".
——于是"每次查询的顺序都一样".
第二项解决"深分页":
"offset 10000"本身也慢,且"数据变动时结果漂移".

"得分截断"这条要"在展示层做",而不是"在排序里做":

// 三种"展示匹配度"的做法
1 排序用"原始得分",展示时"取整"(推荐)
2 排序用"取整后的分",展示"取整"(会制造并列)
3 排序用"档位"(高/中/低),展示"档位"(并列最多)
// 判据:展示"好看"不应该改变"排序"

5. "排序键"的三种写法

写法 说明
sort: [_score, {updated_at: desc}, {id: asc}] 显式三段
只 sort: [_score] 不稳定(同分随机)
只 sort: [{updated_at: desc}] 稳定但"不按相关性"

第一项是"推荐":"先相关性,再时间,最后 ID"——**而"ID 兜底"是"绝对唯一"的保证.

"排序是否稳定"可以"用一条脚本验证",而它比"刷三次页面"可靠:

// 三次查询,比对"前 10 名的 id 序列"是否一致
for i in 1 2 3; do
  curl -s "$Q" | jq -r '.hits.hits[]._id' | tr '
' ',' ; echo
done | sort -u | wc -l
// 判据:输出 1 → 稳定;输出 2 或 3 → 有并列且未兜底

6. 排错:五种"排序问题"

现象 成因 处理方向
刷新顺序变 无兜底键 加 updated_at+id
分页重复/漏 偏移分页 + 同分 改游标
新文档不靠前 只按得分 加时间权重
同分"总在第一页" 得分截断 别取整
实验无差异 噪声 先稳定排序

"推荐流"的随机要"可控",否则它变成"另一类不可复现":

// 三种"有意随机"的做法
1 固定随机种子(同用户同时间 → 同顺序)
2 只随机"同分档内"的位置(保留相关性)
3 每次全随机(最简单,也最不可解释)
// 判据:随机要"能被解释",否则"用户投诉"无从回应

7. 边界:什么情况"不稳定的顺序"可以接受

场景 为什么 做法
推荐流(每次不同是特性) 要"新鲜感" 允许随机
只取"前 3 条"(不分页) 顺序变化影响小 可接受
结果集很小(全部展示) 用户"看的是集合" 可接受
有"个性化"(按用户) 顺序本就因人而异 可接受

最后一条与"实验"有关的边界:"排序不稳定会让 A/B 实验失效":"两组结果的差异"可能小于"同分随机造成的波动"——于是"改了个权重看起来没效果",而"先稳定、再实验"是"能测出差异"的前提.
判断口径一句话:"用户会不会"翻页"或"回头看同一批结果""。会(搜索、列表、后台管理),必须有"全序兜底";不会(推荐、只取前几条),顺序"不必固定"——而"同分不兜底"这个问题,往往要等"用户投诉'结果乱跳'"才被重视.

8. 自测

  1. "相关性得分"与"兜底排序"分别解决什么?为什么后者常被当成细节?
  2. 三种"同分"成因里,哪一种"是人为制造的"?
  3. "没有兜底排序"为什么会造成"分页重复或漏项"?
  4. "热度"为什么"不能当兜底键"?它该放在哪一层?
  5. "全序"需要哪三个键?为什么"ID 兜底"能保证唯一?
  6. 判断"顺序是否需要固定"的那个问题是什么?

补一句"成本"的话:"加两个排序键"是"一行 DSL",而它解决的问题是"用户觉得搜索不可靠"——后者一旦形成印象,之后再怎么调相关性都"不被相信"。所以"稳定"这件事的性价比,高得离谱:它几乎不花成本,却是"可信"的地基。

而那"一行"的成本,比"重调一次打分函数"低两个数量级——这也是为什么它该"先加",而不是"等有空再加"。

而"先加"这件事,恰好是"工程质量"里最常见的一种拖延:大家都知道它重要,但都觉得它"不急"。

9. 一句话总结

"并列"在工程上不存在——只要"排序键"是"有限组合",就总会"撞"。所以"稳定的搜索结果"不是"更好的相关性",而是"在得分之后,再补两个能唯一确定的键"。而"得分 + 时间 + ID"这个三段式,成本几乎为零,收益是"用户不再觉得搜索在乱跳"。

S13-A26 · 同分怎么排:搜索的"并列"其实不存在
https://www.clxhxhhr.top/posts/6701/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。