当前位置: 首页 > news >正文

分布式专题——48 ElasticSearch聚合操作详解

1 聚合的概述

  • ElasticSearch 除了搜索功能外,还提供针对其数据的统计分析功能,这一功能通过**聚合(aggregations)**实现;
  • 作用:能够极其方便地实现对数据的统计、分析、运算
    • 什么品牌的手机最受欢迎?(用于分析类别维度的“热门程度”)
    • 这些手机的平均价格、最高价格、最低价格?(用于分析数值维度的“统计指标”)
    • 这些手机每月的销售情况如何?(用于分析时间维度的“趋势变化”)

1.1 聚合的使用场景

  • 聚合查询的适用场景非常广泛,典型包括商业智能、数据挖掘、日志分析等领域,以下是具体行业/场景的应用示例:
    • 电商平台的销售分析:统计每个地区的销售额、每个用户的消费总额、每个产品的销售量等,用于了解销售情况和趋势

    • 社交媒体的用户行为分析:统计每个用户的发布次数、转发次数、评论次数等;还可按地区、时间、话题等维度拆分分析,用于理解用户行为和趋势

    • 物流企业的运输分析:统计每个区域的运输量、每个车辆的运输次数、每个司机的行驶里程等,用于了解运输情况和优化运输效率

    • 金融企业的交易分析:统计每个客户的交易总额、每个产品的销售量、每个交易员的业绩等,用于了解交易情况和优化业务流程

    • 智能家居的设备监控分析:统计每个设备的使用次数、每个家庭的能源消耗量、每个时间段的设备使用率等,用于了解用户需求和优化设备效能

1.2 基本语法

  • 聚合查询的语法结构与ElasticSearch其他查询相似,通常包含以下三部分:

    • 查询条件:指定需要聚合的文档,可使用标准的ElasticSearch查询语法,如termmatchrange等;

    • 聚合函数:指定要执行的聚合操作,如sum(求和)、avg(求平均)、min(最小值)、max(最大值)、terms(桶聚合,用于分组统计)、date_histogram(基于日期的直方图聚合)等;每个聚合命令都会生成一个聚合结果;

    • 聚合嵌套:聚合命令可以嵌套,以便更细粒度地分析数据;

  • 语法示例结构:

    GET <index_name>/_search
    {"aggs": {"<aggs_name>": { // 聚合名称需要自己定义"agg_type": {"field": "<field_name>"}}}
    }
    
    • aggs_name:聚合函数的名称,由使用者自定义

    • agg_type:聚合种类,比如是桶聚合(terms)或者是指标聚合(avgsumminmax等)

    • field_name:字段名称或者叫域名,即要对哪个字段执行聚合操作

2 聚合的分类

2.1 概述与示例数据

  • ElasticSearch 的聚合主要分为三类,每类都有明确的功能和类比场景;

    • Metric Aggregation(指标聚合):执行数学运算,对文档字段进行统计分析,类似 MySQL 中的 min()max()sum() 等聚合函数操作;

      • MySQL 语句:SELECT MIN(price), MAX(price) FROM products(统计产品价格的最小值和最大值);
      • ElasticSearch DSL 类比实现(以“求价格平均值”为例):
        {"aggs":{"avg_price":{ // 聚合名称"avg":{ // 求平均值"field":"price"}}}
        }
        
    • Bucket Aggregation(桶聚合):将满足特定条件的文档集合放入“桶”中,每个桶关联一个 key,类似 MySQL 中的 GROUP BY 操作;

      • MySQL 语句:SELECT size, COUNT(*) FROM products GROUP BY size(按产品尺寸分组并统计数量);
      • ElasticSearch DSL 类比实现(以“按尺寸分组”为例):
        {"aggs": {"by_size": { // 聚合名称"terms": { // 桶聚合"field": "size"}}}
        }
        
    • Pipeline Aggregation(管道聚合):对其他聚合的结果进行二次聚合,实现更复杂的多维度统计分析;

  • 示例数据

    • 索引与映射操作

      • 删除索引(可选清理操作):DELETE /employees

      • 创建索引并定义映射

        PUT /employees
        {"mappings": {"properties": {"age": { "type": "integer" },        // 整数类型,存储年龄"gender": { "type": "keyword" },     // keyword类型,存储性别(适合精确匹配、分组)"job": {                             // 文本类型,同时开启keyword子字段(用于分组、聚合)"type": "text","fields": {"keyword": {"type": "keyword","ignore_above": 50}}},"name": { "type": "keyword" },       // keyword类型,存储姓名(精确匹配、分组)"salary": { "type": "integer" }      // 整数类型,存储薪资}}
        }
        
    • 批量插入文档:通过 _bulk API 批量插入 20 条员工数据,每条数据包含 nameagejobgendersalary 字段,示例片段如下:

      PUT /employees/_bulk
      { "index" : {  "_id" : "1" } }
      { "name" : "Emma","age":32,"job":"Product Manager","gender":"female","salary":35000 }
      { "index" : {  "_id" : "2" } }
      { "name" : "Underwood","age":41,"job":"Dev Manager","gender":"male","salary": 50000}
      { "index" : {  "_id" : "3" } }
      { "name" : "Tran","age":25,"job":"Web Designer","gender":"male","salary":18000 }
      { "index" : {  "_id" : "4" } }
      { "name" : "Rivera","age":26,"job":"Web Designer","gender":"female","salary": 22000}
      { "index" : {  "_id" : "5" } }
      { "name" : "Rose","age":25,"job":"QA","gender":"female","salary":18000 }
      { "index" : {  "_id" : "6" } }
      { "name" : "Lucy","age":31,"job":"QA","gender":"female","salary": 25000}
      { "index" : {  "_id" : "7" } }
      { "name" : "Byrd","age":27,"job":"QA","gender":"male","salary":20000 }
      { "index" : {  "_id" : "8" } }
      { "name" : "Foster","age":27,"job":"Java Programmer","gender":"male","salary": 20000}
      { "index" : {  "_id" : "9" } }
      { "name" : "Gregory","age":32,"job":"Java Programmer","gender":"male","salary":22000 }
      { "index" : {  "_id" : "10" } }
      { "name" : "Bryant","age":20,"job":"Java Programmer","gender":"male","salary": 9000}
      { "index" : {  "_id" : "11" } }
      { "name" : "Jenny","age":36,"job":"Java Programmer","gender":"female","salary":38000 }
      { "index" : {  "_id" : "12" } }
      { "name" : "Mcdonald","age":31,"job":"Java Programmer","gender":"male","salary": 32000}
      { "index" : {  "_id" : "13" } }
      { "name" : "Jonthna","age":30,"job":"Java Programmer","gender":"female","salary":30000 }
      { "index" : {  "_id" : "14" } }
      { "name" : "Marshall","age":32,"job":"Javascript Programmer","gender":"male","salary": 25000}
      { "index" : {  "_id" : "15" } }
      { "name" : "King","age":33,"job":"Java Programmer","gender":"male","salary":28000 }
      { "index" : {  "_id" : "16" } }
      { "name" : "Mccarthy","age":21,"job":"Javascript Programmer","gender":"male","salary": 16000}
      { "index" : {  "_id" : "17" } }
      { "name" : "Goodwin","age":25,"job":"Javascript Programmer","gender":"male","salary": 16000}
      { "index" : {  "_id" : "18" } }
      { "name" : "Catherine","age":29,"job":"Javascript Programmer","gender":"female","salary": 20000}
      { "index" : {  "_id" : "19" } }
      { "name" : "Boone","age":30,"job":"DBA","gender":"male","salary": 30000}
      { "index" : {  "_id" : "20" } }
      { "name" : "Kathy","age":29,"job":"DBA","gender":"female","salary": 20000}
      

2.2 指标聚合

  • 指标聚合分为单值分析多值分析两类,各自包含不同的聚合函数:

    • 单值分析:只输出一个分析结果,包含以下聚合函数

      • min(最小值)、max(最大值)、avg(平均值)、sum(求和)

      • Cardinality(类似 SQL 中的 distinct Count,用于统计去重后的数量)

    • 多值分析:输出多个分析结果,包含以下聚合函数

      • stats(基础统计,包含 min、max、avg、sum、count)、extended stats(扩展统计,增加方差、标准差等)

      • percentile(百分位,如计算第 50 百分位数即中位数)、percentile rank(百分位排名)

      • top hits(返回排在前面的文档示例)

  • 多指标聚合。例:查询员工的最低、最高和平均工资。通过同时定义 maxminavg 三个单值聚合,分别统计薪资的最大值、最小值和平均值:

    POST /employees/_search
    {"size": 0, // 不返回匹配查询条件的原始文档数据,只返回聚合(或其他统计类)结果"aggs": {"max_salary": {"max": {"field": "salary"}},"min_salary": {"min": {"field": "salary"}},"avg_salary": {"avg": {"field": "salary"}}}
    }
    
  • 多值聚合:对 salary 进行统计(stats 聚合)。通过 stats 聚合一次性输出薪资的多个统计指标(min、max、avg、sum、count):

    POST /employees/_search
    {"size": 0,"aggs": {"stats_salary": {"stats": {"field": "salary"}}}
    }
    
  • Cardinality 聚合:对搜索结果去重(统计不同岗位的数量)。通过 cardinality 聚合统计 job.keyword 字段的去重数量(即不同岗位的种类数):

    POST /employees/_search
    {"size": 0,"aggs": {"cardinate": {"cardinality": {"field": "job.keyword"}}}
    }
    

2.3 桶聚合

2.3.1 概述

  • 桶聚合是按照一定规则,将文档分配到不同的“桶”中,从而达到分类统计的目的;

  • ElasticSearch 提供了多种桶聚合方式,可分为以下类别:

    • Terms 聚合:基于字段的“词项”进行分组(类似 SQL 的 GROUP BY);

      • 字段支持要求:
        • keyword 类型字段默认支持 fielddata,可直接用于 Terms 聚合;
        • text 类型字段需在 Mapping 中显式开启 fielddata,且会按照分词后的结果进行分桶;
    • 数字类型相关的桶聚合

      • Range / Date Range:对数字或日期字段按“区间”分组(如年龄分“20-30岁”“30-40岁”区间);

      • Histogram(直方图) / Date Histogram:对数字或日期字段按“固定间隔”分组(如薪资每 5000 为一个区间,日期每天为一个区间);

  • 桶聚合支持嵌套,即可以在一个桶内再做分桶,实现更细粒度的多层级分析;

  • 桶聚合的应用非常广泛,典型场景包括:

    • 对数据进行分组统计:如按照地区、年龄段、性别等字段分组统计数量或指标

    • 对时间序列数据进行时间段分析:如按照每小时、每天、每月、每季度、每年等时间段拆分分析

    • 对各种标签信息分类并统计数量:如电商商品评价的“好评标签”“差评标签”分类统计(如图中电商商品评价的“时尚简约”“漂亮大方”等标签分组)

  • 下图中展示了电商平台“商品评价”的实际场景,其中:

    • 评价标签(如“时尚简约(50)”“漂亮大方(34)”)是通过Terms 聚合实现的:将包含相同标签的评价文档归入同一“桶”,并统计每个桶的文档数量(即标签出现次数)

    • 评价等级(好评、中评、差评)的分组统计,也是桶聚合的典型应用,通过对“评价等级”字段分组,实现各等级评价的数量统计

    在这里插入图片描述

2.3.2 例:获取 job 的分类信息

  • 通过 terms 聚合对 job.keyword 字段进行分组统计,实现岗位类别的分类分析:

    GET /employees/_search
    {"size": 0, // 不返回匹配查询条件的原始文档数据,只返回聚合(或其他统计类)结果"aggs": {"jobs": { // 自定义聚合名称"terms": {"field": "job.keyword" // 对岗位的keyword字段进行分组}}}
    }
    
  • 聚合支持以下关键属性来定制分析逻辑:

    • field:指定聚合操作的字段(如上述示例中的 job.keyword

    • size:指定返回的聚合结果数量(控制输出的“桶”数量)

    • order:指定聚合结果的排序方式

  • 默认情况下,Bucket 聚合会统计每个桶内的文档数量(记为 _count),并按 _count 降序排序。可以通过 order 属性自定义排序逻辑,示例如下:

    GET /employees/_search
    {"size": 0,"aggs": {"jobs": {"terms": {"field": "job.keyword","size": 10, // 返回前10个岗位分类的聚合结果"order": {"_count": "desc" // 按文档数量降序排序(也可改为"asc"升序)}}}}
    }
    

2.3.3 限定聚合范围

  • 通过 query 子句可以过滤出符合条件的文档,再对这些文档执行聚合操作。下面示例中是对薪资≥10000元的员工文档,按岗位(job.keyword)进行Terms聚合:

    GET /employees/_search
    {"query": {"range": { // 范围查询,筛选薪资≥10000的文档"salary": {"gte": 10000}}},"size": 0, // 不返回原始文档,只返回聚合结果"aggs": {"jobs": {"terms": {"field": "job.keyword", // 对岗位的keyword字段分组"size": 10, // 返回前10个岗位分类"order": {"_count": "desc" // 按文档数量降序排序}}}}
    }
    
  • 问题:直接对Text字段执行Terms聚合会失败。Text字段默认不支持聚合操作,若强行执行会抛出异常。下面示例中对job(Text类型)执行Terms聚合:

    POST /employees/_search
    {"size": 0,"aggs": {"jobs": {"terms": {"field": "job" // 直接对Text字段聚合,会报错}}}
    }
    
  • 异常原因:Text fields are not optimised for operations that require per-document field data like aggregations...(Text字段默认不支持聚合,需改用keyword字段或开启fielddata);

    在这里插入图片描述

  • 解决:对Text字段开启fielddata支持。通过更新Mapping,为Text字段启用fielddata,使其支持聚合:

    PUT /employees/_mapping
    {"properties": {"job": {"type": "text","fielddata": true // 开启fielddata,支持聚合}}
    }
    
  • 开启后,可对Text字段的分词结果执行Terms聚合:

    POST /employees/_search
    {"size": 0,"aggs": {"jobs": {"terms": {"field": "job" // 对Text字段的分词结果分组}}}
    }
    
  • job.keyword(Keyword类型)和job(开启fielddata的Text类型)执行Terms聚合,分桶总数会不同

    • job.keyword 是按“完整岗位名称”分组(如“Java Programmer”作为一个桶);

    • job(Text类型,开启fielddata)是按“分词结果”分组(如“Java”“Programmer”会被拆分为不同桶);

  • 可通过cardinality聚合对比去重数量,示例:

    POST /employees/_search
    {"size": 0,"aggs": {"cardinate": {"cardinality": { // 统计去重后的数量"field": "job" // 对比job和job.keyword的结果差异}}}
    }
    

2.3.4 Range 聚合

  • 按照数字范围对文档进行分桶,且支持自定义桶的 key

  • :按工资范围分桶(自定义桶标识)

    POST employees/_search
    {"size": 0,"aggs": {"salary_range": {"range": {"field": "salary", // 聚合字段为薪资"ranges": [{ "to": 10000 }, // 薪资 < 10000{ "from": 10000, "to": 20000 }, // 10000 ≤ 薪资 < 20000{ "key": ">20000", "from": 20000 } // 自定义桶标识,薪资 ≥ 20000]}}}
    }
    

2.3.5 Histogram聚合

  • 按照固定数值间隔对数字字段分桶,可通过 extended_bounds 定义桶的范围;

  • :工资按 5000 为间隔分桶(范围 0~100000)

    POST employees/_search
    {"size": 0,"aggs": {"salary_histrogram": {"histogram": {"field": "salary", // 聚合字段为薪资"interval": 5000, // 每 5000 为一个桶"extended_bounds": { // 强制桶范围为 0~100000"min": 0,"max": 100000}}}}
    }
    

2.3.6 top_hits 聚合

  • 获取分桶后,桶内最匹配的顶部文档列表(可指定数量和排序规则);

  • :按岗位分组后,获取每个岗位中年龄最大的 3 名员工信息

    POST /employees/_search
    {"size": 0,"aggs": {"jobs": {"terms": {"field": "job.keyword" // 按岗位分组},"aggs": {"old_employee": {"top_hits": {"size": 3, // 每个桶返回 3 条文档"sort": [ // 按年龄降序排序{ "age": { "order": "desc" } }]}}}}}
    }
    

2.3.7 嵌套聚合

  • 核心逻辑:在一个聚合的结果上,再嵌套执行另一层聚合,实现多维度分层分析

  • 示例 1:两层嵌套(岗位 + 薪资统计)。按岗位分组后,对每个岗位的薪资执行 stats 聚合(统计 min、max、avg 等):

    POST employees/_search
    {"size": 0,"aggs": {"Job_salary_stats": {"terms": {"field": "job.keyword" // 第一层:按岗位分组},"aggs": {"salary": {"stats": {"field": "salary" // 第二层:统计每个岗位的薪资指标}}}}}
    }
    
  • 示例 2:三层嵌套(岗位 + 性别 + 薪资统计)。按岗位分组 → 再按性别分组 → 对每个性别组的薪资执行 stats 聚合:

    POST employees/_search
    {"size": 0,"aggs": {"Job_gender_stats": {"terms": {"field": "job.keyword" // 第一层:按岗位分组},"aggs": {"gender_stats": {"terms": {"field": "gender" // 第二层:按性别分组},"aggs": {"salary_stats": {"stats": {"field": "salary" // 第三层:统计每个性别组的薪资指标}}}}}}}
    }
    

2.4 管道聚合

2.4.1 概述

  • 管道聚合的核心是对聚合分析的结果再次进行聚合分析,其结果在原聚合中的输出位置分为两类:

    • Sibling(同级):结果与现有分析结果同级,包含以下类型:

      • Max Bucketmin BucketAvg BucketSum Bucket
      • Stats BucketExtended Stats Bucket
      • Percentiles Bucket
    • Parent(内嵌):结果内嵌到现有聚合分析结果之中,包含以下类型:

      • Derivative(求导)
      • Cumulative Sum(累计求和)
      • Moving Function(移动平均值)

2.4.2 min_bucket 示例

  • 需求:在员工数最多的工种里,找出平均工资最低的工种;

  • 实现

    POST employees/_search
    {"size": 0,"aggs": {"jobs": { // 第一层:按岗位分组"terms": {"field": "job.keyword","size": 10},"aggs": {"avg_salary": { // 第二层:统计每个岗位的平均薪资"avg": {"field": "salary"}}}},"min_salary_by_job": { // 管道聚合:找出平均薪资最低的岗位(与jobs同级)"min_bucket": {"buckets_path": "jobs>avg_salary" // 指定聚合路径}}}
    }
    
  • 说明

    • min_bucket 用于求之前聚合结果的最小值;
    • 通过 buckets_path 指定聚合路径,min_salary_by_job 的结果与 jobs 聚合同级。

2.4.3 stats_bucket 示例

  • 需求:对平均工资进行统计分析(含 min、max、avg、sum、count 等指标);
  • 实现
    POST employees/_search
    {"size": 0,"aggs": {"jobs": {"terms": {"field": "job.keyword","size": 10},"aggs": {"avg_salary": {"avg": {"field": "salary"}}}},"stats_salary_by_job": { // 管道聚合:对平均薪资执行多指标统计"stats_bucket": {"buckets_path": "jobs>avg_salary"}}}
    }
    

2.4.4 percentiles_bucket 示例

  • 需求:计算平均工资的百分位数(如第 25、50、75 百分位等)。
  • 实现
    POST employees/_search
    {"size": 0,"aggs": {"jobs": {"terms": {"field": "job.keyword","size": 10},"aggs": {"avg_salary": {"avg": {"field": "salary"}}}},"percentiles_salary_by_job": { // 管道聚合:计算平均薪资的百分位数"percentiles_bucket": {"buckets_path": "jobs>avg_salary"}}}
    }
    

2.4.5 Cumulative Sum 示例

  • 需求:对平均薪资进行累计求和。
  • 实现
    POST employees/_search
    {"size": 0,"aggs": {"age": { // 第一层:按年龄直方图分桶(间隔1岁)"histogram": {"field": "age","min_doc_count": 0,"interval": 1},"aggs": {"avg_salary": { // 第二层:统计每个年龄桶的平均薪资"avg": {"field": "salary"}},"cumulative_salary": { // 管道聚合:对平均薪资累计求和(内嵌到age聚合中)"cumulative_sum": {"buckets_path": "avg_salary"}}}}}
    }
    

2.5 聚合的作用范围

2.5.1 概述

  • ElasticSearch 聚合分析的默认作用范围是 query 的查询结果集,同时支持以下方式改变聚合的作用范围:
    • Filter:在聚合内部通过过滤器限定聚合的文档范围

    • Post Filter:在聚合之后对结果进行过滤,不影响聚合的作用范围

    • Global:聚合作用于所有文档(无视 query 的过滤条件)

2.5.2 Query 作用范围(默认)

  • 聚合作用于 query 过滤后的文档集;

  • 例:对年龄≥20岁的员工,按岗位(job.keyword)进行 Terms 聚合:

    POST employees/_search
    {"size": 0,"query": {"range": {"age": {"gte": 20}}},"aggs": {"jobs": {"terms": {"field": "job.keyword"}}}
    }
    

2.5.3 Filter 作用范围

  • 在聚合内部通过 filter 限定部分聚合的文档范围,同时可保留全局聚合;

  • 例:

    • older_person 聚合仅作用于年龄≥35岁的员工,统计其岗位分布;

    • all_jobs 聚合作用于所有员工,统计全部岗位分布:

      POST employees/_search
      {"size": 0,"aggs": {"older_person": {"filter": {"range": {"age": {"from": 35}}},"aggs": {"jobs": {"terms": {"field": "job.keyword"}}}},"all_jobs": {"terms": {"field": "job.keyword"}}}
      }
      

2.5.4 Post Filter 作用范围

  • 聚合作用于所有文档,聚合完成后再对结果进行过滤;

  • 例:先统计所有岗位分布,再过滤出包含“Dev Manager”的结果

    POST employees/_search
    {"aggs": {"jobs": {"terms": {"field": "job.keyword"}}},"post_filter": {"match": {"job.keyword": "Dev Manager"}}
    }
    

2.5.5 Global 作用范围

  • 聚合作用于所有文档(无视 query 的过滤条件);

  • 例:query 过滤“年龄≥40岁”的员工,但 global 聚合会统计所有这些员工的平均薪资(即查询范围内的全部文档)

    POST employees/_search
    {"size": 0,"query": {"range": {"age": {"gte": 40}}},"aggs": {"jobs": {"terms": {"field": "job.keyword"}},"all": {"global": {},"aggs": {"salary_avg": {"avg": {"field": "salary"}}}}}
    }
    

2.6 排序

2.6.1 概述

  • 在 ElasticSearch 聚合(尤其是 Terms 聚合)中,可通过 order 指定排序逻辑:
    • 默认情况下,按照桶内文档数量(_count降序排序

    • 可通过 size 指定返回的桶数量

2.6.2 按 _count_key 排序

  • 对“年龄≥20岁”的员工,按岗位(job.keyword)分组后,先按桶内文档数(_count)升序、再按岗位名称(_key)降序排列:

    POST employees/_search
    {"size": 0,"query": {"range": {"age": {"gte": 20}}},"aggs": {"jobs": {"terms": {"field": "job.keyword","order": [{ "_count": "asc" },{ "_key": "desc" }]}}}
    }
    

2.6.2 按聚合指标(平均薪资)排序

  • 先对岗位分组,再统计每个岗位的平均薪资(avg_salary),最后按平均薪资降序排列岗位:

    POST employees/_search
    {"size": 0,"aggs": {"jobs": {"terms": {"field": "job.keyword","order": [{ "avg_salary": "desc" }]},"aggs": {"avg_salary": {"avg": {"field": "salary"}}}}}
    }
    

2.6.3 按统计指标(薪资最小值)排序

  • 先对岗位分组,再对每个岗位的薪资执行 stats 聚合(获取 min、max、avg 等),最后按薪资最小值降序排列岗位:

    POST employees/_search
    {"size": 0,"aggs": {"jobs": {"terms": {"field": "job.keyword","order": [{ "stats_salary.min": "desc" }]},"aggs": {"stats_salary": {"stats": {"field": "salary"}}}}}
    }
    

3 ES聚合分析不准确的原因及解决

3.1 原因

  • ES聚合分析不精准的根本原因:

    • ElasticSearch 在对海量数据进行聚合分析时,会牺牲部分精准度来满足实时性需求
    • 其技术层面的核心原因是:数据分散在多个分片上,聚合时每个分片先取“Top X”(如前3名),再由协调节点合并结果。这种“分片级局部Top + 协调节点全局合并”的机制,会导致最终结果可能不精准(因为分片外的真实Top数据可能未被纳入计算);
  • 在数据处理场景中,存在以下三角权衡关系,且只能同时满足其中两条:

    • Hadoop离线计算:追求精确度大数据量处理,但实时性差

    • 近似计算(如ES聚合):追求数据量实时性,但精确度有损失

    • 有限数据计算:追求精确度实时性,但数据量有限

    在这里插入图片描述

  • 以“获取Top 3岗位”的Terms聚合为例,执行流程和不精准性可拆解为:

    • 执行流程

      • 每个数据分片(如Node1、Node2、Node3)先各自计算本地的“Top 3”结果;

      • 协调节点(Coordinating Nodes)收集所有分片的本地Top 3,再合并出最终的“Top 3”;

        在这里插入图片描述

    • 这种流程会导致结果不精准,以下图为例:

      • 分片1的本地数据:A(6)、B(4)、C(4)、D(3) → 本地Top 3是A、B、C;

      • 分片2的本地数据:A(6)、B(2)、C(1)、D(3) → 本地Top 3是A、D、B;

      • 协调节点合并后结果:A(12)、B(6)、C(4) → 但真实全量数据中,D可能有更高的真实计数(如分片1的D(3) + 分片2的D(3) = 6),却因未进入分片本地Top 3而被遗漏,最终导致返回的Top 3不精准;

      在这里插入图片描述

3.2 解决方案一:设置主分片为1

  • 原理:将索引的主分片数设置为1,避免数据分散在多个分片上,从而让聚合在单个分片的全量数据上执行,保证精准度;
  • 注意点:ElasticSearch 7.x版本已默认将主分片数设为1;
  • 适用场景:数据量小的小集群、小规模业务场景(数据量小,性能开销可接受)。

3.3 解决方案2:调大 shard_size

  • 原理

    • shard_size 指每个分片上聚合的数据条数,需大于等于 sizesize 是聚合结果的返回数量);
    • 官方推荐公式:shard_size = size * 1.5 + 10shard_size 越大,结果越趋近于精准聚合结果;
      • size:聚合结果的返回值(如期望返回前3名,size 就是3);
      • shard_size:每个分片上聚合的数据条数;
    • 可通过 show_term_doc_count_error 参数显示最差情况下的错误值,辅助确定 shard_size 大小;
  • 适用场景:数据量大、分片数多的集群业务场景;

  • 测试:使用Kibana的测试数据

    在这里插入图片描述

    DELETE my_flights
    PUT my_flights
    {"settings": {"number_of_shards": 20},"mappings" : {"properties" : {"AvgTicketPrice" : {"type" : "float"},"Cancelled" : {"type" : "boolean"},"Carrier" : {"type" : "keyword"},"Dest" : {"type" : "keyword"},"DestAirportID" : {"type" : "keyword"},"DestCityName" : {"type" : "keyword"},"DestCountry" : {"type" : "keyword"},"DestLocation" : {"type" : "geo_point"},"DestRegion" : {"type" : "keyword"},"DestWeather" : {"type" : "keyword"},"DistanceKilometers" : {"type" : "float"},"DistanceMiles" : {"type" : "float"},"FlightDelay" : {"type" : "boolean"},"FlightDelayMin" : {"type" : "integer"},"FlightDelayType" : {"type" : "keyword"},"FlightNum" : {"type" : "keyword"},"FlightTimeHour" : {"type" : "keyword"},"FlightTimeMin" : {"type" : "float"},"Origin" : {"type" : "keyword"},"OriginAirportID" : {"type" : "keyword"},"OriginCityName" : {"type" : "keyword"},"OriginCountry" : {"type" : "keyword"},"OriginLocation" : {"type" : "geo_point"},"OriginRegion" : {"type" : "keyword"},"OriginWeather" : {"type" : "keyword"},"dayOfWeek" : {"type" : "integer"},"timestamp" : {"type" : "date"}}}
    }POST _reindex
    {"source": {"index": "kibana_sample_data_flights"},"dest": {"index": "my_flights"}
    }GET my_flights/_count
    GET kibana_sample_data_flights/_search
    {"size": 0,"aggs": {"weather": {"terms": {"field":"OriginWeather","size":5,"show_term_doc_count_error":true}}}
    }GET my_flights/_search
    {"size": 0,"aggs": {"weather": {"terms": {"field":"OriginWeather","size":5,"shard_size":10,"show_term_doc_count_error":true}}}
    }
    

    在这里插入图片描述

    • 当未调整 shard_size(或 shard_size 较小时),聚合结果可能存在偏差;
    • 调大 shard_size 后,通过 show_term_doc_count_error 可查看错误范围(doc_count_error_upper_bound 是被遗漏的term分桶包含文档的最大可能值;sum_other_doc_count 是除返回结果外其他term的文档总数),结果精准度显著提升;

3.4 解决方案3:将 size 设置为全量值

  • 原理:将 size 设置为 2^32 - 1(分片支持的最大值),确保聚合包含所有可能的候选数据,从而解决精度问题;
  • 注意:ElasticSearch 1.x版本中 size=0 代表全量,高版本已取消该逻辑,因此需设置最大值;
  • 弊端:若分片数据量极大,会消耗巨大的CPU资源用于排序,且可能阻塞网络;
  • 适用场景:对聚合精准度要求极高的业务场景(因性能问题,不推荐常规使用)。

3.5 解决方案4:使用Clickhouse/Spark进行精准聚合

  • 原理:借助Clickhouse(列式存储、OLAP引擎)或Spark(分布式计算框架)的强大计算能力,在海量数据场景下实现精准聚合;
  • 适用场景:数据量非常大、聚合精度要求高、响应速度快的业务场景(需依赖外部计算引擎,脱离ElasticSearch自身聚合能力)。

4 ElasticSearch 聚合性能优化

4.1 插入数据时对索引进行预排序

  • 索引预排序(Index Sorting)在插入数据时对索引进行预排序,而非查询时再排序,以此提升范围查询(range query)排序操作的性能;

  • 这是 Elasticsearch 6.X 及之后版本才有的特性;

  • 配置方法:创建新索引时,可配置每个分片内的段如何排序。例:

    PUT /my_index
    {"settings": {"index": {"sort.field": "create_time", // 指定排序字段(如创建时间)"sort.order": "desc" // 指定排序顺序(降序)}},"mappings": {"properties": {"create_time": {"type": "date" // 字段类型为日期}}}
    }
    
  • 预排序会增加 Elasticsearch 的写入成本

    • 在某些场景下,开启索引预排序会导致约 40%-50% 的写性能下降
    • 因此,若业务场景更关注写入性能,不建议开启索引预排序;
    • 若业务更依赖范围查询和排序的读性能,则可考虑使用。

4.2 使用节点查询缓存

  • **节点查询缓存(Node Query Cache)用于有效缓存过滤器(filter)**操作的结果。若多次执行同一 filter 操作,缓存可大幅提升性能;但过滤器中任意值的修改,都需要重新计算并缓存新结果;

  • 使用方式:在包含 filter 的查询请求中,Elasticsearch 会自动尝试使用节点查询缓存优化性能。例:

    GET /your_index/_search
    {"query": {"bool": {"filter": {"term": {"your_field": "your_value"}}}}
    }
    

4.3 使用分片请求缓存

  • 触发条件:在聚合语句中设置 size: 0(含义:只返回聚合结果,不返回查询结果),即可使用分片请求缓存存储结果;

  • GET /es_db/_search
    {"size": 0,"aggs": {"remark_agg": {"terms": {"field": "remark.keyword"}}}
    }
    

4.4 拆分聚合,使聚合并行化

  • 背景:Elasticsearch 中多个聚合条件默认不是并行运行的。若 CPU 资源充足,可将多个聚合拆分为多个查询,借助 _msearch 实现并行聚合,从而缩短响应时间;
  • 常规多条件聚合(串行执行)
    GET /employees/_search
    {"size": 0,"aggs": {"job_agg": {"terms": {"field": "job.keyword"}},"max_salary": {"max": {"field": "salary"}}}
    }
    
  • _msearch 并行聚合(拆分后并行执行)
    GET _msearch
    {"index":"employees"}
    {"size":0,"aggs":{"job_agg":{"terms":{"field": "job.keyword"}}}}
    {"index":"employees"}
    {"size":0,"aggs":{"max_salary":{"max":{"field": "salary"}}}}
    
http://www.dtcms.com/a/589122.html

相关文章:

  • 免费品牌网站制作给娃娃做衣服卖的网站
  • 【AI大模型技术】1.NLP
  • Linux应用开发-18- select、poll、epoll
  • 进程3:进程切换
  • PHP中各种超全局变量使用
  • 深入了解iOS内存管理
  • 介质电磁特性参数
  • 网站建设行业广告语建网站找那家企业好
  • Python中使用sqlite3模块和panel完成SQLite数据库中PDF的写入和读取
  • 佛山网站建设网络公司上海网站seo诊断
  • 操作系统面试题学习
  • Java 大视界 -- Java 大数据在智能教育虚拟学习环境构建与用户体验优化中的应用
  • .NET Core 如何使用 Quartz?
  • excel下拉选项设置
  • 深入解析:利用EBS直接API实现增量快照与精细化数据管理(AWS)
  • 专门做石材地花设计的网站有哪些网站是免费学做网页的
  • [Godot] Google Play审核反馈:如何应对“您的游戏需要进行更多测试才能发布正式版”?
  • Rust 练习册 :深入探索可变长度数量编码
  • dify二次开发部署服务器
  • webrtc降噪-NoiseEstimator类源码分析与算法原理
  • 4.3 Boost 库工具类 optional 的使用
  • 帮人做网站要怎么赚钱吗吉林平安建设网站
  • 文广网站建设sq网站推广
  • Nop平台拆分出核心部分nop-kernel
  • 结构型设计模式1
  • 普中51单片机学习笔记-中断
  • 二十六、STM32的ADC(DMA+ADC)
  • 网站开发的著作权和版权网站品牌推广
  • 【Docker】docker compose
  • 4.1.8 【2022 统考真题】