真实灌5万条文档验证from+size深分页的默认硬性上限与性能衰减规律,并用search_after在同样深度下的真实耗时对比,证明游标式分页如何避开深分页越翻越慢的陷阱。
代码真实索引50000条文档,每条带一个递增的n字段 POST /page_test/_search {"from":0,"size":10,"query":{"match_all":{}},"sort":[{"n":"asc"}]} POST /page_test/_search {"from":10000,"size":10,"query":{"match_all":{}}}
from=0 size=10(第一页): took=9ms, 1ms, 1ms from=10000 size=10: → 400 illegal_argument_exception: "Result window is too large, from + size must be less than or equal to: [10000] but was [10010]. See the scroll api for a more efficient way to request large data sets."
这条真实报错证明了ES对深分页设置了一道硬性的默认上限:index.max_result_window默认值是10000,from+size一旦超过这个数字,查询真实直接被拒绝,而不是放任它执行然后在性能上慢慢变差。这个限制真实是ES主动的保护机制——它在报错里直接点名了真实推荐的替代方案(scroll API,以及后面会验证的search_after),说明官方很清楚深分页本身就是一个真实有性能隐患的操作模式,默认直接把路堵死,逼着使用者改用更合理的方案。
代码PUT /page_test/_settings {"index.max_result_window":60000} POST /page_test/_search {"from":0,"size":10,"query":{"match_all":{}},"sort":[{"n":"asc"}]} POST /page_test/_search {"from":49990,"size":10,"query":{"match_all":{}},"sort":[{"n":"asc"}]}
from=0 size=10(第一页): took=1ms, 1ms, 1ms from=49990 size=10(翻到接近5万条数据的末尾): took=28ms, 19ms, 15ms ← 真实比第一页慢了15到28倍
代码POST /page_test/_search {"size":10,"query":{"match_all":{}},"sort":[{"n":"asc"}],"search_after":[49989]}
search_after从排序值49989继续往后翻10条: took=6ms, 1ms, 1ms ← 预热后真实稳定在1ms,跟from=0的第一页速度完全一样 ← 跟同样深度的from=49990相比(15-28ms),真实快了15到28倍
这组真实结果验证了search_after为什么能真正解决深分页的性能问题:它不依赖"数到第N条"这种需要排序全部前置结果的方式,而是真实传入上一页最后一条记录的排序值(本次是n=49989)作为"游标",让ES直接利用排序字段的索引结构真实跳转到这个值之后的位置继续取结果,不需要像from+size那样真实把前面所有候选结果都排序一遍再丢弃。代价是search_after真实只能"往后翻一页接一页",不能像from+size那样真实随意跳转到任意页码——这也是为什么它更适合"无限滚动加载"这种场景,而不适合"直接跳到第500页"这种传统分页UI。
每一步都是本文上面在本机真实的Elasticsearch 8.15.0上跑出来的原始结果。
from+size 分页与 search_after API