Java ES分页案例如何实现

wen java案例 27

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 ES分页案例如何实现

本案例基于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限制,需重新设计

黄金法则

  1. 永远不要让用户翻到第10000条之后(设计搜索体验时考虑)
  2. 超过10000条必须用search_afterscroll
  3. 记住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万页开始翻”,你该如何设计?欢迎在评论区探讨答案(提示:结合路由、时间分区、搜索引擎架构优化)。

抱歉,评论功能暂时关闭!