深入Elasticsearch度量聚集(1)
本文主要聚集elasticsearch的数值类型度量聚集,主要有两种类型,一种生成单值聚集,另一个生成多值聚集。单值度量聚集主要有平均数、加权平均数,最小值、最大值以及基数。多值聚集包括统计聚集、扩展统计聚集。
1. 环境准备
为了演示上述度量聚集,我们需要创建sports
索引,并存储一些文档。索引数据结果如下:
PUT /sports
{
"mappings": {
"properties": {
"birthdate": {
"type": "date",
"format": "dateOptionalTime"
},
"location": {
"type": "geo_point"
},
"name": {
"type": "keyword"
},
"rating": {
"type": "integer"
},
"sport": {
"type": "keyword"
},
"age": {
"type":"integer"
},
"goals": {
"type": "integer"
},
"role": {
"type":"keyword"
},
"score_weight": {
"type": "float"
}
}
}
}
准备好环境后,我们首先从最常用的单值聚集开始,先说平均值。
2. 单值度量聚集
2.1. 平均数聚集
平均数聚集计算文档中数值类型字段的算术平均数。和其他度量聚集一样,平均数需要数值类型或脚本生成数值。本文主要提及第一类场景
下面我们计算所有运动员的平均年龄:
GET /sports/_search?size=0
{
"aggs" : {
"avg_age" : {
"avg" : { "field" : "age" }
}
}
}
我们指定字段为age
,聚集类型为avg
。输出结果为:
{
"took" : 5104,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 22,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"avg_age" : {
"value" : 27.318181818181817
}
}
}
返回聚集结果包括在aggregations
对象中。该对象包括平均年龄聚集值为27.318。
下面考虑稍微复杂点,计算特定类型运动员的平均年龄:足球、篮球、曲棍球、手球。需要平均数聚集和分组聚集一起使用。分组聚集基于一定条件进行分组,然后对每个分组计算平均数。
我们使用关键词聚集terms
,它为每个值生成一个分组,示例文档中有四类运动,因此会生成四个分组。
GET /sports/_search?size=0
{
"aggs": {
"sport_type": {
"terms": {
"field": "sport"
},
"aggs": {
"avg_age": {
"avg": {
"field": "age"
}
}
}
}
}
}
我们指定sport
字段作为分组条件,age
字段作为平均值度量。结果响应:
"aggregations" : {
"sport_type" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "Football",
"doc_count" : 9,
"avg_age" : {
"value" : 26.444444444444443
}
},
{
"key" : "Basketball",
"doc_count" : 5,
"avg_age" : {
"value" : 28.6
}
},
{
"key" : "Hockey",
"doc_count" : 5,
"avg_age" : {
"value" : 27.4
}
},
{
"key" : "Handball",
"doc_count" : 3,
"avg_age" : {
"value" : 27.666666666666668
}
}
]
}
}
如我们期望的一样,生成了四个分组,每个分组对象包括分组名称key
,分组内的文档数量doc_count
以及每组的平均年龄。我们看到最高平均年龄是篮球组28.6
。
2.2. 缺省值
有时文档中的目标字段可能为空。度量聚集的缺省行为是简单忽略这些文档,但是我们改变设置让缺失值有个默认值。
GET /sports/_search?size=0
{
"aggs" : {
"avg_grade" : {
"avg" : {
"field" : "grade" ,
"missing": 20
}
}
}
}
当grade字段没有值时取缺省值20.
2.3. 加权平均聚集
加权平均聚集从6.4版本引入。为了使用该聚集,首先需要理解常规平均数与加权平均数之间的差异。当计算算数平均数时,所有数值权重相等。而加权平均数中每个数值拥有不同的权重,计算公式为:∑(value * weight) / ∑(weight)
。
我们看看sports索引为什么需要使用加权平均数代替普通的算术平均数。在不同的运动项目中,最佳射手的进球总数有时会有很大的不同。例如,平均而言,曲棍球运动员比足球运动员进球多,篮球运动员比曲棍球运动员得分多。
第二,得分频率通常取决于球员的场上位置。前锋比中场得分多,中场比后卫得分多。如果我们计算的平均得分不考虑这些差异,结果可能会偏向于高频得分运动和得分较高的位置,比如前锋。
我们可以通过计算每项运动的平均得分来解决第一个问题。第二个问题可以通过给不同的位置分配不同的权重来解决。最高的权重可以分配给后卫,因为他们得分较少(因此如果他们得分,权重会更多),而最低权重分配给前锋,因为得分是他们该做的事情。我们在score_weight字段中实现了这个想法,该字段的权值为2(前锋)、3(中场)和4(后卫)。这些权重将保证最终结果正确反映平均得分。相对于这些权重,常规平均值可认为是加权平均值的特殊情况,只不过其中每个值隐含权重为1。
注意这些值是随意给的,并不代表每个位置实际得分频率。设置这些权重仅为了说明加权平均聚集是如何工作的。
GET /sports/_search?size=0
{
"aggs" : {
"scoring_weighted_average": {
"terms": {
"field": "sport"
},
"aggs": {
"weighted_goals_in_sport": {
"weighted_avg": {
"value": {
"field": "goals"
},
"weight": {
"field": "score_weight"
}
}
}
}
}
}
}
响应结果:
"aggregations" : {
"scoring_weighted_average" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "Football",
"doc_count" : 9,
"weighted_goals_in_sport" : {
"value" : 53.214285714285715
}
},
{
"key" : "Basketball",
"doc_count" : 5,
"weighted_goals_in_sport" : {
"value" : 1147.090909090909
}
},
{
"key" : "Hockey",
"doc_count" : 5,
"weighted_goals_in_sport" : {
"value" : 134.30769230769232
}
},
{
"key" : "Handball",
"doc_count" : 3,
"weighted_goals_in_sport" : {
"value" : 212.77777777777777
}
}
]
}
}
我们比较两个平均值之间的差异:
GET /sports/_search?size=0
{
"aggs": {
"sports":{
"terms" : { "field" : "sport" },
"aggs": {
"avg_goals":{
"avg": {"field":"goals"}
}
}
}
}
}
响应结果:
"aggregations" : {
"sports" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "Football",
"doc_count" : 9,
"avg_goals" : {
"value" : 54.888888888888886
}
},
{
"key" : "Basketball",
"doc_count" : 5,
"avg_goals" : {
"value" : 1177.0
}
},
{
"key" : "Hockey",
"doc_count" : 5,
"avg_goals" : {
"value" : 139.2
}
},
{
"key" : "Handball",
"doc_count" : 3,
"avg_goals" : {
"value" : 245.33333333333334
}
}
]
}
}
可以看到在两者的对比情况,手球(245.3 vs. 212.7)、篮球(1177 vs. 1147)、曲棍球(139.2 vs. 134.3)和足球(54.8 vs. 53.2),常规平均值比加权平均值明显高。如果权重表示计算值中的真实模式,那么加权平均值往往比常规平均值更准确。
2.4. 基数聚集
基数聚集计算文档中特定字段的唯一值。我们对运动类型字段应用基数聚集:
GET /sports/_search?size=0
{
"aggs": {
"sports":{
"cardinality" : { "field" : "sport" }
}
}
}
响应结果为:
{
"took" : 3,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 22,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"sports" : {
"value" : 4
}
}
}
计算运动类型基数花费的时间并不多,而且内存占用也不是很多,因为我们的索引中只有4个体育项目。但如果索引有很多个惟一值,则计算基数聚合可能会消耗更多资源内存。例如,为我们的22项索引计算年龄基数显然会使用更多的计算资源:
{
"took" : 2,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 22,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"sports" : {
"value" : 16
}
}
}
如果索引有数千个文档,那么基数聚集会非常消耗内存。准确的基数基数需要载入所有值至hash set中,然后返回其大小。这种方法在高基数集合上不能很好地扩展,因为它需要更多的内存,并且在分布式集群环境中会导致高延迟。
Elasticsearch如何解决这个问题?Elasticsearch底层基于HyperLogLog++算法计算基数聚集,其特点如下:
- 可配置精度,它决定了如何用内存交换精度;
- 在低基数集上具有出色的准确性;
- 固定内存使用:无论索引中有多少文档,内存使用都取决于配置的精度。
换句话说,如上面的例子,基数非常低,那么算法计算是完全准确的。如果数据集基数非常高,则可以设置precision_threshold
来交换内存的准确性。此设置定义了最大计数阈值,低于该值计数应该接近准确。超过该值计数可能会变得不那么精确。最大值为40000。
2.5. 最小、最大聚集
最小、最大聚集是简单的单值聚集,用于计算文档中数值字段的最大、最小值。
GET /sports/_search?size=0
{
"aggs": {
"max_age":{
"max" : { "field" : "age" }
}
}
}
响应结果显示最大年龄为41:
{
"took" : 51,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 22,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"max_age" : {
"value" : 41.0
}
}
}
最小年龄聚集:
GET /sports/_search?size=0
{
"aggs": {
"min_age":{
"min": { "field" : "age" }
}
}
}
响应显示最下年龄为18:
{
"took" : 41,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 22,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"min_age" : {
"value" : 18.0
}
}
}
和前面示例一样,我们可以获取不同类型运动员的最大、最小年龄:
GET /sports/_search?size=0
{
"aggs": {
"sports":{
"terms": {"field":"sport"},
"aggs": {
"max_age":{
"max": {"field":"age"}
},
"min_age":{
"min": {"field":"age"}
}
}
}
}
}
响应如下:
"aggregations" : {
"sports" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "Football",
"doc_count" : 9,
"max_age" : {
"value" : 35.0
},
"min_age" : {
"value" : 19.0
}
},
{
"key" : "Basketball",
"doc_count" : 5,
"max_age" : {
"value" : 36.0
},
"min_age" : {
"value" : 18.0
}
},
{
"key" : "Hockey",
"doc_count" : 5,
"max_age" : {
"value" : 41.0
},
"min_age" : {
"value" : 18.0
}
},
{
"key" : "Handball",
"doc_count" : 3,
"max_age" : {
"value" : 29.0
},
"min_age" : {
"value" : 25.0
}
}
]
}
}
这里同时返回了最大、最小两个值,下面我们看多值度量聚集。
3. 多值度量聚集
前面主要讨论了单值聚集,Elasticsearch也提供了多值聚集————统计聚集和扩展统计聚集,用于文档中数值类型字段,生成不同的统计值度量,最大、最小、平均、求和、计数、标准差、平方差、平方和等,在一个对象中返回多个值。扩展统计聚集非常方便一次获得所有统计度量。
GET /sports/_search?size=0
{
"aggs": {
"age_stats":{
"extended_stats": {"field":"age"}
}
}
}
上面聚集计算所有文档的年龄统计度量。extended_stats
聚集针对数值类型进行计算,响应结果如下:
"aggregations" : {
"age_stats" : {
"count" : 22,
"min" : 18.0,
"max" : 41.0,
"avg" : 27.318181818181817,
"sum" : 601.0,
"sum_of_squares" : 17181.0,
"variance" : 34.67148760330581,
"std_deviation" : 5.888249961007584,
"std_deviation_bounds" : {
"upper" : 39.09468174019698,
"lower" : 15.541681896166649
}
}
}
扩展统计聚集中最重要的是标准差。这时主要统计指标:衡量一组数据的变量量。标准差低表示数据接近平均值,反之高标准差表示数据分布在更大值范围内。
除了常规的标准偏差之外,extended stats聚合还返回一个名为std_deviation_bounds的对象,该对象提供了离均值正负两个标准差的间隔。这个度量对于可视化数据中的差异非常有用。如果你想要一个不同的边界,例如,三个标准差,你可以在要求设置sigma
参数:
GET /sports/_search?size=0
{
"aggs": {
"age_stats":{
"extended_stats": {
"field":"age",
"sigma":3
}
}
}
}
sigma
参数控制在平均值的基础上加减多少个标准差。注意,为了使标准偏差显示准确的值,数据需服从正态分布。
4. 总结
本文讨论几个Elasticsearch度量聚集。单独使用时,度量聚集从数据中反应了许多有用的见解。如果将度量聚集与分组聚集组合使用,可以更深入地了解各种类别的数据的度量。