为了方便在线书店的管理员了解书店的运营情况,我们需要为在线书店开发一些统计功能。一般要完成统计的需求,我们都要用到聚合功能。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": { //聚合的定义:聚合类型 + 聚合bodyaggs 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 AggregationPOST 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 AggregationPOST 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 非常丰富,更多的使用例子可以参考官方文档。
