Elasticsearch cardinality 精度问题

在Elasticsearch7.x项目中,遇到分页查询结果与数据库总数不符的问题,原因是Cardinality聚合在大数据量时存在5%误差。通过调整precision_threshold参数未能解决问题,最终采用TermsAggregation结合StatsBucket解决了精度问题。
摘要由CSDN通过智能技术生成

项目场景:

这里我项目使用的是Elasticsearch 7.x
应工作的需要,用到了Elasticsearch,而最近在项目开发上线前测试的时候发现了一个bug,
就是我有一个ES分页查询逻辑,通过页面查询出来的分页总数和数据库里面的总数对应不上,
首先ES是作为一张大宽表,录入每个人的基本信息+业务信息,所以ES中每个人的数据都会
产生很多条,我分页的时候是以人为维度进行查询,当时想到的就是借用ES提供的聚合查询
cardinality去重统计分页后的总数。

关于在项目中运用到的依赖以及不会的同学可以参考我上之前的文章

Springboot ElasticSearch依赖怎么选

Springboot中如何使用ElasticSearch

问题描述

我有一个ES分页查询逻辑,分页查询的时候需要根据用户的维度进行分组,
每组取最新一条展示,但是通过页面查询出来的分页总数和数据库里面的
总数对应不上。

原因分析:

因为第一次用ES,拿到问题的后觉得可能是自己代码写得有问题

但是DEV环境为啥就是好的,所以我想的第一步是先去检查代码,
经过查看,确实看不出代码有什么问题,所以我又debug了一遍,
因为是开发环境,没有发现问题。

这时候我就有点怀疑是ES的问题,所以我就利用kibana直接在生产上
,按照搜索条件统计了一遍输出结果,发现我的分页结果一摸一样,
然后同样的条件,有去数据库中统计了一遍,终于发现了问题,就是
ES cardinality 导致的精度问题

然后我就是官方查找文档,发现使用cardinality统计总数时,当你的数据
总量大于4w的时候就存在5%误差。说是底层使用HyperLogLog++ (HLL)算法,
亿级别的记录在1秒内完成统计,当然牺牲就是精确度了,对于不需要精确度的地
方还是可以用的。

解决方案:

接着我发现这精度时可以修改的,顿时喜出望外

precision_threshold // 接受 0–40000 之间的数字,更大的值还是会被当作 40000 来处理。
GET /cars/transactions/_search
{
    "size" : 0,
    "aggs" : {
        "distinct_colors" : {
            "cardinality" : {
              "field" : "color",
              "precision_threshold" : 100 
            }
        }
    }
}

接着我将precision_threshold参数调到最大,发现还是问题,顿时想/(ㄒoㄒ)/~~

品着敬业🤣的职责,只能看能不能通过更换统计的方法处理了,
下面是我的代码

这是改动代码最少的方式了,但是代价就是统计会变慢

这是我有问题部分代码

// 构建请求和请求体
SearchRequest request = new SearchRequest("索引名");
SearchSourceBuilder builder = new SearchSourceBuilder();
........

//userId为用户唯一标识,同一用户相同
// 此处就是cardinality丢失精度统计的方法
CardinalityAggregationBuilder aggregation = AggregationBuilders.cardinality("count").field("userId");
builder.aggregation(aggregation);
request.source(builder);

// 执行发送es请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);

// .....

// 获取分组信息
Aggregations aggregations = response.getAggregations();

// 获取上面cardinality去重后的数量
ParsedCardinality cardinality = (ParsedCardinality) aggregations.getAsMap().get("count");

// 这里获取总数
long totleSize =  cardinality.getValue();

下面时修改后的代码

// 构建es请求  
// idxName 是索引名称
SearchRequest request = new SearchRequest(idxName);
//构建es请求参数
SearchSourceBuilder builder = new SearchSourceBuilder();
 
 // ......
 
 // userId 是你要分组的字段 distinct_userId 分组后的名称可以随便取 
TermsAggregationBuilder aggregation = AggregationBuilders.terms("distinct_userId").size(Integer.MAX_VALUE).field("userId");

// stats_map 是统计的别名 distinct_userId 分组后的别名 都可以随便取
        StatsBucketPipelineAggregationBuilder statsBucket = PipelineAggregatorBuilders.statsBucket("stats_map", "distinct_userId._count");
        
 // ......
 
 // 将分组的信息放入builder
 builder.aggregation(statsBucket );
 // 将builder信息放入请求体
 request.source(builder);
 // 执行请求
 SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
 // 拿到分组后的信息
 Aggregations aggregations = response.getAggregations();
 // 拿到统计的值
 ParsedStatsBucket statsMap = (ParsedStatsBucket)aggregations.getAsMap().get("stats_map");
 // 这里就是你要统计的数量
 long  totleSize =  stats.getCount(); 

如果上面还是没有看懂,这里我写出对应的ES统计数量的表达式,
上面也就是我按照表达式翻译成代码实现的

对ES还不太熟的同学可以照我这样,先把表达式写出来看结果对不对,
在照着表达之把它翻译成Java代码

GET /索引名/_search
{
  "size": 0,
  "aggs": {
    "distinct_userId":{
      "terms": {
        "field": "userId",
        "size": 10000000
      }
    },
    "stats_map":{
      "stats_bucket": {
        "buckets_path": "distinct_userId._count"
      }
    }
  }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值