ElasticSearch聚合查詢之composite

此文轉載自:https://blog.csdn.net/qq_18895659/article/details/86540548

簡介composite

composite是一個多桶聚合,它從不同的源創建複合桶,與其他多桶聚合不同,複合聚合可用於高效地對多級聚合中的所有桶進行分頁。這種聚合提供了一種方法來流特定聚合的所有桶,類似於滾動對文檔所做的操作。

組合桶是由爲每個文檔提取/創建的值的組合構建的,每個組合被視爲組合桶。如下爲官方給的例子:

{
    "keyword": ["foo", "bar"],
    "number": [23, 65, 76]
}

如果我們同時對keyword和number兩個字段進行聚合會得出以下的結果:

{ "keyword": "foo", "number": 23 }
{ "keyword": "foo", "number": 65 }
{ "keyword": "foo", "number": 76 }
{ "keyword": "bar", "number": 23 }
{ "keyword": "bar", "number": 65 }
{ "keyword": "bar", "number": 76 }

看到上面的例子是不是恍然大悟, 就像類似sql中的多group by 多字段,可以對多個字段進行聚合,這非常適用於對於多維度出報表的需求,我這裏建議使用的版本爲6.5+,因爲6.5版本以下此功能還處於測試階段,設計和代碼沒有正式的GA功能成熟,並且沒有擔保。當然我這裏也會提供6.5版本以下如何進行多聚合字段的使用。

首先上官方文檔地址:

https://www.elastic.co/guide/en/elasticsearch/reference/6.5/search-aggregations-bucket-composite-aggregation.html

其實官方文檔已經把該功能說得很詳細,如果你只是單純寫DSL實現的話看官方文檔就可以,我接下來就介紹如何調用他的javaAPI來使用,當然如果閱讀源碼能力強的話也可以直接看官方在github的test,地址如下:

https://github.com/elastic/elasticsearch/tree/master/server/src/test/java/org/elasticsearch/search/aggregations/bucket/composite

驗證多字段聚合可行性

上面的例子爲官方例子,我們需要自己弄個例子檢查可行性,我們現在創建一個index,索引名爲composite_test,mapping如下:

{
         "area" : {
            "type" : "keyword"
          },
          "userid" : {
            "type" : "keyword"
          },
          "sendtime" : {
            "type" : "date",
            "format" : "yyyy-MM-dd HH:mm:ss"
          }
}

我們創建好了index,index中一共有三個字段,area,userid,sendtime三個字段,我們也使用MySQL數據建一個一模一樣的表,方便對比聚合出來的數據是否正確,表名爲composite_test。

我們首先對數據表composite_test插入5條記錄,分別如下:
在這裏插入圖片描述
使用group by對三個字段進行聚合,以下爲在數據庫中的實現:

SELECT COUNT(1),area,userid,sendtime FROM composite_test GROUP BY area,userid,sendtime

其結果如下:
在這裏插入圖片描述
以上爲數據庫的聚合實現,所以我們使用ES進行多字段聚合的時候如果結果和以上的一樣則是正確的,我們也一樣往ES composite_test索引中 插入相同的5條數據,在kibana上執行以下命令插入:

POST composite_test/_bulk
{ "index" : {"_type" :"_doc"}}
{"area":"33","userid":"400015","sendtime":"2019-01-17 00:00:00"}
{ "index" : {"_type" : "_doc"}}
{"area":"33","userid":"400015","sendtime":"2019-01-17 00:00:00"}
{ "index" : {"_type" : "_doc"}}
{"area":"35","userid":"400016","sendtime":"2019-01-18 00:00:00"}
{ "index" : { "_type" : "_doc"}}
{"area":"35","userid":"400016","sendtime":"2019-01-18 00:00:00"}
{ "index" : {"_type" : "_doc"}}
{"area":"33","userid":"400017","sendtime":"2019-01-17 00:00:00"}

查詢ES,我們發現已經存在了這5條數據了:在這裏插入圖片描述
接下來我們先使用composite的DSL查詢:

GET composite_test/_search
{"size": 0, 
 "aggs" : {
      "my_buckets": {
        "composite" : {
           "sources" : [
              { "area": { "terms": {"field": "area" } } },
               { "userid": { "terms": {"field": "userid" } } },
               { "sendtime": { "date_histogram": { "field": "sendtime","interval": "1d","format": "yyyy-MM-dd"} } }
             ]
         }
        }
    }
}

DSL中參數的意思請自行參考官方文檔,都說得很詳細的,以上這句理論上是等值於數據庫的那句聚合語句的,查詢結果如下:

[
        {
          "key" : {
            "area" : "33",
            "userid" : "400015",
            "sendtime" : "2019-01-17"
          },
          "doc_count" : 2
        },
        {
          "key" : {
            "area" : "33",
            "userid" : "400017",
            "sendtime" : "2019-01-17"
          },
          "doc_count" : 1
        },
        {
          "key" : {
            "area" : "35",
            "userid" : "400016",
            "sendtime" : "2019-01-18"
          },
          "doc_count" : 2
        }
]

從以上響應的json數組中我們不難看出,該聚合聚合出來的數據是和數據庫聚合出來的數據是一致的,所以 composite是可以使用在多字段聚合上的。論證完可行性,我們接下來使用java來實現,實話說這一塊我是看源代碼纔會使用的,網上資料基本爲0,而且官方java使用文檔裏也沒用,的確是與遇到了不少坑,寫出來方便以後使用能快速回憶。

Java使用composite聚合

高級版本

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>6.5.4</version>
</dependency> 

具體代碼如下:

SearchRequest searchRequest = new SearchRequest("composite_test"); 
searchRequest.types("_doc");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); 
searchSourceBuilder.size(0);
/********************以下組裝聚合的三個字段****************************/
List<CompositeValuesSourceBuilder<?>> sources = new ArrayList<>();
		
DateHistogramValuesSourceBuilder sendtime = new DateHistogramValuesSourceBuilder("sendtime")
                .field("sendtime")
                .dateHistogramInterval(DateHistogramInterval.days(1))
                .format("yyyy-MM-dd").order(SortOrder.DESC).missingBucket(false);
        sources.add(sendtime);
TermsValuesSourceBuilder userid = new TermsValuesSourceBuilder("userid").field("userid").missingBucket(true);
        sources.add(userid);
        TermsValuesSourceBuilder dttype = new TermsValuesSourceBuilder("area").field("area").missingBucket(true);
        sources.add(dttype);
        CompositeAggregationBuilder  composite =new CompositeAggregationBuilder("my_buckets", sources);
composite.size(1000);
/*********************執行查詢******************************/
searchSourceBuilder.aggregation(composite);
searchRequest.source(searchSourceBuilder); 
SearchResponse searchResponse = client.search(searchRequest,RequestOptions.DEFAULT);
/********************取出數據*******************/
Aggregations aggregations = searchResponse.getAggregations();
ParsedComposite parsedComposite = aggregations.get("my_buckets");
List<ParsedBucket> list =  parsedComposite.getBuckets();
Map<String,Object> data = new HashMap<>();
for(ParsedBucket parsedBucket:list){
	data.clear();
	for (Map.Entry<String, Object> m :  parsedBucket.getKey().entrySet()) {
		data.put(m.getKey(),m.getValue());
	}
	data.put("count",parsedBucket.getDocCount());
	    System.out.println(data);
}
/*************************************/

代碼中的client爲RestHighLevelClient,請自行初始化,然後執行代碼,控制檯打印如下:

{area=35, count=2, sendtime=2019-01-18, userid=400016}
{area=33, count=2, sendtime=2019-01-17, userid=400015}
{area=33, count=1, sendtime=2019-01-17, userid=400017}

數據正確,方法可用,其實這個方法是RestHighLevelClient替我們封裝了composite生成DSL。

這裏注意一下missingBucket的設置,這個的意思是如果該字段沒值,爲true的時候會返回null,爲false不返回整條數據,注意這裏是整條數據,而不是單單這個字段而已。

低級版本

以上就是composite的驗證和在java中的使用方法,建議在6.5+版本使用,但這個時候小夥伴可能會問,如果我是6.5以下的版本呢,這裏我也有一個方法,只不過是有點繞,直接上代碼:

SearchRequest searchRequest = new SearchRequest("composite_test"); 
searchRequest.types("_doc");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); 
searchSourceBuilder.size(0);
/********************以下組裝聚合的三個字段****************************/
AggregationBuilder sendtime=AggregationBuilders.dateHistogram("sendtime").field("sendtime").format("yyyy-MM-dd").interval(86400000);
AggregationBuilder area=AggregationBuilders.terms("area").field("area");
AggregationBuilder userid=AggregationBuilders.terms("userid").field("userid");
//實現功能關鍵點
area.subAggregation(userid);
sendtime.subAggregation(area);
/*********************執行查詢******************************/
searchSourceBuilder.aggregation(sendtime);
searchRequest.source(searchSourceBuilder); 
SearchResponse searchResponse = client.search(searchRequest,RequestOptions.DEFAULT);
/********************取出數據*******************/
Aggregations aggregations = searchResponse.getAggregations();
//取出數據
aggHandle(aggregations);
/*************************************/

其結果爲:

{area=33, count=2, sendtime=2019-01-17, userid=400015}, 
{area=33, count=1, sendtime=2019-01-17, userid=400017}, 
{area=35, count=2, sendtime=2019-01-18, userid=400016}

所以使用這個方法也能多值聚合數據,其中實現的關鍵點在於

area.subAggregation(userid);
sendtime.subAggregation(area);

這裏相當於把每個桶給串聯起來,串聯順序無要求,但用這種方法最後取數據的時候比較麻煩,我這裏也分享一個我取數據的方法:

 
private static List<Map<String,Object>> listmap =new ArrayList<Map<String,Object>>();
private static Map<String,Object> map =new HashMap<String,Object>(16);
//使用遞歸的方式將聚合數據中的數據一一取出來
private static void aggHandle(Aggregations agg){
		 String name ="";
		 Long longValue = 0L;
	  for(Aggregation data:agg){
			 name = data.getName();
			 Object obj = agg.get(name);
		 if(obj instanceof Terms){
			 Terms terms = (Terms) obj;
			 name = terms.getName();
			 if(terms.getBuckets().size()==0){
				 listmap.add(clonMap(map));
			 }
			for (Terms.Bucket entry : terms.getBuckets()) {	
				
	     		map.put(name, entry.getKey());
	     		longValue = entry.getDocCount();
	     		map.put(count, longValue);
	     		List<Aggregation> list = entry.getAggregations().asList();
	     		if(list==null||list.isEmpty()){
	    			listmap.add(clonMap(map));
	     		}else{
	     			aggHandle(entry.getAggregations());
	     		}
			}
		 }else if(obj instanceof ParsedDateHistogram){
			 ParsedDateHistogram terms = (ParsedDateHistogram) obj; 
			 List<? extends Bucket> buckets = terms.getBuckets(); 
			 name = terms.getName();
			 if(buckets.size()==0){
				 listmap.add(clonMap(map));
			 }
			 for(Bucket entry:buckets){
				 	map.put(name,entry.getKeyAsString());
		     		longValue = entry.getDocCount();
		     		map.put(count, longValue);
				List<Aggregation> list = entry.getAggregations().asList();
		     	if(list==null||list.isEmpty()){
		    		listmap.add(clonMap(map));
		    		//map.clear();
		     	}else{
		     		aggHandle(entry.getAggregations());
	     		}
			 }
		 }else if(obj instanceof Max){
			 Max max =(Max) obj;
			 name = max.getName();
			 Double value = max.getValue();
			 longValue  =value.longValue();
			 map.put(name, longValue);
			 listmap.add(clonMap(map));
		 }else if(obj instanceof Min){
			 Min min =(Min) obj;
			 Double value = min.getValue();
			 longValue  =value.longValue();
			 map.put(min.getName(), longValue);
			 listmap.add(clonMap(map));
		 }else if(obj instanceof Avg){
			 Avg avg = (Avg) obj;
			 Double value = avg.getValue();
			 longValue  =value.longValue();
			 map.put(avg.getName(), longValue);
			 listmap.add(clonMap(map));
		 }else if(obj instanceof Sum){
			 Sum sum = (Sum) obj;
			 Double value = sum.getValue();
			 longValue  =value.longValue();
			 map.put(sum.getName(), longValue);
			 listmap.add(clonMap(map));
		 }else if(obj instanceof ValueCount){
			 ValueCount count = (ValueCount) obj;
			 longValue  =count.getValue();
			 map.put(count.getName(), longValue);
			 listmap.add(clonMap(map));
		 }else{
			 System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"+obj);
		 }
	   }
	 }
/**
 * 克隆map對象到另外一個map對象裏面去
 * @param map
 * @return
 */
private static Map<String,Object> clonMap(Map<String,Object> mapTo){
	Map<String,Object> map = new HashMap<String,Object>(16);
	for (Map.Entry<String,Object> entry : mapTo.entrySet()) {
		String key = entry.getKey();
		Object value = entry.getValue();
		map.put(key, value);
	}
	return map;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章