Elasticsearch 使用terms对long类型日期统计按月销售

索引mapping:

{
      "properties" : {
        "_class" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "createDate" : {
          "type" : "long"
        },
        "goodsCode" : {
          "type" : "keyword"
        },
        "id" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "orderNumber" : {
          "type" : "keyword"
        },
        "payDate" : {
          "type" : "long"
        },
        "saleQty" : {
          "type" : "double"
        }
      }
    }

payDate字段映射为long类型,在使用date_histogram统计时,统计不出想要的效果。

{
    ...
    "aggs": {
       "agg_name": {
            "date_histogram": {
                "field": "payDate",
                "interval": "1m",
                "format": "yyyy-MM",
                "min_doc_count": 0
            }
        }
    }
}

结果输出的是"yyyy-MM1577836800000",并不是期待的2023-06.

{
    "key_as_string": "yyyy-MM1577836800000",
    "key": 1577836800000,
    "doc_count": 3
}

改用terms+script+子聚合方式统计:

{
  "query": {
    "range": {
      "payDate": {
        "gte": 1685548800000
      }
    }
  },
  "size":0, 
  "aggs": {
    "month_sales":{
      "terms": {
        "script": {
          "source": """
        DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM").withZone(ZoneId.systemDefault());
           return df.format(Instant.ofEpochMilli(doc['payDate'].value));
        """,
        "lang":"painless"
        },
        "order": {
          "_key": "asc"
        }
      },
      "aggs": {
        "sale_sum": {
          "sum": {
            "field": "saleQty"
          }
        }
      }
    }
  }
}

script:使用脚本把pay格式化成yyyy-MM的字符串。
order:根据日期排序
sale_sum:统计每个桶里面商品销售数量

输出结果:

{
    "month_sales": {
        "doc_count_error_upper_bound": 0,
        "sum_other_doc_count": 9723368,
        "buckets": [
            {
                "key": "2023-06",
                "doc_count": 49,
                "sale_sum": {
                    "value": 77.0
                }
            },
            {
                "key": "2023-07",
                "doc_count": 217,
                "sale_sum": {
                    "value": 40012.0
                }
            },
            {
                "key": "2023-08",
                "doc_count": 2187911,
                "sale_sum": {
                    "value": 3486633.0
                }
            },
            {
                "key": "2023-09",
                "doc_count": 2746235,
                "sale_sum": {
                    "value": 4556942.0
                }
            },
            {
                "key": "2023-10",
                "doc_count": 2346896,
                "sale_sum": {
                    "value": 4097690.0
                }
            },
            {
                "key": "2023-11",
                "doc_count": 2442224,
                "sale_sum": {
                    "value": 4120708.0
                }
            },
            {
                "key": "2023-12",
                "doc_count": 2359840,
                "sale_sum": {
                    "value": 4167825.0
                }
            },
            {
                "key": "2024-01",
                "doc_count": 3619202,
                "sale_sum": {
                    "value": 6506941.0
                }
            },
            {
                "key": "2024-02",
                "doc_count": 2673919,
                "sale_sum": {
                    "value": 5066871.0
                }
            },
            {
                "key": "2024-03",
                "doc_count": 3078439,
                "sale_sum": {
                    "value": 4810836.0
                }
            }
        ]
    }
}

key:每个月份
doc_count:每个月的订单数量
sale_sum:每个月商品销售数量

<think>我们被要求用Java和Elasticsearch7实现一个基于脚本的terms聚合,并且要分区(partitioning)。在Elasticsearch中,terms聚合通常用于根据字段的值进行分组。但是,如果我们想根据脚本动态地决定分区(即分组的方式),我们可以使用脚本来自定义分组的逻辑。具体需求:使用脚本将文档动态分配到不同的分区(桶)中,然后对每个分区进行聚合(比如统计每个分区的文档数)。在Elasticsearch中,我们可以通过编写脚本来返回一个值,然后根据这个值进行分组。例如,我们可以根据某个字段的值的范围、字符串的前缀或其他自定义逻辑来分组。步骤:1.构建一个聚合(Aggregation),类型为`terms`,但是使用`script`来生成每个文档的桶键(bucketkey)。2.我们可以设置桶的大小(即返回多少个桶)等参数。注意:在Elasticsearch7中,使用脚本时需要注意脚本语言的选择。由于Elasticsearch6.0以后移除了默认的Groovy脚本,我们可以使用Painless脚本(默认)或者其他支持的脚本语言。示例场景:假设我们有一个索引,包含商品信息,其中有一个价格字段(price)。我们想根据价格范围进行分区(例如0-100,100-500,500-1000,1000以上),然后统计每个价格区间的商品数量。实现思路:-编写一个Painless脚本,根据价格字段的值返回一个字符串(代表分区名称)。-使用terms聚合,将脚本作为分桶的依据。在JavaAPI中,我们可以使用`org.elasticsearch.client`包中的RestHighLevelClient来构建请求。具体代码步骤:1.创建一个SearchRequest对象。2.构建一个SearchSourceBuilder,并在其中添加聚合。3.聚合使用TermsAggregationBuilder,并设置其脚本(Script)和大小(size,即返回的桶的数量)。4.执行搜索请求,并处理聚合结果。注意:由于我们使用脚本,所以需要确保脚本是安全的(例如使用参数化)并且高效。示例代码:假设我们有一个索引"products",字段"price"表示商品价格。我们将分区定义为:-"0-100": price<=100-"100-500": price>100 andprice <=500- "500-1000": price>500 andprice <=1000-"1000+": price>1000脚本可以这样写(Painless): ```javaif(doc['price'].value<=100){return "0-100";} elseif (doc['price'].value <=500) {return"100-500"; }else if(doc['price'].value<=1000) {return"500-1000";} else{return "1000+";} ```在Java代码中构建聚合:```javaimportorg.elasticsearch.action.search.SearchRequest; importorg.elasticsearch.action.search.SearchResponse; importorg.elasticsearch.client.RequestOptions;import org.elasticsearch.client.RestHighLevelClient; importorg.elasticsearch.index.query.QueryBuilders;import org.elasticsearch.script.Script;import org.elasticsearch.script.ScriptType; importorg.elasticsearch.search.aggregations.AggregationBuilders;import org.elasticsearch.search.aggregations.bucket.terms.Terms; importorg.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; importorg.elasticsearch.search.builder.SearchSourceBuilder; importjava.io.IOException;import java.util.Collections; //假设我们已经有了RestHighLevelClient实例:client//创建搜索请求SearchRequestsearchRequest= newSearchRequest("products"); //索引名SearchSourceBuilder searchSourceBuilder= newSearchSourceBuilder();//构建脚本Scriptscript =new Script(ScriptType.INLINE, "painless","if(doc['price'].value<=100){ "+"return'0-100'; "+"}else if(doc['price'].value<=500){ "+"return'100-500'; "+"}else if(doc['price'].value<=1000) {" +"return '500-1000'; "+"}else {" +"return '1000+';" +"}",Collections.emptyMap()); //创建terms聚合,设置脚本和桶的大小(这里我们期望4个桶,所以size至少为4)TermsAggregationBuilderaggregationBuilder= AggregationBuilders.terms("price_ranges").script(script).size(4);//因为我们有4个分区//将聚合添加到搜索源searchSourceBuilder.aggregation(aggregationBuilder);//设置查询(如果需要,这里可以设置查询条件,例如匹配所有)searchSourceBuilder.query(QueryBuilders.matchAllQuery());searchRequest.source(searchSourceBuilder);//执行请求SearchResponseresponse= client.search(searchRequest,RequestOptions.DEFAULT); //处理聚合结果Termsterms =response.getAggregations().get("price_ranges");for (Terms.Bucket bucket: terms.getBuckets()){String key= bucket.getKeyAsString();//分区名称long docCount =bucket.getDocCount();//该分区的文档数System.out.println("Partition:" +key +", Count: "+ docCount);} ```注意:上面的脚本直接使用了`doc['price'].value`,这种方式在脚本中访问文档字段值。但要注意,如果字段有多个值,那么`doc['price'].value`会返回第一个值。如果字段是空的,可能会抛出异常。在实际应用中,我们需要根据数据的实际情况进行健壮性处理。另外,我们也可以将脚本中的阈值参数化,这样更灵活。例如:```javaMap<String,Object>params =new HashMap<>(); params.put("threshold1",100);params.put("threshold2",500); params.put("threshold3",1000); Scriptscript =new Script(ScriptType.INLINE, "painless","if(doc['price'].value<= params.threshold1){ "+"return'0-100'; "+"}else if(doc['price'].value<= params.threshold2){ "+"return'100-500'; "+"}else if(doc['price'].value<= params.threshold3){ "+"return'500-1000';" +"} else{ "+"return'1000+'; "+"}",params);```这样,我们就可以在外部传递参数,而不用硬编码在脚本中。总结:通过使用脚本在terms聚合中,我们可以实现基于自定义逻辑的分区(分组)。这为数据分析提供了极大的灵活性。注意事项:-脚本性能:脚本执行可能会影响查询速度,特别是在大数据集上。因此,应尽量优化脚本,或者考虑在索引时添加一个表示分区的字段。-空值处理:在脚本中处理可能缺失的字段,避免运行时错误。以上就是在Java中使用Elasticsearch7实现基于脚本的terms聚合分区的方法。</think>### 在 Java 中使用 Elasticsearch 7 实现基于脚本的 Terms 聚合分区 在 Elasticsearch 7 中,可以通过脚本动态定义 terms 聚合的分区逻辑,实现更灵活的数据分组。以下是完整实现步骤: #### 1. 添加 Maven 依赖 ```xml <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>7.17.0</version> </dependency> ``` #### 2. 核心实现代码 ```java import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptType; import org.elasticsearch.search.aggregations.AggregationBuilders; import org.elasticsearch.search.aggregations.bucket.terms.Terms; import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; import org.elasticsearch.search.builder.SearchSourceBuilder; import java.util.Collections; import java.util.Map; public class ScriptedTermsAggregation { public void executeScriptedTermsAgg(RestHighLevelClient client, String indexName) { // 1. 创建脚本定义分区逻辑 Script partitionScript = new Script( ScriptType.INLINE, "painless", """ // 示例:按价格范围分区 double price = doc['price'].value; if (price < 50) return 'low'; if (price < 200) return 'medium'; return 'high'; """, Collections.emptyMap() ); // 2. 构建带脚本的 terms 聚合 TermsAggregationBuilder aggregation = AggregationBuilders.terms("price_ranges") .script(partitionScript) .size(10); // 返回的分桶数量 // 3. 构建搜索请求 SearchSourceBuilder sourceBuilder = new SearchSourceBuilder() .query(QueryBuilders.matchAllQuery()) .aggregation(aggregation) .size(0); // 不需要返回原始文档 SearchRequest searchRequest = new SearchRequest(indexName) .source(sourceBuilder); try { // 4. 执行查询并处理结果 SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT); Terms terms = response.getAggregations().get("price_ranges"); for (Terms.Bucket bucket : terms.getBuckets()) { System.out.printf("分区: %s, 文档数: %d%n", bucket.getKey(), bucket.getDocCount()); } } catch (IOException e) { e.printStackTrace(); } } } ``` #### 3. 关键参数说明 - **脚本类型**:使用 `painless`(Elasticsearch 7 默认脚本引擎) - **分区逻辑**:脚本必须返回字符串类型的分区键 - **动态参数**:可通过 `Map<String, Object>` 传递参数到脚本: ```java Map<String, Object> params = new HashMap<>(); params.put("threshold", 100); new Script(ScriptType.INLINE, "painless", "return doc['price'].value > params.threshold ? 'high' : 'low'", params); ``` #### 4. 高级用法:多字段分区 ```java Script multiFieldScript = new Script( ScriptType.INLINE, "painless", "return doc['category'].value + '_' + (doc['price'].value > 100 ? 'premium' : 'standard')", Collections.emptyMap() ); ``` #### 5. 性能优化建议 1. **脚本缓存**:添加 `.executionHint("map")` 启用映射执行模式 ```java .aggregation(AggregationBuilders.terms("agg") .script(script) .executionHint("map")) ``` 2. **分区预计算**:对高频查询,在索引时添加 `runtime_field` 3. **限制桶数量**:通过 `.size()` 控制返回的最大桶数 #### 典型输出示例 ``` 分区: low, 文档数: 1243 分区: medium, 文档数: 5678 分区: high, 文档数: 891 ``` > **注意事项**: > 1. 脚本字段访问需使用 `doc['field'].value` 语法 > 2. 确保字段类型与脚本操作兼容(如数值字段进行数学运算) > 3. 在开发环境启用 `script.debug` 调试脚本逻辑 > 4. 生产环境需在 `elasticsearch.yml` 配置脚本安全策略
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冲上云霄的Jayden

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值