天天看點

Es Bucket聚合(桶聚合) 第二篇-Terms Aggregation與Significant Terms Aggregation

本章将介紹elasticsearch最重要的桶聚合terms aggregation。

1、Terms Aggregation

多值聚合,根據庫中的文檔動态建構桶。基于詞根的聚合,如果聚合字段是text的話,會對一個一個的詞根進行聚合,通常不會在text類型的字段上使用聚合,對标關系型資料中的(Group By)。

官方示例如下:

GET /_search
{
    "aggs" : {
        "genres" : {
            "terms" : { "field" : "genre" }
        }
    }
}           

傳回結果如下:

{
    ...
    "aggregations" : {
        "genres" : {
            "doc_count_error_upper_bound": 0,           // @1
            "sum_other_doc_count": 0,                         // @2
            "buckets" : [                                                 // @3
                {
                    "key" : "electronic",
                    "doc_count" : 6
                },
                {
                    "key" : "rock",
                    "doc_count" : 3
                },
                {
                    "key" : "jazz",
                    "doc_count" : 2
                }
            ]
        }
    }
}           

傳回結果@1:該值表示未進入最終術語清單的術語的最大潛在文檔計數,下文還會詳細分析。

傳回結果@2:當有很多詞根時,Elasticsearch隻傳回最上面的項;這個數字是所有不屬于響應的bucket的文檔計數之和,其搜尋過程在下文會講到。

傳回結果@3:傳回的結果,預設情況下,傳回doc_count排名最前的10個,受size參數的影響,下面會詳細介紹。

1.1 Terms 聚合支援如下常用參數:

  • size

    可以通過size傳回top size的文檔,該術語聚合針對頂層術語(不包含嵌套詞根),其搜尋過程是将請求向所有分片節點發送請求,每個分片節點傳回size條資料,然後聚合所有分片的結果(會對各分片傳回的同樣詞根的數數值進行相加),最終從中挑選size條記錄傳回給用戶端。從這個過程也可以看出,其結果并不是準确的,而是一個近似值。

  • Shard Size

    為了提高該聚合的精确度,可以通過shard_size參數設定協調節點向各個分片請求的詞根個數,然後在協調節點進行聚合,最後隻傳回size個詞根給到用戶端,shard_size >= size,如果shard_size設定小于size,ES會自動将其設定為size,預設情況下shard_size建議設定為(1.5 * size + 10)。

1.2 Calculating Document Count Error

為了闡述傳回結果中的doc_count_error_upper_bound、sum_other_doc_count代表什麼意思,我們通過如下例子來說明Term Aggregations的工作機制。

例如在三個分片上關于産品的初始聚合資訊如下:

Es Bucket聚合(桶聚合) 第二篇-Terms Aggregation與Significant Terms Aggregation

現在統計size=5的term Aggregations,協調節點向Shard A、B、C分别請求前5條聚合資訊,如下圖所示:

Es Bucket聚合(桶聚合) 第二篇-Terms Aggregation與Significant Terms Aggregation

根據這些傳回的結果,在協調節點上聚合,最終得出如下響應結果:

{
    ...
    "aggregations" : {
        "products" : {
            "doc_count_error_upper_bound" : 46,
            "sum_other_doc_count" : 79,
            "buckets" : [
                {
                    "key" : "Product A",
                    "doc_count" : 100
                },
                {
                    "key" : "Product Z",
                    "doc_count" : 52
                }
                {
                    "key" : "Product C",
                    "doc_count" : 50
                }
                {
                    "key" : "Product G",
                    "doc_count" : 45
                }
                ...
            ]
        }
    }
}           

那doc_count_error_upper_bound、sum_other_doc_count又分别代表什麼呢?

  • doc_count_error_upper_bound

    該值表示未進入最終術語清單的術語的最大潛在文檔計數。這是根據從每個碎片傳回的上一項的文檔計數之和計算的(協調節點根據每個分片節點傳回的最後一條資料相加得來的)。這意味着在最壞的情況下,沒有傳回的詞根的最大文檔個數為46個,在此次聚合結果中排名第4。

  • sum_other_doc_count

    未納入本次聚合結果中的文檔總數量,這個容易了解。

Es Bucket聚合(桶聚合) 第二篇-Terms Aggregation與Significant Terms Aggregation

1.3 Per bucket Document Count Error

每個桶的錯誤文檔數量,可以通過參數show_term_doc_count_error=true來展示每個文檔未被納入結果集的數量。

其使用示例如下:

GET /_search
{
    "aggs" : {
        "products" : {
            "terms" : {
                "field" : "product",
                "size" : 5,
                "show_term_doc_count_error": true
            }
        }
    }
}           

對應的傳回值:

{
    ...
    "aggregations" : {
        "products" : {
            "doc_count_error_upper_bound" : 46,
            "sum_other_doc_count" : 79,
            "buckets" : [
                {
                    "key" : "Product A",
                    "doc_count" : 100,
                    "doc_count_error_upper_bound" : 0
                },
                {
                    "key" : "Product Z",
                    "doc_count" : 52,
                    "doc_count_error_upper_bound" : 2
                }
                ...
            ]
        }
    }
}           

1.4 order

可以設定桶的排序,預設是按照桶的doc_count降序排序的。

order的可選值:

  1. "order" : { "_count" : "asc" }
  2. "order" : { "_key" : "asc" }
  3. 支援子聚合的結果作為排序字段。
GET /_search
{
    "aggs" : {
        "genres" : {
            "terms" : {
                    "field" : "genre",
                     "order" : { "max_play_count" : "desc" }  //  "order" : { "playback_stats.max" : "desc" }
            },
            "aggs" : {
                "max_play_count" : { "max" : { "field" : "play_count" } }   // "playback_stats" : { "stats" : { "field" : "play_count" } }
            }
        }
    }
}           

"order" : { "playback_stats.max" : "desc" }其中鍵的書寫規則如下:

用 > 分隔聚合名稱,用.分開METRIC類型的聚合。

1.5 Minimum document count

通過指定min_doc_count來過濾比對文檔數量小于該值的桶。

1.6 Filtering values(值過濾)

對值使用正規表達式進行過濾,示例如下:

GET /_search
{
    "aggs" : {
        "tags" : {
            "terms" : {
                "field" : "tags",
                "include" : ".*sport.*",    //   include 包含
                "exclude" : "water_.*"    //   exclude 排除
            }
        }
    }
}           

精确值比對

"JapaneseCars" : {
    "terms" : {
        "field" : "make",
        "include" : ["mazda", "honda"]
    }
}           

分區過濾:

GET /_search
{
   "size": 0,
   "aggs": {
      "expired_sessions": {
         "terms": {
            "field": "account_id",
            "include": {
               "partition": 0,                          // @1
               "num_partitions": 20               // @2
            },
            "size": 10000,
            "order": {
               "last_access": "asc"
            }
         },
         "aggs": {
            "last_access": {
               "max": {
                  "field": "access_date"
               }
            }
         }
      }
   }
}           

分區的意思就是将值分成多個組,沒一個請求隻處理其中一個組,其中參數 @1表示請求的分組ID,num_partitions表示總共的分組數。

1.7 Multi-field terms aggregation

多字段詞根聚合。terms aggregation不支援從同一文檔中的多個字段收集詞根。因為terms aggregation本身并不收集所有的詞根,而是使用全局序數來生成字段中所有惟一值的清單。全局序數會帶來重要的性能提升,而這在多個字段中是不可能實作的。

有兩種方法可以用于跨多個字段執行term aggregation:

  • script

    使用腳本方式,目前暫不探讨其腳本的使用。

  • copy_to field

    使用copy_to在映射中聚合多個字段。

1.8 Collect mode

收集模式,ES支援兩種收集模式:

  • depth_first:深度優先,預設值。
  • breadth_first:廣度優先。

首先我們先學習一下樹的基本知識(深度周遊與廣度周遊),例如有如下一顆二叉樹:

Es Bucket聚合(桶聚合) 第二篇-Terms Aggregation與Significant Terms Aggregation

深度周遊:深度周遊是從一個節點開始,先周遊完該節點所有的子節點,然後再傳回周遊它的兄弟節點,通常深度周遊分為中序周遊、前序周遊,後序周遊。

  • 中序周遊(周遊左子樹–>通路根–>周遊右子樹):D B E A F C G
  • 前序周遊(通路根–>周遊左子樹–>周遊右子樹):A B D E C F G
    • 後序周遊(周遊左子樹–>周遊右子樹–>通路根):D E B F G C A
  • 廣度周遊(一層一層周遊):A B C D E F G

廣度優先聚合與深度優先聚合的建構流程(聚合流程)與其周遊順序一緻。

下面我們以官方的示例來進一步說明:

例如現在有一個電影的文檔,其索引中的資料如下:

Es Bucket聚合(桶聚合) 第二篇-Terms Aggregation與Significant Terms Aggregation

現在要統計出演電視劇最多的演員(前3),并且和這些演員合作次數最多的演員。

其聚合文法如下:

GET /_search
{
    "aggs" : {
        "actors" : { 
             "terms" : {
                 "field" : "actors",
                 "size" : 3,
                 “shard_size” : 50
                 "collect_mode" : "breadth_first" 
             },
            "aggs" : {
                "costars" : {   // 子聚合
                     "terms" : {
                         "field" : "actors",
                         "size" : 5
                     }
                 }
            }
         }
    }
}           

對應的JAVA示例如下:

public static void test_term_aggregation_collect_mode() {
        RestHighLevelClient client = EsClient.getClient();
        try {
            
            SearchRequest searchRequest = new SearchRequest();
            searchRequest.indices("movies_index");
            SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
            AggregationBuilder aggregationBuild = AggregationBuilders.terms("actors_agg")
                                                        .field("actors")
                                                        .size(3)
                                                        .shardSize(50)
                                                        .collectMode(SubAggCollectionMode.BREADTH_FIRST)
                                                        .subAggregation(AggregationBuilders.terms("costars_agg")
                                                                .field("actors")
                                                                .size(3))
                                                  ;
            sourceBuilder.aggregation(aggregationBuild);
            sourceBuilder.size(0);

            searchRequest.source(sourceBuilder);
            SearchResponse result = client.search(searchRequest, RequestOptions.DEFAULT);
            System.out.println(result);
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            EsClient.close(client);
        }
    }           
{
   ... // 省略
    "aggregations":{
        "sterms#actors_agg":{
            "doc_count_error_upper_bound":0,
            "sum_other_doc_count":30,
            "buckets":[
                {
                    "key":"趙麗穎",
                    "doc_count":3,
                    "sterms#costars_agg":{
                        "doc_count_error_upper_bound":0,
                        "sum_other_doc_count":19,
                        "buckets":[
                            {
                                "key":"趙麗穎",
                                "doc_count":3
                            },
                            {
                                "key":"俞灏明",
                                "doc_count":1
                            },
                            {
                                "key":"馮紹峰",
                                "doc_count":1
                            }
                        ]
                    }
                },
                {
                    "key":"李亞鵬",
                    "doc_count":2,
                    "sterms#costars_agg":{
                        "doc_count_error_upper_bound":0,
                        "sum_other_doc_count":8,
                        "buckets":[
                            {
                                "key":"李亞鵬",
                                "doc_count":2
                            },
                            {
                                "key":"呂麗萍",
                                "doc_count":1
                            },
                            {
                                "key":"周傑",
                                "doc_count":1
                            }
                        ]
                    }
                },
                {
                    "key":"俞灏明",
                    "doc_count":1,
                    "sterms#costars_agg":{
                        "doc_count_error_upper_bound":0,
                        "sum_other_doc_count":5,
                        "buckets":[
                            {
                                "key":"俞灏明",
                                "doc_count":1
                            },
                            {
                                "key":"劉凡菲",
                                "doc_count":1
                            },
                            {
                                "key":"孟瑞",
                                "doc_count":1
                            }
                        ]
                    }
                }
            ]
        }
    }
}           

深度周遊優先的執行路徑:

開始對整個電影庫進行搜尋,從文檔中得出第一個影員,例如趙麗穎,然後立馬執行子聚合,首先刷選出有趙麗穎參與的文檔集中的詞根,并聚合其數量,排名前3的組成一個聚合結果,生成類似于:

{
                    "key":"趙麗穎",
                    "doc_count":3,
                    "sterms#costars_agg":{
                        "doc_count_error_upper_bound":0,
                        "sum_other_doc_count":19,
                        "buckets":[
                            {
                                "key":"趙麗穎",
                                "doc_count":3
                            },
                            {
                                "key":"俞灏明",
                                "doc_count":1
                            },
                            {
                                "key":"馮紹峰",
                                "doc_count":1
                            }
                        ]
                    }
                }           

然後再傳回上一層聚合,再對上一層的下一個詞根執行類似的聚合,最後進行排序,在第一層進行裁剪(刷選)前size個文檔傳回個用戶端。

廣度周遊優先的執行路徑:

首先執行第一層聚合,也就是針對所有文檔中的actors字段進行聚合,得到文檔集中所有的演員,然後按doc_count排序,進行裁剪,刷選前3個演員,然後隻針對這3個演員進行第二層聚合。

看上去廣度周遊優先會非常高效,其實這裡掩藏了一個實作細節,就是廣度優先,會緩存裁剪後剩餘的所有文檔,也就是本例中與這3個演員的所有文檔集在記憶體中,然後基于這些記憶體執行第二層聚合,故如果第一層每個桶如果包含的文檔數量巨大,則會耗費很大的記憶體,容易觸發OOM異常,故廣度優先的使用場景是子聚合所需要處理的資料很少的情況下會非常高效。

參考知識:

http://www.cnblogs.com/bonelee/p/7832738.html

1.9 execution hint

執行提示,類似于MySQL資料庫的hint功能。

Term Aggregation聚合通常基于如下兩種實作方式:

  1. 通過直接使用字段值來聚合每個桶的資料(map)

    隻有當很少的文檔比對查詢時,才應該考慮映射。否則,基于序号的執行模式會快得多。預設情況下,map隻在腳本上運作聚合時使用,因為它們沒有序号。

  2. 通過使用字段的全局序号并為每個全局序号配置設定一個bucket (global_ordinals)

    keyword類型的字段預設使用global_ordinals機制,它使用全局序号動态配置設定bucket,是以記憶體使用與屬于聚合範圍的文檔的值的數量是線性的。

預設情況下,ES會自動選擇,但也可以通過參數execution_hint進行人工幹預,可選值:global_ordinals、map。

1.10 Missing value

missing定義了應該如何處理缺少值的文檔。預設情況下,它們将被忽略,但也可以将它們視為具有一個值。

Terms Aggregation聚合就介紹到這裡了。

2、 Significant Terms Aggregation

傳回集合中出現的有趣或不尋常的項的聚合。

首先從官方示例開始學習。

官方示例的索引結構大概如下(類似一個全國犯罪事件索引庫)

核心字段:

force:接案警局名稱。

crime_type:犯罪類型。

2.1 Single-Set analysis

單一結果集分析,通常前台集合(foreground set)通常通過一組查詢條件指定。請看示例:

GET /_search
{
    "query" : {    // @1
        "terms" : {"force" : "上海交通警局" 
    },
    "aggregations" : {
        "significant_crime_types" : {
            "significant_terms" : { "field" : "crime_type" }     // @2
        }
    }
}           

代碼@1:定義一個查詢,該例中查詢警局為“ShangHai Transport Police”所有犯罪記錄,當成我們關注(感興趣的集合,也就是Significant Terms Aggregation中的(foreground set)。

代碼@2:對crime_type犯罪類型進行significant_terms.

{
    ...
    "aggregations" : {
        "significant_crime_types" : {
            "doc_count": 47347,                                     // @1
            "bg_count": 5064554,                                   // @2
            "buckets" : [                                                   // @3
                {
                    "key": "自行車盜竊案",                    
                    "doc_count": 3640,                               // @4
                    "score": 0.371235374214817,
                    "bg_count": 66799                                // @5
                }
                ,
                {
                    "key": "小汽車盜竊案",             
                    "doc_count": 6640,
                    "score": 0.371235374214815,
                    "bg_count": 66799
                }
                ...
            ]
        }
    }
}           

代碼@1:doc_count:符合查詢條件的總文檔數量,此例表示上海交通警局總共的犯罪記錄數。

代碼@2:bg_count:這是Significant Terms中的background set,應該是該索引目前總共的文檔個數。

代碼@3:是significant_terms針對犯罪類型的聚合結果。

代碼@4:表示上海交通警局總共發生的自行車盜竊案的總記錄數。

代碼@5:表示整個索引庫中所有警局發生的自行車盜竊案的總記錄數。

從這裡的結果,我們可以得出如下結論:

整體自行車犯罪率= 66799/5064554,約等于1%。

上海交通警局自行車盜竊犯罪率(上海交通警局自行車犯罪總記錄數除以上海交通警局的總犯罪記錄)=3640/47347約等于7%。

使用這種查詢來找出異常資料,但它隻給了我們一個用于比較的子集。要發現所有其他警察部隊的異常情況,我們必須對每個不同的警察部隊重複查詢。

如何解決該問題呢?請看下文。

2.2 Multi-set analysis

多結果集對比分析,其思路是通過term aggregation産生多個桶(多個資料集合),然後再使用子聚合針對這些分組再進行一次聚合。

跨多個類别執行分析的一種更簡單的方法是使用父級聚合來分割準備分析的資料。使用父聚合進行分割的示例:

GET /_search
{
    "aggregations": {
        "forces": {
            "terms": {"field": "force"},                  // @1
            "aggregations": {
                "significant_crime_types": {         // @2
                    "significant_terms": {"field": "crime_type"}
                }
            }
        }
    }
}           

代碼@1:首先對字段force進行term聚合,統計各個警局的犯罪記錄總數。

代碼@2:然後子聚合是對犯罪類型進行significant_terms聚合。

我們先來看一下傳回結果:

{
 ...
 "aggregations": {
    "forces": {
        "doc_count_error_upper_bound": 1375,
        "sum_other_doc_count": 7879845,
        "buckets": [
            {
                "key": "廣州交通警局",
                "doc_count": 894038,
                "significant_crime_types": {    
                    "doc_count": 894038,        // @1
                    "bg_count": 5064554,        // @2
                    "buckets": [                         // @3
                        {
                            "key": "搶劫",        // @4
                            "doc_count": 27617,   // @5
                            "score": 0.0599, 
                            "bg_count": 53182      // @6
                        }
                        ...
                }
            }// 省略其他警局的資料。
        ]
    }
  }
}           

主要針對significant_crime_types的結果集做一次解釋:

結果@1:"廣州交通警局"總處理犯案記錄總數為894038。

結果@2:索引庫總處理犯案記錄總數為5064554。

結果@3:"廣州交通警局"各個犯案類型的聚合資料。

結果@4:犯罪類型(crime_type)為“搶劫”類型的聚合資料。

結果@5:"廣州交通警局" “搶劫”類案的處理條數為27617。

結果@6:索引庫總處理犯罪類型為“搶劫”的總數為53182 。

2.3 Significant聚合的分數如何計算

如果術語在子集中(foreground set)出現的頻率和在背景中(background sets)出現的頻率有顯著差異,則認為該術語是重要的。

2.4 Custom background sets

定制background sets集合。通常情況下,ES的Significant聚合使用整個索引庫的内容當成background sets(背景集合),可以通過background_filter參數來指定,其使用示例如下:

2.5 Significant Terms Aggregation限制

  • 聚合字段必須是索引的
  • 不支援浮點類型字段聚合。
  • 由于Significant Terms Aggregation聚合的background sets是整個索引文檔,故如果用作foreground set的查詢傳回結果也是整個文檔集合(match_all)的話,該聚合則失去意義。
  • 如果有相當于match_all查詢沒有查詢條件提供索引的一個子集significant_terms聚合不應該被用作最頂部的聚合——在這個場景中前景是完全一樣的背景設定,是以沒有文檔頻率的差異來的觀察和合理建議。

與Terms Aggregation一樣,其結果是近似值,可以通過size、shard_size來控制其精度。

另一個需要考慮的問題是,significant_terms聚合在切分級别上生成許多候選結果,隻有在合并所有切分的統計資訊之後,才會在reduce節點上對這些結果進行修剪。是以,就RAM而言,将大型子聚合嵌入到一個重要的_terms聚合(稍後将丢棄許多候選項)下是低效且昂貴的。在這種情況下,最好執行兩個搜尋——第一個搜尋提供一個合理的重要術語清單,然後将這個術語短清單添加到第二個查詢中,以傳回并擷取所需的子聚合。

Significant Terms Aggregation支援Terms Aggregation定義的參數,諸如size、sharding_size、missing、collect_mode、execution_hint、min_doc_count等參數。

原文釋出時間為:2019-03-14

本文作者:丁威,《RocketMQ技術内幕》作者。

本文來自

中間件興趣圈

,了解相關資訊可以關注

繼續閱讀