准備數據
為了更好地展示,我們首先來把我們之前的 twitter 的數據做一點小的修改。我們添加一個新的字段 DOB (date of birth),也就是生日的意思。同時,我們也對 province,city 及 country 字段的類型做了調整,並把它們作為 keyword。我們來做如下的操作:
DELETE twitter
PUT twitter
{
"mappings": {
"properties": {
"DOB": {
"type": "date"
},
"address": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"age": {
"type": "long"
},
"city": {
"type": "keyword"
},
"country": {
"type": "keyword"
},
"location": {
"type": "geo_point"
},
"message": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"province": {
"type": "keyword"
},
"uid": {
"type": "long"
},
"user": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
然后,我們再次使用 bulk API 來把我們的數據導入到 Elasticsearch 中:
POST _bulk
{"index":{"_index":"twitter","_id":1}}
{"user":"張三","message":"今兒天氣不錯啊,出去轉轉去","uid":2,"age":20,"city":"北京","province":"北京","country":"中國","address":"中國北京市海淀區","location":{"lat":"39.970718","lon":"116.325747"}, "DOB": "1999-04-01"}
{"index":{"_index":"twitter","_id":2}}
{"user":"老劉","message":"出發,下一站雲南!","uid":3,"age":22,"city":"北京","province":"北京","country":"中國","address":"中國北京市東城區台基廠三條3號","location":{"lat":"39.904313","lon":"116.412754"}, "DOB": "1997-04-01"}
{"index":{"_index":"twitter","_id":3}}
{"user":"李四","message":"happy birthday!","uid":4,"age":25,"city":"北京","province":"北京","country":"中國","address":"中國北京市東城區","location":{"lat":"39.893801","lon":"116.408986"}, "DOB": "1994-04-01"}
{"index":{"_index":"twitter","_id":4}}
{"user":"老賈","message":"123,gogogo","uid":5,"age":30,"city":"北京","province":"北京","country":"中國","address":"中國北京市朝陽區建國門","location":{"lat":"39.718256","lon":"116.367910"}, "DOB": "1989-04-01"}
{"index":{"_index":"twitter","_id":5}}
{"user":"老王","message":"Happy BirthDay My Friend!","uid":6,"age":26,"city":"北京","province":"北京","country":"中國","address":"中國北京市朝陽區國貿","location":{"lat":"39.918256","lon":"116.467910"}, "DOB": "1993-04-01"}
{"index":{"_index":"twitter","_id":6}}
{"user":"老吳","message":"好友來了都今天我生日,好友來了,什么 birthday happy 就成!","uid":7,"age":28,"city":"上海","province":"上海","country":"中國","address":"中國上海市閔行區","location":{"lat":"31.175927","lon":"121.383328"}, "DOB": "1991-04-01"}
我們已經成功地導入數據。我們要特別注意的是,並不是所有的字段都可以做聚合的。一般來說,具有 keyword 或者數值類型的字段是可以做聚合的。我們可以通過 _field_caps 接口來進行查詢:
GET twitter/_field_caps?fields=country
上面的命令給出的結果為:
{
"indices" : [
"twitter"
],
"fields" : {
"country" : {
"keyword" : {
"type" : "keyword",
"searchable" : true,
"aggregatable" : true
}
}
}
}
它表明這個 country 字段既是可以搜索的也是可以聚合的。如果大家不喜歡這種命令,我們可以可以直接到 twitter 索引的 index pattern 中進行查看:
我們需要為 twitter 索引創建一個 index pattern。從上面我們可以看出來 address 字段是可以搜索的,因為它是一個 text 的類型字段,但是它不可以進行聚合。同時,我們可以看出來 address.keyword 這個字段是可以同時搜索的和聚合的。
聚合操作
簡單地說,聚合的語法是這樣的:
"aggregations" : {
"<aggregation_name>" : {
"<aggregation_type>" : {
<aggregation_body>
}
[,"meta" : { [<meta_data_body>] } ]?
[,"aggregations" : { [<sub_aggregation>]+ } ]?
}
[,"<aggregation_name_2>" : { ... } ]*
}
通常,我們也可以使用 aggs 來代替上面的 “aggregations”。
下面,我們來針對我們的數據來進行一些簡單的操作,這樣可以使得大家更加明白一些。
range聚合
我們可以把用戶進行年齡分段,查出來在不同的年齡段的用戶:
GET twitter/_search
{
"size": 0,
"aggs": {
"age": {
"range": {
"field": "age",
"ranges": [
{
"from": 20,
"to": 22
},
{
"from": 22,
"to": 25
},
{
"from": 25,
"to": 30
}
]
}
}
}
}
在這里,我們使用 range 類型的聚合。在上面我們定義了不同的年齡段。通過上面的查詢,我們可以得到不同年齡段的 bucket。顯示的結果是:
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 6,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"age" : {
"buckets" : [
{
"key" : "20.0-22.0",
"from" : 20.0,
"to" : 22.0,
"doc_count" : 1
},
{
"key" : "22.0-25.0",
"from" : 22.0,
"to" : 25.0,
"doc_count" : 1
},
{
"key" : "25.0-30.0",
"from" : 25.0,
"to" : 30.0,
"doc_count" : 3
}
]
}
}
}
在上面,我們也注意到,我們把 size 設置為0。這是因為針對聚合,我們並不關心返回的結果,也就是在默認情況下返回的10個文檔。這樣做的好處是更快的響應以及較小的負載。當然,額外的好處是可以緩存聚合的結果。在第二次進行同樣的聚合時,速度會比以前快。我們可以閱讀另外一篇文章 “cache 在 Elasticsearch 中的應用” 。假如我們設置為1的話,我們可以看到如下的輸出:
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 6,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "twitter",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"user" : "張三",
"message" : "今兒天氣不錯啊,出去轉轉去",
"uid" : 2,
"age" : 20,
"city" : "北京",
"province" : "北京",
"country" : "中國",
"address" : "中國北京市海淀區",
"location" : {
"lat" : "39.970718",
"lon" : "116.325747"
},
"DOB" : "1999-04-01"
}
}
]
},
"aggregations" : {
"age" : {
"buckets" : [
{
"key" : "20.0-22.0",
"from" : 20.0,
"to" : 22.0,
"doc_count" : 1
},
{
"key" : "22.0-25.0",
"from" : 22.0,
"to" : 25.0,
"doc_count" : 1
},
{
"key" : "25.0-30.0",
"from" : 25.0,
"to" : 30.0,
"doc_count" : 3
}
]
}
}
}
從這里,我們可以看出來,在我們的輸出中,也看到了其中的一個文檔的輸出。
我們可以在 bucket 聚合之下,做 sub-aggregation:
GET twitter/_search
{
"size": 0,
"aggs": {
"age": {
"range": {
"field": "age",
"ranges": [
{
"from": 20,
"to": 22
},
{
"from": 22,
"to": 25
},
{
"from": 25,
"to": 30
}
]
},
"aggs": {
"avg_age": {
"avg": {
"field": "age"
}
}
}
}
}
}
上面的意思是我們針對每個桶 20-22,22-25,25-30,分別計算它們的平均年齡。上面顯示的結果是:
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 6,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"age" : {
"buckets" : [
{
"key" : "20.0-22.0",
"from" : 20.0,
"to" : 22.0,
"doc_count" : 1,
"avg_age" : {
"value" : 20.0
}
},
{
"key" : "22.0-25.0",
"from" : 22.0,
"to" : 25.0,
"doc_count" : 1,
"avg_age" : {
"value" : 22.0
}
},
{
"key" : "25.0-30.0",
"from" : 25.0,
"to" : 30.0,
"doc_count" : 3,
"avg_age" : {
"value" : 26.333333333333332
}
}
]
}
}
}
從上面我們可以看出來每個桶的平均年齡是多少。
你甚至可以在 sub-aggregation 之下做更多的聚合,比如:
GET twitter/_search
{
"size": 0,
"aggs": {
"age": {
"range": {
"field": "age",
"ranges": [
{
"from": 20,
"to": 22
},
{
"from": 22,
"to": 25
},
{
"from": 25,
"to": 30
}
]
},
"aggs": {
"avg_age": {
"avg": {
"field": "age"
}
},
"min_age": {
"min": {
"field": "age"
}
},
"max_age": {
"max": {
"field": "age"
}
}
}
}
}
}
在上面,我們除了得到平均的年齡,也可以得到最大及最小的年齡。上面查詢的結果為:
{
"took" : 5,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 6,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"age" : {
"buckets" : [
{
"key" : "20.0-22.0",
"from" : 20.0,
"to" : 22.0,
"doc_count" : 1,
"max_age" : {
"value" : 20.0
},
"avg_age" : {
"value" : 20.0
},
"min_age" : {
"value" : 20.0
}
},
{
"key" : "22.0-25.0",
"from" : 22.0,
"to" : 25.0,
"doc_count" : 1,
"max_age" : {
"value" : 22.0
},
"avg_age" : {
"value" : 22.0
},
"min_age" : {
"value" : 22.0
}
},
{
"key" : "25.0-30.0",
"from" : 25.0,
"to" : 30.0,
"doc_count" : 3,
"max_age" : {
"value" : 28.0
},
"avg_age" : {
"value" : 26.333333333333332
},
"min_age" : {
"value" : 25.0
}
}
]
}
}
}
更多關於 bucket 聚合,請參閱我的文章 “Elasticsearch:透徹理解 Elasticsearch 中的 Bucket aggregation”。
Filters 聚合
在上面,我們使用 ranges 把數據分成不同的 bucket。通常這樣的方法只適合字段為數字的字段。我們按照同樣的思路,可以使用 filter 來對數據進行分類。在這種方法中,我們甚至可以針對非數字字段來進行建立不同的 bucket。這類聚合我們稱之為 Filter aggregagation。定義一個多存儲桶聚合,其中每個存儲桶都與一個過濾器相關聯。 每個存儲桶將收集與其關聯的過濾器匹配的所有文檔。我們可以使用如下的例子:
GET twitter/_search
{
"size": 0,
"aggs": {
"by_cities": {
"filters": {
"filters": {
"beijing": {
"match": {
"city": "北京"
}
},
"shanghai": {
"match": {
"city": "上海"
}
}
}
}
}
}
}
在上面的例子中,我們使用 filters 來分別針對 “北京” 和 “上海” 兩地的文檔進行統計:
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 6,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"by_cities" : {
"buckets" : {
"beijing" : {
"doc_count" : 5
},
"shanghai" : {
"doc_count" : 1
}
}
}
}
}
上面顯示來自 “北京” 的有 5 個文檔,而來自 “上海” 的只有一個文檔。在上面我們為每個 filter 都取了一個名字,當然,我們如果不關心名稱的話,我們甚至可以有如下的簡潔格式:
GET twitter/_search
{
"size": 0,
"aggs": {
"by_cities": {
"filters": {
"filters": [
{
"match": {
"city": "北京"
}
},
{
"match": {
"city": "上海"
}
}
]
}
}
}
}
上面的返回結果為:
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 6,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"by_cities" : {
"buckets" : [
{
"doc_count" : 5
},
{
"doc_count" : 1
}
]
}
}
}
Filter 聚合
在當前文檔集上下文中定義與指定過濾器匹配的所有文檔的單個存儲桶。 通常,這將用於將當前聚合上下文縮小到一組特定的文檔。這類聚合被稱之為 Filter aggregation。你也可以理解為是上面的 Filters aggregation 的特殊情況,在它里面只含有一個 filter 的 Filters aggregation。我們以一個例子來講:
GET twitter/_search
{
"size": 0,
"aggs": {
"beijing": {
"filter": {
"match": {
"city": "北京"
}
},
"aggs": {
"avg_age": {
"avg": {
"field": "age"
}
}
}
}
}
}
在上面,我們針對 “北京” 的文檔,並求一個平均年齡。上面查詢的結果為:
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 6,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"beijing" : {
"doc_count" : 5,
"avg_age" : {
"value" : 24.6
}
}
}
}
date_range 聚合
我們可以使用 date_range 來統計在某個時間段里的文檔數:
POST twitter/_search
{
"size": 0,
"aggs": {
"birth_range": {
"date_range": {
"field": "DOB",
"format": "yyyy-MM-dd",
"ranges": [
{
"from": "1989-01-01",
"to": "1990-01-01"
},
{
"from": "1991-01-01",
"to": "1992-01-01"
}
]
}
}
}
}
在上面我們查詢出生年月(DOB)從1989-01-01到1990-01-01及從1991-01-01到1992-01-01的文檔。顯示的結果是:
"aggregations" : {
"birth_range" : {
"buckets" : [
{
"key" : "1989-01-01-1990-01-01",
"from" : 5.99616E11,
"from_as_string" : "1989-01-01",
"to" : 6.31152E11,
"to_as_string" : "1990-01-01",
"doc_count" : 1
},
{
"key" : "1991-01-01-1992-01-01",
"from" : 6.62688E11,
"from_as_string" : "1991-01-01",
"to" : 6.94224E11,
"to_as_string" : "1992-01-01",
"doc_count" : 1
}
]
}
terms聚合
我們也可以通過 term 聚合來查詢某一個關鍵字出現的頻率。在如下的 term 聚合中,我們想尋找在所有的文檔出現 ”Happy birthday” 里按照城市進行分類的一個聚合。
GET twitter/_search
{
"query": {
"match": {
"message": "happy birthday"
}
},
"size": 0,
"aggs": {
"city": {
"terms": {
"field": "city",
"size": 10
}
}
}
}
注意這里的 10 指的是前 10 名的城市。聚合的結果是:
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"city" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "北京",
"doc_count" : 2
},
{
"key" : "上海",
"doc_count" : 1
}
]
}
}
}
在上面,我們可以看出來,在所有的含有 "Happy birthday" 的文檔中,有兩個是來自北京的,有一個是來自上海。
在正常的情況下,聚合是按照 doc_count 來進行排序的,也就是說哪一個 key 的 doc_count 越多,那么它就排在第一位,以后依次排序。如果你想按照 key 進行排序的話,你可以嘗試如下的方法:
GET twitter/_search
{
"size": 0,
"aggs": {
"top_cities": {
"terms": {
"field": "city",
"order": {
"_key": "asc"
}
}
}
}
}
上面返回的結果是:
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 6,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"top_cities" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "上海",
"doc_count" : 1
},
{
"key" : "北京",
"doc_count" : 5
}
]
}
}
}
在這里,我們看到和之前不一樣的排序。doc_count 為 1 的 “上海” 反而排到前面去了,雖然這個和我們之前的 asc (上升)排序是有點不太一致的地方 (按照拼音 shanghai 應該比 beijing 要大)。這個和語言的處理有關,但是我們確實看到我們是可以控制這個排序的。你也可以使用 _count 來進行升序的排列:
GET twitter/_search
{
"size": 0,
"aggs": {
"top_cities": {
"terms": {
"field": "city",
"order": {
"_count": "asc"
}
}
}
}
}
你也可以通過在 term 聚合中指定順序來使用 nested aggreation 的結果進行排序,而不是按計數對結果進行排序:
GET twitter/_search
{
"size": 0,
"aggs": {
"top_cities": {
"terms": {
"field": "city",
"order": {
"avg_age": "desc"
}
},
"aggs": {
"avg_age": {
"avg": {
"field": "age"
}
}
}
}
}
}
在上面排序是按照 nested aggregation 的結果 avg_age 來按照降序來排列的。顯示的結果為:
"aggregations" : {
"top_cities" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "上海",
"doc_count" : 1,
"avg_age" : {
"value" : 28.0
}
},
{
"key" : "北京",
"doc_count" : 5,
"avg_age" : {
"value" : 24.6
}
}
]
}
}
我們也可以使用 script 來生成一個在索引里沒有的術語來進行統計。比如,我們可以通過如下的 script 來生成一個對文檔人出生年份的統計:
POST twitter/_search
{
"size": 0,
"aggs": {
"birth_year": {
"terms": {
"script": {
"source": "2019 - doc['age'].value"
},
"size": 10
}
}
}
}
在上面,我們通過腳本:
"script": {
"source": "2019 - doc['age'].value"
}
根據年齡來生成出生的年月來進行統計:
"aggregations" : {
"birth_year" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "1989",
"doc_count" : 1
},
{
"key" : "1991",
"doc_count" : 1
},
{
"key" : "1993",
"doc_count" : 1
},
{
"key" : "1994",
"doc_count" : 1
},
{
"key" : "1997",
"doc_count" : 1
},
{
"key" : "1999",
"doc_count" : 1
}
]
}
}
在上面我們可以看到 key 為1991,1993,1994等。這些 key 在我們原有的字段中根本就不存在。
Histogram Aggregation
基於多桶值源的匯總,可以應用於從文檔中提取的數值或數值范圍值。 它根據值動態構建固定大小(也稱為間隔)的存儲桶。
GET twitter/_search
{
"size": 0,
"aggs": {
"age_distribution": {
"histogram": {
"field": "age",
"interval": 2
}
}
}
}
顯示結果:
"aggregations" : {
"age_distribution" : {
"buckets" : [
{
"key" : 20.0,
"doc_count" : 1
},
{
"key" : 22.0,
"doc_count" : 1
},
{
"key" : 24.0,
"doc_count" : 1
},
{
"key" : 26.0,
"doc_count" : 1
},
{
"key" : 28.0,
"doc_count" : 1
},
{
"key" : 30.0,
"doc_count" : 1
}
]
}
}
上面顯示從20-22年齡段,有一個文檔。從22-24也有一個文檔。
同樣地,我們也可以進行 sub aggregation,並按照 sub aggregation 的值進行排序:
GET twitter/_search
{
"size": 0,
"aggs": {
"age_distribution": {
"histogram": {
"field": "age",
"interval": 2,
"order": {
"avg_age": "desc"
}
},
"aggs": {
"avg_age": {
"avg": {
"field": "age"
}
}
}
}
}
}
上面返回的結果是:
{
"took" : 2,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 6,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"age_distribution" : {
"buckets" : [
{
"key" : 30.0,
"doc_count" : 1,
"avg_age" : {
"value" : 30.0
}
},
{
"key" : 28.0,
"doc_count" : 1,
"avg_age" : {
"value" : 28.0
}
},
{
"key" : 26.0,
"doc_count" : 1,
"avg_age" : {
"value" : 26.0
}
},
{
"key" : 24.0,
"doc_count" : 1,
"avg_age" : {
"value" : 25.0
}
},
{
"key" : 22.0,
"doc_count" : 1,
"avg_age" : {
"value" : 22.0
}
},
{
"key" : 20.0,
"doc_count" : 1,
"avg_age" : {
"value" : 20.0
}
}
]
}
}
}
顯然這個是按照 avg_age 的值進行降序排列的。
date_histogram
這種聚合類似於正常的直方圖,但只能與日期或日期范圍值一起使用。 由於日期在 Elasticsearch 中內部以長值表示,因此也可以但不准確地對日期使用正常的直方圖。
GET twitter/_search
{
"size": 0,
"aggs": {
"age_distribution": {
"date_histogram": {
"field": "DOB",
"interval": "year"
}
}
}
}
在上面我們使用 DOB 來作為 date_histogram 的字段來進行聚合統計。我們按照每隔一年這樣的時間間隔來進行。顯示結果:
"aggregations" : {
"age_distribution" : {
"buckets" : [
{
"key_as_string" : "1989-01-01T00:00:00.000Z",
"key" : 599616000000,
"doc_count" : 1
},
{
"key_as_string" : "1990-01-01T00:00:00.000Z",
"key" : 631152000000,
"doc_count" : 0
},
{
"key_as_string" : "1991-01-01T00:00:00.000Z",
"key" : 662688000000,
"doc_count" : 1
},
{
"key_as_string" : "1992-01-01T00:00:00.000Z",
"key" : 694224000000,
"doc_count" : 0
},
{
"key_as_string" : "1993-01-01T00:00:00.000Z",
"key" : 725846400000,
"doc_count" : 1
},
{
"key_as_string" : "1994-01-01T00:00:00.000Z",
"key" : 757382400000,
"doc_count" : 1
},
{
"key_as_string" : "1995-01-01T00:00:00.000Z",
"key" : 788918400000,
"doc_count" : 0
},
{
"key_as_string" : "1996-01-01T00:00:00.000Z",
"key" : 820454400000,
"doc_count" : 0
},
{
"key_as_string" : "1997-01-01T00:00:00.000Z",
"key" : 852076800000,
"doc_count" : 1
},
{
"key_as_string" : "1998-01-01T00:00:00.000Z",
"key" : 883612800000,
"doc_count" : 0
},
{
"key_as_string" : "1999-01-01T00:00:00.000Z",
"key" : 915148800000,
"doc_count" : 1
}
]
}
上面的結果顯示 DOB 從 1989-01-01 到 1990-01-01 有一個文檔。從 1990-01-01 到 1991-01-01 區間沒有一個文檔。
cardinality 聚合
我們也可以使用 cardinality 聚合來統計到底有多少個城市:
GET twitter/_search
{
"size": 0,
"aggs": {
"number_of_cities": {
"cardinality": {
"field": "city"
}
}
}
}
運行上面的查詢,我們可以看到結果是:
{
"took" : 6,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 6,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"number_of_cities" : {
"value" : 2
}
}
}
它顯示我們有兩個城市:北京及上海。它們在文檔中雖然出現多次,但是從唯一性上,只有兩個城市。
Metric 聚合
我們可以使用 Metrics 來統計我們的數值數據,比如我們想知道所有用戶的平均年齡是多少?我們可以用下面的聚合:
GET twitter/_search
{
"size": 0,
"aggs": {
"average_age": {
"avg": {
"field": "age"
}
}
}
}
我們的返回的結果是:
"aggregations" : {
"average_age" : {
"value" : 25.166666666666668
}
}
所有人的平均年齡是 25.166666666666668歲。
我們也可以對只在北京的用戶文檔進行統計:
POST twitter/_search
{
"size": 0,
"query": {
"match": {
"city": "北京"
}
},
"aggs": {
"average_age_beijing": {
"avg": {
"field": "age"
}
}
}
}
上面我們先查詢到所有在北京的用戶,然后再對這些文檔進行求年齡的平均值。返回的結果:
"aggregations" : {
"average_age_beijing" : {
"value" : 24.6
}
}
聚合通常在查詢搜索結果上執行。 Elasticsearch 提供了一個特殊的 global 聚合,該全局全局對所有文檔執行,而不受查詢的影響。
POST twitter/_search
{
"size": 0,
"query": {
"match": {
"city": "北京"
}
},
"aggs": {
"average_age_beijing": {
"avg": {
"field": "age"
}
},
"average_age_all": {
"global": {},
"aggs": {
"age_global_avg": {
"avg": {
"field": "age"
}
}
}
}
}
}
在上面我們在 average_age_all 里添加了一個 gobal 的聚合,這個平均值將會使用所有的6個文檔而不是限於在這個查詢的5個北京的文檔。返回的結果是:
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 5,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"average_age_beijing" : {
"value" : 24.6
},
"average_age_all" : {
"doc_count" : 6,
"age_global_avg" : {
"value" : 25.166666666666668
}
}
}
}
我們也可以對整個年齡進行一個統計,比如:
GET twitter/_search
{
"size": 0,
"aggs": {
"age_stats": {
"stats": {
"field": "age"
}
}
}
}
統計的結果如下:
"aggregations" : {
"age_stats" : {
"count" : 6,
"min" : 20.0,
"max" : 30.0,
"avg" : 25.166666666666668,
"sum" : 151.0
}
}
在這里,我們可以看到到底有多少條數據,並且最大,最小的,平均值及加起來的合都在這里一起顯示。
如果你想了解更多的細節,你可以使用 extended_stats:
GET twitter/_search
{
"size": 0,
"aggs": {
"age_stats": {
"extended_stats": {
"field": "age"
}
}
}
}
上面返回的結果為:
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 6,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"age_stats" : {
"count" : 6,
"min" : 20.0,
"max" : 30.0,
"avg" : 25.166666666666668,
"sum" : 151.0,
"sum_of_squares" : 3869.0,
"variance" : 11.472222222222248,
"std_deviation" : 3.3870669054835996,
"std_deviation_bounds" : {
"upper" : 31.940800477633868,
"lower" : 18.392532855699468
}
}
}
}
對於一些不知道上面的 std_derivation 和 varianace 的開發者來說,我們舉如下的一個例子:
假如你有一個系列的數據: 32,111,138,28,59,77,97,那么它的平均值為 (32+111+138+28+59+77+97) / 7 = 77.4。針對每個值,找出和平均值之間的差異:
32 - 77.4 = -45.4
111 - 77.4 = 33.6
138 - 77.4 = 60.6
28 - 77.4 = -49.4
59 - 77.4 = -18.4
77 - 77.4 = - 0.4
97 - 77.4 = 19.6
針對上面的每個值,他們的平方值為:
(-45.4)^2 = 2061.16
(33.6)^2 = 1128.96
(60.6)^2 = 3672.36
(-49.4)^2 = 2440.36
(-18.4)^2 = 338.56
(- 0.4)^2 = 0.16
(19.6)^2 = 384.16
variance 的值其實就是上面的差值平方和的平均值:
variance = (2061.16+1128.96+3672.36+2440.36+338.56+0.16+384.16) / 7 = 1432.2
那么 std_derivation 的值其實就是上面值的平方根值:std_derivation = sqrt(variance) = sqrt(1432.2) = 37.84。
如果你想對多個指標進行統計並顯示它們之間的關系,你可以使用 matrix_stats:
GET twitter/_search
{
"size": 0,
"aggs": {
"matrix_stats": {
"matrix_stats": {
"fields": ["age", "uid"]
}
}
}
}
上面,我們把兩個整型字段 uid 及 age 放入進行統計。上面的查詢顯示的結果為:
{
"took" : 7,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 6,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"matrix_stats" : {
"doc_count" : 6,
"fields" : [
{
"name" : "uid",
"count" : 6,
"mean" : 4.5,
"variance" : 3.5,
"skewness" : 0.0,
"kurtosis" : 1.7314285714285715,
"covariance" : {
"uid" : 3.5,
"age" : 5.7
},
"correlation" : {
"uid" : 1.0,
"age" : 0.8211574455173661
}
},
{
"name" : "age",
"count" : 6,
"mean" : 25.166666666666668,
"variance" : 13.76666666666667,
"skewness" : -0.143450283024544,
"kurtosis" : 1.8030533098042432,
"covariance" : {
"uid" : 5.7,
"age" : 13.76666666666667
},
"correlation" : {
"uid" : 0.8211574455173661,
"age" : 1.0
}
}
]
}
}
}
我們也可以只得到這個年齡的最大值:
GET twitter/_search
{
"size": 0,
"aggs": {
"age_max": {
"max": {
"field": "age"
}
}
}
}
顯示的結果:
"aggregations" : {
"age_max" : {
"value" : 30.0
}
}
我們也可以在同一個請求里聚合多個指標,比如在如下的請求中,我們可以同時得到最大及最小的值:
GET twitter/_search
{
"size": 0,
"aggs": {
"Min": {
"min": {
"field": "age"
}
},
"Max": {
"max": {
"field": "age"
}
}
}
}
聚合通常適用於從聚合文檔集中提取的值。 可以使用聚合體內的字段鍵從特定字段提取這些值,也可以使用腳本提取這些值。我們可以通過 script 的方法來對我們的 aggregtion 結果進行重新計算:
GET twitter/_search
{
"size": 0,
"aggs": {
"average_age_1.5": {
"avg": {
"field": "age",
"script": {
"source": "_value * params.correction",
"params": {
"correction": 1.5
}
}
}
}
}
}
上面的這個聚合可以幫我們計算平均值再乘以 1.5 倍的結果。運行一下的結果如下:
"aggregations" : {
"average_age_1.5" : {
"value" : 37.75
}
}
顯然我們的結果是之前的 25.166666666666668 的1.5倍。
我們也可以直接使用 script 的方法來進行聚合。在這種情況下,我們可以不指定特定的field。我們可能把很多項進行綜合處理,並把這個結果來進行聚合:
GET twitter/_search
{
"size": 0,
"aggs": {
"average_2_times_age": {
"avg": {
"script": {
"source": "doc['age'].value * params.times",
"params": {
"times": 2.0
}
}
}
}
}
}
在這里我們完全沒有使用 field 這個項。我們直接使用 script 來形成我們的聚合:
"aggregations" : {
"average_2_times_age" : {
"value" : 50.333333333333336
}
}
Percentile aggregation
百分位數(percentile)表示觀察值出現一定百分比的點。 例如,第 95 個百分位數是大於觀察值的 95% 的值。該聚合針對從聚合文檔中提取的數值計算一個或多個百分位數。 這些值可以從文檔中的特定數字字段中提取,也可以由提供的腳本生成。
百分位通常用於查找離群值。 在正態分布中,第 0.13 和第 99.87 個百分位數代表與平均值的三個標准差。 任何超出三個標准偏差的數據通常被視為異常。這在統計的角度是非常有用的。
假如我們考慮如下的一個系列的數據:
77, 78, 85, 86, 86, 86, 87, 87, 88, 94, 99, 103, 111
在上面的 13 個數據中,它的中間位數據為 87,這是因為這個系列的數據是奇數個。我們再來考慮下面偶數個系列的數據:
77, 78, 85, 86, 86, 86, 87, 87, 94, 98, 99, 103
那么顯然上面數據的中間為: (86 + 87)/2 = 86.5。
我們現在來通過一個簡單的例子來展示 Percentile aggregation 的用法:
GET twitter/_search
{
"size": 0,
"aggs": {
"age_quartiles": {
"percentiles": {
"field": "age",
"percents": [
25,
50,
75,
100
]
}
}
}
}
在上面,我們使用了以叫做 age 的字段。它是一個數值的字段。我們通過 percentile aggregation 可以得到 25%,50% 及75% 的人在什么范圍。顯示結果是:
"aggregations" : {
"age_quartiles" : {
"values" : {
"25.0" : 22.0,
"50.0" : 25.5,
"75.0" : 28.0,
"100.0" : 30.0
}
}
}
我們可以看到25%的人平均年齡是低於22.0歲,而50%的人的年齡是低於25.5歲,而所有的人的年齡都是低於30歲的。這里的50%的年齡和我們之前計算的平均年齡是不一樣的。
GET twitter/_search
{
"size": 0,
"aggs": {
"avarage_age": {
"avg": {
"field": "age"
}
}
}
}
這個平均年齡是:
"aggregations" : {
"avarage_age" : {
"value" : 25.166666666666668
}
}
在上面,我們能夠查出來,50%的人的年齡是多少,但是在實際的應用中,我們有時也很希望知道滿足我們的 SLA (Service Level Aggreement) 百分比是多少,這樣你可以找到自己服務的差距,比如達到一個標准的百分比是多少。針對我們的例子,我們可以使用 Percentile Ranks Aggregation。我們使用如下的查詢:
GET twitter/_search
{
"size": 0,
"aggs": {
"age_40_percentage": {
"percentile_ranks": {
"field": "age",
"values": [
40
]
}
}
}
}
上面返回的結果是:
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 6,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"age_40_percentage" : {
"values" : {
"40.0" : 62.5
}
}
}
}
上面的結果表明,有62.5%的人的年齡是在40歲以下的。在我們的實際應用中,比如我們輸入一個指標,我們可以看出來有多少比例是在那個指標以內的。這樣我們可以看出來我們的 SLA 是否滿足條件。
我們可以使用 string stats 聚合來對 string 類型的數據進行統計。
GET twitter/_search
{
"size": 0,
"aggs": {
"message_stats": {
"string_stats": {
"field": "message.keyword"
}
}
}
}
上面顯示的結果為:
"aggregations" : {
"message_stats" : {
"count" : 6,
"min_length" : 9,
"max_length" : 37,
"avg_length" : 18.166666666666668,
"entropy" : 5.406152357759698
}
}
它表明 message 字段的最短長度為 9,最長長度為 37,而它的平均長度為 18.17。
更為復雜的聚合
我們可以結合上面的 bucket 聚合及 metric 聚合形成更為復雜的搜索:
GET twitter/_search
{
"size": 0,
"aggs": {
"cities": {
"terms": {
"field": "city",
"order": {
"average_age": "desc"
},
"size": 5
},
"aggs": {
"average_age": {
"avg": {
"field": "age"
}
}
}
}
}
}
在上面,我們首先通過 terms 來生成每個城市的桶聚合,讓后在每個桶里計算所有文檔的平均年齡。在正常的情況下,這個排序是按照每個城市里文檔的多少由多到少來排序的。在我們上面的搜索中,我們特意添加 average_age 來進行降序排序。這樣返回的結果如下:
"aggregations" : {
"cities" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "上海",
"doc_count" : 1,
"average_age" : {
"value" : 28.0
}
},
{
"key" : "北京",
"doc_count" : 5,
"average_age" : {
"value" : 24.6
}
}
]
}
上面顯示,有兩個城市:上海及北京。在上海城市中有1個文檔,而在北京城市里有5個文檔。同時,我們也計算出來每個城市的平均年齡。由於我們使用了 average_age 來進行降排序,在我們的結果中,我們可以看到 “上海” 城市排在前面,這是因為上海城市的平均年齡比北京的平均年齡高。
Missing 聚合
我們可以通過這個聚合來統計出來缺失某個字段的文檔個數。我們先添加如下的一個文檔:
PUT twitter/_doc/7
{
"user": "張三",
"message": "今兒天氣不錯啊,出去轉轉去",
"uid": 2,
"city": "北京",
"province": "北京",
"country": "中國",
"address": "中國北京市海淀區",
"location": {
"lat": "39.970718",
"lon": "116.325747"
},
"DOB": "1999-04-01"
}
在上面的文檔中,我們故意漏掉 age 這個字段。我們使用如下的聚合來查詢有多少文檔缺失 age 這個字段:
GET twitter/_search
{
"size": 0,
"aggs": {
"total_missing_age": {
"missing": {
"field": "age"
}
}
}
}
上面聚合顯示的結果為:
{
"took" : 603,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 7,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"total_missing_age" : {
"doc_count" : 1
}
}
}
上面顯示一個文檔沒有 age 這個字段。
如果你想對 aggregation 有更多的了解的話,那么可以閱讀我的另外文章
Elasticsearch:aggregation介紹
Elasticsearch:pipeline aggregation 介紹
Elasticsearch:透徹理解Elasticsearch中的Bucket aggregation
Analyzer簡介
我們知道 Elasticsearch 可以實現秒級的搜索速度,其中很重要的一個原因就當一個文檔被存儲的時候,同時它也對文檔的數據進行了索引(indexing)。這樣在以后的搜索中,就可以變得很快。簡單地說,當一個文檔進入到 Elasticsearch 時,它會經歷如下的步驟:
中間的那部分就叫做 Analyzer。我們可以看出來,它分為三個部分:Char Filters, Tokenizer及 Token Filter。它們的作用分別如下:
Char Filter: 字符過濾器的工作是執行清除任務,例如剝離HTML標記。
Tokenizer: 下一步是將文本拆分為稱為標記的術語。 這是由 tokenizer 完成的。 可以基於任何規則(例如空格)來完成拆分。 有關 tokennizer 的更多詳細信息,請訪問以下 URL:https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenizers.html。
Token filter: 一旦創建了 token,它們就會被傳遞給 token filter,這些過濾器會對 token 進行規范化。 Token filter 可以更改 token,刪除術語或向 token 添加術語。
Elasticsearch 已經提供了比較豐富的 analyzer。我們可以自己創建自己的 token analyzer,甚至可以利用已經有的 char filter,tokenizer 及 token filter 來重新組合成一個新的 analyzer,並可以對文檔中的每一個字段分別定義自己的 analyzer。如果大家對 analyzer 比較感興趣的話,請參閱我們的網址https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-analyzers.html。
你也可以閱讀我的另外一篇關於 analyzer 的文章 “Elasticsearch: analyzer”。
在默認的情況下,standard analyzer 是 Elasticsearch 的默認分析器:
沒有 Char Filter
使用 standard tokonizer
把字符串變為小寫,同時有選擇地刪除一些 stop words 等。默認的情況下 stop words 為 _none_,也即不過濾任何 stop words。
下面我們簡單地展示一下我們的 analyzer 是如何實現的。
GET twitter/_analyze
{
"text": [
"Happy Birthday"
],
"analyzer": "standard"
}
原文鏈接:Elastic 中國社區官方博客