为了方便在线书店的管理员了解书店的运营情况,我们需要为在线书店开发一些统计功能。一般要完成统计的需求,我们都要用到聚合功能。ES 除了提供丰富的搜索功能外,也提供丰富的聚合计算 API,使用聚合 API 可以满足对数据进行统计分析的需求。
通俗来讲,聚合就是按照某些条件从数据集合中统计一些信息,例如管理员需要统计销量前十的书本、统计每个出版社书本的数量、统计有多少个出版社等。
ES 中聚合的类型主要有以下 3 种:
Metric Aggregations: 提供求 sum(求总和)、average(求平均数) 等数学运算,可以对字段进行统计分析。
Bucket Aggregations:对满足特定条件的文档进行分组,例如将 A 出版社的书本分为一组,将 B 出版社的书本分为一组,类似于 SQL 里的 Group By 功能。
Pipeline Aggregations:对其他聚合输出的结果进行再次聚合。
ES 的聚合可以进行多种组合来构建的统计查询,从而解决复杂的统计分析的需求。下面是聚合查询的语法:
# 聚合查询的语法
POST your_index/_search
{
"aggs": { //和query 同级别的关键词
"aggs_name1": { //自定义的聚合名字,会从聚合结果中返回
"aggs_type": { //聚合的定义:聚合类型 + 聚合body
aggs body
},
"aggs": { //子聚合
"aggs_name": {
"aggs_type": {
aggs body
}
}
}
},
"aggs_name2": { //可以进行多个同级别的聚合查询
......
}
},
"size": 0 //建议设置为0,这样不会返回 _source
}
如上示例,”aggs” 是与 “query” 同级的关键词,言外之意就是它们可以同时使用,当它们一起使用时,其作用就类似于 SQL 里的 where condition group by column。
一个聚合里可以同时开启多个聚合查询,每个聚合查询的名字不一样,需要自定义,例如例子中的 “aggs_name1” 和 “aggs_name2”
“aggs_type” 是聚合的定义,包括聚合类型和聚合的 body。同时,一个聚合里面还可以嵌套子聚合,在聚合里使用 “aggs” 关键字即可产生一个子聚合。
当我们不需要返回匹配文档的内容时,可以设置 “size” 为 0,一般我们会这样做,毕竟聚合时只对聚合结果感兴趣。
为了更好地理解各种聚合的例子,我们迭代了在线书店的模型,增加了出版社字段和书本销量的字段,并且增加了几条书本数据。访问链接-gitee、链接-github获取脚本并且进行书店模型的创建和书本数据导入。完成了模型创建和数据导入后,下面正式开始各种聚合 API 的学习!
一、Metric Aggregations
当我们想从一个组别的文档中获取某个指标的时候,可以使用 Metric Aggregations 中提供的 API。我们将一系列获取某个指标的聚合操作统称为 Metric Aggregations,Metric Aggregations 分为单值分析和多值分析两类:
单值分析:只输出一个分析结果的聚合操作,例如 min、max、sum、avg、cardinality(类似于 SQL 中的 distinct count)等。
多值分析:会输出多个分析结果的聚合操作,例如:stats、extended_stats、percentiles、percentile ranks、top hits 等。
在了解完 Metric Aggregations 后,现在书店有几个需求急需解决:
- 查看最高售价
- 同时查看最高售价、最低售价、平均售价
- 统计出版社的数量
ok,下面来看看如何利用这部分 API 来解决在线书店的需求。
1. 查看最高售价
要找出书本的最高售价,可以使用 max 聚合,其示例如下:
# 查看最高售价
POST books/_search
{
"aggs": {
"most-expensive": {
"max": { "field": "price" }
}
},
"size": 0
}
# 结果
{
......
"aggregations" : {
"most-expensive" : {
"value" : 20.9
}
}
}
如上示例,最终 “most-expensive” 中的 “vlaue” 为 20.9 即为所有书本中最贵的售价了。
2. 同时查看最高售价、最低售价、平均售价
在介绍聚合语法的时候提到过,一次聚合查询中可以发起多个同级别的聚合操作,所有我们可以同时查询最高售价、最低售价、平均售价,其示例如下:
# 一个请求里同时获取 最高售价、最低售价、平均售价
POST books/_search
{
"aggs": {
"most-expensive": {
"max": { "field": "price" }
},
"cheapest": {
"min": { "field": "price" }
},
"avg-price": {
"avg": { "field": "price" }
}
},
"size": 0
}
# 结果
{
......
"aggregations" : {
"cheapest" : { "value" : 9.9 },
"avg-price" : { "value" : 15.471428571428572 },
"most-expensive" : { "value" : 20.9 }
}
}
如上示例可以看到,我们在一个 aggs 中进行了 “most-expensive”、”cheapest”、”avg-price” 3 种聚合操作,最终求出了价格最贵的为 20.9,最便宜为 9.9,平均价格为 15.471428571428572。
当然除了上述方法外,还可以使用 stat API,其示例如下:
# 使用 stat 查询 最高售价、最低售价、平均售价
POST books/_search
{
"aggs": {
"stat_price": {
"stats": {
"field": "price"
}
}
},
"size": 0
}
# 结果
{
......
"aggregations" : {
"stat_price" : {
"count" : 7,
"min" : 9.9,
"max" : 20.9,
"avg" : 15.471428571428572,
"sum" : 108.3
}
}
}
如上示例可以看到,stats 聚合除了返回最高售价、最低售价、平均售价外还有售价之和(sum)、文档个数的信息。
3. 统计出版社的数量
可以使用 cardinality 聚合获取出版社的数量,其作用类似于 SQL 中的 distinct count。其示例如下:
# 统计出版社的数量
POST books/_search
{
"aggs": {
"cardinality_publisher": {
"cardinality": {
"field": "publisher"
}
}
},
"size": 0
}
# 结果
{
......
"aggregations" : {
"cardinality_publisher" : {
"value" : 3
}
}
}
如上示例,使用 cardinality 聚合后可以得出出版社的数量为 3 个。
Metric Aggregations 提供的 API 远不止这些,由于篇幅的限制无法全部都进行介绍,更多的使用案例你可以参考官方文档。
二、Bucket Aggregations
Bucket 可以理解为一个桶,或者一个分组,当遍历文档库的时候会把符合条件的文档放到一个分组里面去,分组就相当于 SQL 中的 Group By。如下图,我们将书本以出版社字段进行分组。
对数据进行分组后,我们还可以组合 Metric Aggregations 的聚合操作来完成复杂的统计需求,例如找出每个出版社最贵的售价是多少。
ES 提供的 Bucket Aggregations 中常用的有以下几个:
- Terms:根据某个字段进行分组,例如根据出版社进行分组。
- Range、Data Range:根据用户指定的范围参数作为分组的依据来进行聚合操作。
- Histogram、Date Histogram:可以指定间隔区间来进行聚合操作。
同样,我们可以利用上述的 API 来完成以下的需求:
- 统计每个出版社的书本数量。
- 统计每个价格区间的书本数量。
- 统计每个出版社书本的销售量。
ok,下面我们来解决上述的几个需求。
1. 统计每个出版社的书本数量
我们只需要以出版社作为分组条件,然后计算每个分组中文档的个数,得出的结果就是每个出版社拥有的书本数量了。可以使用 Terms Aggregations 来完成这个需求:
# Terms Aggregations 统计每个出版社拥有的书本数量
POST books/_search
{
"aggs": {
"publisher_book_count": {
"terms": {
"field": "publisher",
"size": 3
}
}
},
"size": 0
}
# 结果
{
......
"aggregations" : {
"publisher_book_count" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{ "key" : "linux publisher", "doc_count" : 3 },
{ "key" : "autobiography publisher", "doc_count" : 2 },
{ "key" : "science publisher", "doc_count" : 2 }
]
}
}
}
如上示例,当我们使用 “terms” 关键字的时候就使用了 Terms Aggregations 查询了,其中 “size”: 3 说明只返回聚合后前三组的结果。
从返回结果中可以看到,”key” 是每个分组字段的值,这里是各个出版社的名称,”doc_count” 是这个分组中文档的数量,它的值就是出版社拥有的书本数量。
返回结果中,还有两个字段:”doc_count_error_upper_bound” 和 “sum_other_doc_count”,它们是对本次聚合的评估结果:
- doc_count_error_upper_bound:没有在本次聚合返回的分桶中,包含文档数的可能最大值的和。如果是 0,说明聚合结果是准确的。
- sum_other_doc_count:除了返回结果中的 terms 外,其他没有返回的 terms 的文档数量之和。
对于为何会出现 Terms 聚合统计结果不准确的问题,我们将在后续的章节中进行讨论,现在你只需要知道会有这种情况出现就可以了。
2. 统计每个价格区间的书本数量
要统计每个价格区间的书本数量,可以使用 Range Aggregations 来处理。Range Aggregations 可以根据用户指定的范围参数作为分组的依据来进行聚合操作,所以可以指定 0 ~ 10,10 ~ 20,20 ~ 30 这样的 3 个区间来作为分组,然后统计数据。
Range Aggregations 的使用示例如下:
# 价格区间统计
POST books/_search
{
"aggs": {
"price_range": {
"range": {
"field": "price",
"keyed": true,
"ranges": [
{ "key": "cheap", "from": 0.0, "to": 10.0 },
{ "key": "average", "from": 10.0, "to": 20.0 },
{ "key": "expensive", "from": 20.0, "to": 30.0 }
]
}
}
},
"size": 0
}
# 结果
{
......
"aggregations" : {
"price_range" : {
"buckets" : {
"cheap" : { "from" : 0.0, "to" : 10.0, "doc_count" : 1 },
"average" : { "from" : 10.0, "to" : 20.0, "doc_count" : 5 },
"expensive" : { "from" : 20.0, "to" : 30.0, "doc_count" : 1 }
}
}
}
}
如上示例,使用 “range” 关键字可以进行 Range Aggregation 操作,使用 “keyed”: true 使得我们可以对每个区间进行命名,如例子中我们命名了 cheap、average、expensive 三个区间,使用 “key” 关键字即可指定区间的名字。关键字 from 和 to 指定了区间的开始值和结束值,其取值范围为:[from, to)。
不知道机智的你是不是发现了,当区间值非常多的时候,写这个查询语句是不是会写到手痛么?所以贴心的 ES 为我们提供了 Histogram Aggregation API 来解决这个问题。Histogram Aggregation 也可以对区间进行分组,但这个区间是固定间隔的,例如我们上例的间隔是 10,那用 Histogram Aggregation 可以这样实现:
# 使用 Histogram Aggregation
POST books/_search
{
"aggs": {
"price_histogram": {
"histogram": {
"field": "price",
"interval": 10
}
}
},
"size": 0
}
# 结果
{
"aggregations" : {
"price_histogram" : {
"buckets" : [
{ "key" : 0.0, "doc_count" : 1 },
{ "key" : 10.0, "doc_count" : 5 },
{ "key" : 20.0, "doc_count" : 1 }
]
}
}
}
Histogram Aggregation 返回的结果比较简单,其并不能像 Range Aggregation 那样手动指定每个区间的名字,所以 Histogram Aggregation 更多时候用在显示图表的需求上。
3. 统计每个出版社书本的销售量
要统计每个出版社书本的销售量的话,需要先按每个出版社进行分组,然后对每个分组所有文档的销售量求和,所以使用 Terms Aggregation 和 Sum Aggregation 可以解决这个需求,其示例如下:
# 使用子聚合 组合 Terms Aggregation 和 Sum Aggregation
POST books/_search
{
"aggs": {
"publisher_sales_total": {
"terms": { "field": "publisher" },
"aggs": {
"sales_total": {
"sum": { "field": "sales" }
}
}
}
},
"size": 0
}
# 结果
{
......
"aggregations" : {
"publisher_sales_total" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "linux publisher",
"doc_count" : 3,
"sales_total" : { "value" : 400.0 }
},
{
"key" : "autobiography publisher",
"doc_count" : 2,
"sales_total" : { "value" : 5140.0 }
},
{
"key" : "science publisher",
"doc_count" : 2,
"sales_total" : { "value" : 19800.0 }
}
]
}
}
}
如上示例,我们使用了子聚合的方式组合了 Terms Aggregation 和 Sum Aggregation。从结果可以看出,查询以出版社名称为分组,然后求得每个分组中书本的销售量的总和,并放在了 “sales_total” 中。
同样,Bucket Aggregations 提供的 API 也是非常丰富的,但由于篇幅的限制无法全部都进行介绍,更多的使用案例你可以参考官方文档。
三、Pipeline Aggregations
Pipeline Aggregations 可以对其他聚合输出的结果进行再次聚合,下面通过一个例子来说明其使用方式。
现在有这样一个需求,在销售量最好的 2 个出版社里,找出平均价格最低的出版社。其示例如下:
POST books/_search
{
"aggs": {
"publisher": {
"terms": {
"field": "publisher",
"size": 2,
"order": { "sales_total": "desc" }
},
"aggs": {
"sales_total": {
"sum": { "field": "sales" }
},
"avg_price": {
"avg": { "field": "price" }
}
}
},
"min_avg_price": {
"min_bucket": {
"buckets_path": "publisher>avg_price"
}
}
},
"size": 0
}
# 结果
{
"aggregations" : {
"publisher" : { ...... },
"min_avg_price" : {
"value" : 14.399999999999999,
"keys" : [ "science publisher" ]
}
}
}
如上示例,在 “publisher” 中我们做了以下几件事:
- 按出版社进行分桶。
- 执行子聚合,计算每个出版社的销售总额和书本平均价格。
- 排序结果按 “sales_total”(销售额)倒序排序,并且获取排序后的前两个结果。
最终在 “publisher” 中我们得出了销售额最多的两个出版社和它们书本的平均售价、销售额。
最后使用 Pipeline Aggregations 找出平均售价最低的出版社即可。上面的示例是一个简单的例子,”min_avg_price” 是我们指定的名字,使用 “min_bucket” 求出之前结果的最小值,并且通过 “buckets_path” 关键字来指定路径,例子中我们的路径为 “publisher” 下的 “avg_price”。
通过上面的例子可以看到 Pipeline aggregations 提供的功能了。Pipeline 分析的结果会输出到原查询的结果中,根据位置的不同可以分为两类:
Sibling: 结果和原结果同级, 如上面的列子就是 Sibling。Sibling 可以有 Max Bucket、Min Bucket、Avg Bucket、Sum Bucket 等
Parent: 结果会内嵌到现有的聚合分析结果中。提供如 Derivative (求导)、Moving Function (滑动窗口)等功能。
由于篇幅的限制,更多关于 Pipeline Aggregations 的使用案例可以参考官方文档。
四、总结
今天为你介绍了 ES 提供的聚合 API,并且用多个实例进行了讲解。
要使用聚合功能可以用 “aggs” 开启一次聚合查询,一次聚合查询中可以进行多个聚合查询,甚至可以嵌套对个子聚合查询。
ES 提供的聚合 API 主要分为 3 类:
- 提供指标统计信息的 Metric Aggregations,提供了像 sum、max、min 等常用的指标聚合函数。
- 提供分组功能的 Bucket Aggregations,分组的功能类似于 mysql 中的 group by。我们可以对分组后的每个组数据进行统计,例如求每个分组的 max price 、min price 等。
- 提供对其他聚合结果进行再次聚合的 Pipeline Aggregations。
一般来说,业务中的统计需求是复杂的,我们常常需要组合 Bucket Aggregations 和 Metric Aggregations 来完成需求,这个时候可以使用子聚合、Pipeline Aggregations 来组合多个聚合。
好了今天的内容到此为止,今天介绍的聚合 API 是比较常用的功能了,ES 提供的聚合 API 非常丰富,更多的使用例子可以参考官方文档。