基数聚合
基数聚合属于聚合中的度量聚合,计算不同值的近似计数
需要注意,聚合出的结果是一个近似值,原因是底层结构使用的 HyperLogLog,具有一定误差
基本操作
假定 index 存储的售卖数据,希望查询有多少种不同的商品类型
1 | POST /sales/_search?size=0 |
以之前的学生 index 为例,查询有几个班级下有女学生
1 | { |
返回结果,选取 hits
和 aggregations
部分
1 | { |
因为 size
为 0,所以 hits
中没有返回的文档
aggregations
下自定义结果集名称 class_count
返回的值为 2,说明班级下有女学生的班级数量为 2
近似值与精确阈值参数
由于返回结果是一个近似值,所以 cardinality
支持
precision_threshold
参数,用来设置精确阈值
阈值参数的本质是使用空间换取准确性,默认是 3000,最大支持 40000,超过 40000 的设置为按照 40000 来处理
低于阈值的计数更符合准确值,高于阈值的计数则会更加模糊
1 | { |
计算精确的计数需要将值加载至哈希集合并返回大小,当基数特别大时,需要占用非常多资源
所以 ES 选择 HyperLogLog 算法来实现
cardinality
,这种方式基于值的哈希计数,有一些特性:
- 可配置的精度,可以选择用内存换取精度
- 低基数集合中是精准的
- 固定的内存使用率,无论唯一值有多少,只取决于设置的精度
HyperLogLog + + 算法依赖于散列值的前导零,数据集中散列的精确分布会影响基数的准确性
ES 官方文档提供的数据显示,具体的准确性取决于数据集情况,但大部分场景的准确性都还是良好的,即使阈值设置为 100,在百万、千万基数下,误差范围也控制在了 1% ~ 6%;当阈值设置为 10000,误差基本在 1% 左右
脚本 runtime field
cardinality
像其他聚合操作一样,是可以使用脚本拼接字段的
如果希望对两个字段的组合进行操作,创建一个 runtime field 组合他们然后进行基数聚合
1 | { |
返回结果是 6,因为有两个文档的 runtime field
gender_and_age
值都为 male&20
1 | { |
HyperLogLog
HyperLogLog 是一种基数计数的算法,旨在使用较少内存的情况下得到基数数量的近似精确值
基数计数方式
基数计数(cardinality counting)通常用来统计一个集合中不重复的元素个数,例如统计某个网站的 UV,或者用户搜索网站的关键词数量等
如何计算计数常见的有多种方式
- B 树
- 利用树存储具体元素
- 优点:插入和查找效率高,统计数据时可以快速做到记录与去重,计算基数时只需要统计叶子节点个数即可
- 缺点:没有节省内存,需要存储数据全集
- bitmap
- 使用 bit 位进行标记,即抛弃具体元素内容只存储特征值
- 优点:节约内存,结果可以进行方便的位运算操作(合并 - 与,差集 - 或)
- 缺点:空间占用仍然较大
- 概率算法
- 目前用于基数计数的概率算法有:LC、LLC、HLL 等
- 优点:LL、HLL 等能够在空间更小的情况下进行基数计算
- 缺点:有一定误差
HLL
HLL 中实际存储的是一个长度为 m 的大数组 S,将待统计的数据集合划分成 m 组,每组根据算法记录一个统计值存入数组中
数组的大小 m 由算法实现方自己确定,Redis 中这个数组的大小是 16834,m 越大误差越小,但需要的内存空间也越大
HLL 的数学原理看不懂,大致就是 n 重伯努利原理
通过多次抛硬币,直到抛到正面为止,这是一次伯努利过程;当一直抛硬币,直到多次出现正面,将出现正面的投掷次数值记为 k1、k2、k3...kn,最大值记为 kmax,那么可以得到以下结论:
- n 次伯努利过程的投掷次数都不大于 kmax
- n 次伯努利过程,至少有一次投掷次数等于 kmax
最后经过一系列复杂的推论,最终的结果就是:进行了 n 次进行抛硬币实验,每次分别记录下第一次抛到正面的抛掷次数 k,那么可以用 n 次实验中最大的抛掷次数 kmax 来预估实验组数量 n;这就是 HLL 算法的数学理论基础
此外 HLL 为了提高准确性也做了其他优化
分桶平均
HLL 的基本思想是利用集合中数字的比特串第一个1出现位置的最大值来预估整体基数,但是这种预估方法存在较大误差,为了改善误差情况,HLL 中引入分桶平均的概念
为了避免一组实验中的运气影响,将统计数据划分多个桶,每个桶各自统计自己的 kmax 计算基数预估值,最终对各个桶的基数预估值进行合并,使用调和平均数的方式进一步降低误差
偏差修正
虽然调和平均数能够适当修正算法误差,但作者给出一种分阶段修正算法
当 HLL 算法开始统计数据时,统计数组中大部分位置都是空数据,并且需要一段时间才能填满数组,这种阶段引入一种小范围修正方法;当 HLL 算法中统计数组已满的时候,需要统计的数据基数很大,这时候 hash 空间会出现很多碰撞情况,这种阶段引入一种大范围修正方法
参考
Cardinality aggregation | Elasticsearch Guide 8.5