← 返回题目列表

Elasticsearch 深分页为什么慢?from/size、scroll、search_after 怎么选?

高频 中等 第 12 / 30 题 更新于 2026/07/28
Elasticsearch深分页search_afterscroll

简化版

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。