search_after 解决深分页
search_after 是 Elasticsearch 的游标式分页手段:带着上一页最后一条文档的排序值去请求下一页,因此不需要像 from+size 那样在协调节点上归并前面所有页的结果。代价是只能顺序向后翻、不能跳页,适合一直往下加载的列表、数据导出与审计遍历这类场景。
基本信息
| 原理 | 把上一页末尾的 sort 值作为下一页起点,不重复计算前面的页 |
|---|---|
| 必要条件 | 排序键组合全局稳定且唯一,推荐业务时间加 _id 兜底 |
| 限制 | 不能跳页,只能顺序向后翻 |
| 适用版本 | Elasticsearch 5.0 起提供 |
| 配合 PIT | 用 point in time 固定索引视图,避免翻页期间数据变化导致漂移 |
典型使用场景
- 大批量数据导出:分批遍历全量文档,替代被官方标记为不推荐的 scroll 深分页用法
- 前端无限滚动:用户持续下滑时的下一页请求
- 按时间顺序遍历日志或审计记录,边读边处理
- 跨索引搬运数据,配合 PIT 保持视图一致
- 绕开 from+size 超过 max_result_window(默认一万)的限制
翻页流程
翻页流程
# 首页:排序键必须能确定唯一顺序,用 _id 兜底
GET /my-index/_search
{
"size": 10,
"sort": [ { "publish_date": "desc" }, { "_id": "asc" } ]
}
# 下一页:把上一页最后一条的 sort 数组按顺序填入
GET /my-index/_search
{
"size": 10,
"sort": [ { "publish_date": "desc" }, { "_id": "asc" } ],
"search_after": [1719792000000, "doc-42"]
}
# 配合 PIT 固定视图,用 _shard_doc 作为稳定兜底键
POST /my-index/_pit?keep_alive=1m
GET /_search
{ "size": 10, "pit": { "id": "<pit_id>", "keep_alive": "1m" },
"sort": [ { "publish_date": "desc" }, { "_shard_doc": "asc" } ],
"search_after": [1719792000000, 7] }分页方式对照
| 方式 | 适用深度 | 代价 |
|---|---|---|
| from + size | 浅分页,默认上限一万 | 深分页时协调节点归并开销急剧上升 |
| search_after | 深分页、顺序遍历 | 不能跳页,需要稳定且唯一的排序键 |
| search_after + PIT | 翻页期间数据不能变的遍历 | 需管理 PIT 生命周期,会占用资源 |
| scroll | 一次性大批量导出 | 快照占资源,官方不再推荐用于深分页 |
search_after 的值必须与 sort 数组一一对应且顺序一致,少填或多填一项都会报参数数量不匹配;排序键组合不唯一时,翻页会漏文档或重复返回,因此必须有 _id 或 _shard_doc 这类兜底键。