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. 参考文献
- https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
- Clinton Gormley &Zachary Tong, Elasticsearch: The Definitive Guide,2015