一、 范围分桶聚集
如果使用 SQL 语言类比,桶型聚集与 SQL 语句中的 group by 子句极为相似。桶型聚集(Bucket Aggregation)是 Elasticsearch 官方对这种聚集的叫法,它起的作用是根据条件对文档进行分组。
可以将这里的桶理解为分组的容器,每个桶都与一个分组标准相关联,满足
这个分组标准的文档会落桶中。所以在默认情况下,桶型聚集会根据分组标准返回所有分组,同时还会通过 doc_count 字段返回每一桶中的文档数量。
由于单纯使用桶型聚集只返回桶内文档数量,意义并不大,所以多数情况下
都是将桶型聚集与指标聚集以父子关系的形式组合在起使用。桶型聚集作为父聚集起到分组的作用。而指标聚集则以子聚集的形式出现在桶型聚集中, 起到分组统计的作用。比如将用户按性别分组,然后统计他们的平均年龄。
按返回桶的数量来看,桶型聚集可以分为单桶聚集和多桶聚集。在多桶聚集
中,有些桶的数量的固定的。而有些桶的数量则是在运算时动态决定。由于桶聚集基本都是将所有桶一次返回,返回了过多的通会影响性能,所以单个请求允许返间的最大桶数受 search.max_bucket 参数限制。
这个参数在 7.0 之前的版本中默认值为-1,代表无上限。但在Elasticsearch版本 7 中,这个参数的默认值已经更改为 10000 所以在做桶型聚集时要先做好数据验证,防止桶数量过多影响性能。
1.1 数值范围
range、date_range 与 ip_range 这三种类型的聚集都用于根据字段的值范围内对文档分桶,字段值在同一范围内的文档归入同一桶中。每个值范围都可通过from 和 to 参数指定,范围包含 from 值但不包含 to 值,用数学方法表示就是[from, to)。 在设置范围时,可以设置一个也可以设置多个,范围之间并非一定要连续,可以有间隔也可以有重叠。
range 聚集
range 聚集使用 ranges 参数设置多个数值范围,使用 field 参数指定 1 个数值类型的字段。range 聚集在执行时会将该字段在不同范围内的文档数量统计出来,并在返回结果的 doc_count 字段中展示出来。例如统计航班不同范围内的票价数量,可以按示例的方式发送请求:
POST indexname/_search?filter_path=aggregations{
"aggs": {
"price_ranges": {
"range": {
"field": "AvgTicketPrice",
"ranges": [{
"to": 300
},
{
"from": 300,
"to": 600
},
{
"from": 600,
"to": 900
},
{
"to": 900
}]
}
}
}
}
在返回结果中,每个范围都会包含一个 key 字段,代表了这个范围的标识,它的基本格式是“- "。如果觉得返回的这种 key 格式不容易理解,可以通过在 range 聚集的请求中添加 keyed 和 key 参数定制返回结果的 key 字段值。其中 keyed 是 range 的参数,用于标识是否使用 key 标识范围,所以为布尔类型,key 参数则是与 from、to 参数同级的参数,用于定义返回结果中的 key 字段值。
date_range 聚集
date_range 聚集与 range 聚集类似,只见范围和字段的类型为日期而非数值。
date_range 聚集的范围指定也是通过 ranges 参数设置,具体的范围也是使
用- 两个参数,并且可以使用 keyed 和 key 定义返回结果的标识。
date_range 聚集多了个指定日期格式的参数 format, 可以用于指定 from 和 to 的目期格式。例如,
POST indexname/_search?filter_path=aggregations{
"aggs": {
"mar_flights": {
"date_range": {
"field": "timestamp",
"ranges": [{
"from": "2019-03-01",
"to": "2019-03-30"
}],
"format": "yyyy-MM-dd"
}
}
}
}
ip_range 聚集
ip_ range 聚集根据 ip 类型的字段统计落在指定 IP 范围的文档数量,使用的聚集类型名称为 ip_ range。例如,统计了两个 IP 地址范围的文档数量:
POST indexname/_search?filter_path=aggregations{
"aggs": {
"local": {
"ip_range": {
"field": "clientip",
"ranges": [{
"from": "157.4.77.0",
"to": "157.4.77.255"
},
{
"from": "105.32.127.0",
"to": "105.32.127.255"
}]
}
}
}
}
1.2 间隔范围
histogram、date _ histogram 与 auto_date_histogram 这三种聚集与上一节中使用数值定义范围的聚集很像,也是统计落在某一范围内的文档数量。 但与数值范围聚集不同的是,这三类座集统计范围由固定的间隔定义,也就是范围的结束值和起始值的差值是固定的。
histogram 聚集
histogram 聚集以数值为间隔定义数值范围,字段值具有相同范围的文档将
落入同桶中。例如示例以 100 为间隔做分桶,可以通过返回结果的 doc_count 字段获取票价在每个区间的文档数量:
POST indexname/_search?filter_path=aggregations{
"aggs": {
"price_histo": {
"histogram": {
"field": "AvgTicketPrice",
"interval": 100,
"offset": 50,
"keyed": false,
"order": {
"_count": "asc"
}
}
}
}
}
其中,interval 参数用于指定数值问隔必须为正值,而 offset 参数则代表起
始数值的偏移量,必须位于[0, interval) 范围内。order 参数用于指定排序字段和顺序,可选字段为_key 和_count。当 keyed 参数设置为 true 时,返回结果中每个桶会有一个标识,标识的计算公式为:
bucket_key = Math. floor( ( value- offset)/interval) * interval +offset
date_histogram 聚集
date_histogra 聚集以时间为间隔定义日期范围,字段值具有相同日期范围的文档将落入同一桶中。同样,返回结果中也会包含每个间隔范围内的文档数量doc_count。 例如统计每月航班数量:
POST indexname/_search?filter_path=aggregations{
"aggs": {
"month flights": {
"date_histogram": {
"field": "timestamp",
"interval": "month"
}
}
}
}
在示例使用参数 interval 指定时间间隔为 month,即按月划分范围。时间可以是还有:
毫秒:1ms 10ms
秒:second/1s 10s
分钟:minute/1m 10m
小时:hout/1h 2h
天:day 2d 不支持
星期:week/1w 不支持
月:month/1M 不支持
季度:quarter/1q 不支持
年:year/1y 不支持
auto_date_histogram 聚集
前述两种聚集都是指定间隔的具体值是多少,然后再根据间隔值返回每一
桶中满足条件的文档数。最终会有多少桶取决于两个条件,即间隔值和字段值在所有文档中的实际跨度。反过来,如果预先指定需要返回多少个桶,那么间隔值也可以通过桶的数量以及字段值跨度共同确定。auto_date_histogram 聚集就是这样一种聚集,它不是指定时间间隔值,而是指定需要返回桶的数量。例如在示例中定义需要返回 10 个时间段的桶:
POST indexname/_search?size=0{
"aggs": {
"age_group": {
"auto_date_histogram": {
"field": "timestamp",
"buckets": 10
}
}
}
}
参数 field 设置通过哪一个字段做时间分隔,而参数 buckets 则指明了需要返回多少个桶。 默认情况下, buckets 的数量为 10。需要注意的是,buckets 只是设置了期望返回桶的数量,但实际返回桶的数量可能等于也可能小于 buckets设置的值。例如示例的请求中期望 10 个桶,但实际可能只返回 6 个桶。
auto_date_histogram 聚集在返回结果中还提供了一个 interval 字段,用于说明实际采用的间隔时间。从实现的角度来说,不精确匹配 buckets 数量也有利于提升检索的性能。
1.3 子聚集 (聚集嵌套)
前面介绍的桶型聚集,大部分都只是返回满足聚集条件的文档数量。在实际
应用中,如果需要桶型聚集与 SQL 中的 group by 具有相同的意义,用于将文档分桶后计算各桶特定指标值,比如根据用户性别分组,然后分别求他们的平均年龄。Elasticsearch 这样的功能通过子聚集 (聚集嵌套)来实现。例如,示例中的请求就是先按月对从中国起飞的航班做了分桶,然后又通过聚集嵌套计算每月平均延误时间:
POST indexname/_search?filter_path=aggregations{
"query": {
"term": {
"OriginCountry": "CN"
}
},
"aggs": {
"date_price_histogram": {
"date_histogram": {
"field": "timestamp",
"interval": "month"
},
"aggs": {
"avg_price": {
"avg": {
"field": "FlightDelayMin"
}
}
}
}
}
}
在示例中,search 接口共使用了两个参数,query 参数以 term 查询条件将所有 OriginCountry 字段是 CN 的文档筛选出来参与聚集运算。
aggs 参数则定义了一个名称为 data_price_histogram 的桶型聚集,这个聚集内部又嵌套了个名称为 avg price 的聚集。由于 price 这个聚集位于 data_price histogram 中,所以它会使用这个聚集的分桶结果做运算而不会针对所有文档。所以,最终的效果就是将按月计算从中国出发航班的平均延误时间。
使用嵌套聚集时要注意,嵌套聚集应该位于父聚集名称下而与聚集类型同级,并且需要通过参数再次声明。如果与父聚集一样位于 aggs 参数下,那么这两个聚集就是平级而非嵌套聚集。
二、词项分桶聚集
使用字段值范围分桶主要针对结构化数据,比如年龄、IP 地址等等。但对于字符串类型的字段来说,使用值范围来分桶显然是不合适的。由于字符串类型字段在编入索引时会通过分析器生成词项,所以字符申类型字段的分桶一般通过词项实现。使用词项实现分桶的聚集,包括 terms、significant_terms 和significant_text 聚集。由于使用词项分桶需要加载所有词项数据,所以它们在执行速度上都会比较慢。为了提升性能,Elsticesearch 提供了 sampler 和diversifed_sampler 聚集,可通过缩小样本数量减少运算量。
2.1 terms 聚集
terms 聚集根据文档字段中的词项做分桶,所有包含同一词项的文档将被归人同一桶中,聚集结果中包含字段中的词项及其词频,在默认情况下还会根据词频排序,所以 terms 聚集也可用于热词展示,由于 terms 聚集在统计词项的词频数据时需要打开它的 fielddata 机制。fielddata 机制对内存消耗较大且有导致内存溢出的可能, 所以 terms 聚集一般针对 keyword 非 text 类型。
Fielddata:其实根据倒排索引反向出来的一个正排索引,即 document 到 term的映射。
只要我们针对需要分词的字段设置了 fielddata,就可以使用该字段进行聚合,排序等。我们设置为 true 之后,在索引期间,就会以列式存储在内存中。为什么存在于内存呢,因为按照 term 聚合,需要执行更加复杂的算法和操作,如果基于磁盘或者 OS 缓存,性能会比较差。
fielddata 堆内存要求很高,如果数据量太大,对于 JVM 来及回收来说存在一定的挑战,也就是对 ES 带来巨大的压力。所以 doc_value 的出现我们可以使用磁盘存储,他同样是和 fielddata 一样的数据结构,在倒排索引基础上反向出来的正排索引,并且是预先构建,即在建倒排索引的时候,就会创建 doc values。, 这会消耗额外的存储空间,但是对于 JVM 的内存需求就会减少。总体来看,DocValues 只是比 fielddata 慢一点,大概 10-25%,则带来了更多的稳定性。
cardlinality 聚集可以统计字段中不重复词项的数量,而 terms 聚集则可以将这些词项全部展示出来。与 cardlinality 聚集一样, terms 聚集统计出来的词频也不能保证完全精确。例如:
POST indexname/_search?filter_path=aggregations{
"aggs": {
"country_terms": {
"terms": {
"field": "DestCountry",
"size": 10
}
},
"country_terms_count": {
"cardinality": {
"field": "DestCountry"
}
}
}
}
在示例中定义了两个聚集,由于它们都是定义在 aggs 下,所以不是嵌套聚集。terms 聚集的 field 参数定义了提取词项的字段为 DestCountry, 它的词项在返回结果中会按词频由高到低依次展示,词频会在返回结果的 doc_count 字段中展示。另一个参数 size 则指定了只返回 10 个词项,这相当把 DestCountry 字段中词频前 10 名检索出来。
2.2 significant_terms 聚集
terms 聚集统计在字段中的词项及其词频,聚集结果会按各词项总的词频排序,并讲现次数最多的词项排在最前面,这非常适合做推荐及热词类的应用。但按词频总数不一定可能是总是正确的选择,在一些检索条件已知的情况下,一些词频总数比较低的词项反而是更合适的推荐热词。
举例来说,假设在 10000 篇技术类文章的内容中提到 Elasticsearch 有 200 篇,占比为 2%;但在文章标题含有 NoSQL 的 1000 篇文章中,文章内容提到Elasticsearch 的为 180 篇,占比为 18%。 这种占比显著的提升,说明在文章标题含有 NoSQL 的条件下,Elasticsearch 变得更为重要。换句话说,如果一个词项在某个文档子集中与在文档全集中相比发生了非常显著的变化,就说明这个词项在这个文档子集中是更为重要的词项。
significant_terms 聚集就是针对上述情况的一种聚集查询,它将文档和词项分为前景集 Foreground Set 和背景集(Background Set)。前景集对应一个文档子集,面背景集则对应文档全集。significant_terms 聚集根据 query 指定前景集,运算field 参数指定字段中的词项在前景集和背景集中的词频总数,并在结果的doc_coumt 和 bg_coumt 中保存它们。例如:
POST indexname/_search?filter_path=aggregations{
"query": {
"term": {
"OriginCountry": {
"value": "IE"
}
}
},
"aggs": {
"dest": {
"significant_terms": {
"field": "DestCountry"
}
}
}
}
在示例中,query 参数使用 DSL 指定了前景集为出发国家为 IE (即爱尔兰)的航班,而聚集查询中则使用 significant_ terms 统计到达国家的前景集词频和背景集词频。来看下返回结果:
在返回结果中,前景集文档数量为 119,背景集文档数量为 13059。
在 buckets 返回的所有词项中,国家编码为 GB 的航班排在第一位。它在前景集中的词频为 12,占比约为 10% (12/119); 而在背景集中的词频为 449,占比约为 3. 4% (445/13059)。
词项 GB 在前景集中的占比是背景集中的 3 倍左右,发生了显著变化,所以在这个前景集中 GB 可以被视为热词而排在第一位。GB 代表的国家是英国,从爱尔兰出发去英国的航班比较多想来也是合情合理的。
除了按示例方式使用 quey 参数指定前景集以外,还可以将 terms 聚集与significant_terms 聚集结合起来使用,这样可以一次性列出一个字段的所有前景集的热词。例如:
POST indexname/_search?filter_path=aggregations{
"aggs": {
"orgin_dest": {
"terms": {
"field": "OriginCountry"
},
"aggs": {
"dest": {
"significant_terms": {
"field": "DestCountry"
}
}
}
}
}
}
在示例中,使用 terms 聚集将 OriginCountry 字段的词项全部查询出来做前景集,然后再与 significant_terms 聚集起查询它们的热词。
2.3 significant_text 聚集
如果参与 significant_terms 聚集的字段为 text 类型,那么需要将字段的fielddata 机制开启,否则在执行时会返回异常信息。significant_text 聚集与significant_terms 聚集的作用类型,但不需要开启字段的 fielddata 机制,所以可以把它当成是种专门为 text 类型字段设计的 significant_terms 聚集。例如在kibana_sample_data_logs 中,message 字段即为 text 类型,如果想在这个字段上做词项分析就需要使用 significant_terms 聚集:
POST indexname/_search?filter_path=aggregations{
"query": {
"term": {
"response": {
"value": "200"
}
}
},
"aggs": {
"agent_term": {
"significant_text": {
"field": "message"
}
}
}
}
在示例中,前景集为响应状态码 response 为 200 的日志,significant_text 聚集则查看在这个前景集下 message 字段中出现异常热度的词项。返回结果片段:
通过展示的返回结果可以看出,排在第一位的词项 200 在前景集和背景集中的数量是一样的, 这说明 message 中完整地记录了 200 状态码;而排在第二位的词项 beats 前景集和背景集分别为 3462 和 3732。这说明请求“/beats" 地址的成功率要远高于其他地址。
significant_text 聚集之所以不需要开启fielddata机制是因为它会在检索时对text 字段重新做分析,所以 significant_text 聚集在执行时速度比其他聚集要慢很多。如果希望提升执行效率,则可以使用 sampler 聚集通过减少前景集的样本数量降低运算量。
2.4 样本聚集
sampler 聚集的作用是限定其内部嵌套聚集在运算时采用的样本数量。sampler 提取样本时会按文档检索的相似度排序,按相似度分值由高到低的顺序提取。例如:
POST indexname/_search?filter_path=aggregations{
"query": {
"term": {
"OriginCountry": {
"value": "IE"
}
}
},
"aggs": {
"sample_data": {
"sampler": {
"shard_size": 100
},
"aggs": {
"dest_country": {
"significant_terms": {
"field": "DestCountry"
}
}
}
}
}
}
在示例中共定义了 sample_ data 和 dest_country 两个聚集,其中dest_country 是 sample_data 聚集的子聚集或嵌套聚集,因此 dest_country 在运算时就只从分片上取一部分样本做运算。sampler 聚集的 shard_size 就是定义了每个分片上提取样本的数量,这些样本会根据 DSL 查询结果的相似度得分由高到低的顺序提取。
执行后会发现,这次目的地最热的目的地国家由 GB 变成了 KR,这就是样本范围缩小导致的数据失真。为了降低样本减少对结果准确性的影响,需要将些重复的数据从样本中剔除。换句话说就是样本更加分散,加大样本数据的多样性。Elasticsearch 提供的 diversified_sampler 聚集提供了样本多样性的能力,它提供了field 或 script 两个参数用于去除样本中可能重复的数据。由于相同航班的票价可能是相同的,所以可以将票价相同的航班从样本中剔除以加大样本的多样性,例如:
POST indexname/_search?filter_path=aggregations{
"query": {
"term": {
"OriginCountry": {
"value": "IE"
}
}
},
"aggs": {
"sample_data": {
"diversified_sampler": {
"shard_size": 100,
"field": "AvgTicketPrice"
},
"aggs": {
"dest_country": {
"significant_terms": {
"field": "DestCountry"
}
}
}
}
}
}
diversified_sampler 通过 field 参数设置了 AvgTicketPrice 字段,这样在返回结果中 GB 就又重新回到了第一位。
三、 单桶聚集
前面介绍的桶型聚集都是多桶型聚集,本章主要介绍单桶聚集。
单桶聚集在返回结果中只会形成一个桶,它们都有比较特定的应用场最。在Elasticsearch 中,单桶聚集主要包括 filter,global, missing 等几种类型。另外还有一种 filters 聚集,它虽然属于多桶聚集, 但与 filter 聚集很接近,所以放到一起说明。
3.1 过滤器聚集
过滤器聚集通过定义一个或多个过滤器来区分桶,满足过速器条件的文档将落入这个过滤器形成的桶中。过滤器聚集分为单桶和多桶两种,对应的聚集类型自然就是 filter 和 filters。
filter 桶型聚集属于单桶型聚集,一般会同时嵌套一个指标聚集,用于在过滤后的文档范围内计算指标,例如:
POST indexname/_search?size=0&filter_path=aggregations
{
"aggs": {
"origin_cn": {
"filter": {
"term": {
"OriginCountry": "CN"
}
},
"aggs": {
"cn_ticket_price": {
"avg": {
"field": "AvgTicketPrice"
}
}
}
},
"avg_price": {
"avg": {
"field": "AvgTicketPrice"
}
}
}
}
在示例中一共定义了 3 个聚集,最外层是两个聚集,最后聚集为嵌套聚集,origin_cn 聚集为单过滤器的桶型聚集,它将所有 OriginCountry 为 CN 的文档归入一桶。
origin_cn 桶型聚集嵌套了 cn_ticket_price 指标聚集, 它的作用是计算当前桶内文档 AvgTicketPrice 字段的平均值。另一个外层聚集 avg_price 虽然也是计算 AghckePie 字段的平均值,但它计算的是所有文档的平均值。实际上,使用 query与 agg 结合起来也能实现类似的功能,区别在于过滤器不会做相似度计算,所以效率更高一些也更灵活一些。
多过滤器与单过滤器的作用类似,只是包含有多个过滤器,所以会形成多个桶。多过滤博型聚集使用 filters 参数接收过滤条件的数组,一般也是与指标聚集一同使用。例:
POST indexname/_search?size=0&filter_path=aggregations
{
"aggs": {
"origin_cn_us": {
"filters": {
"filters": [{
"term": {
"OriginCountry": "CN"
}
},
{
"term": {
"OriginCountry": "US "
}
}]
},
"aggs": {
"avg_ price": {
"avg": {
"field": "AvgTicketPrice"
}
}
}
}
}
}
以上示例是使用两个过滤器计算从中国、美国出发的航班平均机票价格。
3.2 global 聚集
global 桶型聚集也是一种单桶型聚集, 它的作用是把索引中所有文档归入一个桶中。这种桶型聚集看似没有什么价值,但当 global 桶型聚集与 query 结合起来使用时,它不会受 query 定义的查询条件影响,最终形成的桶中仍然包含所有文档。global 聚集在使用上非常简单,没有任何参数。
POST indexname/_search?size=0&filter_path=aggregations
{
"query": {
"term": {
"Carrier": {
"value": "Kibana Airlines"
}
}
},
"aggs": {
"kibana_avg_delay": {
"avg": {
"field": "FlightDelayMin"
}
},
"all flights": {
"global": {
},
"aggs": {
"all_avg_delay": {
"avg": {
"field": "FlightDelayMin"
}
}
}
}
}
}
以上示例中 query 使用 term 查询将航空公司为“Kibana Airline"的文档都检索出来,而 kibana _avg delay 定义的平均值聚集会将它们延误时间的平均值计算出来。但另个 all_fights 聚集由于使用了 global 聚集所以在嵌套的 all_avg_delay 聚集中计算出来的是所有航班廷误时间的平均值。
3.3 missing 聚集
missing 聚集同样也是一种单桶型聚集,它的作用是将某一字段缺失的文档归入一桶。
missing 聚集使用 field 参数定义要检查缺失的字段名称,例如:
POST indexname/_search?filter_path=aggregations
{
"aggs": {
"no_price": {
"missing": {
"field": "AvgTicketPrice"
}
}
}
}
示例将 kibana_sample_data_flights 中缺失 AvgTicketPrice 字段的文档归入一桶,通过返回结果的 doc_count 查询数量也可以与指标聚集做嵌套,计算这些文档的某一指标值。