1. 聚合模式
聚合 (Aggregations) 是对数据库中数据域进行统计分析的手段, 关系数据库中我们常会用到 avg,sum,count,group by 这些聚合手段进行简单的统计与分析. 在 ES 中也提供了同样的功能, 根据使用模式, 分为以下几种:
数字指标 (metrics) 聚合: 根据输出的是单值的还是多值的分为单值数字指标与多值数字指标, 计算使用的域可直接从文本中抽取也可使用脚本生成.
分组 (bucket) 聚合: 分组聚合创建文档对象的分组. 每个分组都与一个分组依据 (凭证)相关联(取决于聚合类型), 该依据确定当前上下文中的文档是否 "属于" 其中. 分组聚合还计算并返回每个分组中文档数量. 分组聚合可以嵌套, 即一个分组中还可以定义子分组. 分组聚合支持对父子关系对象和嵌套对象的聚合.
管道 (Pipeline) 聚合: 处理来自其它聚合的数据, 而不是直接计算文档对象的域值得到输出. 管道聚合可以分为两类:
父 (parent) 聚合: 一组管道聚合的输入数据由其父聚合的输出提供, 能够计算新分组或新聚合添加到现有组中.
兄弟 (sibling) 聚合: 输入数据由同级聚合的输出提供, 新产生的聚合域与所使用的输入聚合同级.
文献 1 中还提到了矩阵 (Matrix) 聚合, 它对多个字段进行操作, 并根据字段值生成一个矩阵结果, 该矩阵是对这些字段的一些统计数据. 因为比较小众, 本文中不做讨论.
数字指标聚合, 分组聚合类似于关系数据库中的 avg,sum,count,group by 等聚合形式, 在应用系统中经常会使用. 管道聚合是数字指标聚合及分组聚合的进阶使用, 语法派生于数字指标聚合, 分组聚合, 本文暂不探讨, 有兴趣的同学看参考文献 1.
可将数字指标聚合, 分组聚合的语法和用法总结如下一张导图.
聚合语法与用法
2. 与查询指令结合
聚合指令使用检索 DSL(search DSL)定义, 因而也使用检索指令的 URI(标识为 "_search"), 请求消息体中若包含以 "query" 指示的查询指令, 则以 "aggs" 指示的聚合指令进行聚合操作的对象为 "query" 指令的查询结果; 若不包含 "query" 指令, 则表示进行聚合操作的对象为索引中所有对象.
仍以《编程随笔 - Elasticsearch 知识导图(3): 映射》中第 2 节中的银行账号索引为例, 考察下面一个简单聚合指令, 计算银行余额的均值:
- curl -iXPOST 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
- {
- "size":0,
- "aggs": {
- "avg_balance": {
- "avg": {
- "field": "balance"
- }
- }
- }
- }
- '
该命令计算 bank 索引中所有账户的余额平均值, 若想查询年龄在 30 到 40 之间客户的记录和平均余额, 则可使用下面的指令.
- curl -iXPOST 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
- {
- "query": {
- "range": {
- "age": {
- "lte": 40,
- "gte": 30
- }
- }
- },
- "aggs": {
- "avg_balance": {
- "avg": {
- "field": "balance"
- }
- }
- }
- }
'若只是想了解年龄在 30 到 40 之间客户的平均余额, 则可使用如下聚合指令(注意范围分组中不包含"to" 的值):
- curl -iXPOST 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
- {
- "size":0,
- "aggs": {
- "avg_balance_by_age": {
- "range": {
- "field": "age",
- "ranges": [
- {
- "to": 41,
- "from": 30
- }
- ]
- },
- "aggs": {
- "avg_balance": {
- "avg": {
- "field": "balance"
- }
- }
- }
- }
- }
- }
- '
3. 常用模式设计
3.1. 聚合模式表示
以我们熟悉的 SQL 语言作为范式, 我们将应用中的常用聚合查询使用 SQL 表示为如下模式:
SELECT [$field_1] FROM $index_name WHERE $filter_clause GROUP BY [$field_2] ORDER BY [$field_3]
其中:
[$field_1]是在返回结果显示的字段名集合,$field_1 有可能是实施聚合操作的聚合值, 也可以是分组 [$field_2] 中的字段.
$index_name 是索引名.
[$field_2]是分组依据的字段, 可能为多个字段.
[$field_3]是排序字段, 可能为多个字段.
$filter_clause 是过滤条件.
3.2. 多分组字段
对于聚合中的多个分组字段, 在聚合指令中可以使用两种格式: 一种使用 基于 "terms" 子句的嵌套分组方式, 另一种使用基于 "composite" 子句的多字段分组方式.
本文建议如果有只有一个分组字段, 使用 "terms" 定义分组, 如果包含多个分组字段, 则使用 "composite" 定义多个分组字段.
考虑如下聚合查询用例, 按账户所在的州与性别分组, 获取每组的余额最大值:
SELECT state,gender,max(balance) FROM bank GROUP BY state,gender
使用基于 "composite" 子句的分组方式聚合指令如下所示:
- curl -iXPOST 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
- {
- "size": 0,
- "aggs": {
- "group_by_state_gender": {
- "composite": {
- "sources": [
- {
- "state": {
- "terms": {
- "field": "state.keyword"
- }
- }
- },
- {
- "gender": {
- "terms": {
- "field": "gender.keyword"
- }
- }
- }
- ]
- },
- "aggs": {
- "max_balance": {
- "max": {
- "field": "balance"
- }
- }
- }
- }
- }
- }
- '
返回结果 (部分) 显示如下:
- "aggregations" : {
- "group_by_state_gender" : {
- "after_key" : {
- "state" : "AK",
- "gender" : "F"
- },
- "buckets" : [
- {
- "key" : {
- "state" : "AK",
- "gender" : "F"
- },
- "doc_count" : 10,
- "max_balance" : {
- "value" : 44043.0
- }
- }
- ]
- }
- }
使用基于 "terms" 子句的嵌套分组方式聚合指令如下所示:
- curl -iXPOST 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
- {
- "size": 0,
- "aggs": {
- "group_by_state": {
- "terms": {
- "field": "state.keyword"
- },
- "aggs": {
- "group_by_gender": {
- "terms": {
- "field": "gender.keyword"
- },
- "aggs": {
- "max_balance": {
- "max": {
- "field": "balance"
- }
- }
- }
- }
- }
- }
- }
- }
- '
返回结果 (部分) 显示如下所示:
- "aggregations" : {
- "group_by_state" : {
- "doc_count_error_upper_bound" : 28,
- "sum_other_doc_count" : 978,
- "buckets" : [
- {
- "key" : "TX",
- "doc_count" : 22,
- "group_by_gender" : {
- "doc_count_error_upper_bound" : 0,
- "sum_other_doc_count" : 0,
- "buckets" : [
- {
- "key" : "F",
- "doc_count" : 13,
- "max_balance" : {
- "value" : 49587.0
- }
- },
- {
- "key" : "M",
- "doc_count" : 9,
- "max_balance" : {
- "value" : 42736.0
- }
- }
- ]
- }
- }
- ]
- }
- }
从两种查询方式的结果格式来看, 使用 "composite" 方式的查询指令返回结果更符合我的使用习惯.
3.3. 排序
可对聚合查询的结果用于拍寻, 用于排序字段的可为分组字段, 也可为聚合操作结果. 将上节的查询要求改为如下形式:
SELECT state,gender,max(balance) FROM bank GROUP BY state,gender ORDER BY state ASC ,gender ASC
则查询指令可修改为如下形式:
- curl -iXPOST 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
- {
- "size": 0,
- "aggs": {
- "group_by_state_gender": {
- "composite": {
- "sources": [
- {
- "state": {
- "terms": {
- "field": "state.keyword",
- "order": "ASC"
- }
- }
- },
- {
- "gender": {
- "terms": {
- "field": "gender.keyword",
- "order": "ASC"
- }
- }
- }
- ]
- },
- "aggs": {
- "max_balance": {
- "max": {
- "field": "balance"
- }
- }
- }
- }
- }
- }
'需要注意的是:"composite"形式的聚合查询只支持对分组字段的排序, 如果要使用聚合值作为排序字段, 请使用"terms" 形式用于分组的子句, 如下面的示例.
- curl -iXPOST 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
- {
- "size": 0,
- "aggs": {
- "group_by_state": {
- "terms": {
- "field": "state.keyword",
- "order": {
- "max_balance": "DESC"
- }
- },
- "aggs": {
- "max_balance": {
- "max": {
- "field": "balance"
- }
- }
- }
- }
- }
- }
- '
3.4. 分页
如果聚合查询的返回记录较多, ES 在一次返回结果中默认返回 10 条. 如果需要获取所有记录, 则需要设置分页参数进行多次查询.
仍然考虑 3.2 节的查询示例, 分组结果可能有 100 个左右的分组, 若设置每次查询结果返回 5 个分组, 可以设置如下查询指令:
- curl -iXPOST 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
- {
- "size": 0,
- "aggs": {
- "group_by_state_gender": {
- "composite": {
- "size": 5,
- "sources": [
- {
- "state": {
- "terms": {
- "field": "state.keyword",
- "order": "ASC"
- }
- }
- },
- {
- "gender": {
- "terms": {
- "field": "gender.keyword",
- "order": "ASC"
- }
- }
- }
- ]
- },
- "aggs": {
- "max_balance": {
- "max": {
- "field": "balance"
- }
- }
- }
- }
- }
- }
'对于使用了"composite"形式的查询指令, 在返回结果中包含一个"after_key"对象, 标识本次查询结果的最后一个分组标识, 如果在下次查询中携带该对象, ES 会返回此对象所标识分组后面的分组记录, 查询指令如下所示(注意指令中的"after"对象, 提供了类似游标的功能, 每次根据上次查询结果的"after_key" 进行改变):
- curl -iXPOST 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
- {
- "size": 0,
- "aggs": {
- "group_by_state_gender": {
- "composite": {
- "size": 5,
- "after": {
- "state" : "AR",
- "gender" : "F"
- },
- "sources": [
- {
- "state": {
- "terms": {
- "field": "state.keyword",
- "order": "ASC"
- }
- }
- },
- {
- "gender": {
- "terms": {
- "field": "gender.keyword",
- "order": "ASC"
- }
- }
- }
- ]
- },
- "aggs": {
- "max_balance": {
- "max": {
- "field": "balance"
- }
- }
- }
- }
- }
- }
'对于使用"terms"的嵌套分组方式的聚合查询指令无法使用类似" 游标 " 功能, 只能返回指定数目的分组结果.
3.5. 过滤条件处理
如果聚合查询中有过滤条件, 最简单的方式是在查询指令中增加 "query" 子句, 参看第 2 节的描述.
3.6. 设计模式
现在我们可以对查询要求:
SELECT [$field_1] FROM $index_name WHERE $filter_clause GROUP BY [$field_2] ORDER BY [$field_3]
定义一个常用的聚合查询模式, 如下所示:
- {
- "query": {
- "$filter_clause": {}
- },
- "aggs": {
- "group_by_field": {
- "composite": {
- "size": {},
- "after": {},
- "sources": [
- "[$field_2]",
- "[$field_3]"
- ]
- },
- "aggs": {
- "aggregate_operation": {
- "[$field_1]": {}
- }
- }
- }
- }
- }
考虑如下查询要求:
SELECT state,gender,max(balance) FROM bank WHERE age>=40 GROUP BY state,gender ORDER BY state ASC ,gender ASC
使用上面的设计模式, 可以表示为如下指令:
- curl -iXPOST 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
- {
- "size": 0,
- "query": {
- "range": {
- "age": {
- "gte": 40
- }
- }
- },
- "aggs": {
- "group_by_state_gender": {
- "composite": {
- "size": 5,
- "sources": [
- {
- "state": {
- "terms": {
- "field": "state.keyword",
- "order": "ASC"
- }
- }
- },
- {
- "gender": {
- "terms": {
- "field": "gender.keyword",
- "order": "ASC"
- }
- }
- }
- ]
- },
- "aggs": {
- "max_balance": {
- "max": {
- "field": "balance"
- }
- }
- }
- }
- }
- }
- '
4. SQL 访问支持
最后告诉大家一个好消息, ES 提供 SQL 语言访问, 基于 XPACK 插件实现. 相比于复杂的检索 DSL,SQL 对于习惯于关系数据库的用户更加亲切一些.
上节的查询要求可表示为如下 SQL 访问指令:
- curl -iXPOST 'localhost:9200/_xpack/sql?format=txt' -H 'Content-Type: application/json' -d'
- {
- "query": "SELECT state,gender,max(balance) FROM bank WHERE age>=40 GROUP BY state,gender ORDER BY state ASC ,gender ASC"
- }
- '
查询结果如下所示:
- HTTP/1.1 200 OK
- Cursor: w6XxAgFmAWMBBGJhbmu+AQEBCWNvbXBvc2l0ZQdncm91cGJ5AQNtYXgEMTk5MQAA/wEHYmFsYW5jZQAAAP8AAP8CAAQxOTg3AQ1zdGF0ZS5rZXl3b3JkAAAB/wAAAAQxOTgzAQ5nZW5kZXIua2V5d29yZAAAAf8AAOgHAQoCBDE5ODcAAldZBDE5ODMAAU0AAgEAAAAAAQD/////DwAAAAABBXJhbmdlP4AAAAADYWdlAQAAACj/AQAAAAAAAAAAAAAAAVoDAAIAAAAAAAHZ////DwMBawQxOTg3AAABawQxOTgzAAABbQQxOTkxBXZhbHVlAAMAAAAPAAAADwAAAA8=
- Took-nanos: 12179132
- content-type: text/plain
- content-length: 1920
- state | gender | MAX(balance)
- ---------------+---------------+---------------
- AK |F |44043.0
- AK |M |37074.0
- AL |M |34743.0
- CA |M |25892.0
- DC |F |18956.0
- HI |M |2171.0
- ID |F |19955.0
- ID |M |16163.0
- IL |M |23165.0
- IN |M |11298.0
- KY |F |48972.0
- KY |M |47887.0
- MA |F |35247.0
- MI |F |13109.0
- MN |F |5346.0
- MO |F |49671.0
- MO |M |31865.0
- MS |M |29316.0
- MT |F |37720.0
- NC |M |34754.0
- ND |F |28969.0
- ND |M |46568.0
- NH |F |19630.0
- NH |M |2905.0
- NM |F |13478.0
- NM |M |44235.0
- OH |F |42072.0
- OK |F |28729.0
- OR |M |33882.0
- PA |F |49159.0
- SC |M |29648.0
- TX |M |6507.0
- UT |F |35896.0
- UT |M |43532.0
- VT |F |9597.0
- WA |M |18400.0
- WV |F |16869.0
- WY |M |32849.0
ES 提供的 SQL 访问有一些限制: 如结果的返回字段要么是分组字段, 要么是聚合值; 排序字段不可为聚合值等. 检索 DSL 语法复杂, 但功能更加强大. 若要快速开发, ES 提供的 SQL 访问也不失为一种选择.
5. 参考文献
Clinton Gormley &Zachary Tong, Elasticsearch: The Definitive Guide,2015
本系列文章:
编程随笔 - Elasticsearch 知识导图(1): 全景
编程随笔 - Elasticsearch 知识导图(2): 分布式架构
编程随笔 - Elasticsearch 知识导图(3): 映射
编程随笔 - Elasticsearch 知识导图(4): 搜索
编程随笔 - Elasticsearch 知识导图(5): 聚合
编程随笔 - Elasticsearch 知识导图(6): 管理
来源: http://www.jianshu.com/p/e09751d6cb01