什么是 Elasticsearch 的深度分页问题?如何解决?
结论先行:
from + size分页要求每个分片都返回前from + size条,协调节点再归并取第from到from+size条。翻到第 1000 页时,5 个分片各要返回 1 万条,内存与 CPU 随页码线性增长,因此有了index.max_result_window(默认 10000)的保护阈值。解法是search_after(推荐)、scroll(导出场景)、PIT +search_after(实时且一致)。
一、为什么越翻越慢
from=9990, size=10, 5 个分片
分片1 ── 取前 10000 条 ──┐
分片2 ── 取前 10000 条 ──┤
分片3 ── 取前 10000 条 ──┼──> 协调节点归并 50000 条 ──> 丢弃前 9990 条 ──> 返回 10 条
分片4 ── 取前 10000 条 ──┤
分片5 ── 取前 10000 条 ──┘
代价与 from + size 成正比,与 size 无关。这也是为什么“每页 10 条”翻到第 1000 页比“每页 100 条”翻到第 100 页更慢。
二、四种方案对比
| 方案 | 原理 | 实时性 | 跳页 | 内存 | 适用 |
|---|---|---|---|---|---|
from + size | 各分片返回前 N 条归并 | 实时 | 支持 | 随页码增长 | 前 10000 条内的常规分页 |
search_after | 用上一页最后一条的排序值做游标 | 实时 | 不支持 | 恒定 | 无限下拉、滚动加载 |
scroll | 生成快照上下文,逐批拉取 | 快照 | 不支持 | 上下文占堆 | 全量导出、离线任务 |
PIT + search_after | 时间点快照 + 游标 | 快照内一致 | 不支持 | 中等 | 需要一致性的深翻页 |
三、search_after 的正确用法
必须有一个全局唯一的排序键,通常用“业务字段 + _id”组合:
GET /article/_search
{
"size": 10,
"sort": [
{ "publish_time": "desc" },
{ "_id": "asc" }
],
"search_after": [1735660800000, "a1001"],
"track_total_hits": false
}
下一页把最后一条的 sort 值原样放进 search_after。要点:
sort必须唯一且稳定,只按时间排序会在同一毫秒的多条记录间跳漏或重复。- 不能跳页,只适合“下一页”。
track_total_hits: false避免每次精确统计总量,能显著省时。
四、scroll 与 PIT
POST /article/_search?scroll=1m
{ "size": 1000, "sort": ["_doc"] }
POST /_search/scroll
{ "scroll": "1m", "scroll_id": "DXF1ZXJ5..." }
DELETE /_search/scroll
{ "scroll_id": "DXF1ZXJ5..." }
scroll 会持有段引用,期间不释放合并资源,长时间不清理会导致堆压力与磁盘膨胀,用完必须显式删除。
PIT(Point In Time)是 7.10 之后推荐的方式,为查询打开一个时间点视图,再配合 search_after:
POST /article/_pit?keep_alive=1m
GET /_search
{
"size": 1000,
"pit": { "id": "46ToAw..." , "keep_alive": "1m" },
"sort": [{ "publish_time": "desc" }, { "_id": "asc" }],
"search_after": [1735660800000, "a1001"]
}
PIT 只固定查询视图,不像 scroll 那样占用完整上下文,资源开销更小。
五、工程上的折中
| 做法 | 说明 |
|---|---|
| 限制最大页数 | 只允许翻到 100 页,超出引导用户改筛选条件 |
| 提高窗口 | index.max_result_window 调大只是推迟问题,且更耗内存 |
| 改为“跳页 + 游标” | 前 N 页用 from+size,之后切 search_after |
| 按时间分区 | 只查最近 30 天,缩小候选集 |
常见追问
- 追问:为什么默认限制 10000?每个分片要为
from+size排序并保留结果,页码越大堆压力越大,这是保护性阈值而非性能边界。 - 追问:
search_after能跳页吗?不能,它是有状态的游标;要跳页只能回退到from+size或改用其他存储。 - 追问:scroll 和 PIT 怎么选?导出全量用 scroll(简单、快照稳定);交互式深翻页用 PIT + search_after(资源更省)。
小结
- 深度分页的根因是“每个分片都要返回前 from+size 条”。
- 默认 10000 的窗口是保护阈值,调大只是延后爆炸。
- 无限下拉用
search_after,全量导出用scroll,一致性深翻页用 PIT。 - 排序键必须唯一稳定,否则会漏数据或重复。