这里我项目使用的是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"
}
}
}
}