Skip to main content
 首页 » 编程设计

深入Elasticsearch度量聚集(1)

2022年07月19日133insus

深入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度量聚集。单独使用时,度量聚集从数据中反应了许多有用的见解。如果将度量聚集与分组聚集组合使用,可以更深入地了解各种类别的数据的度量。

下次我们讨论其他度量类型聚集,如 geo bounds, geo centroid, percentiles, percentiles ranks等,敬请期待。


本文参考链接:https://blog.csdn.net/neweastsun/article/details/104521917
阅读延展