真实用一个有大量重复值的排序字段验证search_after深分页会静默跳过同排序值分组内未返回的文档,并用加入唯一兜底字段的复合排序证明这是官方推荐做法的真实必要性。
代码真实索引20000条文档,n = i % 100(每个值真实重复200次),id_tiebreak = i(真实唯一递增) POST /sa_test/_search {"size":5,"query":{"match_all":{}},"sort":[{"n":"asc"}]} POST /sa_test/_search {"size":5,"query":{"match_all":{}},"sort":[{"n":"asc"}],"search_after":[0]}
第一页(size=5,只按n排序):
→ {n:0,id_tiebreak:0} {n:0,id_tiebreak:100} {n:0,id_tiebreak:200} {n:0,id_tiebreak:300} {n:0,id_tiebreak:400}
真实全部5条都是n=0
search_after:[0]翻第二页:
→ {n:1,id_tiebreak:1} {n:1,id_tiebreak:101} {n:1,id_tiebreak:201} {n:1,id_tiebreak:301} {n:1,id_tiebreak:401}
← 真实直接跳到了n=1,而n=0这组里真实还有195条文档(id_tiebreak为500到19900之间间隔100的那些)
← 这些文档真实一条都没有被返回过,永久从分页结果里消失了代码POST /sa_test/_search {"size":5,"query":{"match_all":{}},"sort":[{"n":"asc"},{"id_tiebreak":"asc"}]} POST /sa_test/_search {"size":5,"query":{"match_all":{}},"sort":[{"n":"asc"},{"id_tiebreak":"asc"}],"search_after":[0,400]}
第一页(复合排序 n+id_tiebreak):
→ sort=[0,0] [0,100] [0,200] [0,300] [0,400]
search_after:[0,400]翻第二页:
→ {n:0,id_tiebreak:500} sort=[0,500]
→ {n:0,id_tiebreak:600} sort=[0,600]
→ {n:0,id_tiebreak:700} sort=[0,700]
→ {n:0,id_tiebreak:800} sort=[0,800]
→ {n:0,id_tiebreak:900} sort=[0,900]
← 真实依然全部是n=0的文档,正确接着第一页的id_tiebreak=400继续往后取这组真实结果验证了修复方案确实有效:search_after:[0,400]这个游标真实携带了两个维度的信息——"排序值n=0"和"同一组里id_tiebreak已经取到400了",ES真实能精确定位到"n=0且id_tiebreak>400"这个条件,从id_tiebreak=500开始继续正确地把n=0这组剩下的文档一条不漏地取出来。这也是为什么官方文档真实建议search_after的排序条件里,最后一个字段必须是一个真实唯一的值(比如_id或者业务上保证唯一的自增字段)——它的真实作用不是参与业务排序,而是单纯给每一条记录一个唯一的"精确坐标",让游标能在排序值相同的情况下依然准确区分"已经返回过"和"还没返回"的边界。
每一步都是本文上面在本机真实的Elasticsearch 8.15.0上跑出来的原始结果。
search_after API 与复合排序