为了方便在线书店的管理员了解书店的运营情况,我们需要为在线书店开发一些统计功能。一般要完成统计的需求,我们都要用到聚合功能。ES 除了提供丰富的搜索功能外,也提供丰富的聚合计算 API,使用聚合 API 可以满足对数据进行统计分析的需求。

通俗来讲,聚合就是按照某些条件从数据集合中统计一些信息,例如管理员需要统计销量前十的书本、统计每个出版社书本的数量、统计有多少个出版社等。

ES 中聚合的类型主要有以下 3 种:

  • Metric Aggregations: 提供求 sum(求总和)、average(求平均数) 等数学运算,可以对字段进行统计分析。

  • Bucket Aggregations:对满足特定条件的文档进行分组,例如将 A 出版社的书本分为一组,将 B 出版社的书本分为一组,类似于 SQL 里的 Group By 功能。

  • Pipeline Aggregations:对其他聚合输出的结果进行再次聚合。

ES 的聚合可以进行多种组合来构建的统计查询,从而解决复杂的统计分析的需求。下面是聚合查询的语法:

  1. # 聚合查询的语法
  2. POST your_index/_search
  3. {
  4. "aggs": { //和query 同级别的关键词
  5. "aggs_name1": { //自定义的聚合名字,会从聚合结果中返回
  6. "aggs_type": { //聚合的定义:聚合类型 + 聚合body
  7. aggs body
  8. },
  9. "aggs": { //子聚合
  10. "aggs_name": {
  11. "aggs_type": {
  12. aggs body
  13. }
  14. }
  15. }
  16. },
  17. "aggs_name2": { //可以进行多个同级别的聚合查询
  18. ......
  19. }
  20. },
  21. "size": 0 //建议设置为0,这样不会返回 _source
  22. }

如上示例,”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 后,现在书店有几个需求急需解决:

  1. 查看最高售价
  2. 同时查看最高售价、最低售价、平均售价
  3. 统计出版社的数量

ok,下面来看看如何利用这部分 API 来解决在线书店的需求。

1. 查看最高售价

要找出书本的最高售价,可以使用 max 聚合,其示例如下:

  1. # 查看最高售价
  2. POST books/_search
  3. {
  4. "aggs": {
  5. "most-expensive": {
  6. "max": { "field": "price" }
  7. }
  8. },
  9. "size": 0
  10. }
  11. # 结果
  12. {
  13. ......
  14. "aggregations" : {
  15. "most-expensive" : {
  16. "value" : 20.9
  17. }
  18. }
  19. }

如上示例,最终 “most-expensive” 中的 “vlaue” 为 20.9 即为所有书本中最贵的售价了。

2. 同时查看最高售价、最低售价、平均售价

在介绍聚合语法的时候提到过,一次聚合查询中可以发起多个同级别的聚合操作,所有我们可以同时查询最高售价、最低售价、平均售价,其示例如下:

  1. # 一个请求里同时获取 最高售价、最低售价、平均售价
  2. POST books/_search
  3. {
  4. "aggs": {
  5. "most-expensive": {
  6. "max": { "field": "price" }
  7. },
  8. "cheapest": {
  9. "min": { "field": "price" }
  10. },
  11. "avg-price": {
  12. "avg": { "field": "price" }
  13. }
  14. },
  15. "size": 0
  16. }
  17. # 结果
  18. {
  19. ......
  20. "aggregations" : {
  21. "cheapest" : { "value" : 9.9 },
  22. "avg-price" : { "value" : 15.471428571428572 },
  23. "most-expensive" : { "value" : 20.9 }
  24. }
  25. }

如上示例可以看到,我们在一个 aggs 中进行了 “most-expensive”、”cheapest”、”avg-price” 3 种聚合操作,最终求出了价格最贵的为 20.9,最便宜为 9.9,平均价格为 15.471428571428572。

当然除了上述方法外,还可以使用 stat API,其示例如下:

  1. # 使用 stat 查询 最高售价、最低售价、平均售价
  2. POST books/_search
  3. {
  4. "aggs": {
  5. "stat_price": {
  6. "stats": {
  7. "field": "price"
  8. }
  9. }
  10. },
  11. "size": 0
  12. }
  13. # 结果
  14. {
  15. ......
  16. "aggregations" : {
  17. "stat_price" : {
  18. "count" : 7,
  19. "min" : 9.9,
  20. "max" : 20.9,
  21. "avg" : 15.471428571428572,
  22. "sum" : 108.3
  23. }
  24. }
  25. }

如上示例可以看到,stats 聚合除了返回最高售价、最低售价、平均售价外还有售价之和(sum)、文档个数的信息。

3. 统计出版社的数量

可以使用 cardinality 聚合获取出版社的数量,其作用类似于 SQL 中的 distinct count。其示例如下:

  1. # 统计出版社的数量
  2. POST books/_search
  3. {
  4. "aggs": {
  5. "cardinality_publisher": {
  6. "cardinality": {
  7. "field": "publisher"
  8. }
  9. }
  10. },
  11. "size": 0
  12. }
  13. # 结果
  14. {
  15. ......
  16. "aggregations" : {
  17. "cardinality_publisher" : {
  18. "value" : 3
  19. }
  20. }
  21. }

如上示例,使用 cardinality 聚合后可以得出出版社的数量为 3 个。

Metric Aggregations 提供的 API 远不止这些,由于篇幅的限制无法全部都进行介绍,更多的使用案例你可以参考官方文档

二、Bucket Aggregations

Bucket 可以理解为一个桶,或者一个分组,当遍历文档库的时候会把符合条件的文档放到一个分组里面去,分组就相当于 SQL 中的 Group By。如下图,我们将书本以出版社字段进行分组。

分桶.png

对数据进行分组后,我们还可以组合 Metric Aggregations 的聚合操作来完成复杂的统计需求,例如找出每个出版社最贵的售价是多少。

ES 提供的 Bucket Aggregations 中常用的有以下几个:

  • Terms:根据某个字段进行分组,例如根据出版社进行分组。
  • Range、Data Range:根据用户指定的范围参数作为分组的依据来进行聚合操作。
  • Histogram、Date Histogram:可以指定间隔区间来进行聚合操作。

同样,我们可以利用上述的 API 来完成以下的需求:

  1. 统计每个出版社的书本数量。
  2. 统计每个价格区间的书本数量。
  3. 统计每个出版社书本的销售量。

ok,下面我们来解决上述的几个需求。

1. 统计每个出版社的书本数量

我们只需要以出版社作为分组条件,然后计算每个分组中文档的个数,得出的结果就是每个出版社拥有的书本数量了。可以使用 Terms Aggregations 来完成这个需求:

  1. # Terms Aggregations 统计每个出版社拥有的书本数量
  2. POST books/_search
  3. {
  4. "aggs": {
  5. "publisher_book_count": {
  6. "terms": {
  7. "field": "publisher",
  8. "size": 3
  9. }
  10. }
  11. },
  12. "size": 0
  13. }
  14. # 结果
  15. {
  16. ......
  17. "aggregations" : {
  18. "publisher_book_count" : {
  19. "doc_count_error_upper_bound" : 0,
  20. "sum_other_doc_count" : 0,
  21. "buckets" : [
  22. { "key" : "linux publisher", "doc_count" : 3 },
  23. { "key" : "autobiography publisher", "doc_count" : 2 },
  24. { "key" : "science publisher", "doc_count" : 2 }
  25. ]
  26. }
  27. }
  28. }

如上示例,当我们使用 “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 的使用示例如下:

  1. # 价格区间统计
  2. POST books/_search
  3. {
  4. "aggs": {
  5. "price_range": {
  6. "range": {
  7. "field": "price",
  8. "keyed": true,
  9. "ranges": [
  10. { "key": "cheap", "from": 0.0, "to": 10.0 },
  11. { "key": "average", "from": 10.0, "to": 20.0 },
  12. { "key": "expensive", "from": 20.0, "to": 30.0 }
  13. ]
  14. }
  15. }
  16. },
  17. "size": 0
  18. }
  19. # 结果
  20. {
  21. ......
  22. "aggregations" : {
  23. "price_range" : {
  24. "buckets" : {
  25. "cheap" : { "from" : 0.0, "to" : 10.0, "doc_count" : 1 },
  26. "average" : { "from" : 10.0, "to" : 20.0, "doc_count" : 5 },
  27. "expensive" : { "from" : 20.0, "to" : 30.0, "doc_count" : 1 }
  28. }
  29. }
  30. }
  31. }

如上示例,使用 “range” 关键字可以进行 Range Aggregation 操作,使用 “keyed”: true 使得我们可以对每个区间进行命名,如例子中我们命名了 cheap、average、expensive 三个区间,使用 “key” 关键字即可指定区间的名字。关键字 from 和 to 指定了区间的开始值和结束值,其取值范围为:[from, to)。

不知道机智的你是不是发现了,当区间值非常多的时候,写这个查询语句是不是会写到手痛么?所以贴心的 ES 为我们提供了 Histogram Aggregation API 来解决这个问题。Histogram Aggregation 也可以对区间进行分组,但这个区间是固定间隔的,例如我们上例的间隔是 10,那用 Histogram Aggregation 可以这样实现:

  1. # 使用 Histogram Aggregation
  2. POST books/_search
  3. {
  4. "aggs": {
  5. "price_histogram": {
  6. "histogram": {
  7. "field": "price",
  8. "interval": 10
  9. }
  10. }
  11. },
  12. "size": 0
  13. }
  14. # 结果
  15. {
  16. "aggregations" : {
  17. "price_histogram" : {
  18. "buckets" : [
  19. { "key" : 0.0, "doc_count" : 1 },
  20. { "key" : 10.0, "doc_count" : 5 },
  21. { "key" : 20.0, "doc_count" : 1 }
  22. ]
  23. }
  24. }
  25. }

Histogram Aggregation 返回的结果比较简单,其并不能像 Range Aggregation 那样手动指定每个区间的名字,所以 Histogram Aggregation 更多时候用在显示图表的需求上。

3. 统计每个出版社书本的销售量

要统计每个出版社书本的销售量的话,需要先按每个出版社进行分组,然后对每个分组所有文档的销售量求和,所以使用 Terms Aggregation 和 Sum Aggregation 可以解决这个需求,其示例如下:

  1. # 使用子聚合 组合 Terms Aggregation 和 Sum Aggregation
  2. POST books/_search
  3. {
  4. "aggs": {
  5. "publisher_sales_total": {
  6. "terms": { "field": "publisher" },
  7. "aggs": {
  8. "sales_total": {
  9. "sum": { "field": "sales" }
  10. }
  11. }
  12. }
  13. },
  14. "size": 0
  15. }
  16. # 结果
  17. {
  18. ......
  19. "aggregations" : {
  20. "publisher_sales_total" : {
  21. "doc_count_error_upper_bound" : 0,
  22. "sum_other_doc_count" : 0,
  23. "buckets" : [
  24. {
  25. "key" : "linux publisher",
  26. "doc_count" : 3,
  27. "sales_total" : { "value" : 400.0 }
  28. },
  29. {
  30. "key" : "autobiography publisher",
  31. "doc_count" : 2,
  32. "sales_total" : { "value" : 5140.0 }
  33. },
  34. {
  35. "key" : "science publisher",
  36. "doc_count" : 2,
  37. "sales_total" : { "value" : 19800.0 }
  38. }
  39. ]
  40. }
  41. }
  42. }

如上示例,我们使用了子聚合的方式组合了 Terms Aggregation 和 Sum Aggregation。从结果可以看出,查询以出版社名称为分组,然后求得每个分组中书本的销售量的总和,并放在了 “sales_total” 中。

同样,Bucket Aggregations 提供的 API 也是非常丰富的,但由于篇幅的限制无法全部都进行介绍,更多的使用案例你可以参考官方文档

三、Pipeline Aggregations

Pipeline Aggregations 可以对其他聚合输出的结果进行再次聚合,下面通过一个例子来说明其使用方式。

现在有这样一个需求,在销售量最好的 2 个出版社里,找出平均价格最低的出版社。其示例如下:

  1. POST books/_search
  2. {
  3. "aggs": {
  4. "publisher": {
  5. "terms": {
  6. "field": "publisher",
  7. "size": 2,
  8. "order": { "sales_total": "desc" }
  9. },
  10. "aggs": {
  11. "sales_total": {
  12. "sum": { "field": "sales" }
  13. },
  14. "avg_price": {
  15. "avg": { "field": "price" }
  16. }
  17. }
  18. },
  19. "min_avg_price": {
  20. "min_bucket": {
  21. "buckets_path": "publisher>avg_price"
  22. }
  23. }
  24. },
  25. "size": 0
  26. }
  27. # 结果
  28. {
  29. "aggregations" : {
  30. "publisher" : { ...... },
  31. "min_avg_price" : {
  32. "value" : 14.399999999999999,
  33. "keys" : [ "science publisher" ]
  34. }
  35. }
  36. }

如上示例,在 “publisher” 中我们做了以下几件事:

  1. 按出版社进行分桶。
  2. 执行子聚合,计算每个出版社的销售总额和书本平均价格。
  3. 排序结果按 “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 类:

  1. 提供指标统计信息的 Metric Aggregations,提供了像 sum、max、min 等常用的指标聚合函数。
  2. 提供分组功能的 Bucket Aggregations,分组的功能类似于 mysql 中的 group by。我们可以对分组后的每个组数据进行统计,例如求每个分组的 max price 、min price 等。
  3. 提供对其他聚合结果进行再次聚合的 Pipeline Aggregations。

一般来说,业务中的统计需求是复杂的,我们常常需要组合 Bucket Aggregations 和 Metric Aggregations 来完成需求,这个时候可以使用子聚合、Pipeline Aggregations 来组合多个聚合。

好了今天的内容到此为止,今天介绍的聚合 API 是比较常用的功能了,ES 提供的聚合 API 非常丰富,更多的使用例子可以参考官方文档