聚合分析

聚合(Aggregation)是ES除全文检索外最核心的能力之一,它允许我们对数据进行统计、分组、计算和多维度分析操作,实现类似关系型数据库中GROUP BY、COUNT、SUM、AVG等统计功能,同时ES还支持更复杂的多层嵌套聚合、实时计算,因此广泛用于报表、数据分析、用户画像等场景。

本篇笔记中的示例仍沿用上一篇笔记中的products索引,这里就不重复黏贴了。

ES中的聚合统计

ES中的聚合功能中,最常用的主要有两大类,指标聚合桶聚合

graph TD
    A[聚合 Aggregation]
    A --> B[Bucket 桶聚合]
    A --> C[Metric 指标聚合]
    B --> B1[Terms Aggregation]
    B --> B2[Date Histogram Aggregation]
    B --> B3[Range Aggregation]
    C --> C1[avg / sum / min / max]
    C --> C2[value_count]
    C --> C3[cardinality]
    C --> C4[stats / extended_stats]
    C --> C5[percentiles]

Metric 指标聚合:指标聚合是指在整个文档集合或某个桶上计算数值指标,如最大值、最小值、平均值、求和等,类似SQL中的MAX()AVG()等聚合函数。

Bucket 桶聚合:桶聚合指将文档按某种规则分组,每组叫做一个“桶”,类似SQL中的GROUP BY

除此之外,ES其实还包含两种高级聚合功能,管道聚合矩阵聚合。不过这两个功能一直处于技术预览状态,尤其是矩阵聚合在生产环境中较少使用,这里就不展开介绍了。

聚合请求的基本结构

ES中聚合请求通过aggs(或写全称aggregations,两者完全等价)字段来声明,aggs可以与query同时使用,同时使用时聚合只作用于query过滤后的结果集,基本格式如下。

GET /products/_search
{
  "query": { ... },
  "aggs": {
    "聚合名称(自定义)": {
      "聚合类型": {
        "field": "字段名",
        // ... 其它参数
      }
    }
  },
  "size": 0
}

聚合统计中,一个小技巧是使用size: 0,它表示不返回命中文档列表只返回聚合结果,这可以节省网络带宽和响应体积,在纯统计报表场景下非常常用。

指标聚合

value_count

value_count统计某个字段有值的文档数量。这里注意如果某个文档的目标字段为空,这种数据是不会被计入的,所谓的“字段为空”是指字段不存在或为null,空字符串不算“字段为空”。此外,还需要注意的是_idtext类型的字段默认不能用于聚合统计,_id不能用于聚合和上一篇笔记中_id不能用于排序的理由是类似的,至于text字段则是因为它分词后数量巨大,ES默认也不开启FieldData。ES中通过开启一些配置确实可以让这些字段参与聚合,但绝不推荐这样做。keyword、数字等类型可以正常用于聚合统计。

GET /products/_search
{
  "size": 0,
  "aggs": {
    "product_count": {
      "value_count": { "field": "name.keyword" }
    }
  }
}

聚合的响应结果中,相关统计结果会被ES放入aggregations字段。

{
  // ... 其它字段
  "product_count": {
    "value": 8
  }
}

avg / sum / min / max

avgsumminmax是最基础的单值指标聚合,它们的含义与SQL中的同名函数完全一致,分别是计算平均值、计算累加和、计算最小值和计算最大值。下面例子我们计算products索引中所有产品的平均价格、总库存、最低价格和最高价格。

GET /products/_search
{
  "size": 0,
  "aggs": {
    "avg_price": {
      "avg": { "field": "price" }
    },
    "total_stock": {
      "sum": { "field": "stock" }
    },
    "min_price": {
      "min": { "field": "price" }
    },
    "max_price": {
      "max": { "field": "price" }
    }
  }
}

stats / extended_stats

stats是简写的复合指标聚合,它一次性返回countminmaxavgsum五个指标,无需分别声明5个聚合操作。

GET /products/_search
{
  "size": 0,
  "aggs": {
    "price_stats": {
      "stats": { "field": "price" }
    }
  }
}

extended_statsstats基础上还会额外返回方差、标准差、平方和等统计量。

GET /products/_search
{
  "size": 0,
  "aggs": {
    "price_extended_stats": {
      "extended_stats": { "field": "price" }
    }
  }
}

cardinality

cardinality用于计算某个字段的近似去重数量,它类似于SQL中的COUNT(DISTINCT field)。下面例子统计索引中共有多少个不同的品牌。

GET /products/_search
{
  "size": 0,
  "aggs": {
    "brand_count": {
      "cardinality": { "field": "brand" }
    }
  }
}

cardinality是近似而非精确统计,它的底层没有真去全量去重统计,而是使用HLL++(HyperLogLog++)算法实现的,这意味着统计结果会有一定误差,这个误差率默认通常在5%以内,大部分场景下远低于这个值,通过precision_threshold参数可以调节精度,但代价是更高的内存消耗。

percentiles

percentiles用于计算字段值的百分位分布,它常用于分析数据的分布。

GET /products/_search
{
  "size": 0,
  "aggs": {
    "price_percentiles": {
      "percentiles": {
        "field": "price",
        "percents": [25, 50, 75, 90, 99]
      }
    }
  }
}

响应结果中,50.0对应中位数价格,90.0表示90%的产品价格低于这个值,其它以此类推,通过这个指标我们可以直观了解价格整体分布情况。

桶聚合

Terms Aggregation

terms是最常用的桶聚合,它按某个字段的值对文档进行分组,每个唯一值对应一个桶,表达的含义类似SQL中的GROUP BY。下面例子按产品类目分组,统计每个类目下的数量。

GET /products/_search
{
  "size": 0,
  "aggs": {
    "by_category": {
      "terms": {
        "field": "category",
        "size": 10
      }
    }
  }
}

size参数控制返回桶的最大数量,默认值为10,如果你的字段枚举值非常多时就需要根据实际情况调大这个值,否则ES只会返回文档数最多的前N个桶,其余的会被汇总到一个叫sum_other_doc_count的字段中。

terms聚合默认按结果数降序排列,这可以通过order参数修改排序方式。

GET /products/_search
{
  "size": 0,
  "aggs": {
    "by_category": {
      "terms": {
        "field": "category",
        "size": 10,
        "order": { "_key": "asc" }
      }
    }
  }
}

_key表示按桶的键排序,_count表示按文档数排序,此外也可以按嵌套指标聚合的结果排序,这部分将在嵌套聚合中介绍。

Range Aggregation

range聚合按数值区间分桶,每个区间对应一个桶,区间分桶遵循左闭右开[from, to)原则。下面例子将产品按价格区间分组。

GET /products/_search
{
  "size": 0,
  "aggs": {
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          { "to": 1000 },
          { "from": 1000, "to": 5000 },
          { "from": 5000, "to": 10000 },
          { "from": 10000 }
        ]
      }
    }
  }
}

Date Histogram Aggregation

date_histogram按时间间隔分桶,它是日志统计、订单量趋势分析等时序类报表的重要聚合方式。下面例子按月统计产品上架数量。

GET /products/_search
{
  "size": 0,
  "aggs": {
    "by_month": {
      "date_histogram": {
        "field": "created_at",
        "calendar_interval": "month",
        "format": "yyyy-MM",
        "min_doc_count": 0
      }
    }
  }
}

calendar_interval支持minutehourdayweekmonthquarteryear等时间粒度。min_doc_count: 0表示即使某个时间段内没有文档也会在结果中返回该桶(文档数为0),这在需要绘制连续折线图时非常重要,否则没有数据的时间点会被跳过导致图表断点。

除了calendar_interval我们还可以选择fixed_interval,它用于指定固定时间间隔,如1h7d等,与calendar_interval的区别是它不用关心月份天数差异问题,适合精确时间窗口的统计。

聚合嵌套与多级聚合

Bucket聚合和Metric聚合可以相互嵌套:Bucket聚合可以嵌套子Metric聚合,实现分组后再统计;Bucket聚合也可以嵌套子Bucket聚合,实现多级分组。

Bucket聚合内嵌套Metric聚合

下面例子按品牌分组,然后在每个品牌桶内分别计算平均价格和总库存。

GET /products/_search
{
  "size": 0,
  "aggs": {
    "by_brand": {
      "terms": {
        "field": "brand",
        "size": 10
      },
      "aggs": {
        "avg_price": {
          "avg": { "field": "price" }
        },
        "total_stock": {
          "sum": { "field": "stock" }
        }
      }
    }
  }
}

嵌套聚合中的排序

嵌套的Metric聚合结果也可以作为外层Bucket聚合的排序依据。下面例子按品牌分组后,按各品牌的平均价格降序排列。

GET /products/_search
{
  "size": 0,
  "aggs": {
    "by_brand": {
      "terms": {
        "field": "brand",
        "size": 10,
        "order": { "avg_price": "desc" }
      },
      "aggs": {
        "avg_price": {
          "avg": { "field": "price" }
        }
      }
    }
  }
}

多级Bucket聚合嵌套

下面例子实现了两级分组,先按类目分组,再在每个类目内按品牌分组,最后在每个分组内计算平均价格。

GET /products/_search
{
  "size": 0,
  "aggs": {
    "by_category": {
      "terms": {
        "field": "category",
        "size": 10
      },
      "aggs": {
        "by_brand": {
          "terms": {
            "field": "brand",
            "size": 10
          },
          "aggs": {
            "avg_price": {
              "avg": { "field": "price" }
            }
          }
        }
      }
    }
  }
}

聚合与查询结合

聚合始终只作用于命中文档,结合query就可以实现先筛选再统计的效果。下面例子只统计未删除产品的各类目产品数量和平均价格。

GET /products/_search
{
  "size": 0,
  "query": {
    "term": { "is_deleted": false }
  },
  "aggs": {
    "by_category": {
      "terms": {
        "field": "category",
        "size": 10
      },
      "aggs": {
        "avg_price": {
          "avg": { "field": "price" }
        }
      }
    }
  }
}

空值聚合处理

默认情况下,Metric聚合和Bucket聚合会忽略字段值为null或字段不存在的文档。missing参数可以为缺失值的文档指定一个默认填充值,使这些文档也能参与聚合。下面例子中,如果某个产品文档缺少price字段,则将其视为价格0参与平均价格计算。

GET /products/_search
{
  "size": 0,
  "aggs": {
    "avg_price": {
      "avg": {
        "field": "price",
        "missing": 0
      }
    }
  }
}

terms桶聚合中,missing参数可以让缺失字段值的文档单独形成一个桶。

GET /products/_search
{
  "size": 0,
  "aggs": {
    "by_category": {
      "terms": {
        "field": "category",
        "size": 10,
        "missing": "未分类"
      }
    }
  }
}
作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。