之前的博客寫過Spring與RestHighLevelClient的基礎用法,但是實際使用中,會存在大量的復雜操作,如分組,聚合等。
接下來我們就來看下不太好用的分組聚合基本用法
AggregationBuilder
- 首先RestHighLevelClient中分組需要用到AggregationBuilder,作為分組條件的構建。
我們可以看到AggregationBuilder接口有很多實現類,比如AvgAggregationBuilder、MaxAggregationBuilder、SumAggregationBuilder等常用的統計函數。
在使用上還是和之前的大致一樣,
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 以batchId為分組條件,terms為分組后的字段名稱,field為將被分組的字段名稱
TermsAggregationBuilder aggregation = AggregationBuilders.terms("batchId").field("batchId.keyword")
// 分組求和integral字段,並將求和后的字段名改為score
// subAggregation為子聚合,即在batchId分組后的小組內聚合
.subAggregation(AggregationBuilders.sum("score").field("integral"))
// 注意這里,下面介紹
.subAggregation(AggregationBuilders.topHits("details").size(1));
sourceBuilder.aggregation(aggregation);
BoolQueryBuilder boolBuilder = QueryBuilders.boolQuery();
sourceBuilder.query(boolBuilder);
SearchRequest searchRequest = new SearchRequest("table");
searchRequest.source(sourceBuilder);
SearchResponse search = client.search(searchRequest);
// 和之前不同的是這里需要getAggregations獲取聚合后的數據
Aggregations aggregations = search.getAggregations();
// 從分組后的數據中拿到batchId的數據,這里以batchId分組,則分組后的數據都在batchId里
ParsedStringTerms terms = aggregations.get("batchId");
// 獲取到分組后的所有bucket
List<? extends Terms.Bucket> buckets = terms.getBuckets();
for (Terms.Bucket bucket : buckets) {
// 解析bucket 因為一級聚合為以batchId分組,二級聚合為求和,所以這里還需要getAggregations獲取求和的數據
Aggregations bucketAggregations = bucket.getAggregations();
// 這里我是通過debug才找到返回的參數類型的,我不知道在哪找得到這個東西,所以我們拿到了ParsedTopHits,這里我們是取了一個,所以這個值的數組長度為1
ParsedTopHits topHits = bucketAggregations.get("details");
// 因為求和和下面的topHits都是AggregationBuilders.terms("batchId").field("batchId.keyword")的subAggreation,所以都屬於batchId組內
// 獲取到求和的數據信息
ParsedSum sum = bucketAggregations.get("integral");
// 因為topHits中命中的hits肯定至少有一個,要不然也不會成組,所以這里直接獲取第一個,並解析成map
Map<String, Object> sourceAsMap = topHits.getHits().getHits()[0].getSourceAsMap();
// 將求和后的integral覆蓋到原數據中
sourceAsMap.put("integral", sum.value());
// 打印出統計后的數據
System.out.println(sourceAsMap);
}
注意點:
- 為什么要用subAggregation(AggregationBuilders.topHits("details").size(1))?
- 這里有個坑,如果只用batchId分組並用intrgal求和,那么返回值中只會返回batchId與integral求和后的值這兩個字段。這里舉個例子,現在統計全國所有學校的總人數,那么我們以school分組,求和了人數。但是我們仍然想知道每個學校屬於哪個地區的,這里聚合后就不會返回這些信息,讓人很難受。
- 此時我們就可以使用subAggregation(AggregationBuilders.topHits("details").size(1))來獲取一個通用的數據,拿到學校所屬的地區名稱或者其他學校的上級屬性。
- AggregationBuilders層級
- 我們注意到subAggregation方法是AggregationBuilder的,所以在構建AggregationBuilders的時候,每一個子AggregationBuilder都可以有子AggregationBuilder,如下
TermsAggregationBuilder aggregation = AggregationBuilders.terms("batchId").field("batchId.keyword")
.subAggregation(
AggregationBuilders.terms("folder").field("folder")
.subAggregation(AggregationBuilders.sum("integral").field("integral"))
);
- 這種就是組內再聚合套組內,但是得注意字段的問題,即若你以batchId分組后,那么組內只有batchId與其他的字段,就和第一個注意點一樣,導致組內聚合拿不到相關的字段,導致報錯。
結尾
- 研究了半天最終還是沒有用,因為太復雜。還不知道怎么同時以兩個條件分組,類似於mysql的groupby a,b
- 太難了
更新與2020/10/28 我胡漢三又回來啦
最終還是使用了ES的分組,上面的問題也得以解決。
多條件聚合
- 上面我們說了聚合操作,但是結尾處說了不知道怎么同時以兩個條件分組,類似於mysql的groupby a,b,下面我們來看下怎么以多字段分組;
Script script = new Script("doc['userId.keyword'].values +'#'+ doc['taskId.keyword'].values");
AggregationBuilder aggregation = AggregationBuilders.cardinality("user").script(script);
sourceBuilder.aggregation(aggregation);
-
我們可以看到這里使用了script來解決了多字段分組聚合的問題,這里script的意思是以[userId]#[taskId]為唯一鍵來進行分組,當然這個script可以自己改,分隔符也不限定於#,我們可以改為"doc['userId.keyword'].values +'####'+ doc['taskId.keyword'].values + '####' doc[batchId.keyword].values"來完成三字段分組。
-
這樣我們從bucket中可以獲取到key,則key的格式即為[userId]#[taskId],而getAggregations則為聚合的數據
更新與2020/10/29
- 上面說到聚合,然后今天自信滿滿的進行了自測,發現分頁出現了問題。 當時的寫法是這樣的:
TermsAggregationBuilder aggregationBuilder = AggregationBuilders.terms("user").script(script)
.subAggregation(AggregationBuilders.sum("integral").field("integral"));
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.aggregation(aggregation);
sourceBuilder.from(30);
sourceBuilder.size(10);
當我嘗試變化from發現獲取的值永遠都是前十條。
查詢各種網站后得出的結論也並不樂觀,比如 https://blog.csdn.net/laoyang360/article/details/79112946
截取博客中一段話,如下:
- 可以看到走正常的聚合是行不通的,於是看見了第三條,即此博主沒有深入研究的,推測是不是有可能實現。即一次取全部的最大值的聚合,然后再聚合的數據里進行排序分頁。
- 最終代碼如下:
TermsAggregationBuilder aggregationBuilder = AggregationBuilders.terms("user").script(script)
.subAggregation(AggregationBuilders.sum("integral").field("integral"))
.subAggregation(AggregationBuilders.topHits("sample").size(1)).size(9999);
List<FieldSortBuilder> fieldSorts=new ArrayList<>();
fieldSorts.add(new FieldSortBuilder("integral").order(SortOrder.DESC));
aggregationBuilder.subAggregation(new BucketSortPipelineAggregationBuilder("bucket_field", fieldSorts).from(params.getOffset()).size(params.getPageSize()));
Aggregations aggregations = IntegralQuery.aggregation(boolBuilder, aggregationBuilder);
不知道這樣是不是上面截圖中分區來取的意思,反正是實現了聚合分頁。前提聚合后的.size需要設置大一些,否則下面排序分頁會取不到。這種做法就是先進行聚合分組,然后再分組后bucket中進行排序分頁。
- 不管怎樣,反正算是實現了。
- 當我干完這一切,自信滿滿。發現了這個東西: https://github.com/NLPchina/elasticsearch-sql
- 生活好難。