编程随笔-ElasticSearch知识导图(5):聚合

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. 参考文献

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

本系列文章:

编程随笔-ElasticSearch知识导图(1):全景
编程随笔-ElasticSearch知识导图(2):分布式架构
编程随笔-ElasticSearch知识导图(3):映射
编程随笔-ElasticSearch知识导图(4):搜索
编程随笔-ElasticSearch知识导图(5):聚合
编程随笔-ElasticSearch知识导图(6):管理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,898评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,401评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,058评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,539评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,382评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,319评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,706评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,370评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,664评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,715评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,476评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,326评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,730评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,003评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,275评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,683评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,877评论 2 335

推荐阅读更多精彩内容