Elasticsearch 深度分页:Scroll vs PIT + search_after

一、背景

Elasticsearch 有三种分页方式:

方式 适用场景 深度分页 一致性快照
from + size 前端翻页,浅层 ❌ 默认最多 10000 条 ❌ 每次查询数据可能变
scroll 后台全量导出 ✅ 快照时间点冻结
PIT + search_after 实时深度分页 / 全量遍历 ✅ 快照时间点冻结

其中 ScrollPIT + search_after 都能解决"深度分页 + 快照一致性"问题,但设计思路和适用场景差异明显。Scroll 从 ES 7.x 开始官方不再推荐,PIT + search_after 已成为主流。


二、Scroll 是什么

2.1 工作原理

  1. 首次查询时传 scroll=1m,ES 为本次查询在服务端保留一份快照上下文 (scroll context),返回 _scroll_id
  2. 后续每次请求带上 _scroll_id,ES 从快照位置继续往下拉取
  3. 快照在 TTL(如 1 分钟)内不会释放,期间即使原始索引发生 CRUD,也不影响本次遍历

关键词:服务端有状态上下文存活于 TTL 内快照冻结所有段(segments)

2.2 代码示例(Java High Level REST Client)

java 复制代码
import org.elasticsearch.action.search.*;
import org.elasticsearch.client.*;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;

public class ScrollExample {

    public void exportAllSkus(RestHighLevelClient client) throws Exception {
        // 1. 首次查询,建立 scroll 上下文
        SearchRequest searchRequest = new SearchRequest("store_sku_index");
        searchRequest.scroll(TimeValue.timeValueMinutes(1L));

        SearchSourceBuilder ssb = new SearchSourceBuilder();
        ssb.query(QueryBuilders.termQuery("storeId", 1017395374L));
        ssb.size(1000);
        searchRequest.source(ssb);

        SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
        String scrollId = response.getScrollId();

        try {
            // 2. 处理首批数据
            handleHits(response.getHits().getHits());

            // 3. 循环拉取剩余数据
            while (response.getHits().getHits().length > 0) {
                SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
                scrollRequest.scroll(TimeValue.timeValueMinutes(1L)); // 续期 TTL

                response = client.scroll(scrollRequest, RequestOptions.DEFAULT);
                scrollId = response.getScrollId();
                handleHits(response.getHits().getHits());
            }
        } finally {
            // 4. 主动释放 scroll 上下文,避免占用节点资源
            ClearScrollRequest clearRequest = new ClearScrollRequest();
            clearRequest.addScrollId(scrollId);
            client.clearScroll(clearRequest, RequestOptions.DEFAULT);
        }
    }

    private void handleHits(SearchHit[] hits) {
        for (SearchHit hit : hits) {
            // 业务处理
        }
    }
}

2.3 缺点

  • 不适合并发:同一个 scroll_id 只能串行消费
  • 资源开销大:节点上要冻结整片段文件,占用磁盘 inode/文件句柄;并发多个 scroll 会拖垮集群
  • 无法跳转:只能顺序往后翻,不能"从第 100 页跳到第 500 页"
  • 官方不推荐用于实时业务:ES 7.10 引入 PIT 后,scroll 定位为纯粹的后台批量导出工具

三、PIT + search_after 是什么

3.1 工作原理

  1. 通过 POST /{index}/_pit?keep_alive=1m 单独创建 一个时间点视图(Point In Time),返回 pit_id
  2. 查询请求不带 index path,只在 body 里带 pit.idsearch_after(上一批最后一条命中的 sort 值)
  3. ES 从 search_after 之后的位置继续查,保持在同一个 pit 快照上
  4. 处理完主动 DELETE /_pit 释放

关键词:pit_id 独立创建每次查询无状态(客户端持有游标)多个客户端可并行使用同一个 pit

3.2 代码示例(Java High Level REST Client)

java 复制代码
import org.elasticsearch.action.search.*;
import org.elasticsearch.client.*;
import org.elasticsearch.client.core.*;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.PointInTimeBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortOrder;

public class PitSearchAfterExample {

    public void exportAllSkus(RestHighLevelClient client) throws Exception {
        // 1. 创建 PIT
        OpenPointInTimeRequest openReq =
                new OpenPointInTimeRequest("store_sku_index")
                        .keepAlive(TimeValue.timeValueMinutes(1L));
        OpenPointInTimeResponse openResp = client.openPointInTime(openReq, RequestOptions.DEFAULT);
        String pitId = openResp.getPointInTimeId();

        try {
            Object[] searchAfter = null;
            while (true) {
                SearchSourceBuilder ssb = new SearchSourceBuilder();
                ssb.query(QueryBuilders.termQuery("storeId", 1017395374L));
                ssb.size(1000);
                // ⚠️ 排序字段必须是 unique 且稳定(通常业务字段 + _shard_doc 或 _id 兜底)
                ssb.sort("skuId", SortOrder.ASC);
                ssb.sort("_shard_doc", SortOrder.ASC);
                // ⚠️ 使用 PIT 时,SearchRequest 不指定 index
                ssb.pointInTimeBuilder(
                        new PointInTimeBuilder(pitId).setKeepAlive(TimeValue.timeValueMinutes(1L)));

                if (searchAfter != null) {
                    ssb.searchAfter(searchAfter);
                }

                SearchRequest req = new SearchRequest();
                req.source(ssb);

                SearchResponse resp = client.search(req, RequestOptions.DEFAULT);
                SearchHit[] hits = resp.getHits().getHits();
                if (hits.length == 0) break;

                for (SearchHit hit : hits) {
                    // 业务处理
                }

                // 用最后一条命中的 sort 值作为下一页的游标
                searchAfter = hits[hits.length - 1].getSortValues();

                // PIT 可能会自动续期(ES 服务端在 keep_alive 内每次使用会续期)
                pitId = resp.pointInTimeId(); // ES 7.12+ 支持自动返回新的 pit_id
            }
        } finally {
            // 2. 主动关闭 PIT
            ClosePointInTimeRequest closeReq = new ClosePointInTimeRequest(pitId);
            client.closePointInTime(closeReq, RequestOptions.DEFAULT);
        }
    }
}

3.3 优点

  • 无服务端会话:pit_id 只是快照标识,查询本身无状态,天然支持并发/断点续查
  • 资源占用小:pit 不冻结段文件,只在 refresh 时保留必要的引用
  • 可跳转:只要有 sort 值,就能从任意位置继续,不必顺序遍历
  • 可组合聚合、可控 sort:比 scroll 更灵活

3.4 缺点

  • sort 字段必须全局唯一稳定 :如果 sort 值有重复,可能漏数据或重复。通常用业务字段 + _shard_doc 兜底
  • 写入频繁的索引会看到"旧版本":PIT 期间新写入的文档不会出现在遍历中(和 scroll 一样)
  • 需要客户端管理游标:每次调用要传上一批的 sort 值,断线要重传

四、核心差异对比

维度 Scroll PIT + search_after
服务端状态 有 scroll context 只有 pit,查询本身无状态
并发消费 ❌ 单线程串行 ✅ 多线程/多节点可并行
游标持有方 服务端 (_scroll_id) 客户端 (search_after 数组)
资源开销 高(冻结段) 低(只保留引用)
能否跳转 ❌ 只能顺序 ✅ 只要有 sort 值
推荐版本 逐步废弃(ES 7+ 建议迁移) ES 7.10+ 主流方案
续期方式 每次请求带 scroll TTL keep_alive 由每次请求自动续期
适用场景 后台一次性全量导出 深度分页、实时数据 sync、并行导出

五、实际应用场景

5.1 用 Scroll 的场景

  • 定时任务全量导出:每晚 T+1 把某索引 dump 到数仓/Excel,串行、单机、TTL 够长即可
  • 数据迁移 reindex:从旧集群/旧索引一次性拉取全量数据
  • 对并发无要求 + 逻辑简单

5.2 用 PIT + search_after 的场景

  • 实时业务的深度分页:C 端下拉列表翻页(如京东到家门店 SKU 翻页 100+ 页)

  • 并行数据导出:多个 worker 各自持有 sort 起点,并行消费同一个 pit

  • 失败重试友好:客户端保存 sort 值就能断点续传,不需要重新遍历

    这里的接口实际只用 searchAfter 而未使用 pitId(见前文实验结果),说明该接口选择了 无 PIT 的 search_after 变种------牺牲快照一致性,换取 pit 生命周期管理成本,适合"允许少量脏读"的分页场景。

5.3 变种:无 PIT 的 search_after

如果业务能容忍"分页期间数据被写入导致的漂移",可以完全不用 pit,只用 sort + search_after。ES 本身也支持这种模式:

java 复制代码
SearchSourceBuilder ssb = new SearchSourceBuilder();
ssb.query(...);
ssb.sort("skuId", SortOrder.ASC);
ssb.size(50);
if (searchAfter != null) ssb.searchAfter(searchAfter);
// 注意:不设置 pointInTimeBuilder,SearchRequest 保留 index
SearchRequest req = new SearchRequest("store_sku_index");
req.source(ssb);

代价:分页期间新写入的文档可能穿插进结果,或旧文档删除后跳过。收益:零快照开销,pit 生命周期不用管。京东到家 searchAfterStoreSkus 用的就是这种。


六、迁移建议

如果历史代码还在用 scroll,考虑以下迁移路径:

  1. 一次性后台任务 → 保留 scroll(简单够用)
  2. C 端翻页 / 实时业务 → 迁移到 PIT + search_after,添加 _shard_doc 兜底 sort
  3. 允许脏读的分页 → 用无 PIT 的 search_after(sort + search_after)

迁移时注意事项:

  • Scroll 是服务端游标,客户端只要 scroll_id 即可继续
  • PIT + search_after 需要客户端保存 sort 值,重启/失败后能否恢复取决于是否持久化了 sort 值
  • 排序字段一定要唯一稳定,推荐:业务主键 + _shard_doc(或 _id)

七、快速决策清单

  • 你要导全量、串行 OK、离线任务 ? → Scroll
  • 你要实时翻页、并发消费、支持断点续传 ? → PIT + search_after
  • 你要分页但允许期间数据漂移、不想管快照生命周期 ? → 无 PIT 的 search_after
  • 你要只翻前几页(< 10000) ? → from + size 就够了

参考

相关推荐
leoZ2312 小时前
Git 集成实战完全指南(二):智能分支管理
大数据·git·elasticsearch
Elastic 中国社区官方博客10 小时前
4 个英伟达人工智能任务,1 个 Elasticsearch 接口:嵌入、聊天、completion 和重排序
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai
Elastic 中国社区官方博客14 小时前
从多模态 LLM 中引导构建音频嵌入
大数据·人工智能·elasticsearch·搜索引擎·ai·全文检索·音视频
Elasticsearch14 小时前
Elastic 被评为 2026 年 Gartner® 可观测性平台魔力象限™领导者
elasticsearch
幂丽管理88814 小时前
新版通达信顶天立地1主1副1选股套装工具
elasticsearch·逻辑回归·启发式算法·memcached
Elasticsearch14 小时前
Elastic 以满分成绩领先最新 AV-Comparatives 恶意软件防护测试
elasticsearch
BerryS3N17 小时前
VS Code Git 工作树:解锁多分支并行开发新体验
大数据·git·elasticsearch
工具派19 小时前
从 git commit 到交付 PDF:我的一条在线工具流实录
git·elasticsearch·pdf
Elasticsearch1 天前
从课程表到 AI 岗位:我如何用 Elastic Agent Builder 构建 SkillGap Radar
elasticsearch