深入Elasticsearch度量聚集(1)

深入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等,敬请期待。

发布了395 篇原创文章 · 获赞 761 · 访问量 143万+

猜你喜欢

转载自blog.csdn.net/neweastsun/article/details/104521917