有个人长的像洋葱,走着走着就哭了…….

如何深入理解ES的聚合

Elasticsearch | 作者 Charele | 发布于2022年05月19日 | 阅读数:1387

前些天发过一个贴子,
https://elasticsearch.cn/question/12608
那只是为了说明深度优先和广度优先的区别,
那个里面也谈了一些基础概念(比如聚合本质上,是一个Lucene的收集器),那个概念是理解聚合的前提。
 
聚合分许多类型,我只说keyword类型的termAgg(就是桶聚合)
为了简化,不涉及子聚合 (也就没有深度还是广度那个东西了)

也不会深入的说search的执行过程,这是另外一个主题。
 先初步认识一下它长啥样:
111.png

 
已邀请:

Charele - Cisco4321

赞同来自:

1 先来说说聚合的分类,
111.png

可以把ES的聚合大致分成两类,Bcukets和Metrics,
 其中max这种聚合就属于Metrics,相对来说简单一些。
 另外,Buckets里边有一个类别,叫可延迟桶聚合(绿处),
这种,就是那种支持深度 / 广度选择的聚合类型,
如果不是这种的子类,估计参数加进去报错。
 
另外,不讨论pipleline,放一起说的话,过于复杂
 
pipeline可以和普通聚合联用,
但从代码上来说,你可以看到,它并没有父类,
它并不属于普通聚合这个范围,
222.png

Charele - Cisco4321

赞同来自:

2
想了一下,Terms聚合还是太复杂了。
如果你看网上文章和视频,会觉得好像桶聚合并不复杂,就是按要查的key创建n个桶,然后把数据放到相应的桶里去,几句话就说完了。
但ES代码远远没有这么简单!
 
  比如如何形成/取得ordinal,正式桶如何update成临时桶,临时桶又update成正式桶,
比如这里就是取得Top N里的桶号,如何形成这个,就得说半天。
111.png

太复杂了,有点只可意会,难以言述的感觉。
 
这个主题只专注于聚合的流程,而不是在于聚合的内部实现。
还是说简单的,max聚合。 
那个里面说道,聚合一般是这个样子形成的:
XXXAggregationBuilder -> XXXAggregatorFactory -> XXXAggregator
不是所有聚合都严格这样,但max聚合就是这样子的,
 
形成过程其实很重要,有时候它关系到参数(或相关数据是从哪来的),不过先不管。
我们直面这个:
222.png

Charele - Cisco4321

赞同来自:

3
查询大致可分成3类,
普通的,dfs的,还有那个preFilter的(当然还有其他特殊的:PIT的,异步的等等)
只说普通的:分为N个阶段,只关注前两个
 
阶段1: 查询  ---> 阶段2:取数据 ---> 3, 4,,,
 (另外,针对一个分片的查询,ES作了优化。个人感觉没啥意义,实际中谁会只用一个分片?)
 
 请求发到分片节点时,形成一个SearchContext(简称sc),对请求作解析(同时也会解析聚合),
后面的执行可以看成全是这个sc来完成的,
111.png

注意:这个includeAggregations,不是查询里面有没有聚合,
而是要不要解析聚合相关的东西。
 
阶段1是要解析的,阶段2就不解析了。
当然,如果你指定了size=0,跟本就不会执行阶段2的了(只走形式)
 
 
 

Charele - Cisco4321

赞同来自:

4 接上楼,
这里的解析做了什么呢,很简单,
XXXAggregationBuilder -> XXXAggregatorFactory -> XXXAggregator,
它生成了聚合工厂(中间这个)
然后存到sc里面。
222.png

1> 数组是因为一个查询里面会包含多个顶层聚合
2> 这里的工厂都是顶层聚合的工厂,并没有子聚合

Charele - Cisco4321

赞同来自:

5
下面就是聚合真正执行的地方了,3个地方,
其中红色的也是查询执行的地方
传进去的参数就是上面说的那个sc
111.png

 
这里分绿,红,紫,下面会分别说:

Charele - Cisco4321

赞同来自:

6
我拿这个最简单的max聚合做例子,找出最大的年纪
111.png

 
先说上面的绿框处,聚合的预处理 preProcess
222.png

1> 看名字,是创建顶层聚合器
其实这里它也同时创建了里面的子聚合器(如果有)
  2> 用“多重收集器”来包装一下
上面提到,ES聚合器,就是一个Lucene收集器。
这里用一个“多重收集器”对像来包装一下,
执行这个“多重收集器”,就等于执行里面每个收集器
(本例中,只有一个聚合,也会包装)
 3> 预收集
这个本例中无用,不说了。
  4> 下面还有一些,
比如你查询里有 "profile":"true" 参数,这个profile其实也是一个额外的收集器。这里略过。
 
结果保存在sc里面,sc是贯穿整个查询的

Charele - Cisco4321

赞同来自:

7
下面说说5楼的那个红框框里的,执行查询的地方。
 
下面说下聚合的大概意思。
比如一个普通查询(没有聚合),它也是一个Lucene收集器,作用是啥呢?
遇到一个文档,看看它符不符合你的查询条件,如果是,就把doc号收集保存起来。
然后传回到协调节点,在第2阶段,用doc号跟来取数据
 
聚合也是一个收集器,它的作用(拿我们这里的max来说)
先设一个maxAge,
遇到一个文档,看看里面的年令age,是不是大于这个maxAge,如果大,就把maxAge = age
最后,把这个maxAge传回协调节点。(最后多个分片的结果会合并,找出最大的)

111.png

这个就是把“聚合”加入到收集器列表里面。
 
查询时,查询收集器起作用,聚合收集器也在里面起作用。
实际查询时,执行是一个纯Lucene方法, 它不认识什么ES聚合,它只认识收集器。
上面说了,聚合也是一个收集器。
 下面看下max聚合器的实现:
 
 
 

Charele - Cisco4321

赞同来自:

8
下面的代码来自MaxAggregator这个类,
简单明了:
111.png

就是遇到一个doc号文档,和当前的比比看,如果这个更大,
就替换掉。
 
另外,那个bucket是用在子聚合里面的,
在我们的例子里,就一个顶层聚合,它就是0,忽略之。
 
 

Charele - Cisco4321

赞同来自:

9 下面讲5楼的最后一块,紫框框里的过程
 
其实这就是收集聚合结果的一个过程,
聚合结果,也是用一个类来表示的,InternalAggregation
(对于max聚合来说,结果是org.elasticsearch.search.aggregations.metrics.Max这个子类) 
111.png

跟上面的preCollection()一样,红框里的东西,对我们这个简单的查询来说,是无作用的。
下面看看绿框的过程,

222.png

因为我们没有子聚合,所以这里桶号就是0,就是把这个上面选出来的最大值,返回。
 
 
 

Charele - Cisco4321

赞同来自:

10
上面讲完了聚合在一个分片上的执行过程
没有子聚合,没有桶,所以显得很简单。
 
多个分片的聚合结果是要合并的,这样才能找出真正的max,下面说说合并过程。
合并过程发生在查询的第2阶段,在向分片节点发请求取数据之前,
111.png

 

Charele - Cisco4321

赞同来自:

11
继续:
222.png

看这两个条件,如果是一个分片(即一个聚合结果),一般认为是不需要合并的。
但是有些聚合,即使只有一个分片,也需要合并过程,比如global聚合。
 
我们这里的max聚合,如果是一个分片,是无须合并的。

Charele - Cisco4321

赞同来自:

12
合并过程也是很简单,找到最大的。
简单得连那个合并上下文reduceContext参数都用不上:-)
333.png

 
Okay,这儿只注重聚合的执行流程。
 
 
像带子聚合的桶聚合这种,大家可以自己去看代码理解,
 
那种复杂的,要完全理清,绝对不是件容易的事。

要回复问题请先登录注册