ElasticSearch 聚合分析

碼農充電站發表於2021-02-24

公號:碼農充電站pro
主頁:https://codeshellme.github.io

ES 中的聚合分析(Aggregations)是對資料的統計分析功能,它的優點是實時性較高,相比於 Hadoop 速度更快。

1,聚合的分類

ES 中的聚合分析主要有以下 3 大類,每一類都提供了多種統計方法:

一般使用聚合分析時,通常將 size 設定為 0,表示不需要返回查詢結果,只需要返回聚合結果。

一個示例:

# 多個 Metric 聚合,找到最低最高和平均工資
POST index_name/_search
{
  "size": 0,              # size 為 0
  "aggs": {
    "max_salary": {       # 自定義聚合名稱
      "max": {            # 聚合型別
        "field": "salary" # 聚合欄位
      }
    },
    "min_salary": {       # 自定義聚合名稱
      "min": {            # 聚合型別
        "field": "salary" # 聚合欄位
      }
    },
    "avg_salary": {       # 自定義聚合名稱
      "avg": {            # 聚合型別
        "field": "salary" # 聚合欄位
      }
    }
  }
}

2,Metrics 聚合

Metrics 聚合可以分為單值分析和多值分析:

  • 單值分析:分析結果是單個值
    • max
    • min
    • avg
    • sum
    • cardinality:類似 distinct count
      • 注意 cardinality 對 keyword 型別資料和 text 型別資料的區別
      • keyword 型別不會進行分詞處理,而 text 型別會進行分詞處理
  • 多值分析:分析結果是多個值

2.1,示例

示例,一個員工表定義:

DELETE /employees
PUT /employees/
{
  "mappings" : {
      "properties" : {
        "age" : {
          "type" : "integer"
        },
        "gender" : {
          "type" : "keyword"
        },
        "job" : {
          "type" : "text",
          "fields" : {
            "keyword" : {  # 子欄位名稱
              "type" : "keyword", # 子欄位型別
              "ignore_above" : 50
            }
          }
        },
        "name" : {
          "type" : "keyword"
        },
        "salary" : {
          "type" : "integer"
        }
      }
    }
}

插入一些測試資料:

PUT /employees/_bulk
{ "index" : {  "_id" : "1" } }
{ "name" : "Emma","age":32,"job":"Product Manager","gender":"female","salary":35000 }
{ "index" : {  "_id" : "2" } }
{ "name" : "Underwood","age":41,"job":"Dev Manager","gender":"male","salary": 50000}
{ "index" : {  "_id" : "3" } }
{ "name" : "Tran","age":25,"job":"Web Designer","gender":"male","salary":18000 }
{ "index" : {  "_id" : "4" } }
{ "name" : "Rivera","age":26,"job":"Web Designer","gender":"female","salary": 22000}
{ "index" : {  "_id" : "5" } }
{ "name" : "Rose","age":25,"job":"QA","gender":"female","salary":18000 }
{ "index" : {  "_id" : "6" } }
{ "name" : "Lucy","age":31,"job":"QA","gender":"female","salary": 25000}
{ "index" : {  "_id" : "7" } }
{ "name" : "Byrd","age":27,"job":"QA","gender":"male","salary":20000 }
{ "index" : {  "_id" : "8" } }
{ "name" : "Foster","age":27,"job":"Java Programmer","gender":"male","salary": 20000}
{ "index" : {  "_id" : "9" } }
{ "name" : "Gregory","age":32,"job":"Java Programmer","gender":"male","salary":22000 }
{ "index" : {  "_id" : "10" } }
{ "name" : "Bryant","age":20,"job":"Java Programmer","gender":"male","salary": 9000}
{ "index" : {  "_id" : "11" } }
{ "name" : "Jenny","age":36,"job":"Java Programmer","gender":"female","salary":38000 }
{ "index" : {  "_id" : "12" } }
{ "name" : "Mcdonald","age":31,"job":"Java Programmer","gender":"male","salary": 32000}
{ "index" : {  "_id" : "13" } }
{ "name" : "Jonthna","age":30,"job":"Java Programmer","gender":"female","salary":30000 }
{ "index" : {  "_id" : "14" } }
{ "name" : "Marshall","age":32,"job":"Javascript Programmer","gender":"male","salary": 25000}
{ "index" : {  "_id" : "15" } }
{ "name" : "King","age":33,"job":"Java Programmer","gender":"male","salary":28000 }
{ "index" : {  "_id" : "16" } }
{ "name" : "Mccarthy","age":21,"job":"Javascript Programmer","gender":"male","salary": 16000}
{ "index" : {  "_id" : "17" } }
{ "name" : "Goodwin","age":25,"job":"Javascript Programmer","gender":"male","salary": 16000}
{ "index" : {  "_id" : "18" } }
{ "name" : "Catherine","age":29,"job":"Javascript Programmer","gender":"female","salary": 20000}
{ "index" : {  "_id" : "19" } }
{ "name" : "Boone","age":30,"job":"DBA","gender":"male","salary": 30000}
{ "index" : {  "_id" : "20" } }
{ "name" : "Kathy","age":29,"job":"DBA","gender":"female","salary": 20000}

min 聚合分析:

# Metric 聚合,找到最低的工資
POST employees/_search
{
  "size": 0,
  "aggs": {
    "min_salary": {
      "min": {    # 聚合型別,求最小值
        "field":"salary"
      }
    }
  }
}

# 返回結果
"hits": {
  "total": {
    "value": 20,      # 一共統計了多少條資料
    "relation": "eq"
  },
  "max_score": null,
  "hits": [...]       # 因為 size 為 0
},
"aggregations": {
  "min_salary": {     # 自定義的聚合名稱                       
    "value": 9000,
  }
}

stats 聚合分析:

# 輸出多值
POST employees/_search
{
  "size": 0,
  "aggs": {
    "stats_salary": {
      "stats": {      # stats 聚合
        "field":"salary"
      }
    }
  }
}

# 返回多值結果
"aggregations": {
  "stats_salary": {  # 自定義的聚合名稱                       
     "count": 20,
     "min": 9000,
     "max": 50000,
     "avg": 24700,
     "sum": 494000
  }
}

2.2,top_hits 示例

# 指定 size,不同崗位中,年紀最大的3個員工的資訊
POST employees/_search
{
    "size": 0,
	"aggs":{  
       "old_employee":{  # 聚合名稱
          "top_hits":{    # top_hits 分桶
            "size":3,
            "sort":[      # 根據 age 倒序排序,選前 3 個
              {"age":{"order":"desc"}}
            ]
          }
        }
    }
}

3,Bucket 聚合

Bucket 聚合按照一定的規則,將文件分配到不同的中,達到分類的目的。

Bucket 聚合支援巢狀,也就是在桶裡再次分桶。

Bucket 聚合演算法:

  • Terms:根據關鍵字(字串)分桶。text 型別的欄位需要開啟 fielddata 配置。
    • 注意 keyword 型別不會做分詞處理,text 型別會做分詞處理。
    • 另外 size 引數可以控制桶的數量
  • Range:按照範圍進行分桶,主要針對數字型別的資料
  • Date range
  • Histogram:直方圖分桶,指定一個間隔值,來進行分桶。
  • Date histogram

3.1,Terms 示例

示例:

# 對 keword 進行聚合
POST employees/_search
{
  "size": 0,      # size 為 0
  "aggs": {
    "jobs": {     # 自定義聚合名稱
      "terms": {  # terms 聚合
        "field":"job.keyword" # job 欄位的 keyword 子欄位
      }
    }
  }
}

# 返回值結構示例
"aggregations": {
  "genres": {
    "doc_count_error_upper_bound": 0,   
    "sum_other_doc_count": 0,           
    "buckets": [     # 很多桶,這是一個陣列                    
      {
        "key": "electronic",
        "doc_count": 6
      },
      {
        "key": "rock",
        "doc_count": 3
      },
      {
        "key": "jazz",
        "doc_count": 2
      }
    ]
  }
}

對 Text 欄位進行 terms 聚合查詢會出錯,示例:

# 對 Text 欄位進行 terms 聚合查詢
POST employees/_search
{
  "size": 0,
  "aggs": {
    "jobs": {
      "terms": {
        "field":"job"  # job 是 text 型別
      }
    }
  }
}

# 對 Text 欄位開啟 fielddata,以支援 terms aggregation
PUT employees/_mapping
{
  "properties" : {
    "job":{
       "type":     "text",
       "fielddata": true  # 開啟 fielddata
    }
  }
}

3.2,Terms 效能優化

當某個欄位的寫入和 Terms 聚合比較頻繁的時候,可用通過開啟 eager_global_ordinals 配置來對 Terms 操作進行優化。

示例:

PUT index_name
{
  "mappings": {
    "properties": {
      "foo": {  # 欄位名稱
        "type": "keyword",
        "eager_global_ordinals": true # 開啟
      }
    }
  }
}

3.3,巢狀聚合示例

Bucket 聚合支援新增子聚合來進一步分析,子聚合可以是一個 Metrics 或者 Bucket

示例 1:

# 指定 size,不同崗位中,年紀最大的3個員工的資訊
POST employees/_search
{
  "size": 0,
  "aggs": {
    "jobs": {
      "terms": {  # 先做了一個 terms 分桶
        "field":"job.keyword"
      },
      "aggs":{   # 巢狀一個聚合,稱為子聚合,
        "old_employee":{  # 聚合名稱
          "top_hits":{    # top_hits 分桶
            "size":3,
            "sort":[      # 根據 age 倒序排序,選前 3 個
              {"age":{"order":"desc"}}
            ]
          }
        }
      }
    }
  }
}

示例 2 :

POST employees/_search
{
  "size": 0,
  "aggs": {
    "Job_salary_stats": {
      "terms": {     # 先做了一個 terms 分桶
        "field": "job.keyword"
      },
      "aggs": {
        "salary": {
          "stats": { # 子聚合是一個 stats 
            "field": "salary"
          }
        }
      }
    }
  }
}

# 多次巢狀
POST employees/_search
{
  "size": 0,
  "aggs": {          # 第 1 層
    "Job_gender_stats": {
      "terms": {
        "field": "job.keyword"  # 先根據崗位分桶
      },
      "aggs": {     # 第 2 層
        "gender_stats": {
          "terms": {
            "field": "gender"   # 再根據性別分桶
          },
          "aggs": { # 第 3 層
            "salary_stats": {
              "stats": {        # 最後根據工資統計 stats
                "field": "salary"
              }
            }
          }
        }
      }
    }
  }
}

3.4,Range 示例

對員工的工資進行區間聚合:

# Salary Ranges 分桶,可以自己定義 key
POST employees/_search
{
  "size": 0,
  "aggs": {
    "salary_range": {     # 自定義聚合名稱
      "range": {          # range 聚合
        "field":"salary", # 聚合的欄位
        "ranges":[        # range 聚合規則/條件
          {
            "to":10000    # salary < 10000
          },
          {
            "from":10000, # 10000 < salary < 20000
            "to":20000
          },
          {               # 如果沒有定義 key,ES 會自動生成
            "key":"可以使用 key 自定義名稱", 
            "from":20000  # salary > 20000
          }
        ]
      }
    }
  }
}

3.5,Histogram 示例

示例,工資0到10萬,以 5000一個區間進行分桶:

# Salary Histogram
POST employees/_search
{
  "size": 0,
  "aggs": {
    "salary_histrogram": {   # 自定義聚合名稱
      "histogram": {         # histogram 聚合
        "field":"salary",    # 聚合的欄位
        "interval":5000,     # 區間值
        "extended_bounds":{  # 範圍
          "min":0,
          "max":100000
        }
      }
    }
  }
}

4,Pipeline 聚合

Pipeline 聚合用於對其它聚合的結果進行再聚合。

根據 Pipeline 聚合原聚合的位置區別,分為兩類:

  • Pipeline 聚合原聚合同級,稱為 Sibling 聚合
    • Max_bucketMin_bucketAvg_bucketSum_bucket
    • Stats_bucketExtended-Status_bucket
    • Percentiles_bucket
  • Pipeline 聚合內嵌在原聚合之內,稱為 Parent 聚合
    • Derivative:求導
    • Cumulative-sum:累計求和
    • Moving-function:滑動視窗

4.1,Sibling 聚合示例

示例:

# 平均工資最低的工作型別
POST employees/_search
{
  "size": 0,
  "aggs": {
    "jobs": {             # 自定義聚合名稱
      "terms": {
        "field": "job.keyword",  # 先對崗位型別進行分桶
        "size": 10
      },
      "aggs": {
        "avg_salary": {
          "avg": {
            "field": "salary"   # 再計算每種工資崗位的平價值
          }
        }
      }
    },
    "min_salary_by_job":{ # 自定義聚合名稱
      "min_bucket": {     # pipeline 聚合
        "buckets_path": "jobs>avg_salary"
      }                   # 含義是:對 jobs 中的 avg_salary 進行一個 min_bucket 聚合
    }
  }
}

4.2,Parent 聚合示例

示例:

# 示例 1
POST employees/_search
{
  "size": 0,
  "aggs": {
    "age": {   # 自定義聚合名稱
      "histogram": {
        "field": "age",
        "min_doc_count": 1,
        "interval": 1
      },
      "aggs": {
        "avg_salary": { # 自定義聚合名稱
          "avg": {
            "field": "salary"
          }
        },              # 自定義聚合名稱
        "derivative_avg_salary":{ # 注意 derivative 聚合的位置,與 avg_salary 同級
          "derivative": {         # 而不是與 age 同級
            "buckets_path": "avg_salary" # 注意這裡不再有箭頭 > 
          }
        }
      }
    }
  }
}

# 示例 2
POST employees/_search
{
  "size": 0,
  "aggs": {
    "age": {
      "histogram": {
        "field": "age",
        "min_doc_count": 1,
        "interval": 1
      },
      "aggs": {
        "avg_salary": {
          "avg": {
            "field": "salary"
          }
        },
        "cumulative_salary":{
          "cumulative_sum": { # 累計求和
            "buckets_path": "avg_salary"
          }
        }
      }
    }
  }
}

5,聚合的作用範圍

ES 聚合的預設作用範圍是 Query 的查詢結果,如果沒有寫 Query,那預設就是在索引的所有資料上做聚合。

比如:

POST employees/_search
{
  "size": 0,
  "query": {    # 在 query 的結果之上做聚合
    "range": {
      "age": {"gte": 20}
    }
  },
  "aggs": {
    "jobs": {
      "terms": {"field":"job.keyword"}
    }
  }
}

ES 支援通過以下方式來改變聚合的作用範圍:

  • Query:ES 聚合的預設作用範圍。
    • 一般設定 size 為 0
    • 如果沒有寫 Query,那預設就是在索引的所有資料上做聚合。
  • Filter:寫在某個聚合的內部,只控制某個聚合的作用範圍。
    • 一般設定 size 為 0
  • Post Filter:對聚合沒有影響,只是對聚合的結果進行再過濾。
    • 不再設定 size 為 0
    • 使用場景:獲取聚合資訊,並獲取符合條件的文件。
  • Global:會覆蓋掉 Query 的影響。

5.1,Filter 示例

示例:

POST employees/_search
{
  "size": 0,
  "aggs": {
    "older_person": {  # 自定義聚合名稱
      "filter":{       # 通過 filter 改變聚合的作用範圍
        "range":{
          "age":{"from":35}
        }
      }, # end older_person
      "aggs":{         # 在 filter 的結果之上做聚合
         "jobs":{      # 自定義聚合名稱
           "terms": {"field":"job.keyword"}
         }
       }
    }, # end older_person
    "all_jobs": {     # 又一個聚合,沒有 filter
      "terms": {"field":"job.keyword"}
    }
  }
}

5.2,Post Filter 示例

示例:

POST employees/_search
{
  "aggs": {
    "jobs": {       # 自定義聚合名稱
      "terms": {"field": "job.keyword"}
    }
  }, # end aggs
  "post_filter": {  # 一個 post_filter,對聚合的結果進行過濾
    "match": {
      "job.keyword": "Dev Manager"
    }
  }
}

5.3,Global 示例

POST employees/_search
{
  "size": 0,
  "query": {    # 一個 query
    "range": {
      "age": {"gte": 40}
    }
  },
  "aggs": {
    "jobs": {   # 一個聚合
      "terms": {"field":"job.keyword"}
    },
    "all":{            # 又一個聚合,名稱為 all
      "global":{},     # 這裡的 global 會覆蓋掉上面的 query,使得聚合 all 的作用範圍不受 query 的影響
      "aggs":{         # 子聚合
        "salary_avg":{ # 自定義聚合名稱
          "avg":{"field":"salary"}
        }
      }
    }
  }
}

6,聚合中的排序

6.1,基於 count 的排序

聚合中的排序使用 order 欄位,預設按照 _count_key 進行排序。

  • _count:表示按照文件數排序,如果不指定 _count,預設按照降序進行排序。
  • _key:表示關鍵字(字串值),如果文件數相同,再按照 key 進行排序。

示例 1:

# 使用 count 和 key
POST employees/_search
{
  "size": 0,
  "query": {
    "range": {
      "age": {"gte": 20}
    }
  },
  "aggs": {
    "jobs": {               # 自定義聚合名稱
      "terms": {            # terms 聚合
        "field":"job.keyword",
        "order":[           # order 排序
          {"_count":"asc"}, # 先安裝文件數排序
          {"_key":"desc"}   # 如果文件數相同,再按照 key 排序
        ]
      }
    }
  }
}

6.2,基於子聚合的排序

也可以基於子聚合排序。

示例 2:

# 先對工作種類進行分桶
# 再以工作種類的平均工資進行排序
POST employees/_search
{
  "size": 0,
  "aggs": {
    "jobs": {      # 自定義聚合名稱
      "terms": {
        "field":"job.keyword",
        "order":[  # 基於子聚合的排序
             {"avg_salary":"desc"}
           ]                
       }, # end terms
    "aggs": {         # 子聚合
      "avg_salary": { # 子聚合名稱
        "avg": {"field":"salary"}
       }
      }
    } # end jobs
  }
}

如果子聚合是多值輸出,也可以基於 子聚合名.屬性 來進行排序,如下:

POST employees/_search
{
  "size": 0,
  "aggs": {
    "jobs": {
      "terms": {
        "field":"job.keyword",
        "order":[  # 基於子聚合的屬性排序
            {"stats_salary.min":"desc"}
         ]
     }, # end terms
    "aggs": {
      "stats_salary": { # 子聚合是多值輸出
        "stats": {"field":"salary"}
        }
      }
    } # end jobs
  }
}

7,聚合分析的原理及精準度

下面介紹聚合分析的原理及精準度的問題。

7.1,分散式系統的三個概念

分散式系統中有三個概念:

  • 資料量
  • 精準度
  • 實時性

對於分散式系統(資料分佈在不同的分片上),這三個指標不能同時具備,同時只能滿足其中的 2 個條件

  • Hadoop 離線計算:可以同時滿足大資料量和精準度
  • 近似計算:可以同時滿足大資料量和實時性
  • 有限資料計算:可以同時滿足精準度和實時性

在這裡插入圖片描述

ES 屬於近似計算,具備了資料量實時性的特點,失去了精準度

7.2,聚合分析的原理

ES 是一個分散式系統,資料分佈在不同的分片上。

因此,ES 在進行聚合分析時,會先在每個主分片上做聚合,然後再將每個主分片上的聚合結果進行彙總,從而得到最終的聚合結果。

在這裡插入圖片描述

7.3,聚合分析的精準度

分散式聚合的原理,會天生帶來精準度的問題,但並不是所有的聚合分析都有精準度問題:

  • 比如 Min 聚合 就不會有精準度問題。
    • 因為求總的最小值,與先在所有主分片求最小值,再彙總每個主分片的最小值,它們最終的結果是一樣的。
  • 比如 Terms 聚合 就有精準度問題。

下面來看下 Terms 聚合存在的問題,下圖中的:

  • A(6) 表示 A 類的文件數有 6 個。
  • B(4) 表示 B 類的文件數有 4 個。
  • C(4) 表示 C 類的文件數有 4 個。
  • D(3) 表示 D 類的文件數有 3 個。

下圖是 Terms 聚合流程:

在這裡插入圖片描述

上圖中,在進行 Terms 聚合時(最終結果只要按照數量排序的前 3 個),需要分別在分片 P0P1上做聚合,然後再將它們的聚合結果進行彙總。

正確的聚合結果應該是 A(12),B(6),D(6),但是由於分片的原因,ES 計算出來的結果是 A(12),B(6),C(4)。這就是 Terms 聚合存在的精準度問題。

7.4,show_term_doc_count_error 引數

開啟 show_term_doc_count_error 配置可以使得 terms 聚合的返回結果中有一個 doc_count_error_upper_bound 值(最小為0),通過該值可以瞭解精準程度;該值越小,說明 Terms 的精準度越高

POST index_name/_search
{
  "size": 0,
  "aggs": {
    "weather": {  # 自定義聚合名稱
      "terms": {  # terms 聚合
        "field":"OriginWeather",
        "show_term_doc_count_error":true # 開啟
      }
    }
  }
}

7.5,如何提高 terms 精準度

提高 terms 聚合的精準度有兩種方式:

  • 將主分片數設定為 1。
    • 因為 terms 的不準確是由於分片導致的,如果將主分片數設定為 1,就不存在不準確的問題。
    • 這種方式在資料量不是很大的時候,可以是使用。
  • shard_size 的值儘量調大(意味著從分片上額外獲取更多的資料,從而提升準確度)。
    • shard_size 值變大後,會使得計算量變大,進而使得ES 的整體效能變低,精準度變高
    • 所以需要權衡 shard_size 值與精準度的平衡。
    • shard_size 值的預設值是 【size * 1.5 + 10】。

設定 shard_size 的語法:

POST my_flights/_search
{
  "size": 0,
  "aggs": {
    "weather": {
      "terms": {
        "field":"OriginWeather",
        "size":1,
        "shard_size":1,
        "show_term_doc_count_error":true
      }
    }
  }
}

(本節完。)


推薦閱讀:

ElasticSearch 查詢

ElasticSearch URI 查詢

ElasticSearch DSL 查詢

ElasticSearch 文件及操作

ElasticSearch 搜尋模板與建議


歡迎關注作者公眾號,獲取更多技術乾貨。

碼農充電站pro

相關文章