# Elasticsearch (六) - Aggregation 聚合 Elasticsearch 除了提供搜尋的功能外,也提供了資料統計的功能,也就是本篇要介紹的聚合。聚合提供了多種分析的方式來滿足大多數的資料統計需求,例如 : * 一個月內最大筆金額的訂單是哪一個 ? * 這次促銷活動期間賣最差的商品是哪一項 ? * 今年度每月的平均業績是多少 ? <!-- more --> 而聚合主要的功能有以下四個 : * Metric Aggregation (指標型聚合) * Bucket Aggregation (桶型聚合) * Pipeline Aggregation (管道型聚合) * Matrix Aggregation (矩陣型聚合) 下面我們就來介紹每一種聚合的功能和使用方式。 ## Metric Aggregation (指標型聚合) 可以持續追蹤和計算一組 Document 指標的聚合,簡單來說就是可以用來計算最大值、最小值、平均值、總和等等的功能。 指標型聚合又分為單值分析和多值分析,單值分析只會輸出一個結果,多值分析會輸出多個結果。 ### 單值分析 單值分析可以使用的功能包含 : * min : 最小值 * max : 最大值 * avg : 平均值 * sum : 總合 * value_count : 指定欄位的個數 * cardinality : 基數值,就是不重複的數值的個數,類似 SQL 的 distinct count ```json= GET <Index>/_search { "aggs": { "<NAME>": { "<AGG_TYPE>": { "field": "<Field_Name>" } }, "<NAME>": { "<AGG_TYPE>": { "field": "<Field_Name>" } }, ... } } ``` NAME 可以自由填入想要的名稱,AGG_TYPE 可以填入上方列出的幾種功能,例如 : min。 **範例** 首先先建立一組資料,以便後面操作使用。 ```json= PUT math POST math/final/_bulk {"index" :{}} {"sudent":"Amy", "score":100} {"index" :{}} {"sudent":"Andy", "score":99} {"index" :{}} {"sudent":"Benson", "score":95} {"index" :{}} {"sudent":"Bob", "score":88} {"index" :{}} {"sudent":"Cathy", "score":81} {"index" :{}} {"sudent":"Daniel", "score":79} {"index" :{}} {"sudent":"Edward", "score":77} {"index" :{}} {"sudent":"Frank", "score":70} {"index" :{}} {"sudent":"Gigi", "score":66} {"index" :{}} {"sudent":"Henry", "score":60} {"index" :{}} {"sudent":"Ian", "score":58} {"index" :{}} {"sudent":"Jerry", "score":58} {"index" :{}} {"sudent":"John", "score":33} {"index" :{}} {"sudent":"Jordon", "score":38} ``` 接著我們對 score 這個 Field 做指標型聚合分析,這裡 size 設為 0 只是為了不要顯示搜尋的 Document,指令如下 : ```json= GET math/_search { "size": 0, "aggs": { "min_socre": { "min": { "field": "score" } }, "max_socre": { "max": { "field": "score" } }, "avg_socre": { "avg": { "field": "score" } }, "sum_score": { "sum":{ "field": "score" } }, "cardinality_score": { "cardinality": { "field": "score" } }, "value_count_score": { "value_count": { "field": "score" } } } } ``` 下面是輸出的結果,剛才總共新增了 14 筆 Document,所以 value_count_score 是 14。再特別看到 cardinality_score 的結果是 13,這是因為有兩個 score 為 58,所以不重複的數值有 13 個。 ```json= { "took" : 10, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : 14, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "sum_score" : { "value" : 1002.0 }, "avg_socre" : { "value" : 71.57142857142857 }, "value_count_score" : { "value" : 14 }, "max_socre" : { "value" : 100.0 }, "min_socre" : { "value" : 33.0 }, "cardinality_score" : { "value" : 13 } } } ``` ### 多值分析 多值分析常用的功能包含 : * stats : 列出一系列的數值型別統計 * extended_stats : stats 的擴充,可以列出更多統計資料,例如 : 標準差 * percentiles : 百分位數統計 * percentile_ranks : 百分等級統計 * top_hits : 搜尋結果的前幾個 多值分析的使用格式大多與單值分析相同,本文介紹的只有 percentile_ranks 和 top_hits 稍有不同。請見下方範例。 **範例** 下面的範例可以看到 percentile_ranks 有特別指定 values,這裡指定 values 是代表要看這個值在百分等級統計出來排名是多少。以這個範例來說,要看 50 分和 78 分的百分等級排名,其實就和學校入學考試成績所公布的 PR 值是同樣的概念。 而 top_hits 預設會會傳搜尋結果的前 3 筆,這裡我們自訂為 2 筆。這裡同樣也可以指定要回傳哪些欄位以及排序等等。 ```json= GET math/_search { "size": 0, "aggs": { "stats_score": { "stats": { "field": "score" } }, "extended_stats_score": { "extended_stats": { "field": "score" } }, "percentiles_score": { "percentiles": { "field": "score" } }, "percentile_ranks_score": { "percentile_ranks": { "field": "score", "values": [50, 78] } }, "top_hits": { "top_hits": { "size": 2 } } } } ``` 下面是回傳的結果,特別看到 percentiles_score 和 percentile_ranks_score。percentiles_score 左邊所列出來的是 PR 值,右邊是對應的分數。而 percentile_ranks_score 左邊列出來的是要查看的分數,右邊是對應的 PR 值。 ```json= { "took" : 4, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : 14, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "stats_score" : { "count" : 14, "min" : 33.0, "max" : 100.0, "avg" : 71.57142857142857, "sum" : 1002.0 }, "percentile_ranks_score" : { "values" : { "50.0" : 15.714285714285717, "78.0" : 57.14285714285714 } }, "extended_stats_score" : { "count" : 14, "min" : 33.0, "max" : 100.0, "avg" : 71.57142857142857, "sum" : 1002.0, "sum_of_squares" : 77418.0, "variance" : 407.3877551020404, "std_deviation" : 20.183848867399902, "std_deviation_bounds" : { "upper" : 111.93912630622837, "lower" : 31.203730836628765 } }, "percentiles_score" : { "values" : { "1.0" : 33.0, "5.0" : 34.0, "25.0" : 58.0, "50.0" : 73.5, "75.0" : 88.0, "95.0" : 99.79999999999998, "99.0" : 100.0 } }, "top_hits" : { "hits" : { "total" : 14, "max_score" : 1.0, "hits" : [ { "_index" : "math", "_type" : "final", "_id" : "5yvN4HMBm9Vi6FgNxTCj", "_score" : 1.0, "_source" : { "sudent" : "Amy", "score" : 100 } }, { "_index" : "math", "_type" : "final", "_id" : "6yvN4HMBm9Vi6FgNxTCj", "_score" : 1.0, "_source" : { "sudent" : "Cathy", "score" : 81 } } ] } } } } ``` ## Bucket Aggregation (桶型聚合) 桶型聚合會建立一個或多個桶子,並將 Document 分類放進這些桶子。常見的桶型聚合有以下幾個 : * terms : 依照單詞進行分桶 * range : 依照指定區間進行分桶 * date_range : 依照日期進行分桶 * histogram : 依照指定數值作為間隔區間分桶 * date_histogram : 依照指定時間間隔分桶 ### terms terms 會依照單詞進行分桶。 **範例** terms 預設會回傳 10 筆分桶的結果,這裡因為我們的 Document 有 14 筆,所以指定回傳 15 筆來顯示出所有的分桶情況。 ```json= GET math/_search { "size":0, "aggs": { "terms_score": { "terms": { "field": "score", "size": 15 } } } } ``` 下面是輸出的結果,可以看到總共分了 13 個桶子,兩個 socre 為 58 的 Docuemnt 被分進了同一個桶子。 ```json= { "took" : 1, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : 14, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "terms_score" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 0, "buckets" : [ { "key" : 58, "doc_count" : 2 }, { "key" : 33, "doc_count" : 1 }, { "key" : 38, "doc_count" : 1 }, { "key" : 60, "doc_count" : 1 }, { "key" : 66, "doc_count" : 1 }, { "key" : 70, "doc_count" : 1 }, { "key" : 77, "doc_count" : 1 }, { "key" : 79, "doc_count" : 1 }, { "key" : 81, "doc_count" : 1 }, { "key" : 88, "doc_count" : 1 }, { "key" : 95, "doc_count" : 1 }, { "key" : 99, "doc_count" : 1 }, { "key" : 100, "doc_count" : 1 } ] } } } ``` ### range range 會依照指定區間進行分桶。 **範例** 指定區間時,會包含 from 指定的數值,to 所指定的數值則不會被包含,也就是 from <= x < to。 以下面這個範例來說會分成三個桶子,分別是 x < 60、60 <= x < 90、x >= 90。 ```json= GET math/_search { "size":0, "aggs": { "range_score": { "range": { "field": "score", "ranges": [ {"to": 60}, {"from": 60, "to": 90}, {"from": 90} ] } } } } ``` 下面是輸出的結果,可以看到分成了三個桶子,每個桶子各分到了一些 Document。 ```json= { "took" : 1, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : 14, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "range_score" : { "buckets" : [ { "key" : "*-60.0", "to" : 60.0, "doc_count" : 4 }, { "key" : "60.0-90.0", "from" : 60.0, "to" : 90.0, "doc_count" : 7 }, { "key" : "90.0-*", "from" : 90.0, "doc_count" : 3 } ] } } } ``` ### date_range date_range 會依照日期進行分桶。 **範例** 首先要先建立一組有日期的 Document,如下 : ```json= PUT order POST order/day/_bulk {"index" :{}} {"price":100, "date":"2020-07-13"} {"index" :{}} {"price":530, "date":"2020-07-14"} {"index" :{}} {"price":100, "date":"2020-07-15"} {"index" :{}} {"price":385, "date":"2020-07-16"} {"index" :{}} {"price":300, "date":"2020-07-17"} {"index" :{}} {"price":790, "date":"2020-07-18"} {"index" :{}} {"price":560, "date":"2020-07-19"} {"index" :{}} {"price":530, "date":"2020-07-20"} {"index" :{}} {"price":385, "date":"2020-07-21"} {"index" :{}} {"price":150, "date":"2020-07-22"} {"index" :{}} {"price":115, "date":"2020-07-23"} {"index" :{}} {"price":100, "date":"2020-07-24"} {"index" :{}} {"price":530, "date":"2020-07-25"} {"index" :{}} {"price":750, "date":"2020-07-26"} {"index" :{}} {"price":258, "date":"2020-07-27"} {"index" :{}} {"price":100, "date":"2020-07-28"} {"index" :{}} {"price":100, "date":"2020-07-29"} ``` 接著指定要分成不同桶子的時間區間。這裡的日期可以用 `now-10d` 這種寫法,就是現在時間減 10 天。詳細用法請參考 [Elasticsearch 官網](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-daterange-aggregation.html)。 ```json= GET order/_search { "size":0, "aggs": { "date_range_order": { "date_range": { "field": "date", "ranges": [ {"to": "2020-07-23"}, {"from": "2020-07-23"} ] } } } } ``` 下面是輸出的結果,可以看到以 2020-07-23 這個日期分開為兩個桶子。 ```json= { "took" : 4, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : 17, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "date_range_order" : { "buckets" : [ { "key" : "*-2020-07-23T00:00:00.000Z", "to" : 1.5954624E12, "to_as_string" : "2020-07-23T00:00:00.000Z", "doc_count" : 10 }, { "key" : "2020-07-23T00:00:00.000Z-*", "from" : 1.5954624E12, "from_as_string" : "2020-07-23T00:00:00.000Z", "doc_count" : 7 } ] } } } ``` ### histogram histogram 會依照指定數值作為間隔區間分桶。 **範例** 這個範例指定 100 為間隔來分桶,interval 可以指定間隔的數值。min_doc_count 可以指定顯示的桶子至少要有多少 Document,這裡設為 1 代表只顯示有被分配到 Document 的桶子。 ```json= GET order/_search { "size":0, "aggs": { "histogram_order": { "histogram": { "field": "price", "interval": 100, "min_doc_count": 1 } } } } ``` 下面是輸出的結果,可以看到每隔 100 就分了一個桶子,且 400 因為是空桶所以沒有顯示出來。 ```json= { "took" : 3, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : 17, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "histogram_order" : { "buckets" : [ { "key" : 100.0, "doc_count" : 7 }, { "key" : 200.0, "doc_count" : 1 }, { "key" : 300.0, "doc_count" : 3 }, { "key" : 500.0, "doc_count" : 4 }, { "key" : 700.0, "doc_count" : 2 } ] } } } ``` ### date_histogram date_histogram 會依照指定時間間隔分桶。 **範例** 這個範例指定的時間區間是 1 週,也就是 interval 指定的 1w。更多時間區間請參考 [Elasticsearch 官網](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-datehistogram-aggregation.html#calendar_intervals)。 ```json= GET order/_search { "size":0, "aggs": { "date_histogram_order": { "date_histogram": { "field": "date", "interval": "1w" } } } } ``` 下面是輸出的結果,可以看到 7-13、7-20、7-27 都是間隔 7 天。 ```json= { "took" : 1, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : 17, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "date_histogram_order" : { "buckets" : [ { "key_as_string" : "2020-07-13T00:00:00.000Z", "key" : 1594598400000, "doc_count" : 7 }, { "key_as_string" : "2020-07-20T00:00:00.000Z", "key" : 1595203200000, "doc_count" : 7 }, { "key_as_string" : "2020-07-27T00:00:00.000Z", "key" : 1595808000000, "doc_count" : 3 } ] } } } ``` ### 子分析 桶型聚合支援分析之後再進一步分析,再次分析可以使用桶型聚合或是指標型聚合。只要在 `aggs` 中再加一層 `aggs` 就可以使用子分析。 **範例** 分桶之後再分桶,這個範例是先依照日期分桶,再依照價錢做分桶。 ```json= GET order/_search { "size":0, "aggs": { "date_histogram_order": { "date_histogram": { "field": "date", "interval": "1w" }, "aggs": { "histogram_order": { "histogram": { "field": "price", "interval": 100, "min_doc_count": 1 } } } } } } ``` 下面是輸出的結果,可以看到每個 buckets 裡面都還有一個 buckets。 ```json= { "took" : 5, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : 17, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "date_histogram_order" : { "buckets" : [ { "key_as_string" : "2020-07-13T00:00:00.000Z", "key" : 1594598400000, "doc_count" : 7, "histogram_order" : { "buckets" : [ { "key" : 100.0, "doc_count" : 2 }, { "key" : 300.0, "doc_count" : 2 }, { "key" : 500.0, "doc_count" : 2 }, { "key" : 700.0, "doc_count" : 1 } ] } }, { "key_as_string" : "2020-07-20T00:00:00.000Z", "key" : 1595203200000, "doc_count" : 7, "histogram_order" : { "buckets" : [ { "key" : 100.0, "doc_count" : 3 }, { "key" : 300.0, "doc_count" : 1 }, { "key" : 500.0, "doc_count" : 2 }, { "key" : 700.0, "doc_count" : 1 } ] } }, { "key_as_string" : "2020-07-27T00:00:00.000Z", "key" : 1595808000000, "doc_count" : 3, "histogram_order" : { "buckets" : [ { "key" : 100.0, "doc_count" : 2 }, { "key" : 200.0, "doc_count" : 1 } ] } } ] } } } ``` ## Pipeline Aggregation (管道型聚合) 管道型聚合會聚合其他聚合分析的結果和他們的指標。簡單來說就是針對其他聚合分析的結果再次進行聚合分析,且支援鍊式呼叫。例如,訂單每個月的平均銷收額是多少 ? 管道型聚合主要分為兩類 : * Parent : 在父聚合的結果上進行聚合分析並且可以計算出新的桶子或是將新的聚合結果加入到現有的桶子中。 下面是 Parent 的結構,可以看到有兩層 `aggs`,第一層有 `agg1`,第二層有 `agg2` 和 `agg3`。`agg3` 是要加入的管道型聚合,`agg3` 會對 `agg1` 的結果進一步聚合分析,而 `agg3` 是 `agg1` 的子聚合,所以這種結構歸類為 Parent。 ```json= GET _search { "aggs": { "<NAME>": { // agg1 "<AGG_TYPE>": { "field": "<Field_Name>" } }, "aggs": { "<NAME>": { // agg2 "<AGG_TYPE>": { "field": "<Field_Name>" } }, "<NAME>": { // agg3 "<AGG_TYPE>": { "field": "<Field_Name>" } } } } } ``` * Sibling : 在兄弟 (同級) 聚合的結果上進行聚合分析。計算出一個新的聚合結果,結果與兄弟聚合的結果同級。 下面是 Sibiling 的結構,可以看到有兩層 `aggs`,第一層有 `agg1` 和 `agg3`,第二層有 `agg2`。`agg3` 是要加入的管道型聚合,`agg3` 會對 `agg1` 的結果進一步聚合分析,而 `agg1` 和 `agg3` 是在同一層的 `aggs`,也就代表他們是同等級的聚合,所以這種結構歸類為 Sibling。 ```json= GET _search { "aggs": { "<NAME>": { // agg1 "<AGG_TYPE>": { "field": "<Field_Name>" }, "aggs": { "<NAME>": { // agg2 "<AGG_TYPE>": { "field": "<Field_Name>" } } } }, "<NAME>": { // agg3 "<AGG_TYPE>": { "buckets_path": "<buckets_path>" } } } } ``` ### buckets_path Syntax 每個管道型聚合都需要指定 buckets_path 這個參數,這個參數用於指定其他的聚合。 指定 buckets_path 的語法 : 1. 聚合分隔符號 : `>`,指定父字聚合關係。例如,"agg_parent>agg_son"。 2. 指標分隔符號 : `.`,指定聚合的指標。 3. 聚合名稱 : `<name of the aggregation>`,直接指定聚合名稱。 4. 指標名稱 : `<name of the metric>`,直接指定指標名稱。 5. 完整路徑 : `agg_name[>agg_name]*[.metrics]`,綜合上方的方式指定完整的路徑。 ### Parent Parent 提供了以下幾個常用的功能 : * derivative * moving_avg * cumulative_sum * bucket_sort * bucket_script * bucket_selector #### derivative derivative 用於計算和父聚合的結果的導數值。簡單來說就是當分出了第一桶之後,第一桶的結果會被拿去和第二桶的結果計算他們的導數,以此類推。而導數在這裡的定義其實是差距,也就是說第二桶的結果會去扣掉第一桶的結果,所得的值即為最後的導數值。 **範例** 下面這個範例首先可以看到 `sales_per_week` 先做了桶型聚合,接著 `sales` 再使用指標型聚合的 sum 來做子分析,將每一桶分到的 Document 的 price 加起來。 而 `derivative_sales` 會在每一桶做完之後去取得每一桶的導數 (差值)。 這個範例其實就是一個簡單的每週銷售額成長幅度的例子,首先 `sales_per_week` 先分類出同一週的銷售狀況 (Document) 放在同一堆 (Bucket),這時再將每一堆的銷售額 (Price) 加起來就會得到每一週各自的銷售總額。 `derivative_sales` 會再將前一週的銷售額減掉本週的銷售額就可以獲得銷售額的成長量。 ```json= GET order/_search { "size": 0, "aggs": { "sales_per_week": { "date_histogram": { "field": "date", "interval": "1w" }, "aggs": { "sales": { "sum": { "field": "price" } }, "derivative_sales": { "derivative": { "buckets_path": "sales" } } } } } } ``` 下面是輸出的結果,可以看到第一桶沒有輸出 `derivative_sales`,因為他沒有前一桶可以計算。第二桶和第三桶都可以看到 `derivative_sales` 是負的,因為前一桶的 sales 都比較大,所以扣下來就是負的。以這個範例來說,也可以看出來每週的銷售額是呈現負成長。 ```json= { "took" : 3, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : 17, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "sales_per_week" : { "buckets" : [ { "key_as_string" : "2020-07-13T00:00:00.000Z", "key" : 1594598400000, "doc_count" : 7, "sales" : { "value" : 2765.0 } }, { "key_as_string" : "2020-07-20T00:00:00.000Z", "key" : 1595203200000, "doc_count" : 7, "sales" : { "value" : 2560.0 }, "derivative_sales" : { "value" : -205.0 } }, { "key_as_string" : "2020-07-27T00:00:00.000Z", "key" : 1595808000000, "doc_count" : 3, "sales" : { "value" : 458.0 }, "derivative_sales" : { "value" : -2102.0 } } ] } } } ``` #### moving_avg moving_avg 用於平滑化數據,例如像是 CPU 負載或是記憶體使用量等等。 **範例** ```json= GET order/_search { "size": 0, "aggs": { "sales_per_week": { "date_histogram": { "field": "date", "interval": "5d" }, "aggs": { "sales": { "sum": { "field": "price" } }, "moving_avg_sales": { "moving_avg": { "buckets_path": "sales" } } } } } } ``` 下面是輸出的結果,可以看到 moving_avg_sales 透過移動平均的計算得出了一個平滑化的數值。 ```json= { "took" : 1, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : 17, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "sales_per_week" : { "buckets" : [ { "key_as_string" : "2020-07-12T00:00:00.000Z", "key" : 1594512000000, "doc_count" : 4, "sales" : { "value" : 1115.0 } }, { "key_as_string" : "2020-07-17T00:00:00.000Z", "key" : 1594944000000, "doc_count" : 5, "sales" : { "value" : 2565.0 }, "moving_avg_sales" : { "value" : 1115.0 } }, { "key_as_string" : "2020-07-22T00:00:00.000Z", "key" : 1595376000000, "doc_count" : 5, "sales" : { "value" : 1645.0 }, "moving_avg_sales" : { "value" : 1840.0 } }, { "key_as_string" : "2020-07-27T00:00:00.000Z", "key" : 1595808000000, "doc_count" : 3, "sales" : { "value" : 458.0 }, "moving_avg_sales" : { "value" : 1775.0 } } ] } } } ``` #### cumulative_sum cumulative_sum 用於不斷累加每一桶的結果。 **範例** ```json= GET order/_search { "size": 0, "aggs": { "sales_per_week": { "date_histogram": { "field": "date", "interval": "1w" }, "aggs": { "sales": { "sum": { "field": "price" } }, "cumulative_sum_sales": { "cumulative_sum": { "buckets_path": "sales" } } } } } } ``` 下面是輸出的結果,可以看到 cumulative_sum_sales 每次都會持續累加上去。 ```json= { "took" : 2, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : 17, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "sales_per_week" : { "buckets" : [ { "key_as_string" : "2020-07-13T00:00:00.000Z", "key" : 1594598400000, "doc_count" : 7, "sales" : { "value" : 2765.0 }, "cumulative_sum_sales" : { "value" : 2765.0 } }, { "key_as_string" : "2020-07-20T00:00:00.000Z", "key" : 1595203200000, "doc_count" : 7, "sales" : { "value" : 2560.0 }, "cumulative_sum_sales" : { "value" : 5325.0 } }, { "key_as_string" : "2020-07-27T00:00:00.000Z", "key" : 1595808000000, "doc_count" : 3, "sales" : { "value" : 458.0 }, "cumulative_sum_sales" : { "value" : 5783.0 } } ] } } } ``` #### bucket_sort bucket_sort 用於排序分桶的結果。 **範例** 這裡指定以 sales 聚合的結果來做排序。 ```json= GET order/_search { "size": 0, "aggs": { "sales_per_week": { "date_histogram": { "field": "date", "interval": "1w" }, "aggs": { "sales": { "sum": { "field": "price" } }, "bucket_sort_sales": { "bucket_sort": { "sort": [ {"sales": {"order":"desc"}} ], "size": 3 } } } } } } ``` 下面是輸出的結果,可以看到依照 sales 的大小排序顯示。 ```json= { "took" : 1, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : 17, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "sales_per_week" : { "buckets" : [ { "key_as_string" : "2020-07-13T00:00:00.000Z", "key" : 1594598400000, "doc_count" : 7, "sales" : { "value" : 2765.0 } }, { "key_as_string" : "2020-07-20T00:00:00.000Z", "key" : 1595203200000, "doc_count" : 7, "sales" : { "value" : 2560.0 } }, { "key_as_string" : "2020-07-27T00:00:00.000Z", "key" : 1595808000000, "doc_count" : 3, "sales" : { "value" : 458.0 } } ] } } } ``` #### bucket_script bucket_script 用於對多桶或單桶的結果進行計算。 **範例** 下面這個範例在 `bucket_script_sales` 的 `buckets_path` 中可以看到指定了一個參數 `TotalSales`,這個參數對應到的是 `sales` 這個聚合的結果。 隨後便可以在 script 中編寫一段運算,這裡只簡單將結果乘以 2。 ```json= GET order/_search { "size": 0, "aggs": { "sales_per_week": { "date_histogram": { "field": "date", "interval": "1w" }, "aggs": { "sales": { "sum": { "field": "price" } }, "bucket_script_sales": { "bucket_script": { "buckets_path": { "TotalSales" : "sales" }, "script": "params.TotalSales * 2" } } } } } } ``` 下面是輸出的結果,可以看到 `bucket_script_sales` 的結果都是 `sales` 的兩倍。 ```json= { "took" : 1, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : 17, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "sales_per_week" : { "buckets" : [ { "key_as_string" : "2020-07-13T00:00:00.000Z", "key" : 1594598400000, "doc_count" : 7, "sales" : { "value" : 2765.0 }, "bucket_script_sales" : { "value" : 5530.0 } }, { "key_as_string" : "2020-07-20T00:00:00.000Z", "key" : 1595203200000, "doc_count" : 7, "sales" : { "value" : 2560.0 }, "bucket_script_sales" : { "value" : 5120.0 } }, { "key_as_string" : "2020-07-27T00:00:00.000Z", "key" : 1595808000000, "doc_count" : 3, "sales" : { "value" : 458.0 }, "bucket_script_sales" : { "value" : 916.0 } } ] } } } ``` #### bucket_selector bucket_selector 用於依照指定的條件取出特定的桶子。 **範例** 下面的範例可以看到用了和 bucket_script 一樣的編寫 Script 的方式來指定條件。 ```json= GET order/_search { "size": 0, "aggs": { "sales_per_week": { "date_histogram": { "field": "date", "interval": "1w" }, "aggs": { "sales": { "sum": { "field": "price" } }, "bucket_sort_sales": { "bucket_selector": { "buckets_path": { "TotalSales" : "sales" }, "script": "params.TotalSales > 1000" } } } } } } ``` 下面是輸出的結果,可以看到原本應該還會有一桶的結果是 458,但因為小於 1000 所以被過濾掉了。 ```json= { "took" : 20, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : 17, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "sales_per_week" : { "buckets" : [ { "key_as_string" : "2020-07-13T00:00:00.000Z", "key" : 1594598400000, "doc_count" : 7, "sales" : { "value" : 2765.0 } }, { "key_as_string" : "2020-07-20T00:00:00.000Z", "key" : 1595203200000, "doc_count" : 7, "sales" : { "value" : 2560.0 } } ] } } } ``` ### Sibling Sibiling 提供了以下這些常用的功能,這些功能大致與指標型聚合提供的是一樣的,差別在於指標型聚合是在計算一組 Document,而桶型聚合是在計算一組 Bucket。如下 : * avg_bucket * max_bucket * min_bucket * sum_bucket * stats_bucket * extended_stats_bucket * percentiles_bucket 下面僅以 avg_bucket 和 max_bucket 作為範例。 #### avg_bucket avg_bucket 用於計算同級聚合中指定的指標 (Metric) 的平均值。簡單來說就是當分了很多桶之後,每個桶又透過指標型聚合得出了一個值,例如總合。這時候,avg_bucket 就可以將每一桶的總和加起來算平均值。 avg_bucket 要求要計算的兄弟聚合一定要是多桶聚合,也就是結果不能只有一個桶子。如果只有一個桶子就沒有必要使用 avg_bucket。 **範例** 下面這個範例首先可以看到 `sales_per_week` 先做了桶型聚合,接著 `sales` 再使用指標型聚合的 sum 來做子分析,將每一桶分到的 Document 的 price 加起來。 最後 `avg_weekly_sales` 會指定他的兄弟聚合 `sales_per_week` 的 子聚合 `sales` 來做加總計算平均值。 這個範例其實就是一個簡單的每週平均銷售額的例子,首先 `sales_per_week` 先分類出同一週的銷售狀況 (Document) 放在同一堆 (Bucket),這時再將每一堆的銷售額 (Price) 加起來就會得到每一週各自的銷售總額。 `avg_weekly_sales` 會從這個計算出來的每週銷售額加起來算平均值,就可以取得每週的平均銷售額。 ```json= GET order/_search { "size": 0, "aggs": { "sales_per_week": { "date_histogram": { "field": "date", "interval": "1w" }, "aggs": { "sales": { "sum": { "field": "price" } } } }, "avg_weekly_sales": { "avg_bucket": { "buckets_path": "sales_per_week>sales" } } } } ``` 下面是輸出的結果,可以看到 `avg_weekly_sales` 顯示了計算出的每桶的平均。 ```json= { "took" : 2, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : 17, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "sales_per_week" : { "buckets" : [ { "key_as_string" : "2020-07-13T00:00:00.000Z", "key" : 1594598400000, "doc_count" : 7, "sales" : { "value" : 2765.0 } }, { "key_as_string" : "2020-07-20T00:00:00.000Z", "key" : 1595203200000, "doc_count" : 7, "sales" : { "value" : 2560.0 } }, { "key_as_string" : "2020-07-27T00:00:00.000Z", "key" : 1595808000000, "doc_count" : 3, "sales" : { "value" : 458.0 } } ] }, "avg_weekly_sales" : { "value" : 1927.6666666666667 } } } ``` #### max_bucket max_bucket 用於取得有最大值的桶子。 **範例** 同樣以每週銷售額的例子來說, `max_bucket` 取得的就是銷售額最高的那一週。 ```json= GET order/_search { "size": 0, "aggs": { "sales_per_week": { "date_histogram": { "field": "date", "interval": "1w" }, "aggs": { "sales": { "sum": { "field": "price" } } } }, "max_bucket_sales": { "max_bucket": { "buckets_path": "sales_per_week>sales" } } } } ``` 下面是輸出的結果,可以看到 `max_bucket_sales` 輸出的值是最大的。 ```json= { "took" : 4, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : 17, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "sales_per_week" : { "buckets" : [ { "key_as_string" : "2020-07-13T00:00:00.000Z", "key" : 1594598400000, "doc_count" : 7, "sales" : { "value" : 2765.0 } }, { "key_as_string" : "2020-07-20T00:00:00.000Z", "key" : 1595203200000, "doc_count" : 7, "sales" : { "value" : 2560.0 } }, { "key_as_string" : "2020-07-27T00:00:00.000Z", "key" : 1595808000000, "doc_count" : 3, "sales" : { "value" : 458.0 } } ] }, "max_bucket_sales" : { "value" : 2765.0, "keys" : [ "2020-07-13T00:00:00.000Z" ] } } } ``` ## Matrix Aggregation (矩陣型聚合) 矩陣型聚合會在多個欄位 (Field) 上操作,並根據要求的欄位提取出值,產生一個矩陣結果。 目前矩陣型聚合只提供一個功能,matrix_stats。 ### matrix_stats matrix_stats 用於以矩陣型式列出指定欄位的一些數值計算。包含總數、平均值等等。但是這些結果實務上是較少用到的。 **範例** 下面這個範例指定了兩個欄位。 ```json= GET order/_search { "size": 0, "aggs": { "matrix_stats_order": { "matrix_stats": { "fields": ["price", "date"] } } } } ``` 下面是輸出的結果,可以看到針對兩個欄位分別都計算出了一些數據。詳細每個數據代表什麼請參考 [Elasticsearch 官網](https://www.elastic.co/guide/en/elasticsearch/reference/7.8/search-aggregations-matrix-stats-aggregation.html#search-aggregations-matrix-stats-aggregation)。 ```json= { "took" : 7, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : 17, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "sales_per_week" : { "doc_count" : 17, "fields" : [ { "name" : "date", "count" : 17, "mean" : 1.5952896E12, "variance" : 1.90356479999994976E17, "skewness" : -1.4125983884605922E-15, "kurtosis" : 1.7916666666667205, "covariance" : { "date" : 1.90356479999994976E17, "price" : -1.4347800000000446E10 }, "correlation" : { "date" : 1.0, "price" : -0.1375604941903739 } }, { "name" : "price", "count" : 17, "mean" : 340.1764705882353, "variance" : 57149.904411764706, "skewness" : 0.4856145273558581, "kurtosis" : 1.9509282067535343, "covariance" : { "date" : -1.4347800000000446E10, "price" : 57149.904411764706 }, "correlation" : { "date" : -0.1375604941903739, "price" : 1.0 } } ] } } } ``` ## Query + Aggregation 介紹了這麼多聚合的功能,最終的目的就是要讓查詢也可以搭配聚合,類似於 SQL 的結果可以透過 group by 來分群一樣。 **範例** 下面是一個簡單的範例,可以看到先透過 match 來查詢出 price 為 100 的 Document。接著再透過 date_range 來依照時間分桶。 ```json= GET order/_search { "size": 0, "query": { "match": { "price": "100" } }, "aggs": { "date_range_order": { "date_range": { "field": "date", "ranges": [ {"to": "2020-07-23"}, {"from": "2020-07-23"} ] } } } } ``` 下面是輸出的結果,可以看到查詢出的 5 筆 Document 被依照時間分成了兩桶。 ```json= { "took" : 1, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : 5, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "date_range_order" : { "buckets" : [ { "key" : "*-2020-07-23T00:00:00.000Z", "to" : 1.5954624E12, "to_as_string" : "2020-07-23T00:00:00.000Z", "doc_count" : 2 }, { "key" : "2020-07-23T00:00:00.000Z-*", "from" : 1.5954624E12, "from_as_string" : "2020-07-23T00:00:00.000Z", "doc_count" : 3 } ] } } } ``` ## Summary 本篇介紹了四種型式的聚合以及每個聚合所提供的一些功能。以上介紹的只是一些常用的功能,想了解更多功能請參考 [Elasticsearch 官網](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html)。 ## 參考 [1] [Elasticsearch 聚合分析詳解](https://codertw.com/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80/422875/) [2] [elasticsearch系列六:聚合分析(聚合分析簡介、指標聚合、桶聚合)](https://www.twblogs.net/a/5d417166bd9eee51fbf9c6cc) [3] [Elasticsearch 系列 (3):Aggregation 聚合分析簡介](https://medium.com/@maiccaejfeng/elasticsearch-%E7%B3%BB%E5%88%97-3-aggregation-%E8%81%9A%E5%90%88%E5%88%86%E6%9E%90%E7%B0%A1%E4%BB%8B-c1604957f605) [4] [Elasticsearch聚合 之 Histogram 直方圖聚合](https://www.cnblogs.com/xing901022/p/4954823.html) [5] [Elasticsearch聚合-Bucket Aggregations ](https://my.oschina.net/bingzhong/blog/1917915) [6] [Aggregation | Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html) [7] [aggregation 詳解4(pipeline aggregations)](https://www.cnblogs.com/licongyu/p/5506228.html) [8] [Elasticsearch聚合——Pipeline Aggregations](https://my.oschina.net/bingzhong/blog/1975879) [9] [es聚合操作時提示Fielddata is disabled on text fields by default](https://blog.csdn.net/u011403655/article/details/71107415) [10] [矩陣統計(Matrix Stats)](https://xiaoxiami.gitbook.io/elasticsearch/ji-chu/36aggregationsju-he-fen-679029/364ju-zhen-ju-540828-matrix-aggregations/ju-zhen-tong-8ba128-matrix-stats) ###### tags: `Elasticsearch` `NoSQL`