聚合可以做什么?
- count
- avg
- filter and count
- 每月新增
- top
- 是否存在不正常或不符合规则的数据
关键概念
Buckets
- group by
- 将数据按某种标准划分成不同集合
- 桶嵌套: Cincinnati would be placed inside the Ohio state bucket, and the entire Ohio bucket would be placed inside the USA country bucket.
Metrics
- count、avg、top
- 统计桶中文档的指标
Because buckets can be nested, we can derive a much more complex aggregation:
- Partition documents by country (bucket).
- Then partition each country bucket by gender (bucket).
- Then partition each gender bucket by age ranges (bucket).
- Finally, calculate the average salary for each age range (metric) This will give you the average salary per <country, gender, age> combination.
嵌套桶
GET /cars/transactions/_search{ "size" : 0, "aggs": { "colors": { "terms": { "field": "color" }, "aggs": { "avg_price": { "avg": { "field": "price" } }, "make" : { "terms" : { "field" : "make" }, "aggs" : { "min_price" : { "min": { "field": "price"} }, "max_price" : { "max": { "field": "price"} } } } } } }}
条形图
按范围统计
GET /cars/transactions/_search{ "size" : 0, "aggs":{ "price":{ "histogram":{ "field": "price", "interval": 20000 }, "aggs":{ "revenue": { "sum": { "field" : "price" } } } } }}
GET /cars/transactions/_search{ "size" : 0, "aggs": { "makes": { "terms": { "field": "make", "size": 10 }, "aggs": { "stats": { "extended_stats": { "field": "price" } } } } }}
上述代码会按受欢迎度返回制造商列表以及它们各自的统计信息。我们对其中的 stats.avg 、 stats.count 和 stats.std_deviation 信息特别感兴趣,并用 它们计算出标准差:
std_err = std_deviation / count
按时间统计
GET /cars/transactions/_search{ "size" : 0, "aggs": { "sales": { "date_histogram": { "field": "sold", "interval": "month", "format": "yyyy-MM-dd" } } }}
返回空Buckets
GET /cars/transactions/_search{ "size" : 0, "aggs": { "sales": { "date_histogram": { "field": "sold", "interval": "month", "format": "yyyy-MM-dd", "min_doc_count" : 0, "extended_bounds" : { "min" : "2014-01-01", "max" : "2014-12-31" } } } }}
范围限定的聚合
GET /cars/transactions/_search{ "query" : { "match" : { "make" : "ford" } }, "aggs" : { "colors" : { "terms" : { "field" : "color" } } }}
全局桶
全局 桶包含 所有 的文档,它无视查询的范围。因为它还是一个桶,我们可以像平常一样将聚合嵌套在内:
GET /cars/transactions/_search{ "size" : 0, "query" : { "match" : { "make" : "ford" } }, "aggs" : { "single_avg_price": { "avg" : { "field" : "price" } }, "all": { "global" : {}, "aggs" : { "avg_price": { "avg" : { "field" : "price" } } } } }}
过滤和聚合
选择合适类型的过滤(如:搜索命中、聚合或两者兼有)通常和我们期望如何表现用户交互有关。选择合适的过滤器(或组合)取决于我们期望如何将结果呈现给用户。
- 在 filter 过滤中的 non-scoring 查询,同时影响搜索结果和聚合结果。
- filter 桶影响聚合。
- post_filter 只影响搜索结果。
- 过滤
GET /cars/transactions/_search{ "size" : 0, "query" : { "constant_score": { "filter": { "range": { "price": { "gte": 10000 } } } } }, "aggs" : { "single_avg_price": { "avg" : { "field" : "price" } } }}
- 过滤桶
GET /cars/transactions/_search{ "size" : 0, "query":{ "match": { "make": "ford" } }, "aggs":{ "recent_sales": { "filter": { "range": { "sold": { "from": "now-1M" } } }, "aggs": { "average_price":{ "avg": { "field": "price" } } } } }}
- post_filter
GET /cars/transactions/_search{ "size" : 0, "query": { "match": { "make": "ford" } }, "post_filter": { "term" : { "color" : "green" } }, "aggs" : { "all_colors": { "terms" : { "field" : "color" } } }}
- 性能考虑(Performance consideration) 当你需要对搜索结果和聚合结果做不同的过滤时,你才应该使用 post_filter , 有时用户会在普通搜索使用 post_filter 。
不要这么做! post_filter 的特性是在查询 之后 执行,任何过滤对性能带来的好处(比如缓存)都会完全失去。
在我们需要不同过滤时, post_filter 只与聚合一起使用。
多桶排序
内置排序
GET /cars/transactions/_search{ "size" : 0, "aggs" : { "colors" : { "terms" : { "field" : "color", "order": { "_count" : "asc" } } } }}
我们为聚合引入了一个 order 对象, 它允许我们可以根据以下几个值中的一个值进行排序:
_count
按文档数排序。对 terms 、 histogram 、 date_histogram 有效。
_term
按词项的字符串值的字母顺序排序。只在 terms 内使用。
_key
按每个桶的键值数值排序(理论上与 _term 类似)。 只在 histogram 和 date_histogram 内使用。
按度量排序
GET /cars/transactions/_search{ "size" : 0, "aggs" : { "colors" : { "terms" : { "field" : "color", "order": { "avg_price" : "asc" } }, "aggs": { "avg_price": { "avg": {"field": "price"} } } } }}GET /cars/transactions/_search{ "size" : 0, "aggs" : { "colors" : { "terms" : { "field" : "color", "order": { "stats.variance" : "asc" } }, "aggs": { "stats": { "extended_stats": {"field": "price"} } } } }}
基于“深度”度量排序
GET /cars/transactions/_search{ "size" : 0, "aggs" : { "colors" : { "histogram" : { "field" : "price", "interval": 20000, "order": { "red_green_cars>stats.variance" : "asc" } }, "aggs": { "red_green_cars": { "filter": { "terms": {"color": ["red", "green"]}}, "aggs": { "stats": {"extended_stats": {"field" : "price"}} } } } } }}
创建一个汽车售价的直方图,但是按照红色和绿色(不包括蓝色)车各自的方差来排序
近似聚合
去重统计、百分位计算都是一个大概的统计,要求越精确,占用的资源越高。
去重统计
字段按哈希值存储
GET /cars/transactions/_search{ "size" : 0, "aggs" : { "distinct_colors" : { "cardinality" : { "field" : "color", "precision_threshold" : 100 } } }}
precision_threshold 接受 0–40,000 之间的数字,更大的值还是会被当作 40,000 来处理。
示例会确保当字段唯一值在 100 以内时会得到非常准确的结果。尽管算法是无法保证这点的,但如果基数在阈值以下,几乎总是 100% 正确的。高于阈值的基数会开始节省内存而牺牲准确度,同时也会对度量结果带入误差。
对于指定的阈值,HLL 的数据结构会大概使用 precision_threshold * 8 字节的内存,所以就必须在牺牲内存和获得额外的准确度间做平衡。
在实际应用中, 100 的阈值可以在唯一值为百万的情况下仍然将误差维持 5% 以内。
速度优化
预先计算哈希值,只对内容很长或者基数很高的字段有用,计算这些字段的哈希值的消耗在查询时是无法忽略的。
PUT /cars/{ "mappings": { "transactions": { "properties": { "color": { "type": "string", "fields": { "hash": { "type": "murmur3" } } } } } }}GET /cars/transactions/_search{ "size" : 0, "aggs" : { "distinct_colors" : { "cardinality" : { "field" : "color.hash" } } }}
百分位计算
百分位数展现某以具体百分比下观察到的数值。
百分位数通常用来找出异常。在(统计学)的正态分布下,第 0.13 和 第 99.87 的百分位数代表与均值距离三倍标准差的值。任何处于三倍标准差之外的数据通常被认为是不寻常的,因为它与平均值相差太大。
GET /website/logs/_search{ "size" : 0, "aggs" : { "load_times" : { "percentiles" : { "field" : "latency" } }, "avg_load_time" : { "avg" : { "field" : "latency" } } }}GET /website/logs/_search{ "size" : 0, "aggs" : { "zones" : { "terms" : { "field" : "zone" }, "aggs" : { "load_times" : { "percentiles" : { "field" : "latency", "percents" : [50, 95.0, 99.0] } }, "load_avg" : { "avg" : { "field" : "latency" } } } } }}
percentiles 度量告诉我们落在某个百分比以下的所有文档的最小值。例如,如果 50 百分位是 119ms,那么有 50% 的文档数值都不超过 119ms。 percentile_ranks 告诉我们某个具体值属于哪个百分位。119ms 的 percentile_ranks 是在 50 百分位。 这基本是个双向关系,例如:
50 百分位是 119ms。
119ms 百分位等级是 50 百分位。
GET /website/logs/_search{ "size" : 0, "aggs" : { "zones" : { "terms" : { "field" : "zone" }, "aggs" : { "load_times" : { "percentile_ranks" : { "field" : "latency", "values" : [210, 800] } } } } }}
算法的特性:
- 百分位的准确度与百分位的 极端程度 相关,也就是说 1 或 99 的百分位要比 50 百分位要准确。这只是数据结构内部机制的一种特性,但这是一个好的特性,因为多数人只关心极端的百分位。
- 对于数值集合较小的情况,百分位非常准确。如果数据集足够小,百分位可能 100% 精确。
- 随着桶里数值的增长,算法会开始对百分位进行估算。它能有效在准确度和内存节省之间做出权衡。 不准确的程度比较难以总结,因为它依赖于 聚合时数据的分布以及数据量的大小。
与 cardinality 类似,我们可以通过修改参数 compression 来控制内存与准确度之间的比值。
TDigest 算法用节点近似计算百分比:节点越多,准确度越高(同时内存消耗也越大),这都与数据量成正比。 compression 参数限制节点的最大数目为 20 * compression 。
因此,通过增加压缩比值,可以以消耗更多内存为代价提高百分位数准确性。更大的压缩比值会使算法运行更慢,因为底层的树形数据结构的存储也会增长,也导致操作的代价更高。默认的压缩比值是 100 。
通过聚合发现异常指标
significant_terms ,我们想要显著的共性(注:uncommonly common)
而不想要最流行的- 基于统计的推荐(Recommending Based on Statistics)编辑 统计异常就是与统计背景相比在前景特征组中过度展现的那些影片
Doc Values and Fielddata
倒排索引在搜索时非常快速,但是在按字段排序时不理想。需要转置倒排索引--即列存储。
Doc Values
列存储将所有单字段的值存储在单数据列中,对其进行操作是十分高效的,例如排序。
在 Elasticsearch 中,Doc Values 就是一种列式存储结构,默认情况下每个字段的 Doc Values 都是激活的,Doc Values 是在索引时创建的,当字段索引时,Elasticsearch 为了能够快速检索,会把字段的值加入倒排索引中,同时它也会存储该字段的 Doc Values
。
Elasticsearch 中的 Doc Values 常被应用到以下场景:
- 对一个字段进行排序
- 对一个字段进行聚合
- 某些过滤,比如地理位置过滤
- 某些与字段相关的脚本计算 因为文档值被序列化到磁盘,我们可以依靠操作系统的帮助来快速访问。
Fielddata
doc values 不生成分析的字符串,然而,这些字段仍然可以使用聚合,那怎么可能呢?
答案是一种被称为 fielddata 的数据结构。与 doc values 不同,fielddata 构建和管理 100% 在内存中,常驻于 JVM 内存堆。这意味着它本质上是不可扩展的,有很多边缘情况下要提防。
在分析字符串上下文中 fielddata 的挑战?
高基数字段在加载到 fielddata 时会消耗大量内存。 分析的过程会经常(尽管不总是这样)生成大量的 token,这些 token 大多都是唯一的。 这会增加字段的整体基数并且带来更大的内存压力。
Fielddata 是 延迟 加载。
- 选择堆大小(Choosing a Heap Size)
- 不要超过可用 RAM 的 50%
- 不要超过 32 GB
断路器
断路器通过内部检查(字段的类型、基数、大小等等)来估算一个查询需要的内存。它然后检查要求加载的 fielddata 是否会导致 fielddata 的总量超过堆的配置比例。
如果估算查询的大小超出限制,就会 触发 断路器,查询会被中止并返回异常。这都发生在数据加载 之前 ,也就意味着不会引起 OutOfMemoryException 。
在 Fielddata的大小 中,我们提过关于给 fielddata 的大小加一个限制,从而确保旧的无用 fielddata 被回收的方法。 indices.fielddata.cache.size 和 indices.breaker.fielddata.limit 之间的关系非常重要。 如果断路器的限制低于缓存大小,没有数据会被回收。为了能正常工作,断路器的限制 必须 要比缓存大小要高。
Fielddata 的过滤
PUT /music/_mapping/song{ "properties": { "tag": { "type": "string", "fielddata": { "filter": { "frequency": { "min": 0.01, "min_segment_size": 500 } } } } }}
Fielddata 过滤对内存使用有 巨大的 影响,权衡也是显而易见的:我们实际上是在忽略数据。但对于很多应用,这种权衡是合理的,因为这些数据根本就没有被使用到。内存的节省通常要比包括一个大量而无用的长尾项更为重要
预加载fileddata
有三种方式可以解决这个延时高峰:
- 预加载 fielddata
- 预加载全局序号
- 缓存预热 所有的变化都基于同一概念:预加载 fielddata,这样在用户进行搜索时就不会碰到延迟高峰。
优化聚合查询
深度优先与广度优先(Depth-First Versus Breadth-First)
如果我们想要查询出演影片最多的十个演员以及与他们合作最多的演员。
- 默认深度优先
- 特殊场景采用广度优先:
- 广度优先仅仅适用于每个组的聚合数量远远小于当前总文档数的情况下,因为广度优先会在内存中缓存裁剪后的仅仅需要缓存的每个组的所有数据,以便于它的子聚合分组查询可以复用上级聚合的数据。
{ "aggs" : { "actors" : { "terms" : { "field" : "actors", "size" : 10, "collect_mode" : "breadth_first" }, "aggs" : { "costars" : { "terms" : { "field" : "actors", "size" : 5 } } } } }}
总结
聚合给 Elasticsearch带来了难以言喻的强大能力和灵活性。桶与度量的嵌套能力,基数与百分位数的快速估算能力,定位信息中统计异常的能力,
Elasticsearch 默认给 大多数 字段启用 doc values,所以在一些搜索场景大大的节省了内存使用量,但是需要注意的是只有不分词的 string 类型的字段才能使用这种特性。
内存的管理形式可以有多种形式,这取决于我们特定的应用场景:
- 在规划时,组织好数据,使聚合运行在 not_analyzed 字符串而不是 analyzed 字符串,这样可以有效的利用 doc values 。
- 在测试时,验证分析链不会在之后的聚合计算中创建高基数字段。(见聚合与分析中 高基数内存的影响(High-Cardinality Memory Implications) )
- 在搜索时,合理利用近似聚合和数据过滤。
- 在节点层,设置硬内存大小以及动态的断熔限制。
- 在应用层,通过监控集群内存的使用情况和 Full GC 的发生频率,来调整是否需要给集群资源添加更多的机器节点