Elasticsearch按priority分组后按dueDate日期范围嵌套聚合实现
问题背景
Elasticsearch中存储的Value实体结构如下:
class Value { private Integer priority; private Date dueDate; }
priority字段可选值为1、2,本次不讨论枚举设计合理性问题- 需要统计4类文档总数:
- priority为1且dueDate为今日或过去日期
- priority为1且dueDate为null或未来日期
- priority为2且dueDate为今日或过去日期
- priority为2且dueDate为null或未来日期
原有实现问题
初始查询仅对priority字段做terms聚合,只能统计priority=1和priority=2的总文档数,代码如下:
SearchRequest request = new SearchRequest.Builder() .index(MY_INDEX) .query(getQuery(someKeys)) .aggregations(ELASTIC_AGG_PRIORITY, Aggregation.of(a -> a.terms(terms -> terms.field(ELASTIC_AGG_PRIORITY).missing("")))) .build();
尝试添加嵌套聚合拆分dueDate维度时,遇到两个核心问题:
- 范围查询无法正确传递日期参数
- 无法在聚合中同时匹配未来日期、null值两类dueDate
另外原有嵌套聚合代码存在子聚合与外层聚合重名问题,会导致结果被覆盖,也是结果不符合预期的原因之一。
原有范围查询方法:
public static Query getRangeQuery(String field) { return QueryBuilders.range().field(field).lte(JsonData.of(new Date())).build()._toQuery(); }
原有未达预期的嵌套聚合代码:
SearchRequest request = new SearchRequest.Builder() .index(MY_INDEX) .query(getQuery(someKeys)) .aggregations(ELASTIC_AGG_ASSIGNEE, Aggregation.of(a -> a.terms(terms -> terms.field(ELASTIC_AGG_ASSIGNEE).missing("")))) .aggregations(ELASTIC_AGG_PRIORITY, Aggregation.of(a -> a.terms(terms -> terms.field(ELASTIC_AGG_PRIORITY).missing("")) // 此处子聚合名和外层聚合名重复,会覆盖结果 .aggregations(ELASTIC_AGG_PRIORITY, Aggregation.of(filterAgg -> filterAgg .filter(QueryUtil.getRangeQuery("dueDate")))) ) ).build();
解决方案
1. 日期查询逻辑修正
直接传入new Date()会带当前时分秒,导致当日未到当前时间点的文档被误判为未来日期,需要取当日零点作为分界点,同时指定日期格式、时区避免解析偏差。
修正后的过去/今日日期范围查询方法:
import java.time.LocalDate; import java.time.ZoneId; import java.time.ZonedDateTime; // 取业务时区的当日零点,生产环境建议固定时区,例如ZoneId.of("Asia/Shanghai") private static final ZonedDateTime TODAY_START = LocalDate.now().atStartOfDay(ZoneId.systemDefault()); public static Query getPastOrTodayRangeQuery(String field) { return QueryBuilders.range() .field(field) .lte(JsonData.of(TODAY_START)) .format("strict_date_optional_time") .build() ._toQuery(); }
2. 多层聚合实现
在priority的terms聚合下,添加两个独立命名的filter子聚合:
- 第一个子聚合匹配dueDate ≤ 今日零点的文档,对应今日/过去日期
- 第二个子聚合用
bool组合两个should条件:dueDate > 今日零点、dueDate字段不存在(即null值),满足任意一个即命中,对应未来/null日期
完整查询代码:
// 聚合名称常量,避免重名覆盖 private static final String AGG_PRIORITY = "priority_group"; private static final String AGG_DUE_PAST_TODAY = "due_past_today"; private static final String AGG_DUE_FUTURE_NULL = "due_future_null"; SearchRequest request = new SearchRequest.Builder() .index(MY_INDEX) .query(getQuery(someKeys)) // 不需要assignee聚合可删除该行 .aggregations(ELASTIC_AGG_ASSIGNEE, Aggregation.of(a -> a.terms(terms -> terms.field(ELASTIC_AGG_ASSIGNEE).missing("")))) // 外层priority分组聚合 .aggregations(AGG_PRIORITY, Aggregation.of(priorityAgg -> priorityAgg.terms(terms -> terms.field("priority").size(2)) // 子聚合:due为今日/过去 .aggregations(AGG_DUE_PAST_TODAY, Aggregation.of(subAgg -> subAgg.filter(getPastOrTodayRangeQuery("dueDate")) )) // 子聚合:due为未来/null .aggregations(AGG_DUE_FUTURE_NULL, Aggregation.of(subAgg -> subAgg.filter(f -> f.bool(b -> b .should(s -> s.range(r -> r .field("dueDate") .gt(JsonData.of(TODAY_START)) .format("strict_date_optional_time") )) .should(s -> s.bool(mn -> mn.mustNot(ex -> ex.exists(e -> e.field("dueDate"))))) .minimumShouldMatch("1") )) )) )) .size(0) // 仅需聚合结果时设为0,不返回文档内容提升查询性能 .build();
结果取值逻辑
查询返回后,从AGG_PRIORITY的聚合结果中遍历buckets:
- key为1的bucket:
AGG_DUE_PAST_TODAY的doc_count为priority=1且dueDate为今日/过去的文档数AGG_DUE_FUTURE_NULL的doc_count为priority=1且dueDate为未来/null的文档数
- key为2的bucket:
AGG_DUE_PAST_TODAY的doc_count为priority=2且dueDate为今日/过去的文档数AGG_DUE_FUTURE_NULL的doc_count为priority=2且dueDate为未来/null的文档数
注意:如果索引中
dueDate字段的存储格式不是默认的strict_date_optional_time,需要将代码中format参数替换为实际使用的日期格式,例如yyyy-MM-dd。
内容的提问来源于stack exchange,提问作者f.trajkovski
相关产品推荐
相关产品推荐

