一起學 Elasticsearch 系列-聚合查詢
聚合查詢是 Elasticsearch 中一種強大的數據分析工具,用于從索引中提取和計算有關數據的統計信息。聚合查詢可以執行各種聚合操作,如計數、求和、平均值、最小值、最大值、分組等,以便進行數據匯總和分析。
下面是一些常見的聚合查詢類型:
- Metric Aggregations(指標聚合):這些聚合操作返回基于字段值的度量結果,如求和、平均值、最小值、最大值等。常見的指標聚合包括 Sum、Avg、Min、Max、Stats 等。
- Bucket Aggregations(桶聚合):類比SQL中的group by,主要用于統計不同類型數據的數量,這些聚合操作將文檔劃分為不同的桶(buckets),并對每個桶中的文檔進行聚合計算。常見的桶聚合包括 Terms(按字段值分組)、Date Histogram(按時間間隔分組)、Range(按范圍分組)等。
- Pipeline Aggregations(管道聚合):這些聚合操作通過在其他聚合結果上執行額外的計算來產生新的聚合結果。例如,使用 Moving Average 聚合可以計算出移動平均值。
聚合查詢通常與查詢語句結合使用,可以在查詢結果的基礎上進行進一步的數據分析和統計。聚合查詢語法使用 JSON 格式,可以通過 Elasticsearch 的 REST API 或各種客戶端庫進行發送和解析。
聚合查詢支持嵌套,即一個聚合內部可以包含別的子聚合,從而實現非常復雜的數據挖掘和統計需求。
在ES中,用于進行聚合的字段可以是exact value也可以是分詞字段,對于分詞字段,可以使用特定的聚合操作來進行分組聚合,例如Terms Aggregation、Date Histogram Aggregation等。
對于text字段的聚合,可以通過開啟fielddata來實現,但通常不建議這樣做,因為fielddata會將聚合使用的數據結構從磁盤(doc_values)轉換為堆內存(field_data),在處理大量數據時容易導致內存溢出(OOM)問題。
如果需要在text字段上執行聚合,可以考慮在該字段上添加.keyword子字段,并使用該子字段進行聚合操作,以獲得更準確的結果。
doc_values & fielddata
在 Elasticsearch 中,聚合操作主要依賴于 doc_values 或 fielddata 來進行。
- Doc Values(文檔值):Doc Values 是一種以列式存儲格式保存字段值的數據結構,它用于支持快速的聚合、排序和統計操作。Doc Values 在磁盤上存儲,并被加載到 JVM 堆內存中進行計算。它們適用于精確值(如 keyword 類型)和數字類型的字段,在大多數情況下是默認啟用的。
- Fielddata(字段數據):Fielddata 是一種將字段值加載到堆內存中的數據結構,它用于支持復雜的文本分析和聚合操作。Fielddata 適用于文本類型的字段,例如 text 類型,因為它們需要進行分詞和分析。但是,由于 Fielddata 需要大量的堆內存資源,特別是在處理大數據集時,容易導致內存溢出(OOM)的問題,因此不建議隨意啟用。
在設計索引時,需要根據字段類型和使用場景的不同,合理選擇是否啟用 Doc Values 或 Fielddata,以平衡性能和資源消耗的需求。
當執行聚合操作時,Elasticsearch 需要訪問所有匹配文檔的字段值。對于非文本字段,默認情況下Elasticsearch 使用 doc values 來實現。對于文本字段,必須首先啟用 fielddata。然而,由于 fielddata 占用大量內存,Elasticsearch 默認禁用了它。
如果你確實需要對一個文本字段啟用 fielddata(雖然大多數場景下不推薦這么做,因為可能導致內存消耗過大),你可以通過更新映射(mapping)來實現。
以下是如何在 my_field 字段上啟用 fielddata 的示例:
PUT my_index/_mapping
{
"properties": {
"my_field": {
"type": "text",
"fielddata": true
}
}
}
注意:更改 fielddata 設置只會影響新的數據,已經索引的數據不會受到更改。如果你想讓更改生效,需要重新索引(reindex)你的數據
另外,一般情況下,建議使用 mapping 中的 keyword 類型來進行聚合、排序或腳本,而不是啟用 text 類型的 fielddata。這是因為 keyword 類型字段默認開啟了 doc values,比在 text 上啟用 fielddata 更加高效且節省內存。
multi-fields
在 Elasticsearch 中,一個字段有可能是 multi-fields(多字段)類型,這意味著同一份數據可以被索引為不同類型的字段。常見的情況就是,一個字段既被索引為 text 類型用于全文搜索,又被索引為 keyword 類型用于精確值搜索、排序和聚合。
當你在一個字段名后面加上 .keyword(例如 field.keyword),這說明你是在引用這個字段的 keyword 子字段。這個 keyword 子字段在索引時并不會被分詞器拆分成單獨的詞條,而是作為一個完整的字符串被存儲。這樣,你就可以對這個字段進行精確值匹配、排序或者聚合操作。
舉例來說,如果你有一個 message 字段并且想要對其進行聚合,你應該使用 message.keyword 而非 message。因為如果你直接對 message 進行聚合,Elasticsearch 就會嘗試對每一個獨立的詞條進行聚合,而不是對整個字段值進行聚合。
如果你的字段沒有 .keyword 子字段,那可能是在定義 mapping 時沒有包含這一部分,或者這個字段的類型本身就是 keyword。
分桶聚合
分桶(Bucket)聚合是一種特殊類型的聚合,它將輸入文檔集合中的文檔分配到一個或多個桶中,每個桶都對應于一個鍵(key)。
下面是一些常用的分桶聚合類型:
- terms:基于文檔中某個字段的值,將文檔分組到各個桶中。
- date_histogram:基于日期字段,將文檔按照指定的時間間隔分組到各個桶中。
- histogram:基于數值字段,將文檔按照指定的數值范圍分組到各個桶中。
- range:根據設置的范圍,將數據分為不同的桶。
以下是一個使用 terms 分桶聚合的例子:
假設你有一個包含博客文章的 blog 索引,你想知道每個作者寫了多少篇文章,可以使用以下查詢:
GET /blog/_search
{
"size": 0,
"aggs": {
"authors": {
"terms": { "field": "author.keyword" }
}
}
}
在這個查詢中:
- size: 0 表示我們只對聚合結果感興趣,不需要返回任何具體的搜索結果。
- "aggs" (或者 "aggregations") 塊定義了我們的聚合。
- "authors" 是我們自己為這個聚合命名的標簽,你可以用任何你喜歡的標簽名。
- "terms": { "field": "author.keyword" } 定義了我們要進行聚合的方式和字段。這里,我們告訴 Elasticsearch 使用 terms 聚合,并且使用 author.keyword 字段的值作為分桶的依據。
Elasticsearch 將返回一個包含每個作者以及他們所寫的文章數量的列表。注意,由于 Elasticsearch 默認只返回前十個桶,如果你的數據中有更多的作者,可能需要設置 size 參數來獲取更多的結果。
Histogram
histogram 是桶聚合的一種類型,它可以按照指定的間隔將數字字段的值劃分為一系列桶。每個桶代表了這個區間內的所有文檔。
以下是一個例子,我們根據價格字段創建一個間隔為 50 的直方圖:
GET /products/_search
{
"size": 0,
"aggs" : {
"prices" : {
"histogram" : {
"field" : "price",
"interval" : 50
}
}
}
}
在這個例子中,“prices” 是一個 histogram 聚合,它以 50 為間隔將產品的價格劃分為一系列的桶。
指標聚合
在 Elasticsearch 中,指標聚合是對數據進行統計計算的一種方式,例如求和、平均值、最小值、最大值等。以下是一些常用的指標聚合類型:
- avg:計算字段的平均值。
- sum:計算字段的總和。
- min:查找字段的最小值。
- max:查找字段的最大值。
- count:計算匹配文檔的數量。
- stats:提供了 count、sum、min、max 和 avg 的基本統計。
下面是一個示例,假設我們有一個包含售賣商品的 “sales” 索引,我們想要知道所有銷售記錄中的平均價格,可以使用 avg 聚合如下操作:
GET /sales/_search
{
"size": 0,
"aggs": {
"average_price": {
"avg": { "field": "price" }
}
}
}
Percentiles
percentiles 是指標聚合的一種,它用于計算數值字段的百分位數。給定一個列表百分比,Elasticsearch 可以計算每個百分比下的數值。
以下是一個例子,我們計算價格字段的 1st, 5th, 25th, 50th, 75th, 95th, and 99th 百分位數:
GET /products/_search
{
"size": 0,
"aggs" : {
"price_percentiles" : {
"percentiles" : {
"field" : "price",
"percents" : [1, 5, 25, 50, 75, 95, 99]
}
}
}
}
在這個例子中,“price_percentiles” 是一個 percentiles 聚合,它計算了價格在各個百分位點的數值。
注意,對于大數據集,計算精確的百分位數可能需要消耗大量資源。因此,Elasticsearch 默認使用一個名為 TDigest 的算法來提供近似的計算結果,同時還能保持內存使用的可控性。
cardinality
如果你想在 Elasticsearch 中進行去重操作,可以使用 terms 聚合加上 cardinality 聚合。這是一個示例,假設我們有一個包含user_id的 "users" 索引,并且我們想要知道有多少唯一的 user_id:
GET /users/_search
{
"size": 0,
"aggs": {
"distinct_user_ids": {
"cardinality": {
"field": "user_id.keyword"
}
}
}
}
在這個查詢中:
- "distinct_user_ids" 是我們自己為這個聚合命名的標簽。
- "cardinality": { "field": "user_id.keyword" } 使用了 cardinality 聚合,該聚合會返回指定字段(在這里是 user_id.keyword)的不同值的數量。
Elasticsearch 將返回一個結果,告訴我們有多少個不同的 user_id。請注意,cardinality 聚合可能并不總是完全精確,特別是對于大型數據集,因為它在內部使用了一種叫做 HyperLogLog 的算法來近似計算基數,這種算法會在保持內存消耗相對較小的情況下提供接近準確的結果。如果你需要完全精確的結果,可能需要考慮其他方法,例如使用腳本或者將數據導出到外部系統進行處理。
管道聚合
在 Elasticsearch 中,管道聚合(pipeline aggregations)是指這樣一種聚合:它以其他聚合的結果作為輸入,并進行進一步處理。
常見的管道聚合包括:
- avg_bucket
- sum_bucket
- min_bucket
- max_bucket
- stats_bucket
- extended_stats_bucket
- percentiles_bucket
這些都是 bucket 級別的管道聚合,它們會在一組數據桶上操作。
下面給出一個示例,假設我們有一個銷售記錄索引 "sales",每個銷售記錄都有售價 "price" 和銷售日期 "date" 字段。如果我們想要計算每月平均銷售價格,并找出所有月份中平均價格最高的月份,可以使用 date_histogram 聚合加上 avg 以及 max_bucket 聚合來實現:
GET /sales/_search
{
"size": 0,
"aggs": {
"sales_per_month": {
"date_histogram": {
"field": "date",
"calendar_interval": "month"
},
"aggs": {
"avg_price": {
"avg": { "field": "price" }
}
}
},
"max_avg_price": {
"max_bucket": {
"buckets_path": "sales_per_month>avg_price"
}
}
}
}
在這個查詢中:
- "sales_per_month" 是一個按月聚合銷售記錄的 date_histogram 聚合。
- "avg_price" 是一個嵌套在 "sales_per_month" 下的 avg 聚合,用于計算每月的平均銷售價格。
- "max_avg_price" 是一個 max_bucket 聚合,它會找出 "sales_per_month" 中所有子桶的 "avg_price" 最大值。
注意到 "max_avg_price" 中的 "buckets_path": "sales_per_month>avg_price"。buckets_path 參數指定了此管道聚合的輸入來源,> 符號表示路徑層次,即先取 "sales_per_month" 聚合的結果,再取其中的 "avg_price" 聚合的結果作為輸入。
返回的結果中會包含每個月的平均銷售價格,以及所有月份中平均銷售價格的最大值。
嵌套聚合
嵌套聚合就是在聚合內使用聚合,在 Elasticsearch 中,嵌套聚合通常用于處理 nested 類型的字段。nested 類型允許你將一個文檔中的一組對象作為獨立的文檔進行索引和查詢,這對于擁有復雜數據結構(例如數組或列表中的對象)的場景非常有用。
假設我們有一個 users 索引,每個 user 文檔都有一個 purchases 字段,該字段是一個列出用戶所有購買記錄的數組,每個購買記錄包含 product_id 和 price。如果我們想要找出價格超過 100 的所有產品的 ID,可以使用 nested 聚合:
GET /users/_search
{
"size": 0,
"aggs": {
"all_purchases": {
"nested": {
"path": "purchases"
},
"aggs": {
"expensive_purchases": {
"filter": { "range": { "purchases.price": { "gt": 100 } } },
"aggs": {
"product_ids": { "terms": { "field": "purchases.product_id" } }
}
}
}
}
}
}
在這個查詢中:
- "all_purchases" 是一個 nested 聚合,指定了 nested 對象的路徑 purchases。
- "expensive_purchases" 是一個嵌套在 "all_purchases" 下的 filter 聚合,它會過濾出 price 大于 100 的購買記錄。
- "product_ids" 是一個嵌套在 "expensive_purchases" 下的 terms 聚合,它會提取出所有滿足條件的 product_id。
返回的結果將包含所有 price 大于 100 的產品的 ID 列表。
請注意,在處理 nested 數據時,你需要確保 mapping 中相應的字段已經被設置為 nested 類型,否則該查詢可能無法按預期工作。
基于查詢結果的聚合 & 基于聚合結果的查詢
基于查詢結果的聚合:在這種情況下,我們首先執行一個查詢,然后對查詢結果進行聚合。
例如,如果我們要查詢所有包含某關鍵字的文檔,并計算它們的平均價格,可以這樣做:
GET /products/_search
{
"query": {
"match": {
"description": "laptop"
}
},
"aggs": {
"average_price": {
"avg": {
"field": "price"
}
}
}
}
在上述例子中,我們首先通過 match 查詢找到描述中包含 "laptop" 的所有產品,然后對這些產品的價格進行平均值聚合。
基于聚合結果的查詢:這種情況下,我們先執行聚合,然后基于聚合的結果執行過濾操作。
這通常用于在聚合結果中應用一些額外的過濾條件。例如,如果我們想對所有產品進行銷售數量聚合,然后從結果中過濾出銷售數量大于10的產品,可以這樣做:
GET /sales/_search
{
"size": 0,
"aggs": {
"sales_per_product": {
"terms": {
"field": "product_id"
}
}
},
"post_filter": {
"bucket_selector": {
"buckets_path": {
"salesCount": "sales_per_product._count"
},
"script": {
"source": "params.salesCount > 10"
}
}
}
}
在上述例子中,我們首先執行了一個 terms 聚合,按產品ID匯總銷售記錄。然后我們使用 bucket_selector post-filter 進一步篩選出銷售數量大于10的桶(每個桶對應一個產品)。
聚合排序
(1) count
在 Elasticsearch 中,聚合排序允許你基于某一聚合的結果來對桶進行排序。例如,你可能希望查看銷售量最高的10個產品,可以使用 terms 聚合以及其 size 和 order 參數來實現:
GET /sales/_search
{
"size": 0,
"aggs": {
"top_products": {
"terms": {
"field": "product_id",
"size": 10,
"order": { "_count": "desc" }
}
}
}
}
在這個例子中,top_products 是一個 terms 聚合,用于按 product_id 對銷售記錄進行分組。
"size": 10 的意思是只返回銷售量最高的前10個產品(即只返回前10個桶)。
"order": { "_count": "desc" } 表示按桶中文檔的數量(也就是銷售量)降序排序。_count 是一個內置的排序鍵,代表桶中文檔的數量。
返回的結果將包含銷售量最高的前10個產品的 ID 列表。
(2) term
_term 在 Elasticsearch 的聚合排序中用來指定按照詞條(即桶的鍵)來排序。
GET /sales/_search
{
"size": 0,
"aggs": {
"products": {
"terms": {
"field": "product_id",
"order": { "_term": "asc" }
}
}
}
}
在這個例子中,通過 "order": { "_term": "asc" } 指定了按照 product_id 的值升序排序這些桶。
返回的結果將包含按照 product_id 升序排列的產品 ID 列表,每個產品 ID 對應一個桶,并且每個桶內包含對應產品的銷售記錄。
需要注意的是,在新版本的 Elasticsearch 中(7.0 以后),_term 已經被 key 替代用于排序。
GET /sales/_search
{
"size": 0,
"aggs" : {
"products" : {
"terms" : {
"field" : "product_id",
"order" : { "_key" : "asc" }
}
}
}
}