ElasticSearch Terms Aggregation 聚合

ElasticSearch(后续简称为ES)提供了对数据的统计分析服务。在之前的开发中使用Terms Aggregation 对数据进行聚合统计,遇到了一些问题,查阅了ES的官方文档和技术博文了解Terms Aggregation的用法。

一、聚合基本概念

ES 聚合的两个主要概念:

  • 桶(Buckets):满足特定条件的文档的集合
  • 指标(Metrics):对桶内的文档进行统计计算

每个聚合都是一个或者多个桶和另个或者多个指标的组合,用粗略的SQL语句解释就是:

SELECT COUNT(1) FROM table GROUP BY field

其中,COUNT(1) 相当于指标, GROUP BY field 相当于桶。

简单例子:

GET /_search
{
    "aggs" : {
        "genres" : {
            "terms" : { "field" : "genre" }
        }
    }
}

response:

{
    "aggregations" : {
        "genres" : {
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0,
            "buckets" : [
                {
                    "key" : "electronic",
                    "doc_count" : 6
                },
                {
                    "key" : "rock",
                    "doc_count" : 3
                },
                {
                    "key" : "jazz",
                    "doc_count" : 2
                }
            ]
        }
    }
}

terms aggregation 应该是一个 keyword 类型或者其他适合桶聚合的数据类型字段。如果想要在 text 上使用,需要开启 fielddata。
默认的,terms aggregation 会根据 doc_count 返回前10个 terms 的桶。

二、Size 参数

如果我们想返回所有的聚合桶该怎么办呢?在低版本的 ES 中,如2.x 版本 ,可以设置 “size”:0 来保证全部返回,代表 Global,但是在高版本的ES中,如 ES6+ 版本,size 的值必须设置大于0。
上文提到 terms aggregation 默认返回 doc_count 前10个 terms 的桶。Size 参数可以设置返回的 terms 桶个数。ES 是一个分布式的搜索引擎,每个索引都可以有多个分片,用来将一份大索引的数据切分成多个小的物理索引,解决单个索引数据量过大导致的性能问题,另外每个 shard 还可以配置多个副本,来保证高可靠以及更好的抗并发的能力。将一个索引切分成多个 shard,大多数时候是没有问题的,但是在 ES 里面如果索引被切分成多个 shard,在使用 terms aggregation 进行聚合时,可能会出现问题。

默认情况下,节点在搜索过程中会请求每个 shard 它们自己前几的 term 桶,一旦所有的分片响应,它就会减少结果到最终列表返回给客户端。这就意味着如果特有的 terms 数量大于 Size,那么返回的列表就有可能会稍有偏差不精确。就是说 term 统计的偏差可能会导致本该在前几中返回的桶而未被返回。 官方建议:如果想要在嵌套 terms aggregation 检索所有的 terms 或 terms 组合,应该使用 Composite aggregation ,它可以进行分页,而 terms aggregation 旨在返回前几的 terms。

文档统计是近似的
在 terms aggregation 中文档统计并不总是精确的。这是因为每个 shard 提供各自满足的 terms 有序列表的视图,它们会被组合起来给到最终视图。

考虑以下场景:
从有3个 shard 的索引请求以 product 字段聚合后按照文档数量降序排序的前5 terms。在下面的例子中,每个 shard 需要给出各自的前5 terms。

GET /_search
{
    "aggs" : {
        "products" : {
            "terms" : {
                "field" : "product",
                "size":5
             }
        }
    }
}

该索引每个 shard 各自的文档数量:

序号Shard AShard BShard C
1Product A (25)Product A (30)Product A (45)
2Product B (18)Product B (25)Product C (44)
3Product C (6)Product F (17)Product Z (36)
4Product D (3)Product Z (16)Product G (30)
5Product E (2)Product G (15)Product E (29)
6Product F (2)Product H (14)Product H (28)
7Product G (2)Product I (10)Product Q (2)
8Product H (2)Product Q (6)Product D (1)
9Product I (1)Product J (6)
10Product J (1)Product C (4)

每个 shard 返回的前五 terms:

序号Shard AShard BShard C
1Product A (25)Product A (30)Product A (45)
2Product B (18)Product B (25)Product C (44)
3Product C (6)Product F (17)Product Z (36)
4Product D (3)Product Z (16)Product G (30)
5Product E (2)Product G (15)Product E (29)

将3个 shard 返回的 terms 列表组合起来返回的前5 terms:

序号Product
1Product A (100)
2Product Z (52)
3Product C (50)
4Product G (45)
5Product B (43)

Product A 从所有 shard 返回,所以它的文档统计是准确的。Product C 由 Shard A 与 Shard C 返回,显示的文档统计时50,但并不是一个准确的统计。Product C 在 Shard B 由于统计值只有4而不够高没有被 shard 放入返回的前5 term 列表中。Product Z 也只由两个 shard 返回,但不存在于第3个 shard 中。因此,在合并结果生成最终的 terms 列表时没有办法知道是 Product C 还是 Product Z 的文档统计错误。 Product H 在三个 shard 中的文档统计和为44,但它没有包含在最终的 terms 列表中,那是因为它在每个 shard 中的文档统计均未排到前5。

三、Shard Size 参数

请求的 Size 越大,结果越准确,但是计算最终结果的成本也越高。原因主要有两个:其一,在 shard 级别对更大优先级队列的管理;其二,在节点与客户端直接有更大的数据传输。shard_size 参数可用于最小化更大的请求 size 所带来的额外工作。它决定了协调节点从每个节点请求的 terms 数量。一旦所有 shard 都响应了,协调节点就会基于 size 参数将它们减少到最终的结果。这样,就提高了返回 terms 的准确性,并且避免将大量存储桶流回到客户端的开销。shard_size 不能小于 size,当其值小于 size,ES 会将它重置到 size 的大小。默认情况下,shard_size =(size * 1.5 + 10)。

四、Aggregation的一些问题

1.DSL聚合时默认展示10个桶的数据,如果桶的数量大于10,需要在分桶的field上加上size指定返回的数量。

"terms":{
        "field":"product",
         "size": 50
 }

2.聚合结果的doc_count字段代表文档的数量。

3.cardinality
cardinality 类似SQL里的distnict && count(统计去重后的数量)是近似值
(1)精度可配置,用来控制内存的使用(更精确 = 更多内存),设置阈值越大,越精确,但是容易造成OOM;
(2)阈值precision_threshold 参数的值定义了在何种基数水平下我们希望得到一个近乎精确的结果,基数在阈值以下几乎100%精确;
(3)precision_threshold 接受 0–40,000 之间的数字,更大的值还是会被当作 40,000来处理。

4.“execution_hint”: “map”使用
在计算离散度比较大的字段统计值时,合理修改Terms Aggregation的执行方式,可以节省内存和提高计算速度。“execution_hint”: “map” 在查询结果集小的情况下速度很快,但如果结果集很大,map方式不一定比默认的执行方式快。

"terms":{
        "field":"product",
         "execution_hint": "map"
 }

其中的解释引用自elastic中文社区query+aggs查询性能问题kennywu76的回答。

Terms Aggregation默认的计算方式并非直观感觉上的先查询,然后在查询结果上直接做聚合。ES假定用户需要聚合的数据集是海量的,如果将查询结果全部读取回来放到内存里计算,内存消耗会非常大。因此ES利用了一种叫做global ordinals的数据结构来对聚合的字段来做bucket分配,这个ordinals用有序的数值来代表字段里唯一的一个字符串,因此为每个ordinals值分配一个bucket就等同于为每个唯一的term分配了bucket。 之后遍历查询结果的时候,可以将结果映射到各个bucket里,就可以很快的统计出每个bucket里的文档数了。这种计算方式主要开销在构建global ordinals和分配bucket上,如果索引包含的原始文档非常多,查询结果包含的文档也很多,那么默认的这种计算方式是内存消耗最小,速度最快的。如果指定execution_hint:map则会更改聚合执行的方式,这种方式不需要构造global ordinals,而是直接将查询结果拿回来在内存里构造一个map来计算,因此在查询结果集很小的情况下会显著的比global ordinals快。要注意的是这中间有一个平衡点,当结果集大到一定程度的时候,map的内存开销带来的代价可能就抵消了构造global ordinals的开销,从而比global ordinals更慢,所以需要根据实际情况测试对比一下才能找好平衡点。

参考资料

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值