Java Elasticsearch 分页案例:深度解析与最佳实践
目录导读
- 前言:为什么ES分页如此重要?
- ES分页基础扫盲:from/size vs scroll vs search_after
- 实战案例一:浅分页(from/size)实现
- 实战案例二:深度分页(scroll)实现
- 实战案例三:实时游标(search_after)实现
- 性能对比与选型建议
- 常见问题FAQ(含问答)
- 总结与黄金法则
前言:为什么ES分页如此重要?
在实际开发中,Elasticsearch(简称ES)常用于日志分析、电商搜索、内容管理系统等海量数据检索场景,当搜索结果超过10条时,分页是必选项。不合理的分页设计会导致内存溢出、查询超时、集群雪崩,使用from/size翻到第100页,ES内部需要从每个分片取出from+size条数据,再排序合并,数据量越大性能越差。

本案例基于Java High Level REST Client(7.x版本)和Spring Boot环境,给出三种主流分页方式的完整代码及适用场景。
ES分页基础扫盲
| 分页方式 | 原理 | 适用深度 | 实时性 | 内存消耗 |
|---|---|---|---|---|
| from/size | 跳过前N条,取M条 | 0~10000页 | 高 | 随页码递增 |
| scroll | 生成快照游标,分批读取 | 任意深度 | 低 | 较高(维护上下文) |
| search_after | 基于排序值定位,无跳页 | 任意深度 | 高 | 低 |
核心区别:from/size有默认max_result_window限制(默认10000条);scroll不适合实时系统;search_after是推荐的主流方案。
实战案例一:浅分页(from/size)实现
适用场景:用户手动翻页,总记录数小于1万,对实时性要求高(如管理系统、后台列表)。
Java代码示例
public SearchResponse fromSizePage(String index, int pageNo, int pageSize) {
SearchRequest request = new SearchRequest(index);
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 计算from
int from = (pageNo - 1) * pageSize;
sourceBuilder.from(from);
sourceBuilder.size(pageSize);
sourceBuilder.query(QueryBuilders.matchAllQuery()); // 实际用业务查询
request.source(sourceBuilder);
return restHighLevelClient.search(request, RequestOptions.DEFAULT);
}
注意:当from + size > 10000时,ES会抛出Result window is too large异常,解决方案:修改索引设置max_result_window,但推荐改用其他分页方式。
实战案例二:深度分页(scroll)实现
适用场景:导出全量数据、后台一次性处理(非用户交互),如生成报表、数据迁移。
Java代码示例
// 初始化scroll,有效期5分钟
public SearchResponse scrollInit(String index, int size) {
SearchRequest request = new SearchRequest(index);
SearchSourceBuilder source = new SearchSourceBuilder();
source.size(size);
source.query(QueryBuilders.matchAllQuery());
request.source(source);
request.scroll(TimeValue.timeValueMinutes(5));
return restHighLevelClient.search(request, RequestOptions.DEFAULT);
}
// 滚动获取下一页
public SearchResponse scrollNext(String scrollId) {
SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
scrollRequest.scroll(TimeValue.timeValueMinutes(5));
return restHighLevelClient.scroll(scrollRequest, RequestOptions.DEFAULT);
}
// 完成后清除scroll上下文
public void clearScroll(String scrollId) {
ClearScrollRequest clearRequest = new ClearScrollRequest();
clearRequest.addScrollId(scrollId);
restHighLevelClient.clearScroll(clearRequest, RequestOptions.DEFAULT);
}
重要:必须手动清除scroll上下文,否则会耗尽集群内存,注意scroll保存的是快照,新写入的数据在滚动期间不可见。
实战案例三:实时游标(search_after)实现
适用场景:无限滚动加载(如Feed流、消息列表),支持任意深度翻页且实时。
Java代码示例
public SearchResponse searchAfterPage(String index, int pageSize, Object[] sortValues) {
SearchRequest request = new SearchRequest(index);
SearchSourceBuilder source = new SearchSourceBuilder();
source.size(pageSize);
source.sort("timestamp", SortOrder.DESC); // 必须有一个唯一排序字段
source.sort("_id", SortOrder.ASC); // 避免重复
if (sortValues != null && sortValues.length > 0) {
source.searchAfter(sortValues);
}
source.query(QueryBuilders.termQuery("status", "active"));
request.source(source);
return restHighLevelClient.search(request, RequestOptions.DEFAULT);
}
// 第一次调用时 sortValues=null
// 从response中提取下一次的sortValues
Object[] nextSort = hit.getSortValues();
核心要点:
- 排序字段必须全局唯一(通常
timestamp+_id组合) - 不支持跳页(只能“下一页”)
- 相比scroll,它不占用服务端内存,且能看到最新数据
性能对比与选型建议
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 前10页用户翻页 | from/size | 简单直接,性能可接受 |
| 无限滚动/移动端分页 | search_after | 无深度限制,实时性佳 |
| 全量数据导出 | scroll | 快照隔离,适合批量处理 |
| 聚合结果分页 | 不建议分页 | 聚合本身有top_hits限制,需重新设计 |
黄金法则:
- 永远不要让用户翻到第10000条之后(设计搜索体验时考虑)
- 超过10000条必须用
search_after或scroll - 记住
search_after是阿里、美团等大厂在ES分页实践中的首选
常见问题FAQ(含问答)
Q1:from/size分页时如何避免es内存溢出?
A:设置max_result_window为合理值(如500000),但更推荐业务上限制翻页深度(如只允许查看前100页),如果必须翻很多页,改用search_after。
Q2:scroll分页如何保证数据一致性?
A:scroll在初始化时生成搜索快照,后续滚动不会看到新写入或更新的数据,如果需要最新数据,请使用search_after或重新发起scroll。
Q3:search_after分页时排序字段如何选择?
A:必须选择每个文档都存在的字段,且尽量唯一,常见组合:timestamp(保证顺序)+ _id(避免时间相同导致重复),注意_id是字符串,排序时按字典序。
Q4:为什么search_after不能跳页?
A:因为它是基于上一页最后一条记录的排序值来定位下一页起始位置,底层是游标式移动,无法计算偏移量,如果想跳页,只能用from/size,但深度受限。
总结与黄金法则
- 轻量翻页(<10页):用
from/size,代码简单 - 无限加载:用
search_after,平衡性能与实时 - 数据导出:用
scroll,适合离线任务 - 永远不要:在用户界面实现“跳转到第1000页”功能
在Java实现中,推荐使用Spring Data Elasticsearch或官方High Level Client,注意关闭scroll上下文、给排序字段加索引(如timestamp字段)、监控search_after的响应延迟。
最后留一个思考题:如果你的ES集群索引文档数量超过10亿,且业务要求支持“从第10万页开始翻”,你该如何设计?欢迎在评论区探讨答案(提示:结合路由、时间分区、搜索引擎架构优化)。