Elasticsearch中进行深分页(附源码)

简介

ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。

如需了解更多请查阅我的例外一篇博客:

分页方式 from+size弊端

es 默认采用的分页方式是 from+ size 的形式,在深度分页的情况下,这种使用方式效率是非常低的,还有一个无法解决的问题是,es 目前支持最大的 skip 值是 max_result_window ,默认为 10000 。也就是当 from + size > max_result_window 时,es 将返回错误,如下:

java es深分页查询 es深分页原理_java es深分页查询


这篇文章解决es的使用过程的查询条数不能超过10000的限制,因为随着页数越来越大,ES或者关系数据库响应越来越慢,甚至内存溢出OOM。

虽然es可以设置查询最大数量的配置max_result_window ,但是我个人建议不要去改。

在ES中有三种方式可以实现分页:from+size、scroll、search_after,下面介绍3种es提供的分页方式。

ElasticSearch java api使用

最新ES的提供了2种Java客户端,分别为低级REST客户端、高级REST客户端。
官网推荐使用高级Rest客户端,其他的客户端慢慢的都会被高级Rest客户端取代。

参照es官网文档:
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.4/java-rest-high-document-delete.html

一、常见深度分页方式 from+size

  1. 使用from+size方式进行分页,受max_result_window默认参数10000条文档的限制,不建议针对该参数进行修改。
  2. 默认分页方式,适用小数据量场景,大数据量场景应避免使用。
  3. 通过性能测试,随着分页越来越深,执行时间和堆内存使用逐渐升高的趋势,在并发情况下from+size容易,造成集群服务的OOM问题。

参考官网:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-search.html

kibana开发工具操作

GET testpage/_search
{
  "size": 10
}

java es深分页查询 es深分页原理_Scroll_02


java源码

SearchRequest searchRequest = new SearchRequest(
                "testpage");      

SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//设置查询大小
searchSourceBuilder.size(5000);
searchRequest.source(searchSourceBuilder);
//3、发送请求
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);

二、scroll深分页

官网api地址:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-search-scroll.html

  1. Scroll游标方式分页查询适用大数据量场景,只能向后增量查找,无法向前或者跳页查询,适用增量滚动抽取、数据迁移、重建索引等场景。
  2. 通过性能案例分析,滚动分页查找性能消耗相差不大,不会像from+size方式随着分页的深入性能逐渐升高的问题,且不会存在OOM问题。
  3. 该分页方式是查询的历史快照,对文档的更改(索引的更新或者删除)只会影响以后的搜索请求,不适用实时性查询场景。

kibana开发工具操作

scroll=5m为Scroll游标过期时间

GET testpage/_search?&scroll=5m
{
  "size": 10000
}


GET _search/scroll
{
  "scroll":"5m",
  "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAADYYWZDBWWnZfcjJRcXEzNEF0eFZtaWpYUQ=="

}

Scroll游标方式分页查询,首先查询第一次的游标id和数据,第二次查询使用第一次的游标id,循环遍历查询。

java es深分页查询 es深分页原理_from+size_03


java源码

SearchRequest searchRequest = new SearchRequest(
                "testpage");
        //设置滚动对象,并设置游标过期时间
        Scroll scroll = new Scroll(TimeValue.timeValueSeconds(60));
        searchRequest.scroll(scroll);

        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        //设置查询大小
        searchSourceBuilder.size(5000);
        searchRequest.source(searchSourceBuilder);

        List<Map<String, Object>> result = new ArrayList<>();

        //设置游标
        String scrollId;
        //3、发送请求
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);

        do {
            for (SearchHit hit : searchResponse.getHits().getHits()) {
                //获取需要数据
                Map<String, Object> sourceAsMap = hit.getSourceAsMap();
                result.add(sourceAsMap);
            }
            //每次循环完后取得scrollId,用于记录下次将从这个游标开始取数
            scrollId = searchResponse.getScrollId();
            SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
            scrollRequest.scroll(scroll);
            //进行下次查询
            searchResponse = client.scroll(scrollRequest, RequestOptions.DEFAULT);
        } while (searchResponse.getHits().getHits().length != 0);
        //清除滚屏
        ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
        //也可以选择setScrollIds()将多个scrollId一起使用
        clearScrollRequest.addScrollId(scrollId);
        client.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);

Clear Scroll API

使用scroll分页之后,es建议可以使用Clear Scroll API删除最后一个scroll标识符 ,以释放搜索上下文。滚动过期时会自动发生这种情况,但是最好在滚动会话完成后立即进行。

scrollRequest.scroll(TimeValue.timeValueSeconds(60L)); 
scrollRequest.scroll("60s");

滚动间隔为 TimeValue
滚动间隔为 60s
如果没有scroll为设置值SearchScrollRequest,则一旦初始滚动时间到期(即,在初始搜索请求中设置的滚动时间),搜索上下文就会失效。

三、search_after深分页

  1. 分页方式弥补了 scroll 方式打开scroll 占用内存资源问题
  2. search_after可并行的拉取大量数据
  3. search_after分页方式通过唯一排序值定位,将每次需要处理的数据控制在一定范围,避免深度分页带来的开销,适用深度分页的场景
    参考官网:https://www.elastic.co/guide/en/elasticsearch/reference/master/search-request-body.html#request-body-search-search-after

相当于sql里面根据id排序,然后获取最后一个id,从最后一个查询,循环遍历

kibana开发工具操作

根据num排序,每次查询10条数据

GET testpage/_search
{
  "size": 10,
  "search_after": [
    "0"
  ],
  "sort": [
    {
      "num": "asc"
    }
  ]
}

GET testpage/_search
{
  "size": 10,
  "search_after": [
    "10"
  ],
  "sort": [
    {
      "num": "asc"
    }
  ]
}

java es深分页查询 es深分页原理_from+size_04

java源码

SearchRequest searchRequest = new SearchRequest(
                "testpage");

        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        //设置查询大小
        searchSourceBuilder.size(2);
        //设置唯一排序值定位
        searchSourceBuilder.sort("num", SortOrder.ASC);
        searchRequest.source(searchSourceBuilder);

        List<Map<String, Object>> result = new ArrayList<>();

        //3、发送请求
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);

        do {
            for (SearchHit hit : searchResponse.getHits().getHits()) {
                //获取需要数据
                Map<String, Object> sourceAsMap = hit.getSourceAsMap();
                result.add(sourceAsMap);
            }

            //取得最后的排序值sort,用于记录下次将从这个地方开始取数
            SearchHit[] hits = searchResponse.getHits().getHits();
            Object[] lastNum = hits[hits.length - 1].getSortValues();

            //设置searchAfter的最后一个排序值
            searchSourceBuilder.searchAfter(lastNum);
            searchRequest.source(searchSourceBuilder);

            //进行下次查询
            searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
        } while (searchResponse.getHits().getHits().length != 0);

源码

码云:https://gitee.com/lhblearn/EsDemo