Elasticsearch中路径层级令牌的分层聚合解决方案咨询
问题描述
我有一个存储层级数据的Elasticsearch索引locations_hierarchy,示例文档如下:
{ "_index" : "locations_hierarchy", "_type" : "_doc", "_id" : "1", "_score" : 1.0, "_source" : { "category_path" : "Art Gallery, Paintings, Case 1" } }, { "_index" : "locations_hierarchy", "_type" : "_doc", "_id" : "2", "_score" : 1.0, "_source" : { "category_path" : "Display Gallery, Small Worlds, Case 1, Shelf 2" } }
我希望实现分层嵌套聚合的效果,示例输出如下:
{ "key" : "Art Gallery", "doc_count" : 4, "category_sub" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 0, "buckets" : [ { "key" : "Paintings", "doc_count" : 2, "category_sub_sub" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 0, "buckets" : [ { "key" : "Case 1", "doc_count" : 1 }, { "key" : "Case 3", "doc_count" : 1 } ] } }
之前尝试用聚合的include/exclude参数手动处理,但操作繁琐,也无法适配层级数量不固定的场景。想知道Elasticsearch有没有针对这类路径层级字段的分层聚合方案?
解决方案
思路1:优化数据存储结构(推荐)
如果可以修改索引结构,直接把层级路径拆分为结构化格式,能从根源上简化聚合操作:
方式1:存储为层级数组
将category_path拆分为数组字段category_hierarchy,示例文档改为:
{ "_source": { "category_hierarchy": ["Art Gallery", "Paintings", "Case 1"] } }
然后使用多层terms聚合,结合脚本提取对应层级的元素:
{ "aggs": { "level_1": { "terms": { "script": "doc['category_hierarchy'][0]" }, "aggs": { "level_2": { "terms": { "script": "doc['category_hierarchy'].length > 1 ? doc['category_hierarchy'][1] : ''" }, "aggs": { "level_3": { "terms": { "script": "doc['category_hierarchy'].length > 2 ? doc['category_hierarchy'][2] : ''" } } } } } } } }
这种方式可根据业务最大层级数提前定义聚合层级,也能通过脚本适配不同长度的层级数据。
方式2:存储为嵌套对象
如果需要明确层级的父子关系,用nested类型存储每个层级节点:
{ "_source": { "categories": [ {"level": 1, "name": "Art Gallery"}, {"level": 2, "name": "Paintings"}, {"level": 3, "name": "Case 1"} ] } }
需先在索引映射中设置categories为nested类型,再通过nested聚合+terms聚合实现分层统计:
{ "aggs": { "level_1": { "nested": { "path": "categories" }, "aggs": { "filter_level_1": { "filter": { "term": {"categories.level": 1} }, "aggs": { "category_name": { "terms": {"field": "categories.name.keyword"}, "aggs": { "level_2": { "nested": { "path": "categories" }, "aggs": { "filter_level_2": { "filter": { "term": {"categories.level": 2} }, "aggs": { "category_name": { "terms": {"field": "categories.name.keyword"} } } } } } } } } } } } } }
思路2:基于原字符串字段的动态聚合(无需修改数据)
如果无法修改现有索引结构,可通过脚本+分词预处理实现动态分层聚合:
步骤1:添加path_hierarchy分词器字段
修改索引映射,为category_path添加使用path_hierarchy分词器的子字段:
{ "mappings": { "_doc": { "properties": { "category_path": { "type": "text", "fields": { "hierarchy": { "type": "text", "analyzer": { "type": "custom", "tokenizer": "path_hierarchy", "delimiter": "," } } } } } } } }
该分词器会将Art Gallery, Paintings, Case 1拆分为:
Art GalleryArt Gallery,PaintingsArt Gallery,Paintings,Case 1
步骤2:脚本实现分层聚合
使用terms聚合结合脚本提取各层级节点:
{ "aggs": { "level_1": { "terms": { "script": { "source": """ def parts = doc['category_path.keyword'].value.split(','); return parts.length > 0 ? parts[0].trim() : ''; """ } }, "aggs": { "level_2": { "terms": { "script": { "source": """ def parts = doc['category_path.keyword'].value.split(','); return parts.length > 1 ? parts[1].trim() : ''; """ } }, "aggs": { "level_3": { "terms": { "script": { "source": """ def parts = doc['category_path.keyword'].value.split(','); return parts.length > 2 ? parts[2].trim() : ''; """ } } } } } } } } }
这种方案无需修改原数据,但脚本聚合性能略低于结构化存储方案,适合数据量不大的场景。
补充说明
- 如果层级数量完全不确定,Elasticsearch原生不支持无限递归的动态聚合,需在应用层递归调用聚合接口:每次以上一层的key作为过滤条件,查询下一层的节点。
- 优先选择优化数据结构的方案,不仅聚合性能更高,也更易维护。
内容的提问来源于stack exchange,提问作者sheharbano
相关产品推荐
相关产品推荐

