Elasticsearch 深分页为什么慢?from/size、scroll、search_after 怎么选?
简化版
Elasticsearch 深分页慢,是因为 from + size 会让每个分片取出并排序大量被跳过的数据,页数越深浪费越大。普通浅分页用 from/size,深度翻页用 search_after 配合稳定排序和 PIT,大批量导出用 scroll 或专门的数据导出方案。
详细版
from/size 的语义很像 SQL 的 offset:
GET /products/_search
{
"from": 10000,
"size": 20,
"query": {
"match": { "name": "phone" }
}
}
问题在于分布式场景下,每个 shard 都要取出足够多的候选结果,协调节点再合并排序,最后丢弃前面的 from 条。页码越深,被取出又丢弃的数据越多。
选择建议:
| 场景 | 推荐方式 |
|---|---|
| 前几页搜索结果 | from/size |
| 无限滚动、下一页 | search_after |
| 需要分页期间视图稳定 | search_after + PIT |
| 全量导出、批处理扫描 | scroll 或异步导出 |
search_after 不是随机跳页,它依赖上一页最后一条记录的 sort 值继续往后查。所以它适合“下一页”,不适合直接跳到第 500 页。
完整版教学
一、深分页慢在哪里
假设一个索引有 5 个分片,请求第 1001 页:
from = 10000
size = 10
每个分片都不知道自己哪些结果会进入全局前 10010 条,所以需要先取出本分片排名靠前的一批候选结果。协调节点拿到多个分片结果后,再做全局排序,最后丢掉前 10000 条,只返回 10 条。
浪费发生在两个地方:
- shard 侧要收集和排序大量候选结果;
- coordinating node 要合并排序,再丢弃大部分结果。
这就是深分页比浅分页慢很多的根本原因。
二、from/size 适合什么场景
from/size 最大优点是简单,支持页码跳转:
GET /articles/_search
{
"from": 0,
"size": 10,
"query": {
"match": { "title": "Java" }
}
}
它适合:
- 搜索结果前几页;
- 后台管理列表浅分页;
- 用户需要跳页但数据量不大;
- 对性能要求不高的小索引。
Elasticsearch 默认有 index.max_result_window 限制,常见默认值是 10000。不要简单把这个值调得很大,因为它会放大内存和排序压力。
三、search_after 如何解决深分页
search_after 的思路不是“跳过 N 条”,而是“从上一页最后一条之后继续查”。
第一页:
GET /articles/_search
{
"size": 10,
"sort": [
{ "created_at": "desc" },
{ "tie_breaker_id": "asc" }
],
"query": {
"match": { "title": "Elasticsearch" }
}
}
假设最后一条的 sort 值是:
["2026-07-19T10:00:00Z", "abc123"]
下一页:
GET /articles/_search
{
"size": 10,
"sort": [
{ "created_at": "desc" },
{ "tie_breaker_id": "asc" }
],
"search_after": ["2026-07-19T10:00:00Z", "abc123"],
"query": {
"match": { "title": "Elasticsearch" }
}
}
关键要求:
- 排序字段必须稳定;
- 最好加一个带 doc values 的唯一字段作为 tie-breaker,例如把文档 id 复制到
tie_breaker_id; - 客户端要保存上一页最后一条的 sort 值;
- 不支持任意跳页。
四、PIT 为什么经常和 search_after 一起出现
分页过程中索引可能持续写入和刷新。如果第一页和第二页看到的索引视图不一致,就可能出现重复或漏数据。
PIT,即 point in time,可以为搜索创建一个相对稳定的时间点视图。常见组合是:
打开 PIT -> search_after 翻页 -> 关闭 PIT
这样用户连续翻页时,看到的是同一个时间点附近的结果集合,稳定性更好。
五、scroll 适合导出,不适合用户搜索翻页
scroll 会保持一个搜索上下文,适合批量扫描大量数据,例如离线导出、数据迁移、批处理任务。
但它不适合普通用户搜索分页:
- 会占用集群资源;
- 上下文保持时间长会影响集群;
- 用户搜索更关注实时性和交互体验;
- 官方更推荐用
search_after + PIT做深度分页。
六、常见误区与追问
| 分页方式 | 是否适合跳页 | 是否保持稳定视图 | 典型用途 |
|---|---|---|---|
from/size | 适合浅页 | 否 | 搜索结果前几页、后台小列表 |
search_after | 不适合任意跳页 | 单独使用不保证 | 无限滚动、下一页 |
search_after + PIT | 不适合任意跳页 | 是 | 深翻页且要求结果视图稳定 |
| scroll | 不适合用户翻页 | 保持搜索上下文 | 批量导出、离线扫描 |
第 1 页最后一条 sort = [1000, "doc-9"]
第 2 页请求 search_after = [1000, "doc-9"]
系统不是跳过 1000 条,而是从这个排序锚点继续向后取。
易错点:
search_after解决的是“继续向后翻”的成本,不解决“直接跳到第 N 页”的产品需求。
如果 5 个 shard 查询 from=100000,size=20,每个 shard 至少要维护大量候选结果,协调节点再合并后丢掉前 100000 条;而 search_after 只围绕上次最后一条 sort 值继续取下一批,内存压力和排序浪费会小很多。
- 误区:把
index.max_result_window调大就解决深分页了。 这只是放开限制,底层仍要收集、排序、丢弃大量候选结果,内存和 CPU 压力会被放大。 - 误区:
search_after可以直接跳到第 500 页。 它依赖上一页最后一条记录的 sort 值,适合顺序翻页,不适合随机页码跳转。 - 误区:有 PIT 就不需要稳定排序字段。 PIT 保证搜索视图相对稳定,但仍需要确定性排序和 tie-breaker 避免相同排序值导致重复或遗漏。
- 追问:为什么要加 tie-breaker? 如果只按时间排序,很多文档可能有相同时间戳,追加唯一字段能让全局顺序稳定。
- 追问:scroll 为什么不推荐给在线搜索? scroll 会维持搜索上下文,占用集群资源,更适合批处理导出,而不是用户长时间停留的交互分页。
- 追问:产品必须支持跳到第 500 页怎么办? 通常要重新审视需求,可限制最大页数、用筛选条件缩小结果、改成游标翻页,或把离线报表查询交给更合适的系统。
七、加强记忆
记住“from/size 是跳过,search_after 是接着走,scroll 是批量扫”。浅分页用 from/size,深分页和无限滚动用 search_after,需要稳定视图加 PIT,全量导出才考虑 scroll。