1. 聚合模式

  聚合(Aggregations)是对数据库中数据域进行统计分析的手段,关系数据库中我们常会用到avg,sum,count,group by这些聚合手段进行简单的统计与分析。在ES中也提供了同样的功能,根据使用模式,分为以下几种:

  • 数字指标(metrics)聚合:根据输出的是单值的还是多值的分为单值数字指标与多值数字指标,计算使用的域可直接从文本中抽取也可使用脚本生成。
  • 分组(bucket)聚合:分组聚合创建文档对象的分组。每个分组都与一个分组依据 (凭证)相关联(取决于聚合类型),该依据确定当前上下文中的文档是否“属于”其中。分组聚合还计算并返回每个分组中文档数量。分组聚合可以嵌套,即一个分组中还可以定义子分组。分组聚合支持对父子关系对象和嵌套对象的聚合。
  • 管道(Pipeline)聚合:处理来自其它聚合的数据,而不是直接计算文档对象的域值得到输出。管道聚合可以分为两类:
    • 父(parent)聚合:一组管道聚合的输入数据由其父聚合的输出提供,能够计算新分组或新聚合添加到现有组中。
    • 兄弟(sibling)聚合:输入数据由同级聚合的输出提供,新产生的聚合域与所使用的输入聚合同级。

  文献1中还提到了矩阵(Matrix)聚合,它对多个字段进行操作,并根据字段值生成一个矩阵结果,该矩阵是对这些字段的一些统计数据。因为比较小众,本文中不做讨论。
  数字指标聚合、分组聚合类似于关系数据库中的avg,sum,count,group by等聚合形式,在应用系统中经常会使用。管道聚合是数字指标聚合及分组聚合的进阶使用,语法派生于数字指标聚合、分组聚合,本文暂不探讨,有兴趣的同学看参考文献1。
  可将数字指标聚合、分组聚合的语法和用法总结如下一张导图。
编程随笔-ElasticSearch知识导图(5):聚合_第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. 参考文献

  1. https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
  2. Clinton Gormley &Zachary Tong, Elasticsearch: The Definitive Guide,2015