2019獨角獸企業重金招聘Python工程師標準>>>
一、Elasticsearch的聚合
ES的聚合相當于關系型數據庫里面的group by,例如查找在性別字段男女人數的多少并且按照人數的多少進行排序,在使用MySQL的時候,可以使用如下的句子
- select?sex,count(*)?from?table_name?group?by?sex?order?by?count(*)??
在ES里面想要實現這種的語句,就叫做聚合,比如這種的聚合使用DSL語句的話如下所示:
- GET?/index/type/_search??
- {??
- ????"size"?:?0,??
- ????"aggs"?:?{???
- ????????"agg_sex"?:?{???
- ????????????"terms"?:?{???
- ??????????????"field"?:?"sex"??
- ????????????}??
- ????????}??
- ????}??
- }??
這樣就可以實現最以上例子中的group by的功能,當然這只是最簡單的聚合的使用,在ES里面的聚合有多重多樣的,比如說有度量聚合,可以用來計算某一個字段的平均值最大值等,在此給出一個簡單的度量聚合的例子
- GET?/index/type/_search??
- {??
- ???"size"?:?0,??
- ???"aggs":?{??
- ??????"agg_sex":?{??
- ?????????"terms":?{??
- ????????????"field":?"sex"??
- ?????????},??
- ?????????"agg_age":?{???
- ????????????"avg_age":?{???
- ???????????????"avg":?{??
- ??????????????????"field":?"age"???
- ???????????????}??
- ????????????}??
- ?????????}??
- ??????}??
- ???}??
- }??
這個DSL語句就是將先按照性別進行聚合,并且對不同的性別給出一個平均的年齡,使用之后ES的給出結果如下所示:
- {??
- ...??
- ???"aggregations":?{??
- ??????"agg_sex":?{??
- ?????????"buckets":?[??
- ????????????{??
- ???????????????"key":?"male",??
- ???????????????"doc_count":?4,??
- ???????????????"avg_age":?{???
- ??????????????????"value":?25??
- ???????????????}??
- ????????????},??
- ????????????{??
- ???????????????"key":?"female",??
- ???????????????"doc_count":?2,??
- ???????????????"avg_age":?{??
- ??????????????????"value":?23??
- ???????????????}??
- ????????????}??
- ?????????]??
- ??????}??
- ???}??
- ...??
- }??
?
在度量聚合里面有min,max,sum,avg聚合等,還有stats,extern_stats等聚合,其中stats的聚合給出的信息會包括min,max,count等基本的信息,更多詳細的細節請參考ES官網給出的指導https://www.elastic.co/guide/en/elasticsearch/guide/current/aggregations.html
以上只是給出的度量聚合,但是在實際中我們經常使用的是桶聚合,什么是桶聚合呢,個人理解就是將符合某一類條件的文檔選出來,所有的某一類的聚合就稱為桶,例如你可以按照某一個分類將所有的商品聚合起來,這種情況下就可以認為某一個分類的商品稱為一個桶,下面將詳細介紹幾個常用的桶聚合,并且會給出Java使用時候的代碼
二、桶聚合
桶聚合是在實際使用時候用處比較多的一種聚合,簡單的桶聚合包括term聚合,range聚合,date聚合,IPV4聚合等聚合,因為自己使用的僅僅是其中的三個,在此就簡單的介紹三個,分別是term聚合,range聚合,以及date聚合
1、term聚合
term聚合就是第一部分給出的簡單的例子,按照不同的字段進行聚合
2、range聚合
range聚合為按照自定義的范圍來創造桶,將每一個范圍的數據進行聚合,并且這個聚合一般適用于字段類型為long或者int,double的字段,可以進行直接的聚合,例如,我們想統計不同年齡段的人的個數,DSL如下所示:
- GET?/index/type/_search??
- {??
- ????"aggs"?:?{???
- ????????"agg_age"?:?{???
- ?????????"field":"age"??
- ????????????"ranges"?:?[??
- ?????????????{?"to"?:?18},??
- ?????????????{?"from"?:?19,"to"?:?50},??
- ?????????????{"from"?:?51}??
- ????????????]??
- ????????}??
- ????}??
- }??
?
3、daterange聚合
?
date range聚合和range聚合類似,但是所使用的類型是datetime這種類型,使用的時候與range有些區別,給出一個簡單的使用date range聚合的DSL例子,如下所示:
- GET?/index/type/_search??
- {??
- ????"aggs"?:?{???
- ????????"agg_year"?:?{???
- ?????????"field":"date"??
- ????????????"ranges"?:?[??
- ?????????????{?"to"?:?"2008-08-08"},??
- ?????????????{?"from"?:?"2008-08-09","to"?:?"2012-09-01"},??
- ?????????????{"from"?:?"2012-09-02"}??
- ????????????]??
- ????????}??
- ????}??
- }??
上面的DSL是簡單的按照時間格式進行區間的聚合,但是有些時候我們可能想要一些按照年份聚合或者月份聚合的情況,這個時候應該怎么辦呢?在date range里面可以指定日期的格式,例如下面給出一個按照年份進行聚合的例子:
- GET?/index/type/_search??
- {??
- ????"aggs"?:?{???
- ????????"agg_year"?:?{???
- ?????????"field":"date"??
- ?????????"format":"YYYY",??
- ????????????"ranges"?:?[??
- ?????????????{?"to"?:?"1970"},??
- ?????????????{?"from"?:?"1971","to"?:?"2012"},??
- ?????????????{"from"?:?"2013"}??
- ????????????]??
- ????????}??
- ????}??
- }??
我們可以指定格式來進行聚合
?
三、對于上述三種聚合java的實現
首先先給出一個具體的使用ES java api實現搜索并且聚合的完整例子,例子中使用的是terms聚合,按照分類id,將所有的分類進行聚合
- ????public?void?aggsearch()?{??
- ????????init();??
- ????????SearchResponse?response?=?null;??
- ??
- ????????SearchRequestBuilder?responsebuilder?=?client.prepareSearch("iktest")??
- ????????????????.setTypes("iktest").setFrom(0).setSize(250);??
- ????????AggregationBuilder?aggregation?=?AggregationBuilders??
- ????????????????.terms("agg")??
- ????????????????.field("category_id")??
- ????????????????.subAggregation(??
- ????????????????????????AggregationBuilders.topHits("top").setFrom(0)??
- ????????????????????????????????.setSize(10)).size(100);??
- ????????response?=?responsebuilder.setQuery(QueryBuilders.boolQuery()??
- ??
- ????????.must(QueryBuilders.matchPhraseQuery("name",?"中學歷史")))??
- ????????????????.addSort("category_id",?SortOrder.ASC)??
- ????????????????.addAggregation(aggregation)//?.setSearchType(SearchType.DFS_QUERY_THEN_FETCH)??
- ????????????????.setExplain(true).execute().actionGet();??
- ??
- ????????SearchHits?hits?=?response.getHits();??
- ??
- ????????Terms?agg?=?response.getAggregations().get("agg");??
- ????????System.out.println(agg.getBuckets().size());??
- ????????for?(Terms.Bucket?entry?:?agg.getBuckets())?{??
- ????????????String?key?=?(String)?entry.getKey();?//?bucket?key??
- ????????????long?docCount?=?entry.getDocCount();?//?Doc?count??
- ????????????System.out.println("key?"?+?key?+?"?doc_count?"?+?docCount);??
- ??
- ????????????//?We?ask?for?top_hits?for?each?bucket??
- ????????????TopHits?topHits?=?entry.getAggregations().get("top");??
- ????????????for?(SearchHit?hit?:?topHits.getHits().getHits())?{??
- ????????????????System.out.println("?->?id?"?+?hit.getId()?+?"?_source?[{}]"??
- ????????????????????????+?hit.getSource().get("category_name"));??
- ????????????????;??
- ????????????}??
- ????????}??
- ????????System.out.println(hits.getTotalHits());??
- ????????int?temp?=?0;??
- ????????for?(int?i?=?0;?i?<?hits.getHits().length;?i++)?{??
- ????????????//?System.out.println(hits.getHits()[i].getSourceAsString());??
- ????????????System.out.print(hits.getHits()[i].getSource().get("product_id"));??
- ????????????//?if(orderfield!=null&&(!orderfield.isEmpty()))??
- ????????????//?System.out.print("\t"+hits.getHits()[i].getSource().get(orderfield));??
- ????????????System.out.print("\t"??
- ????????????????????+?hits.getHits()[i].getSource().get("category_id"));??
- ????????????System.out.print("\t"??
- ????????????????????+?hits.getHits()[i].getSource().get("category_name"));??
- ????????????System.out.println("\t"??
- ????????????????????+?hits.getHits()[i].getSource().get("name"));??
- ????????}??
- ????}??
- }??
以上的例子實現的是按照category_id字段進行分類的聚合,并且將在name字段查找包含“中學歷史”的這個詞,并且按照category_id進行排序,在此給出的只是一個搜索實現的函數,里面的字段名字,以及index,type等很多字段均為自己定義的index里面的名字,上面給出的是terms聚合時候的代碼,如果使用的是range聚合或者date range聚合,只需要改變aggregation就可以
?
使用range聚合的時候:
- aggregation?=?AggregationBuilders.range("agg")??
- ????????????????????.field("price").addUnboundedTo(50)??
- ????????????????????.addRange(51,?100).addRange(101,?1000)??
- ????????????????????.addUnboundedFrom(1001);??
使用date range聚合的時候:
- aggregation?=?AggregationBuilders.dateRange("agg")??
- ????????????????????.field("date").format("yyyy")??
- ????????????????????.addUnboundedTo("1970").addRange("1970",?"2000")??
- ????????????????????.addRange("2000",?"2010").addUnboundedFrom("2009");??
以上所有的聚合均是先過濾搜索,然后對于召回得到的結果進行一個聚合,例如我們在name字段搜索中學歷史這個詞,最終得到四個分類分別為1,2,3,4那么聚合的時候就是這四個分類,但是有時候我們可能會需要對于搜索的結果進行一個過濾,但是我們不想對聚合的結果進行過濾,那么我們就要使用一下的部分了
?
四、先聚合再過濾
以上將的簡單的聚合都是先過濾或者搜索,然后對結果進行聚合,但是有時候我們需要先進行聚合,然后再對結果進行一次過濾,但是我們不希望這個時候聚合會發生變化,什么時候會遇到這種情況呢,我們以美團為例做一個說明,在主頁我們直接點解美食,得到如下所示的圖
點美食之后出現全部的分類,包括各種的菜系,下面我們點一個具體的菜系
?
從程序上來說,我們點第二次菜系的時候,出現的所有的菜品均是烤串之類的菜品了,但是在分類里面還是所有的分類都會有,如果按照之前的ES的聚合,會將所有搜索出來的品的分類進行一個聚合,但是點完烤串之后,所有的分類都是烤串了,那么就應該所有的分類只有一個烤串了,不應該有其他的,這樣的話肯定是不可以的,那么如何才能實現這種聚合的,這個時候我們就需要先聚合,然后進行再次的過濾,但是過濾的時候并不影響之前的聚合結果,這就是先聚合再過濾,在ES里面也有這種情況的考慮,這個時候使用的是postfilter
postfilter解決了僅僅過濾搜索結果,但是并不影響聚合結果,下面給出一個java使用時候的例子以及比較
函數一為第三部分給出的完整的搜索函數,按照分類聚合
函數二的改變只是對于一的
- response?=?responsebuilder.setQuery(QueryBuilders.boolQuery()??
- ??
- ????????.must(QueryBuilders.matchPhraseQuery("name",?"中學歷史")))??
- ????????????????.addSort("category_id",?SortOrder.ASC)??
- ????????????????.addAggregation(aggregation)??
- ????????????????????????????????.setPostFilter(QueryBuilders.rangeQuery("price").gt(1000).lt(5000))??
- ????????????????.setExplain(true).execute().actionGet();??
添加了按照price進行過濾,最后結果顯示,聚合的結果兩次完全一樣,但是函數二召回的結果為函數一結果的子集。
?
五、后續學習
如何多次的過濾以及召回,比如先過濾后聚合再過濾再次聚合然后再次過濾這種的應該如何實現,需要學習。