GeekIBLi

统计去重数据 (近似度量)

2021-07-08

cardinality用法

常用写法如下👇
curl -X GET "localhost:9200/cars/transactions/_search?pretty" -H 'Content-Type: application/json' -d'

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"size" : 0,
"aggs" : {
"months" : {
"date_histogram": {
"field": "sold",
"interval": "month"
},
"aggs": {
"distinct_colors" : {
"cardinality" : {
"field" : "color"
}
}
}
}
}
}

精度问题

cardinality 度量是一个 近似算法。 它是基于 HyperLogLog++ (HLL)算法的。 HLL 会先对我们的输入作哈希运算,然后根据哈希运算的结果中的 bits 做概率估算从而得到基数。

我们不需要理解技术细节, 但我们最好应该关注一下这个算法的 特性 :

  • 可配置的精度,用来控制内存的使用(更精确 = 更多内存)。
  • 小的数据集精度是非常高的。
  • 我们可以通过配置参数,来设置去重需要的固定内存使用量。无论数千还是数十亿的唯一值,内存使用量只与你配置的精确度相关。

要配置精度,我们必须指定 precision_threshold 参数的值。 这个阈值定义了在何种基数水平下我们希望得到一个近乎精确的结果。参考以下示例:

curl -X GET "localhost:9200/cars/transactions/_search?pretty" -H 'Content-Type: application/json' -d'

1
2
3
4
5
6
7
8
9
10
11
{
"size" : 0,
"aggs" : {
"distinct_colors" : {
"cardinality" : {
"field" : "color",
"precision_threshold" : 100
}
}
}
}

⚠️ ⚠️
precision_threshold 接受 0–40000 之间的数字,更大的值还是会被当作 40000 来处理

示例会确保当字段唯一值在 100 以内时会得到非常准确的结果。尽管算法是无法保证这点的,但如果基数在阈值以下,几乎总是 100% 正确的。高于阈值的基数会开始节省内存而牺牲准确度,同时也会对度量结果带入误差。

对于指定的阈值,HLL 的数据结构会大概使用 precision_threshold * 8 字节的内存,所以就必须在牺牲内存和获得额外的准确度间做平衡。

在实际应用中, 100 的阈值可以在唯一值为百万的情况下仍然将误差维持 5% 以内

速度问题

如果想要获得唯一值的数目, 通常 需要查询整个数据集合(或几乎所有数据)。 所有基于所有数据的操作都必须迅速,原因是显然的。 HyperLogLog 的速度已经很快了,它只是简单的对数据做哈希以及一些位操作。

但如果速度对我们至关重要,可以做进一步的优化。 因为 HLL 只需要字段内容的哈希值,我们可以在索引时就预先计算好。 就能在查询时跳过哈希计算然后将哈希值从 fielddata 直接加载出来。

预先计算哈希值只对内容很长或者基数很高的字段有用,计算这些字段的哈希值的消耗在查询时是无法忽略的。
尽管数值字段的哈希计算是非常快速的,存储它们的原始值通常需要同样(或更少)的内存空间。这对低基数的字符串字段同样适用,Elasticsearch 的内部优化能够保证每个唯一值只计算一次哈希。
基本上说,预先计算并不能保证所有的字段都更快,它只对那些具有高基数和/或者内容很长的字符串字段有作用。需要记住的是,预计算只是简单的将查询消耗的时间提前转移到索引时,并非没有任何代价,区别在于你可以选择在 什么时候 做这件事,要么在索引时,要么在查询时。

创建索引时添加如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PUT /cars/
{
"mappings": {
"transactions": {
"properties": {
"color": {
"type": "string",
"fields": {
"hash": {
"type": "murmur3"
}
}
}
}
}
}
}

多值字段的类型是 murmur3 ,这是一个哈希函数。

现在当我们执行聚合时,我们使用 color.hash 字段而不是 color 字段:
curl -X GET "localhost:9200/cars/transactions/_search?pretty" -H 'Content-Type: application/json' -d'

1
2
3
4
5
6
7
8
9
10
{
"size" : 0,
"aggs" : {
"distinct_colors" : {
"cardinality" : {
"field" : "color.hash"
}
}
}
}

现在 cardinality 度量会读取 “color.hash“ 里的值(预先计算的哈希值),取代动态计算原始值的哈希。

单个文档节省的时间是非常少的,但是如果你聚合一亿数据,每个字段多花费 10 纳秒的时间,那么在每次查询时都会额外增加 1 秒,如果我们要在非常大量的数据里面使用 cardinality ,我们可以权衡使用预计算的意义,是否需要提前计算 hash,从而在查询时获得更好的性能,做一些性能测试来检验预计算哈希是否适用于你的应用场景。。