笔记源自 : Elastic 中国社区官方博客
文章目录
Aggregation 简介
官方博客解释 :
聚合框架有助于基于搜索查询提供聚合数据。它基于称为聚合的简单构建块,可以组合以构建复杂的数据摘要。
聚合可以被视为在一组文档上构建分析信息的工作单元。执行的上下文定义了该文档集的内容(例如,在执行的查询的上下文中执行顶级聚合/搜索请求的过滤器)。
由于每个存储桶( bucket )有效地定义了一个文档集(属于该 bucket 的所有文档),因此可以在 bucket 级别上关联聚合,并且这些聚合将在该存储桶的上下文中执行。这就是聚合的真正力量所在:聚合可以嵌套!
注意一:
- bucketing聚合可以具有子聚合(bucketing 或 metric)。 将为其父聚合生成的桶计算子聚合。
嵌套聚合的级别/深度没有硬性限制(可以在“父”聚合下嵌套聚合,“父”聚合本身是另一个更高级聚合的子聚合)。
注意二:
- 聚合可以操作于 double 类型的上限的数据。 因此,当在绝对值大于2 ^ 53的 long 上运行时,结果可能是近似的。
Aggregation 相当于数据库中分组与统计 Group by/Count(*)
Aggregation 请求是搜索 API 的一部分,它可以带有一个 query 的结构或者不带。
语法
"aggregations" : {
"<aggregation_name>" : {
"<aggregation_type>" : {
<aggregation_body>
}
[,"meta" : { [<meta_data_body>] } ]?
[,"aggregations" : { [<sub_aggregation>]+ } ]?
}
[,"<aggregation_name_2>" : { ... } ]*
}
通常,我们使用 'aggs’来代替上面的 “aggregations”。
aggregation_name : 查询出来显示的名称
aggregation_name : 聚合参数,按照哪种方式聚合
aggregation_body :聚合方法体,设置索引中聚合的属性字段等
sub_aggregation : 嵌套聚合查询
测试数据 :
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
}
}
}
}
“aggregatable” : true 表明这个 country 字段既是可以搜索的是可以聚合的
Terms 按关键字
通过 term 聚合来查询某一个关键字出现的频率
在如下的 term 聚合中,我们想寻找在所有的文档出现 ”Happy birthday” 里按照城市进行分类的一个聚合。“size”: 10指的是前 10 名的城市。
GET twitter/_search
{
"query": {
"match": {
"message": "happy birthday"
}
},
"size": 0,
"aggs": {
"city": {
"terms": {
"field": "city",
"size": 10
}
}
}
}
返回 :
"aggregations" : {
"city" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "北京",
"doc_count" : 2
},
{
"key" : "上海",
"doc_count" : 1
}
]
}
}
在正常的情况下,聚合是按照 doc_count 来进行排序的,也就是说哪一个 key 的 doc_count 越多,那么它就排在第一位,以后依次排序。如果你想按照 key 进行排序的话,你可以尝试如下的方法 添加 order:
GET twitter/_search
{
"size": 0,
"aggs": {
"top_cities": {
"terms": {
"field": "city",
"order": {"_key": "asc"}
}
}
}
}
这里排序安装Key排序 ,我们看到和之前不一样的排序。doc_count 为 1 的 “上海” 反而排到前面去了,虽然这个和我们之前的 asc (上升)排序是有点不太一致的地方 (按照拼音 shanghai 应该比 beijing 要大)
返回
"aggregations" : {
"top_cities" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "上海",
"doc_count" : 1
},
{
"key" : "北京",
"doc_count" : 5
}
]
}
}
也可以使用 _count 来进行升序的排列:
GET twitter/_search
{
"size": 0,
"aggs": {
"top_cities": {
"terms": {
"field": "city",
"order": {"avg_age": "desc"}
},
"aggs": {
"avg_age": {
"avg": {"field": "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
}
}
]
}
}
在这里通过内嵌的聚合结果 avg_age作为排序条件,将avg_age结果传给父级。
是否可以多级传递?
Range 分组聚合
把用户进行年龄分段,查出来在不同的年龄段的用户:
GET twitter/_search
{
"size": 0,
"aggs": {
"age_group": {
"range": {
"field": "age",
"ranges": [
{"from": 20,"to": 22},
{"from": 22,"to": 25},
{"from": 25,"to": 30}
]
}
}
}
}
返回 :
把 size 设置为0。这是因为针对聚合,我们并不关心返回的结果,也就是在默认情况下返回的10个文档。这样做的好处是更快的响应以及较小的负载。当然,额外的好处是可以缓存聚合的结果。在第二次进行同样的聚合时,速度会比以前快。
Script 运算聚合
可以使用 script 来生成一个在索引里没有的术语来进行统计。比如,我们可以通过如下的 script 来生成一个对文档人出生年份的统计:
POST twitter/_search
{
"size": 0,
"aggs": {
"birth_year": {
"terms": {
"script": {"source": "2019 - doc['age'].value"},
"size": 10
}
}
}
}
source": "2019 - doc[‘age’].value 根据年龄来生成出生的年月(2019年减去每个年龄的值)来进行统计:
"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 在我们原有的字段中根本就不存在。
我们可以通过 script 的方法来对我们的 aggregtion 结果进行重新计算:
可以是 sum,max,min等运算
GET twitter/_search
{
"size": 0,
"aggs": {
"average_age_1.5": {
"avg": {
"field": "age",
"script": {
"source": "_value * params.correction",
"params": {
"correction": 10
}
}
}
}
}
}
返回 :
"aggregations" : {
"average_age_1.5" : {
"value" : 251.66666666666666
}
}
没有script返回值 :
"aggregations" : {
"average_age_1.5" : {
"value" : 25.166666666666668
}
}
结果是之前的 25.166666666666668 的10倍。
也可以直接使用 script 的方法来进行聚合。在这种情况下,我们可以不指定特定的 field。我们可能把很多项进行综合处理,并把这个结果来进行聚合:
GET twitter/_search
{
"size": 0,
"aggs": {
"average_2_times_age": {
"avg": {
"script": {
"source": "doc['age'].value * params.times",
"params": {
"times": 2.0
}
}
}
}
}
}
使用 doc[‘age’].value指定age属性
Histogram 间隔聚合
基于多桶值源的汇总,可以应用于从文档中提取的数值或数值范围值。 它根据值动态构建固定大小(也称为间隔)的存储桶。根据指定的间隔分桶。
例 :
GET twitter/_search
{
"size": 0,
"aggs": {
"age_distribution": {
"histogram": {
"field": "age",
"interval": 2
}
}
}
}
根据年龄梅间隔 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}
]
}
}
数据 :
为左闭右开区间
Metric 嵌套聚合
可嵌套聚合查询,进行 sub aggregation,在聚合中再进一步计算,可与query查询过滤后再聚合,并按照 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"
}
}
}
}
}
}
order : 根据计算平均年龄 avg_age ,并根据 avg_age 倒序排序
"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}
}
]
}
}
Date_histogram 日期范围
这种聚合类似于正常的直方图,但只能与日期或日期范围值一起使用。 由于日期在 Elasticsearch 中内部以 long 类型值表示,因此也可以但不准确地对日期使用正常的直方图。
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
}
]
}
Cardinality 统计数量
使用 cardinality 聚合来统计到底有多少个城市:
GET twitter/_search
{
"size": 0,
"aggs": {
"number_of_cities": {
"cardinality": {
"field": "city"
}
}
}
}
返回 :
"aggregations" : {
"number_of_cities" : {
"value" : 2
}
}
我们有两个城市:北京 及 上海。它们在文档中虽然出现多次,但是从唯一性上,只有两个城市。
Filters 分桶聚合
在上面,我们使用 ranges 把数据分成不同的 bucket。通常这样的方法只适合字段为数字的字段。我们按照同样的思路,可以使用 filter 来对数据进行分类。在这种方法中,我们甚至可以针对非数字字段来进行建立不同的 bucket。
我们使用 filters 来分别针对 “北京” 和 “上海” 两地的文档进行统计:
GET twitter/_search
{
"size": 0,
"aggs": {
"by_cities": {
"filters": {
"filters": {
"beijing" : {"match": {"city": "北京"}},
"shanghai": {"match": {"city": "上海"}}
}
}
}
}
}
返回 :
"aggregations" : {
"by_cities" : {
"buckets" : {
"beijing" : {
"doc_count" : 5
},
"shanghai" : {
"doc_count" : 1
}
}
}
}
如果不关心名称,可简写 :
GET twitter/_search
{
"size": 0,
"aggs": {
"by_cities": {
"filters": {
"filters": [
{"match": {"city": "北京"}},
{"match": {"city": "上海"}}
]
}
}
}
}
返回 :
"aggregations" : {
"by_cities" : {
"buckets" : [
{
"doc_count" : 5
},
{
"doc_count" : 1
}
]
}
}
在当前文档集上下文中定义与指定过滤器匹配的所有文档的单个存储桶。 通常,这将用于将当前聚合上下文缩小到一组特定的文档。这类聚合被称之为 Filter aggregation。你也可以理解为是上面的 Filters aggregation 的特殊情况,在它里面只含有一个 filter 的 Filters aggregation。(在Filters 中内嵌一个聚合)
我们以一个例子来讲,针对 “北京” 的文档,并求一个平均年龄 :
GET twitter/_search
{
"size": 0,
"aggs": {
"beijing": {
"filter": {"match": {"city": "北京"}},
"aggs": {
"avg_age": {
"avg": {"field": "age"}
}
}
}
}
}
返回 :
"aggregations" : {
"beijing" : {
"doc_count" : 5,
"avg_age" : {
"value" : 24.6
}
}
}
Avg 计算平均值
用户的平均年龄是多少?我们可以用下面的聚合:
GET twitter/_search
{
"size": 0,
"aggs": {
"average_age": {
"avg": {
"field": "age"
}
}
}
}
我们的返回的结果是:
"aggregations" : {
"average_age" : {
"value" : 25.166666666666668
}
}
可以对只在北京的用户文档进行统计(查询过滤后在聚合):
POST twitter/_search
{
"size": 0,
"query": {
"match": {
"city": "北京"
}
},
"aggs": {
"average_age_beijing": {
"avg": {
"field": "age"
}
}
}
}
我们可以在 bucket 聚合之下,做 sub-aggregation(在一次聚合查询中,再嵌套聚合查询):
新增 avg_age,针对每个桶 20-22,22-25,25-30,分别计算它们的平均年龄。 :
GET twitter/_search
{
"size": 0,
"aggs": {
"age_group": {
"range": {
"field": "age",
"ranges": [
{"from": 20,"to": 22},
{"from": 22,"to": 25},
{"from": 25,"to": 30}
]
},
"aggs": {
"avg_age": {
"avg": {"field": "age"}
}
}
}
}
}
返回 :
Max/Mix 最大/最小值
可以在 sub-aggregation 之下做更多的聚合,比如:
GET twitter/_search
{
"size": 0,
"aggs": {
"age_group": {
"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"}
}
}
}
}
}
Date_range 日期聚合
使用 date_range 来统计在某个时间段里的文档数:
GET twitter/_search
{
"size": 0,
"aggs": {
"birth_range": {
"date_range": {
"field": "DOB",
"ranges": [
{
"from": "1989-01-01",
"to": "1990-01-01"
},
{
"from": "1991-01-01",
"to": "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
}
]
}
}
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 这个字段。我们使用total_missing_age如下的聚合来查询有多少文档缺失 age 这个字段:
GET twitter/_search
{
"size": 0,
"aggs": {
"total_missing_age": {
"missing": {
"field": "age"
}
}
}
}
返回 :
"aggregations" : {
"total_missing_age" : {
"doc_count" : 1
}
}
Global 全局聚合
聚合通常在查询搜索结果上执行。 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 的聚合(仅仅针对global所修饰的当前聚合),这个平均值将会使用所有的6个文档而不是限于在这个查询的5个北京的文档。返回的结果是:
"aggregations" : {
"average_age_beijing" : {
"value" : 24.6
},
"average_age_all" : {
"doc_count" : 6,
"age_global_avg" : {
"value" : 25.166666666666668
}
}
}
Stats 多项统计
对整个年龄进行一个统计,比如:
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
如果你想了解更多的细节,你可以使用 extended_stats:
GET twitter/_search
{
"size": 0,
"aggs": {
"age_stats": {
"extended_stats": {
"field": "age"
}
}
}
}
"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
}
}
}
Percentile 百分位数
官方解释,写的很拗口。
总结说,就是给出一个百分比,计算指定属性值满足该百分比的百分位数
例 :
GET twitter/_search
{
"size": 0,
"aggs": {
"age_quartiles": {
"percentiles": {
"field": "age",
"percents": [
25,
50,
75,
100
]
}
}
}
}
在上面,我们使用了以叫做 age 的字段。它是一个数值的字段。我们通过 percentiles 可以得到 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%的年龄和我们之前计算的平均年龄是不一样的。
Percentile Ranks
在上面,我们能够查出来,50%的人的年龄是多少,但是在实际的应用中,我们有时也很希望知道满足我们的 SLA (Service Level Aggreement) 百分比是多少,这样你可以找到自己服务的差距,比如达到一个标准的百分比是多少。针对我们的例子,我们可以使用 Percentile Ranks Aggregation。我们使用如下的查询:
GET twitter/_search
{
"size": 0,
"aggs": {
"age_40_percentage": {
"percentile_ranks": {
"field": "age",
"values": [
40
]
}
}
}
}
返回 :
"aggregations" : {
"age_40_percentage" : {
"values" : {
"40.0" : 62.5
}
}
}
结果表明,有62.5%的人的年龄是在40岁以下的。在我们的实际应用中,比如我们输入一个指标,我们可以看出来有多少比例是在那个指标以内的。这样我们可以看出来我们的 SLA 是否满足条件。
String_stats 字符串统计
我们可以使用 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。