一. 问题描述

开发环境: 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;
}