MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

区间聚合:ElasticSearch中的数值范围分析

2022-07-191.3k 阅读

ElasticSearch中的区间聚合基础

区间聚合概念

在ElasticSearch中,区间聚合(Range Aggregation)是一种针对数值类型数据进行范围分析的强大工具。它允许我们将文档按照指定的数值范围进行分组,并对每个分组内的文档进行统计分析。例如,我们有一批商品数据,其中包含价格字段,通过区间聚合,我们可以将商品按价格范围分为低价、中价、高价等不同区间,并统计每个区间内商品的数量、平均价格等信息。

这种聚合方式在数据分析、业务统计等场景中有着广泛的应用。比如电商平台分析不同价格区间的商品销售情况,金融机构分析不同金额区间的交易数量等。

区间定义方式

在ElasticSearch中定义区间主要有两种常见方式:固定范围和动态范围。

固定范围:明确指定每个区间的起始值和结束值。例如,对于价格数据,我们可以定义区间为[0, 100),[100, 200),[200, +∞)。这里左闭右开的区间表示法是一种常见的约定,即包含起始值但不包含结束值。

动态范围:根据数据的特征动态生成区间。比如,我们可以基于数据的最小值、最大值以及指定的间隔来自动划分区间。例如,数据的价格范围是从10到1000,我们可以指定间隔为100,ElasticSearch会自动生成[10, 110),[110, 210),[210, 310)等区间。

简单区间聚合示例

准备数据

假设我们有一个索引products,其中包含商品文档,每个文档有name(商品名称)和price(商品价格)字段。我们可以通过以下代码向ElasticSearch中插入一些示例数据:

POST products/_bulk
{ "index": { "_id": "1" } }
{ "name": "Product A", "price": 50 }
{ "index": { "_id": "2" } }
{ "name": "Product B", "price": 150 }
{ "index": { "_id": "3" } }
{ "name": "Product C", "price": 250 }

执行区间聚合

现在我们执行一个简单的区间聚合,将商品按价格分为三个区间:[0, 100),[100, 200),[200, +∞)。请求如下:

POST products/_search
{
    "size": 0,
    "aggs": {
        "price_ranges": {
            "range": {
                "field": "price",
                "ranges": [
                    { "from": 0, "to": 100 },
                    { "from": 100, "to": 200 },
                    { "from": 200 }
                ]
            }
        }
    }
}

在上述请求中,size: 0表示我们不关心具体返回哪些文档,只关注聚合结果。aggs部分定义了聚合操作,price_ranges是自定义的聚合名称,range表示这是一个区间聚合,field指定了要基于哪个字段进行聚合(这里是price字段),ranges数组中定义了具体的区间。

结果分析

ElasticSearch返回的结果类似如下:

{
    "took": 1,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 3,
            "relation": "eq"
        },
        "max_score": null,
        "hits": []
    },
    "aggregations": {
        "price_ranges": {
            "buckets": [
                {
                    "key": "0.0-100.0",
                    "from": 0,
                    "to": 100,
                    "doc_count": 1
                },
                {
                    "key": "100.0-200.0",
                    "from": 100,
                    "to": 200,
                    "doc_count": 1
                },
                {
                    "key": "200.0-*",
                    "from": 200,
                    "doc_count": 1
                }
            ]
        }
    }
}

aggregationsprice_ranges部分,buckets数组中的每个元素对应一个区间。key表示区间范围,fromto明确了区间的边界,doc_count表示该区间内文档的数量。从结果中可以看到,价格在[0, 100)区间的商品有1个,[100, 200)区间的商品有1个,[200, +∞)区间的商品有1个。

区间聚合中的嵌套聚合

嵌套聚合概念

在实际应用中,单纯的区间划分可能无法满足复杂的数据分析需求。这时,我们可以在区间聚合的基础上使用嵌套聚合,即在每个区间内再进行其他类型的聚合操作,比如计算平均值、求和、最大值、最小值等。

示例:区间内计算平均价格

还是以上面的products索引为例,我们在每个价格区间内计算商品的平均价格。请求如下:

POST products/_search
{
    "size": 0,
    "aggs": {
        "price_ranges": {
            "range": {
                "field": "price",
                "ranges": [
                    { "from": 0, "to": 100 },
                    { "from": 100, "to": 200 },
                    { "from": 200 }
                ]
            },
            "aggs": {
                "avg_price": {
                    "avg": {
                        "field": "price"
                    }
                }
            }
        }
    }
}

在这个请求中,我们在price_ranges区间聚合下又定义了一个aggs部分,其中avg_price是自定义的嵌套聚合名称,avg表示这是一个求平均值的聚合操作,field指定要基于price字段计算平均值。

结果分析

返回的结果如下:

{
    "took": 1,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 3,
            "relation": "eq"
        },
        "max_score": null,
        "hits": []
    },
    "aggregations": {
        "price_ranges": {
            "buckets": [
                {
                    "key": "0.0-100.0",
                    "from": 0,
                    "to": 100,
                    "doc_count": 1,
                    "avg_price": {
                        "value": 50.0
                    }
                },
                {
                    "key": "100.0-200.0",
                    "from": 100,
                    "to": 200,
                    "doc_count": 1,
                    "avg_price": {
                        "value": 150.0
                    }
                },
                {
                    "key": "200.0-*",
                    "from": 200,
                    "doc_count": 1,
                    "avg_price": {
                        "value": 250.0
                    }
                }
            ]
        }
    }
}

在每个区间的bucket中,除了keyfromtodoc_count外,新增了avg_price部分,其中value表示该区间内商品的平均价格。通过这种方式,我们可以更深入地了解每个价格区间内商品价格的集中趋势。

动态区间聚合

动态区间聚合原理

动态区间聚合允许我们根据数据的实际情况自动生成区间,而不需要手动指定每个区间的边界。这在处理大量数据且事先不清楚数据分布范围时非常有用。ElasticSearch提供了auto类型的区间聚合方式来实现动态区间划分。

示例:基于数据自动划分价格区间

假设我们有大量的商品价格数据,我们希望根据数据的分布自动将价格划分为5个区间。请求如下:

POST products/_search
{
    "size": 0,
    "aggs": {
        "price_ranges": {
            "range": {
                "field": "price",
                "ranges": [
                    {
                        "keyed": true,
                        "to": "auto",
                        "partition": 5
                    }
                ]
            }
        }
    }
}

在这个请求中,to设置为auto表示动态生成区间,partition指定要划分的区间数量为5。keyed设置为true表示返回的结果将以区间为键的形式呈现,方便查看。

结果分析

返回的结果大致如下:

{
    "took": 1,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 3,
            "relation": "eq"
        },
        "max_score": null,
        "hits": []
    },
    "aggregations": {
        "price_ranges": {
            "buckets": {
                "0.0-70.0": {
                    "key": "0.0-70.0",
                    "from": 0,
                    "to": 70,
                    "doc_count": 1
                },
                "70.0-140.0": {
                    "key": "70.0-140.0",
                    "from": 70,
                    "to": 140,
                    "doc_count": 1
                },
                "140.0-210.0": {
                    "key": "140.0-210.0",
                    "from": 140,
                    "to": 210,
                    "doc_count": 1
                },
                "210.0-280.0": {
                    "key": "210.0-280.0",
                    "from": 210,
                    "to": 280,
                    "doc_count": 0
                },
                "280.0-350.0": {
                    "key": "280.0-350.0",
                    "from": 280,
                    "to": 350,
                    "doc_count": 0
                }
            }
        }
    }
}

ElasticSearch根据数据中的价格范围自动划分了5个区间,并统计了每个区间内的文档数量。从结果可以看到,不同区间的边界是根据数据分布动态生成的,即使某个区间内没有文档(如210.0 - 280.0280.0 - 350.0区间),也会在结果中体现出来。

区间聚合与日期范围结合

日期范围区间聚合概念

在很多实际场景中,我们不仅要对数值进行区间聚合,还需要结合日期范围进行分析。例如,分析不同时间段内不同价格区间的商品销售情况。ElasticSearch支持将日期字段与数值区间聚合相结合,为我们提供更全面的数据分析能力。

示例:分析每月不同价格区间的商品销售数量

假设我们有一个sales索引,每个文档包含date(销售日期)、product_name(商品名称)和price(销售价格)字段。我们要分析每个月内不同价格区间的商品销售数量。请求如下:

POST sales/_search
{
    "size": 0,
    "aggs": {
        "monthly_sales": {
            "date_histogram": {
                "field": "date",
                "calendar_interval": "month",
                "format": "yyyy - MM"
            },
            "aggs": {
                "price_ranges": {
                    "range": {
                        "field": "price",
                        "ranges": [
                            { "from": 0, "to": 100 },
                            { "from": 100, "to": 200 },
                            { "from": 200 }
                        ]
                    }
                }
            }
        }
    }
}

在这个请求中,外层的date_histogram聚合按月份对销售数据进行分组,field指定日期字段为datecalendar_interval设置为month表示按月分组,format指定输出的日期格式。内层的price_ranges区间聚合在每个月的分组内对价格进行区间划分。

结果分析

返回的结果大致如下:

{
    "took": 1,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 3,
            "relation": "eq"
        },
        "max_score": null,
        "hits": []
    },
    "aggregations": {
        "monthly_sales": {
            "buckets": [
                {
                    "key_as_string": "2023 - 01",
                    "key": 1672531200000,
                    "doc_count": 2,
                    "price_ranges": {
                        "buckets": [
                            {
                                "key": "0.0-100.0",
                                "from": 0,
                                "to": 100,
                                "doc_count": 1
                            },
                            {
                                "key": "100.0-200.0",
                                "from": 100,
                                "to": 200,
                                "doc_count": 1
                            },
                            {
                                "key": "200.0-*",
                                "from": 200,
                                "doc_count": 0
                            }
                        ]
                    }
                },
                {
                    "key_as_string": "2023 - 02",
                    "key": 1675123200000,
                    "doc_count": 1,
                    "price_ranges": {
                        "buckets": [
                            {
                                "key": "0.0-100.0",
                                "from": 0,
                                "to": 100,
                                "doc_count": 0
                            },
                            {
                                "key": "100.0-200.0",
                                "from": 100,
                                "to": 200,
                                "doc_count": 0
                            },
                            {
                                "key": "200.0-*",
                                "from": 200,
                                "doc_count": 1
                            }
                        ]
                    }
                }
            ]
        }
    }
}

monthly_salesbuckets数组中,每个元素对应一个月份。key_as_string以指定格式显示月份,key是时间戳形式的月份标识,doc_count表示该月内的销售文档总数。在每个月的price_ranges部分,又按价格区间进行了统计,展示了每个月不同价格区间的商品销售数量。通过这种方式,我们可以清晰地看到不同月份不同价格区间的销售趋势。

区间聚合性能优化

数据建模优化

在使用区间聚合前,合理的数据建模非常重要。例如,如果我们经常要对某个数值字段进行区间聚合,应确保该字段在索引时设置为合适的类型,并且避免过多的嵌套结构。对于数值类型,选择合适的精度也能在一定程度上提高性能。比如,如果我们的价格数据通常不会有小数部分,将其定义为整数类型可以减少存储空间和查询计算量。

索引设置优化

在创建索引时,可以调整一些参数来优化区间聚合性能。例如,适当增加number_of_shardsnumber_of_replicas可以提高查询的并行度,但同时也会增加存储和维护成本。此外,合理设置refresh_interval也很关键。如果设置得过小,会导致频繁的索引刷新,影响性能;如果设置过大,数据的实时性会受到影响。对于区间聚合场景,如果对实时性要求不是特别高,可以适当增大refresh_interval

查询优化

在执行区间聚合查询时,尽量减少不必要的字段返回。我们可以通过设置size: 0只获取聚合结果,避免返回大量文档数据。同时,对于复杂的嵌套聚合,可以合理调整聚合的顺序,将数据量小的聚合操作放在外层,以减少中间结果的处理量。另外,如果可能,尽量使用缓存机制,对于一些不经常变化的数据,缓存聚合结果可以显著提高查询效率。

复杂区间聚合场景

多字段联合区间聚合

在实际业务中,有时需要基于多个数值字段进行联合区间聚合。例如,在分析员工数据时,我们可能既要考虑员工的年龄区间,又要考虑员工的薪资区间。假设我们有一个employees索引,每个文档包含age(年龄)和salary(薪资)字段。我们要分析不同年龄区间内不同薪资区间的员工数量。请求如下:

POST employees/_search
{
    "size": 0,
    "aggs": {
        "age_ranges": {
            "range": {
                "field": "age",
                "ranges": [
                    { "from": 20, "to": 30 },
                    { "from": 30, "to": 40 },
                    { "from": 40 }
                ]
            },
            "aggs": {
                "salary_ranges": {
                    "range": {
                        "field": "salary",
                        "ranges": [
                            { "from": 5000, "to": 10000 },
                            { "from": 10000, "to": 15000 },
                            { "from": 15000 }
                        ]
                    }
                }
            }
        }
    }
}

在这个请求中,首先按年龄进行区间聚合,然后在每个年龄区间内再按薪资进行区间聚合。通过这种方式,我们可以得到不同年龄层次员工的薪资分布情况。

区间聚合与过滤结合

在进行区间聚合时,我们可能只想对满足特定条件的文档进行分析。例如,在分析商品销售数据时,我们只关注某个品牌的商品在不同价格区间的销售情况。假设sales索引中的文档包含brand(品牌)、price(价格)和quantity(销售数量)字段。我们要分析Brand X品牌商品不同价格区间的销售总量。请求如下:

POST sales/_search
{
    "size": 0,
    "query": {
        "match": {
            "brand": "Brand X"
        }
    },
    "aggs": {
        "price_ranges": {
            "range": {
                "field": "price",
                "ranges": [
                    { "from": 0, "to": 100 },
                    { "from": 100, "to": 200 },
                    { "from": 200 }
                ]
            },
            "aggs": {
                "total_quantity": {
                    "sum": {
                        "field": "quantity"
                    }
                }
            }
        }
    }
}

在这个请求中,query部分使用match查询过滤出brandBrand X的文档。然后在这些文档上进行价格区间聚合,并在每个区间内计算销售总量。通过这种结合过滤的方式,我们可以更精准地分析特定子集数据的区间聚合情况。

区间聚合在不同行业的应用案例

电商行业

在电商平台中,区间聚合可用于分析商品价格分布、销量分布等。例如,分析不同价格区间的商品销量,帮助商家制定价格策略。通过区间聚合,电商平台可以了解到哪些价格区间的商品最受欢迎,哪些价格区间的商品竞争较小等信息。同时,结合日期范围的区间聚合可以分析不同时间段内不同价格区间商品的销售趋势,为商家的库存管理和促销活动提供依据。

金融行业

在金融领域,区间聚合常用于分析交易金额分布、风险等级分布等。例如,银行可以通过分析不同金额区间的交易数量和频率,来识别潜在的异常交易行为。对于投资公司,通过区间聚合分析不同风险等级投资产品的收益分布,可以更好地为客户提供投资建议和资产配置方案。

医疗行业

在医疗数据管理中,区间聚合可用于分析患者年龄分布、疾病严重程度分布等。例如,医院可以通过分析不同年龄区间内患者所患疾病的种类和数量,了解不同年龄段的疾病高发情况,从而有针对性地开展预防和治疗工作。同时,对于疾病严重程度的区间聚合分析,可以帮助医院合理分配医疗资源,提高治疗效率。

通过以上对ElasticSearch区间聚合的深入介绍,包括基础概念、各种示例、性能优化以及复杂场景和应用案例,相信读者对区间聚合在数值范围分析中的应用有了全面且深入的理解,能够在实际项目中灵活运用这一强大功能进行数据分析和业务决策。