如无特别说明,本文讨论的内容均基于 es 7.*
Term Aggregation的深度优先以及广度优先
Term aggregation 是我们常用的聚合查询,对于有 父子聚合 的场景下,理解其执行父聚合以及子聚合的时机对我们优化聚合查询有很大的帮助,collect mode就指定了它们的执行时机,共有两种模式,一种是depth first,另一种是breadth first。
depth first
一般来说,depth first适合大多数场景,因为大多数情况下需要聚合的字段不会是大量的唯一值。
- 聚合过程
- 1.先计算出一个桶,然后再根据这个桶计算出子聚合的结果,构建出一棵聚合树。然后不断重复前面的过程,直到所有的桶计算完毕。
- 2.对步骤一计算出的结果进行排序,也就是对各个聚合树进行排序。
- 3.根据过滤条件和 size 等参数修剪结果。
- 适合场景
- 大多数term都是重复的
- 要求返回的aggs size比较大
- 原因:不需要缓存父聚合的doc_id,直接聚合成一棵棵聚合树,每棵聚合树的每个节点数据结构为(value, doc_count),并且大多数的聚合树不会被修剪
- 聚合计算下,多层聚合会让一个文档与其他文档产生关联,也就是说会形成一棵棵聚合树。深度优先就是先计算获得所有的聚合树,然后再进行后续处理。
breadth first
- 聚合过程
- 1.先计算出第一层聚合的结果。
- 2.根据步骤一得出的结果进行排序。
- 3.根据过滤条件和 size 等参数修整第一层节点。
- 4.根据各个节点进行后续的子聚合计算。
- 适合场景
- 大多数term都是唯一的
- 要求返回的aggs size 比较小
- 原因:缓存父聚合的doc_id,聚合树第一层节点,即根节点,的数据结构为(value, doc_count, set(doc_id)),为了保证使用的缓存尽量小,则 avg(doc_count_per_bucket) * size(buckets) 尽量小
- 对于基数大于请求的大小的字段或基数未知的字段(例如,数字字段或脚本),默认值为breadth_first
- 当使用breadth_first模式时,属于最上层存储桶的文档集将被缓存以供后续重播,因此这样做会产生内存开销,该开销与匹配文档的数量成线性关系。
- 使用广度优先设置时,仍可以使用order参数来引用子聚合中的数据。父级聚合知道需要先调用此子级聚合,然后再调用其他任何子级聚合。
- 这里的主要意思是,如果父聚合的排序时候使用的是子聚合的结果,则会在执行父聚合前先执行该子聚合。
depth first vs. breadth first
- 关键点:
- 父子聚合的组合数的多少 + 返回buckets数量大小(分别对应计算的难度+使用率)
- 父聚合字段的基数大,父子聚合的组合数多,需要返回的buckets数量小,适合广度优先
- 父聚合字段的基数小,父子聚合的组合数少,需要返回的buckets数量大,适合深度优先
- 比如:
- 父聚合字段有10000,父子聚合的组合数约为 100000,需要返回buckets为5,则可能适合广度优先
- 父聚合字段有10,父子聚合的组合数约为 100,需要返回buckets为100,则可能适合深度优先
- 父子聚合的组合数的多少 + 返回buckets数量大小(分别对应计算的难度+使用率)
Term Aggregation的Execution hint
global_ordinals
我们知道doc values存储的时候,会对应original value分配一个ordinal以减小磁盘的使用,也减小聚合过程中内存的使用。
- 聚合过程
- segment级别的聚合:
- 在聚合字段的doc values数据结构中,完成 (doc_id, ordinal) -> (ordinal, set(doc_id)) 的聚合计算
- shard级别的聚合:
- 在shard内部多个segment之上构建 global ordinla map,其数据结构为 (segment_id, ordinal, global ordinal),当ordinal对应的初始值相同时,其对应的global ordinal也相同。
- 根据 global ordinla map,完成 (ordinal, set(doc_id)) -> (global ordinal, set(doc_id) 的转换。
- 根据 global ordinal 进行分桶,并根据doc count进行排序,选出前n个桶,完成 (global ordinal, set(doc_id) -> (global ordinal, doc count) 的聚合计算。
- 根据 global ordinal map以及segment的doc values,把global ordinal替换成原始值,完成 (global ordinal, doc count) -> (segment_id, ordinal, doc count) -> (original value, doc count) 的转换。
- index级别的聚合:
- 在协调节点完成全局前n个结果的聚合。
- segment级别的聚合:
- global ordinals 的有效性
- 因为global ordinals为shard上的所有segment提供了统一的map,所以当新的segment变为可见时(常见为refresh的时候),还需要完全重建它们。所以,global ordinals 更加适用于 历史数据。
map
相对而言map更加简单,主要的不同点在于shard级别聚合的时候不再构建global ordinal map,而是直接返回original value到shard
- 聚合过程
- segment级别的聚合:
- 在聚合字段的doc values数据结构中,完成 (doc_id, ordinal) -> (ordinal, set(doc_id)) 的聚合计算
- 把ordinal替换成初始值,完成 (ordinal, set(doc_id)) -> (original, set(doc_id)) 的转换
- shard级别的聚合:
- 根据 original value进行分桶,并根据doc count进行排序,选出前n个桶,完成 (original, set(doc_id) -> (original, doc count) 的聚合计算。
- index级别的聚合:
- 在协调节点完成全局前n个结果的聚合。
- segment级别的聚合:
global_ordinals vs. map
- 适合global_ordinals模式
- 聚合字段基数不大
- refresh 间隔比较大
- 不再写入的索引,比如历史索引
- 开启 eager_global_ordinals 配置,在写入索引时就构建global ordinals map。但是可能会对写入索引的速度有影响
- 适合map模式
- 聚合字段基数很大
- 需要注意可能会引起更大的内存消耗量