用ES这条语句去es集群中查询数据的时候出现一个非常诡异的问题 , 每次查询的结果居然不一样.???
q.Query(req).
Sort("_score", false).
Sort("_id", false)
developer.aliyun.com/article/789…
一、为啥 _score + _id 排序还会出现结果抖动?
核心逻辑链:_score 重复 → 跨分片合并有 “优先级争议” → 主副分片争议解法不同 → 轮询路由切换分片 → 结果抖动。
咱们一步步拆,结合实际场景:
1. 前提:_score 重复是绕不开的 “导火索”
你排序是「先按 _score 降序,再按 _id 降序」,但 _score(相关性得分)太容易重复了:
- 比如查询 “安全管理平台”,100 个文档都完全命中关键词,ES 计算的
_score全是 3.8(完全相同); - 这时候排序就只能靠
_id兜底,但_id的作用范围有 “漏洞”—— 只在「同一个分片内」有用,跨分片没用。
2. 关键问题:跨分片合并时,_id 说了不算!
ES 集群数据是分片存储的(比如 2 个分片),查询时不会把所有分片的文档全拉到一起排序(性能太差),而是按「分片内先排 → 跨分片合并」的流程:
-
分片内排序:
_score相同的文档,按_id降序排(符合你的配置),比如:- 分片 1:doc-10(_id=10)→ doc-08(_id=8)(_id 降序);
- 分片 2:doc-09(_id=9)→ doc-07(_id=7)(_id 降序);
-
跨分片合并:ES 要把两个分片的结果合并成最终 Top N,但这里有个 “争议”—— 分片 1 的 doc-10(_id=10)和分片 2 的 doc-09(_id=9),
_score相同,且不在一个分片,没法直接比_id; -
ES 的解决办法:用「分片优先级」来决定顺序(比如按分片 ID、响应速度、负载等),但 主分片和副分片的 “分片优先级规则不一样” :
- 主分片合并规则:按「分片 ID 从小到大」→ 先拿分片 1 的 doc-10、doc-08,再拿分片 2 的 doc-09、doc-07 → 最终排序:10→8→9→7;
- 副分片合并规则:按「分片响应速度从快到慢」→ 先拿分片 2 的 doc-09、doc-07,再拿分片 1 的 doc-10、doc-08 → 最终排序:9→7→10→8;
这里要注意:_id 确实是全局唯一的,但它只能解决 “同一个分片内” 的排序,跨分片合并时,「分片优先级」比 _id 排序优先级更高 —— 所以哪怕 _id 唯一,跨分片的文档顺序还是会乱。
3. 触发条件:轮询路由让查询 “在两个答案间切换”
ES 为了负载均衡,默认会让相邻查询「轮流命中主分片或副分片」:
- 第一次查询:路由到主分片 → 拿到 Top3:[10,8,9];
- 第二次查询:路由到副分片 → 拿到 Top3:[9,7,10];
- 最终现象:两次查询的 Top10 结果不一样(部分文档位置互换、甚至被挤出),这就是 “结果抖动”。
总结抖动的核心原因:
不是 _id 不唯一,而是「跨分片合并时主副分片的优先级规则不同」,再加上「轮询路由切换分片」,导致相同查询拿到不同合并结果。
二、s.Preference("default-fixed-key") 是怎么减少抖动的?
preference 参数的核心作用:把查询 “绑死” 在同一组分片(主或副),不让它在主副分片间切换,从而让合并规则固定,结果稳定。
1. 先搞懂 preference 的工作原理:
- ES 会根据
preference后面的字符串(比如fixed-key)计算一个哈希值; - 这个哈希值会映射到集群中的「特定一组分片」(比如只映射到主分片,或只映射到某几个副分片);
- 之后,所有带这个
preference参数的查询,都会被强制路由到这一组分片,不会再轮询切换到其他分片。
2. 具体怎么解决抖动?
结合之前的场景:
- 你添加
s.Preference("fixed-key")后,查询会被固定路由到「主分片」(假设哈希映射到主分片); - 第一次查询:路由到主分片 → 合并规则是 “按分片 ID 排序” → 结果 [10,8,9];
- 第二次查询:还是路由到主分片 → 合并规则不变 → 结果还是 [10,8,9];
- 哪怕
_score重复、跨分片合并有争议,但因为「合并规则固定」,结果也不会变 —— 抖动直接消失。