zoukankan      html  css  js  c++  java
  • Elasticsearch 搜索数量不能超过10000的解决方案

    一. 问题描述

    开发环境: JDK1.8、Elasticsearch7.3.1、RestHighLevelClient

    问题: 最近在通过Java客户端操作ES进行分页查询(from+size)时,需要返回满足条件的数据总数。我发现满足条件的数据总数一旦超过10000条,使用SearchResponse的getHits().getTotalHits().value返回的结果永远是10000。为什么会被限制只能搜索10000条数据呢?如何查询精确的数据总数呢?

    Tips: 本文侧重点在如何精确的获取数据总数,如果想知道如何深度搜索,请参考我的另一篇博客 Elasticsearch from+size与scroll混合使用实现深度分页搜索

    二. 问题分析

    查看官方文档: Elasticsearch 7.3

    Elasicsearch通过index.max_result_window参数控制了能够获取的数据总数from+size的最大值,默认是10000条。但是,由于数据需要从其它节点分别上报到协调节点,因此搜索请求的数据越多,会导致在协调节点占用分配给Elasticsearch的堆内存和搜索、排序时间越大。针对这种满足条件数量较多的深度搜索,官方建议我们使用Scroll。

    三. 解决方案

    3.1 调大index.max_result_window(不推荐)

    既然知道了是index.max_result_window参数限制了搜索数量,我们可以通过适当调高index.max_result_window的值,以此来满足需求。设置方法如下:

    • kibana上执行
    新建索引: 
    PUT your_index
    {
      "settings": {
        "max_result_window": "100000"
      }
    }
    
    在原有索引的基础上,调大index.max_result_window的默认值:
    PUT your_index/_settings?preserve_existing=true
    {
      "max_result_window": "100000"
    }
    
    • 服务器上执行
    curl -H "Content-Type: application/json" -X PUT 'http://127.0.0.1:9200/your_index/_settings?preserve_existing=true' -d '{"max_result_window" : "100000"}'

    这个方案我个人不太推荐,除非能预估出生产环境中索引内数据总量可能达到的上限,否则在未来实际数据量可能会超过设置的值,仍然会再次引发搜索数量受限的问题。

    3.2 cardinality(不推荐)

    cardinality字面意思是基数,作为聚合函数,它的作用与Mysql中的distinct类似,用于统计给定字段的不同值的数量。值得注意的是,cardinality获取的仅仅是估计值。使用方式如下:

    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
    
    // 设置聚合函数
    AggregationBuilder aggregationBuilder = AggregationBuilders.cardinality("distinct_id").field("_id");
    sourceBuilder.aggregation(aggregationBuilder);
    
    // 调用ES客户端,发起请求,得到响应结果
    response = search("INDEX_NAME索引名称", sourceBuilder);
    
    // 获取总记录数
    total = ((ParsedCardinality)response.getAggregations().getAsMap().get("distinct_id")).getValue();

    其中,“distinct_id"是我为聚合函数随便起的名称,可以任意指定,”_id"是希望进行分组统计的字段名称。上方这一段代码实际上可以翻译成以下执行语句:

    GET index_name/_search
    {
      "aggs": {
        "distinct_id": {
          "cardinality": {
            "field": "_id"
          }
        }
      }
    }

    3.3 track_total_hits(推荐)

    文档: track_total_hits
    使用方式:

    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
    sourceBuilder.trackTotalHits(true);
    // 省略查询方法...
    SearchResponse sumResponse = search(sourceBuilder);
    if(sumResponse != null) {
        // 满足条件的总记录数
        long total = sumResponse.getHits().getTotalHits().value;
    }



    Elasticsearch from+size与scroll混合使用实现深度分页搜索

     

    一. 需求

    环境准备: JDK1.8 Elasticsearch7.3.1 RestHighLevelClient客户端
    对Elasticsearch做深度分页,比如第1500页,每页20条记录,且需要支持前后翻页。

    二. 思考

    由于index.max_result_window的限制,直接使用from+size无法搜索满足条件10000条以上的记录。如果贸然增大index.max_result_window值,那么你怎么知道系统未来会在索引内存多少条数据?

    就算这一次设置值暂时解决了问题,那么未来又陷入瓶颈了怎么办?重新设值吗?调大后会增大内存压力的问题难道就不需要考虑吗?

    这时就需要使用scroll了,但scroll不能盲目的使用,它虽然支持深度分页,纯粹的使用scroll只能不断地向后翻页,我们还需要考虑如何向前翻页。

    三. 实现方案

    不改变index.max_result_window的默认值,但搜索手段根据搜索数量划分为以下两种:

    1. 搜索数量<=10000
      使用from+size的方式分页和搜索数据。
    2. 搜索数量>10000
      使用scroll的方式搜索数据。针对对每次分页查询请求,我都会创建游标,接着手动滚动到包含请求数据的那一屏,最后取出请求页面中的目标数据。

    比如现在准备查询第1413页,页面容量为10条数据,游标每次移动1000条记录,总记录数为1000000(这个值不重要了)。如果以1作为第一条数据的下标,则有以下规律:

    滚屏次数数据的下标范围
    1 1~1000
    2 1001~2000
    15 14001 ~ 15000
    n (n-1) * 1000 + 1 ~ n*1000

    第1413页的第一条数据的下标=(1413-1)*10+1=14121
    第1413页的最后一条数据的下标=14121+10-1=14130
    只需要移动15次游标,则在第15次游标查询返回的1000条数据中,一定包含了第1413页的所有数据。

    但我们还需要考虑另一种情况,比如现在准备查询第934页,页面容量为15条数据,游标仍然保持每次移动1000条记录。
    第934页的第一条数据的下标=(934-1)*15+1=13996
    第934页的最后一条数据的下标=13996+15-1=14010
    注意,我们的游标只能获取13001~14000和14001~15000范围内的数据,第934页会横跨两次游标执行结果,针对这种情况,我在代码中做了特殊处理。

    接下来是代码:

    • 定义搜索条件
    // 自定义搜索条件
    BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
    boolQueryBuilder.must(QueryBuilders.matchQuery("name", "麦当劳"));
    
    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
    sourceBuilder.query(boolQueryBuilder);
    // 设置请求超时时间
    sourceBuilder.timeout(new TimeValue(20, TimeUnit.SECONDS));
    // 排序
    sourceBuilder.sort("salary", SortOrder.ASC);
    • 与ES客户端交互的底层逻辑
      esClient就是RestHighLevelClient的对象
    protected SearchResponse search(String requestIndexName, SearchSourceBuilder sourceBuilder) throws Exception {
        SearchRequest searchRequest = new SearchRequest(requestIndexName);
        searchRequest.source(sourceBuilder);
        return esClient.search(searchRequest, RequestOptions.DEFAULT);
    }
    
    protected SearchResponse search(String requestIndexName,SearchSourceBuilder searchSourceBuilder,
                                            TimeValue timeValue) throws IOException {
        SearchRequest searchRequest = new SearchRequest(requestIndexName);
        searchSourceBuilder.size(ElasticsearchConstant.MAX_SCROLL_NUM);
        searchRequest.source(searchSourceBuilder);
        searchRequest.scroll(timeValue);
        return esClient.search(searchRequest, RequestOptions.DEFAULT);
    }
    
    protected SearchResponse searchScroll(String scrollId, TimeValue timeValue) throws IOException {
        SearchScrollRequest searchScrollRequest = new SearchScrollRequest(scrollId);
        searchScrollRequest.scroll(timeValue);
        return esClient.scroll(searchScrollRequest, RequestOptions.DEFAULT);
    }
    • 搜索逻辑(核心代码)
    // 本次搜索满足条件的数据总数
    long total = 0;
    // 精度
    int accuracy = 1;
    // 希望被忽略的记录条数
    int ignoreLogNum = (pageNum - 1) * pageSize;
    // 待查询页面内第一条记录的下标
    int firstSelectLogNum = 1;
    // 待查询页面内最后一条记录的下标
    int lastSelectLogNum = -1;
    // 当前游标查询返回结果中最后一条记录的下标
    int lastAllowLogNum = -1;
    // 游标Id
    String scrollId = null;
    // Elasticsearch 搜索返回结果对象
    SearchResponse response = null;
    
    try {
        firstSelectLogNum = ignoreLogNum + 1;
        lastSelectLogNum = firstSelectLogNum + pageSize - 1;
        String indexName = ElasticsearchConstant.SUB_INDEX_NAME_PREFIX + bizSubLogQuery.getProductNum().toLowerCase();
        if(firstSelectLogNum > ElasticsearchConstant.MAX_RESULT_WINDOW) {
            // 构建游标查询 此时游标已经移动了1次
            response = search(indexName, sourceBuilder, TimeValue.timeValueMinutes(1));
            if(response != null && response.getHits().getHits().length > 0) {
                // 游标总共需要移动的次数
                int scrollNum = firstSelectLogNum / ElasticsearchConstant.MAX_SCROLL_NUM + 1;
                lastAllowLogNum = scrollNum * ElasticsearchConstant.MAX_SCROLL_NUM;
                accuracy = firstSelectLogNum - (firstSelectLogNum / ElasticsearchConstant.MAX_SCROLL_NUM) * ElasticsearchConstant.MAX_SCROLL_NUM;
                // 游标Id
                scrollId = response.getScrollId();
                // 游标还需移动scrollNum-1次
                while(--scrollNum > 0 && scrollId != null) {
                    response = searchScroll(scrollId, TimeValue.timeValueMinutes(1));
                    scrollId = response.getScrollId();
                }
            }
        } else {
            // 分页参数
            sourceBuilder.from((pageNum - 1) * pageSize);
            sourceBuilder.size(pageSize);
    
            // 获取满足记录的总条数
            response = search(indexName, sourceBuilder);
        }
    
        // 查询总数
        sourceBuilder.size(0);
        sourceBuilder.trackTotalHits(true);
        SearchResponse sumResponse = search(indexName, sourceBuilder);
        if(sumResponse != null) {
            total = sumResponse.getHits().getTotalHits().value;
        }
    } catch (ElasticsearchStatusException ese) {
        if (RestStatus.NOT_FOUND == ese.status()) {
            log.error("待搜索的产品不存在");
        } else {
            log.error(ese.getMessage());
        }
    } catch (IOException ioe) {
        log.error("搜索失败,网络连接出现异常", ioe);
    } catch (Exception e) {
        log.error("搜索失败,未知异常", e);
    }
    
    if (response == null) {
        return new PageInfo<>();
    }
    
    // 搜索结果,使用集合来存放
    List<Map<String, String>> list = new ArrayList<>();
    
    // 游标一次性最高可能返回1000条数据,需要通过页面容量来约束
    int maxPageSize = pageSize;
    
    for (int i = 0; i < response.getHits().getHits().length; i++) {
        if(i+1 >= accuracy) {
            SearchHit hit = response.getHits().getAt(i);
            if(--maxPageSize < 0) {
                break;
            }
            try {
                list.add(JacksonUtils.jsonStrToMap(hit.getSourceAsString(), String.class, String.class));
            } catch (JsonProcessingException e) {
                log.error("jackson转换异常", e);
            }
        }
    }
    
    if(scrollId != null && maxPageSize>0 && lastAllowLogNum!=-1 && lastSelectLogNum>lastAllowLogNum) {
        // 存在目标数据不在本次游标查询的结果范围内
        // 需要再次移动游标 (务必保证游标移动的步长大于页面容量)
        try {
            response = searchScroll(scrollId, TimeValue.timeValueMinutes(1));
            for(int i = 0; i < maxPageSize && i < response.getHits().getHits().length; i++) {
                SearchHit hit = response.getHits().getAt(i);
                try {
                    list.add(JacksonUtils.jsonStrToMap(hit.getSourceAsString(), String.class, String.class));
                } catch (JsonProcessingException e) {
                    log.error("jackson转换异常", e);
                }
            }
        } catch (IOException ioe) {
            log.error("搜索失败,网络连接出现异常", ioe);
        }
    }
     
  • 相关阅读:
    POJ 1659 Frogs' Neighborhood (贪心)
    HDU 2544 最短路 (Floyd)
    CodeForces 632C Grandma Laura and Apples (模拟)
    CodeForces 731F Video Cards (数论+暴力)
    CodeForces 731C Socks (DFS或并查集)
    CodeForces 731B Coupons and Discounts (水题模拟)
    CodeForces 731A Night at the Museum (水题)
    UVaLive 6834 Shopping (贪心)
    zzuli 1484 继续双线
    zzuli 1875多线DP
  • 原文地址:https://www.cnblogs.com/gqzdev/p/14034962.html
Copyright © 2011-2022 走看看