You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Elasticsearch按priority分组后按dueDate日期范围嵌套聚合实现

问题背景

Elasticsearch中存储的Value实体结构如下:

class Value {
  private Integer priority;
  private Date dueDate;
}
  • priority字段可选值为1、2,本次不讨论枚举设计合理性问题
  • 需要统计4类文档总数:
    1. priority为1且dueDate为今日或过去日期
    2. priority为1且dueDate为null或未来日期
    3. priority为2且dueDate为今日或过去日期
    4. 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维度时,遇到两个核心问题:

  1. 范围查询无法正确传递日期参数
  2. 无法在聚合中同时匹配未来日期、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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 13:09:12