# ElasticSearch 分布式搜索以及数据分析引擎
# 概述
- Elasticsearch是一个分布式的开源的搜索引擎,可以提供近乎实时的存储、搜索服务
- ElasticSearch是用java开发的,并作为Apache许可条款下的开放源码发布,是当前流行的企业级搜索引擎
- Elaticsearch的横向扩展很好,可以扩展到上百台服务器,处理PB级别的数据
- HOS平台提供 Spring-Data-ElasticSearch 以及 RestHighLevelClient(ElasticSearch JAVA API) 两种方式供开发者使用,均支持对elasticsearch的常用操作
# 配置
# 引入依赖
<dependency>
<groupId>com.mediway.hos</groupId>
<artifactId>hos-framework-elasticsearch-starter</artifactId>
</dependency>
# 配置项说明
## spring-data-elasticsearch配置项
spring:
elasticsearch:
rest:
# 服务地址,可设置多个
uris:
- http://localhost:9200
# 用户名
username:
# 密码
password:
## 读取数据的超时时间,默认为30s
read-timeout: 30s
## 链接超时时间,默认为1s
connection-timeout: 1s
其他配置项请参考spring-data-elasticsearch官方文档
# Spring-Data-ElasticSearch代码示例
# 1、创建实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Document(indexName = "sysuser_index")
public class SysUser implements Serializable {
//Id
@Id
private Long id;
//年龄
@Field(type = FieldType.Integer)
private Integer age;
//昵称
@Field(type = FieldType.Text,analyzer = "ik_max_word")
private String nickname;
//标识
@Field(type = FieldType.Text)
private String sign;
//状态
@Field(type = FieldType.Boolean)
private boolean status;
//用户名
@Field(type = FieldType.Text,analyzer = "ik_max_word")
private String username;
//角色
@Field(type = FieldType.Keyword)
private String role;
}
补充说明:
该类中与elasticSearch功能相关的注解为:@Document
、@Id
、@Field
三个注解
@Document:
用在类上,表明这个类是ElasticSearch映射实体。
属性名称 属性描述 indexName 索引名称,必须属性 @Id: 使用在字段上,表明主键字段。该注解为Spring-data二级功能的通用注解。
@Field: 使用在字段上,表明该字段用于映射,只要添加了@Field注解,数据保存时都会保存到elasticSearch中。
属性名称 属性描述 type 字段类型,默认值为 FieldType.Auto
(根据第一次插入数据的类型自动生成)。对应elasticSearch中字段类型,常用的有FieldType.Integer
、FieldType.Long
、FieldType.Double
、FieldType.Date
、FieldType.Text
,FieldType.Keyword
(关键字)analyzer 字段所使用的分析器,type属性为 FieldType.Text
生效,默认值standard
,在插入数据之后,elasticsearch会根据分析器自动拆分关键词
详细请参考Spring-Data-ElasticSearch官方文档-映射相关注解 (opens new window)
# 2、创建操作DAO
/**
*
* 继承ElasticsearchRepository类,包含两个参数,
* 第一个参数为实体类,此处使用的是SysUser,
* 第二个参数为Id(主键)类型
*
*/
@Repository
public interface SysUserDAO extends ElasticsearchRepository<SysUser,Long> {
}
# 3、自动装配以及调用
通过@Autowired
自动装配 SysUserDAO
即可使用
@Autowired
SysUserDAO sysUserDAO;
# 4、使用介绍
前提:完成配置中的相关步骤,且完成了实体类以及DAO的类
# 新增/更新
数据新增与更新操作主要通过<S extends T> S save(S entity);
方法实现,例如
//初始化对应SysUser对象,如果是更新操作首先要通过查询获取到相应对象
SysUser user=new SysUser();
user.setId(1001l);
user.setAge(33);
user.setUsername("王老五");
user.setNickname("老王");
user.setSign("内测");
user.setStatus(true);
user.setRole("管理员");
//保存或更新数据,如果存在数据会进行覆盖
sysUserDAO.save(user);
# 删除
删除操作通过void deleteById(ID id);
实现,例如
//根据id进行删除,id为主键
sysUserDAO.deleteById(id);
# 查询
sysUserDAO中默认提供下面两个方法:
//根据主键获取数据
Optional<T> findById(ID id);
//获取所有数据
Iterable<T> findAll();
需要自定义查询有三种方式实现:直接使用方法名定义查询、
@Query注解、
通过ElasticsearchOperations
类操作、
# 使用方法名定义查询
需要写在定义的SysUserDAO
类中,
通过按指定规则定义查询方法名称,无需写实现,springdata会自动完成。
固定语法为 find...By..
。
使用此方式字段名称要和定义的实体对象中的名称保持一致,入参不能为null。
详细内容可以参考
Spring-Data-elasticsearch官方文档-查询名称定义 (opens new window)
下面为几个简单的例子
//根据username字段检索数据
List<SysUser> findByUsername(String username);
//根据username字段,和age字段检索数据
List<SysUser> findByUsernameAndAge(String username,Integer age);
//根据username字段检索数据,并根据age正序排序
List<SysUser> findByUsernameOrderByAgeAsc(String username);
//根据username字段检索数据,根据sort对象中的方式排名,获取前10个
List<SysUser> findFirst10ByUsername(String username, Sort sort);
//根据username字段检索数据,根据pageable对象中的设置的分页方式获取
Page<SysUser> findByUsername(String username, Pageable pageable);
//根据username字段检索并根据username字段排名,获取第一项
SysUser findFirstByOrderByUsernameAsc(String username);
# @Query注解
需要写在定义的SysUserDAO
类中,通过@Query
注解实现查询,其功能类似于直接接拼写查询sql。
@Query
对应 调用 elasticsearch restful API 的 _search(查询)功能时,
RequestBody 中 query字段中的内容。
通过使用 ?+参数位置 方式指定到对应参数
例如完整请求的JSON为
{
"query":{
"bool":{
"must":{
"match":{
"username":"张三"
}
}
}
}
}
则,@Query
注解中的值为
"bool":{
"must":{
"match":{
"username":"张三"
}
}
}
一个完整的方法:
/**
* 根据userName匹配username字段
*
* @param userName
* @return
*/
@Query("{" +
"\"bool\":{" +
"\"must\":{" +
"\"match\":{" +
"\"username\":\"?0\"" +
"}" +
"}" +
"}" +
"}")
List<SysUser> findUsers(String userName);
# 通过ElasticsearchOperations
类操作
MatchQueryBuilder、MatchPhraseQueryBuilder、TermQueryBuilder、BoolQueryBuilder等 组成较为复杂的查询,Pageable实现分页、SortBuilder实现排序、AbstractAggregationBuilder实现聚合
例如根据username匹配数据
// 关键字查询,查询username字段中值和name参数匹配的所有数据
// TermQueryBuilder对应term字段,为精确查找
// TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("username"/*字段名*/, name/*值*/);
// 关键字查询,查询username字段中值和name参数匹配的所有数据
// MatchPhraseQueryBuilder对应match_phrase字段,为模糊短语查找
// MatchPhraseQueryBuilder termQueryBuilder = QueryBuilders.matchPhraseQuery("username"/*字段名*/, name/*值*/);
// 关键字查询,查询username字段中值和name参数匹配的所有数据
// MatchPhraseQueryBuilder对应match字段,,为模糊查找
MatchQueryBuilder termQueryBuilder = QueryBuilders.matchQuery("username"/*字段名*/, name/*值*/);
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
nativeSearchQueryBuilder.withQuery(termQueryBuilder);
NativeSearchQuery nativeSearchQuery = nativeSearchQueryBuilder.build();
//IndexCoordinates.of("sysuser_index")定义使用的索引
//Map.class 定义数据的格式
SearchHits<Map> search = this.elasticsearchOperations.search(nativeSearchQuery, Map.class, IndexCoordinates.of("sysuser_index"/*索引名*/));
根据username(用户名)、年龄、角色检索数据,根据年龄排序,并分页
//复合查询使用
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
// 查询必须满足的条件,类似于AND
// 含义为字段role中值和role参数匹配
boolQueryBuilder.must(QueryBuilders.termQuery("role", role));
// 查询可能满足的条件,类似于OR
// 含义为字段username中值和username1参数,或者username2参数与匹配
boolQueryBuilder.should(QueryBuilders.termQuery("username", username1));
boolQueryBuilder.should(QueryBuilders.termQuery("username", username2));
// 设置在可能满足的条件中,至少必须满足其中1条.多个或条件中至少满足一项
boolQueryBuilder.minimumShouldMatch(1);
// 必须不满足的条件
// 含义为字段age中值不等于8
boolQueryBuilder.mustNot(QueryBuilders.termQuery("age", 8));
//范围条件1,对应字段age
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("age");
// 大于等于ageL
rangeQueryBuilder.gte(ageL);
// 小于等于ageH
rangeQueryBuilder.lte(ageH);
//范围条件2,对应字段age
RangeQueryBuilder rangeQueryBuilder1 = QueryBuilders.rangeQuery("age");
// 大于ageL
rangeQueryBuilder1.gt(ageL);
// 小于ageH
rangeQueryBuilder1.lt(ageH);
// 日期格式下使用
// rangeQueryBuilder2.gte("2016-01-01");
// rangeQueryBuilder2.lte("2020-01-01");
// rangeQueryBuilder2.format("yyyy-MM-dd");
//分页,page为当前页数,size为每页条目数
Pageable pageable= PageRequest.of(page,size);
//按照age倒序排序
SortBuilder sort= SortBuilders.fieldSort("age").order(SortOrder.DESC);
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
nativeSearchQueryBuilder.withQuery(boolQueryBuilder);
nativeSearchQueryBuilder.withQuery(rangeQueryBuilder);
nativeSearchQueryBuilder.withQuery(rangeQueryBuilder1);
//只展示部分字段
nativeSearchQueryBuilder.withFields("id","username","age","nickname","role");
nativeSearchQueryBuilder.withPageable(pageable);
nativeSearchQueryBuilder.withSort(sort);
NativeSearchQuery nativeSearchQuery = nativeSearchQueryBuilder.build();
//IndexCoordinates.of("sysuser_index")定义使用的索引
//Map.class 定义数据的格式
SearchHits<Map> search =this.elasticsearchOperations.search(nativeSearchQuery, Map.class, IndexCoordinates.of("sysuser_index"));
根据角色分组统计数据,并统计每个角色下各个年龄的数量
List resultArr=new ArrayList();
//根据年龄分组,
// terms中的字段为分组的名称,是由用户自定义
// field中的数据为字段名称
AbstractAggregationBuilder subAggregationBuilder= AggregationBuilders.terms("age_group").field("age");
//根据角色分组
// terms中的字段为分组的名称,是由用户自定义
// field中的数据为字段名称
AbstractAggregationBuilder aggregationBuilder= AggregationBuilders.terms("role_group").field("role").subAggregation(subAggregationBuilder);
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
nativeSearchQueryBuilder.addAggregation(aggregationBuilder);
NativeSearchQuery nativeSearchQuery = nativeSearchQueryBuilder.build();
//IndexCoordinates.of("sysuser_index")定义使用的索引
//Map.class 定义数据的格式
SearchHits<Map> search =this.elasticsearchOperations.search(nativeSearchQuery, Map.class, IndexCoordinates.of("sysuser_index"));
Aggregations aggs=search.getAggregations();
//获取主分组信息
Terms sourceType = aggs.get("role_group");
//遍历主分组获取角色名称信息以及角色个数
for (Terms.Bucket bucket : sourceType.getBuckets()) {
Map<String, Object> result = new HashMap<>();
result.put("webName", bucket.getKeyAsString());
result.put("value",bucket.getDocCount());
//获取角色子分组信息,打印年龄以及相应年龄的个数
Terms ageSourcetype = bucket.getAggregations().get("age_group");
for (Terms.Bucket agebucket : ageSourcetype.getBuckets()) {
System.out.println(agebucket.getKeyAsString() + "::" + agebucket.getDocCount());
}
resultArr.add(result);
}
# RestHighLevelClient代码示例
首先通过@Autowired
注解自动注入RestHighLevelClient对象
前提:引入jar,完成相应配置项
# 新增
主要通过IndexResponse index(IndexRequest indexRequest, RequestOptions options)
方法完成,其中IndexRequest为index请求对象(用于设置索引、主键、字段等),
RequestOptions为请求设置对象(设置请求头等信息)
例如:
//创建对象
SysUser user=new SysUser();
user.setId(3001l);
user.setAge(38);
user.setUsername("老李");
user.setNickname("无");
user.setSign("测试");
user.setStatus(true);
user.setRole("管理");
//转化为Map
Map<String,Object> sysuserMap= BeanMap.create(user);
//创建index请求
//调用 sysuser_index 索引,id使用user对象的id,数据为map中的数据
IndexRequest request =new IndexRequest("sysuser_index").source(sysuserMap).id(String.valueOf(user.getId()));
//返回的结果
IndexResponse response = null;
String resultStr="";
try {
//添加数据,如果存在直接覆盖
response = client.index(request, RequestOptions.DEFAULT);
String result=response.getResult().name();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
# 更新
主要通过UpdateResponse update(UpdateRequest updateRequest, RequestOptions options)
方法完成,其中UpdateRequest为index更新请求对象(用于设置索引、主键、字段等),
RequestOptions为请求设置对象(设置请求头等信息)
例如:
//创建对象
SysUser user=new SysUser();
user.setId(2001l);
user.setAge(22);
user.setUsername("老张");
user.setSign("测试");
user.setNickname("张弓长");
user.setStatus(true);
user.setRole("管理");
//转化为Map
Map<String,Object> sysuserMap= BeanMap.create(user);
//创建update请求
//调用 sysuser_index 索引,id使用user对象的id,需要更新的数据为map中的数据
UpdateRequest request =new UpdateRequest("sysuser_index",String.valueOf(user.getId())).doc(sysuserMap);
UpdateResponse response = null;
String resultStr="";
try {
//添加数据,如果存在直接覆盖
response = client.update(request, RequestOptions.DEFAULT);
String result=response.getResult().name();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
# 删除
主要通过DeleteResponse delete(DeleteRequest deleteRequest, RequestOptions options)
方法完成,其中DeleteRequest为index删除请求对象(用于设置索引、主键、字段等),
RequestOptions为请求设置对象(设置请求头等信息)
例如:
//根据ID删除数据
DeleteRequest deleteRequest = new DeleteRequest("sysuser_index", id);
client.delete(deleteRequest, RequestOptions.DEFAULT);
# 查询
主要通过SearchResponse search(SearchRequest searchRequest, RequestOptions options)
方法完成,其中SearchRequest为index查询请求对象(用于设置索引、主键、source等),
RequestOptions为请求设置对象(设置请求头等信息)。
与ElasticsearchOperations
类似,该方式也是通过
MatchQueryBuilder、MatchPhraseQueryBuilder、TermQueryBuilder、BoolQueryBuilder等
组成较为复杂的查询,Pageable实现分页、SortBuilder实现排序、AbstractAggregationBuilder实现聚合
对象的组合实现复杂查询
例如: 根据username(用户名)、年龄、角色检索数据,根据年龄排序,并分页
//复合查询
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
// 查询必须满足的条件,类似于AND
// 含义为字段role中值和role参数匹配
boolQueryBuilder.must(QueryBuilders.termQuery("role", role));
// 查询可能满足的条件,类似于OR
// 含义为字段username中值和username1参数,或者username2参数与匹配
boolQueryBuilder.should(QueryBuilders.termQuery("username", username1));
boolQueryBuilder.should(QueryBuilders.termQuery("username", username2));
// 设置在可能满足的条件中,至少必须满足其中1条.多个或条件中至少满足一项
boolQueryBuilder.minimumShouldMatch(1);
// 必须不满足的条件
// 含义为字段age中值不等于8
boolQueryBuilder.mustNot(QueryBuilders.termQuery("age", 8));
//范围条件1,对应字段age
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("age");
// 大于等于ageL
rangeQueryBuilder.gte(ageL);
// 小于等于ageH
rangeQueryBuilder.lte(ageH);
//范围条件2,对应字段age
RangeQueryBuilder rangeQueryBuilder1 = QueryBuilders.rangeQuery("age");
// 大于ageL
rangeQueryBuilder1.gt(ageL);
// 小于ageH
rangeQueryBuilder1.lt(ageH);
// 日期格式下使用
// rangeQueryBuilder2.gte("2016-01-01");
// rangeQueryBuilder2.lte("2020-01-01");
// rangeQueryBuilder2.format("yyyy-MM-dd");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(boolQueryBuilder);
searchSourceBuilder.query(rangeQueryBuilder);
searchSourceBuilder.query(rangeQueryBuilder1);
//分页,page为当前页数,size为每页条目数
searchSourceBuilder.from((page - 1) * size);
searchSourceBuilder.size(size);
//按照age倒序排序
SortBuilder sort= SortBuilders.fieldSort("age").order(SortOrder.DESC);
searchSourceBuilder.sort(sort);
SearchRequest searchRequest = new SearchRequest("sysuser_index");
searchRequest.source(searchSourceBuilder);
//进行查询操作
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
//获取返回的数据
SearchHit[] result=searchResponse.getHits().getHits();
根据role(角色)字段分组并获取数据:
List resultArr=new ArrayList();
//根据年龄分组,
// terms中的字段为分组的名称,是由用户自定义
// field中的数据为字段名称
AbstractAggregationBuilder subAggregationBuilder= AggregationBuilders.terms("age_group").field("age");
//根据角色分组
// terms中的字段为分组的名称,是由用户自定义
// field中的数据为字段名称
AbstractAggregationBuilder aggregationBuilder= AggregationBuilders.terms("role_group").field("role").subAggregation(subAggregationBuilder);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.aggregation(aggregationBuilder);
SearchRequest searchRequest = new SearchRequest("sysuser_index");
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
Aggregations aggs=searchResponse.getAggregations();
//获取主分组信息
Terms sourceType = aggs.get("role_group");
//遍历主分组获取角色名称信息以及角色个数
for (Terms.Bucket bucket : sourceType.getBuckets()) {
Map<String, Object> result = new HashMap<>();
result.put("webName", bucket.getKeyAsString());
result.put("value",bucket.getDocCount());
//获取角色子分组信息,打印年龄以及相应年龄的个数
Terms ageSourcetype = bucket.getAggregations().get("age_group");
for (Terms.Bucket agebucket : ageSourcetype.getBuckets()) {
System.out.println(agebucket.getKeyAsString() + "::" + agebucket.getDocCount());
}
resultArr.add(result);
}
# 使用kibana
elasticsearch提供了一个可视化插件kibana。 Kibana 是一种数据可视化和挖掘工具,可以用于日志和时间序列分析、应用程序监控和运营智能使用案例。
启动kibana,并打开地址http://localhost:5601/
这里主要介绍关于使用kibana查询数据以及操作数据,使用Dev Tools打开,如下图
# 删除数据
# 添加/更新数据
# 查询数据
# 查询所有数据
# 复合查询
# 聚合
# 使用示例
# 复杂查询
/**
* elasticsearchOperations较为复杂的检索
*
*根据用户名、年龄、角色查询数据,并分页
*
* @param username1
* @param username2
* @param ageL
* @param ageH
* @param role
* @return
*/
@ApiOperation(value = "根据用户名、年龄、角色查询数据,并分页")
@GetMapping("/seleteUserByUARO")
public BaseResponse<SearchHit[]> seleteUserByUARO(
@RequestParam("username1") String username1,
@RequestParam("username2") String username2,
@RequestParam("ageL") Integer ageL,
@RequestParam("ageH") Integer ageH,
@RequestParam("role") String role,
@RequestParam("page") Integer page,
@RequestParam("size") Integer size
) throws IOException {
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
// 查询必须满足的条件,类似于AND
// 含义为字段role中值和role参数匹配
boolQueryBuilder.must(QueryBuilders.termQuery("role", role));
// 查询可能满足的条件,类似于OR
// 含义为字段username中值和username1参数,或者username2参数与匹配
boolQueryBuilder.should(QueryBuilders.termQuery("username", username1));
boolQueryBuilder.should(QueryBuilders.termQuery("username", username2));
// 设置在可能满足的条件中,至少必须满足其中1条.多个或条件中至少满足一项
boolQueryBuilder.minimumShouldMatch(1);
// 必须不满足的条件
// 含义为字段age中值不等于8
boolQueryBuilder.mustNot(QueryBuilders.termQuery("age", 8));
//范围条件1,对应字段age
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("age");
// 大于等于ageL
rangeQueryBuilder.gte(ageL);
// 小于等于ageH
rangeQueryBuilder.lte(ageH);
//范围条件2,对应字段age
RangeQueryBuilder rangeQueryBuilder1 = QueryBuilders.rangeQuery("age");
// 大于ageL
rangeQueryBuilder1.gt(ageL);
// 小于ageH
rangeQueryBuilder1.lt(ageH);
// 日期格式下使用
// rangeQueryBuilder2.gte("2016-01-01");
// rangeQueryBuilder2.lte("2020-01-01");
// rangeQueryBuilder2.format("yyyy-MM-dd");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(boolQueryBuilder);
searchSourceBuilder.query(rangeQueryBuilder);
searchSourceBuilder.query(rangeQueryBuilder1);
//分页,page为当前页数,size为每页条目数
searchSourceBuilder.from((page - 1) * size);
searchSourceBuilder.size(size);
//按照age倒序排序
SortBuilder sort= SortBuilders.fieldSort("age").order(SortOrder.DESC);
searchSourceBuilder.sort(sort);
SearchRequest searchRequest = new SearchRequest("sysuser_index");
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
SearchHit[] result=searchResponse.getHits().getHits();
return BaseResponse.success(result);
}
使用postman请求:
返回结果:
# 聚合
/**
* 聚合示例
* 根据role(角色)字段分组并获取数据
*
* @return
*/
@ApiOperation(value = "根据用户名查询数据")
@GetMapping("/seleteUserAgg")
public BaseResponse<List> seleteUserAgg() {
List resultArr=new ArrayList();
AbstractAggregationBuilder subAggregationBuilder= AggregationBuilders.terms("age_group").field("age");
AbstractAggregationBuilder aggregationBuilder= AggregationBuilders.terms("role_group").field("role").subAggregation(subAggregationBuilder);
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
nativeSearchQueryBuilder.addAggregation(aggregationBuilder);
NativeSearchQuery nativeSearchQuery = nativeSearchQueryBuilder.build();
SearchHits<Map> search =this.elasticsearchOperations.search(nativeSearchQuery, Map.class, IndexCoordinates.of("sysuser_index"));
Aggregations aggs=search.getAggregations();
//获取主分组信息
Terms sourceType = aggs.get("role_group");
//遍历主分组获取角色名称信息以及角色个数
for (Terms.Bucket bucket : sourceType.getBuckets()) {
Map<String, Object> result = new HashMap<>();
result.put("webName", bucket.getKeyAsString());
result.put("value",bucket.getDocCount());
//获取角色子分组信息,打印年龄以及相应年龄的个数
Terms ageSourcetype = bucket.getAggregations().get("age_group");
for (Terms.Bucket agebucket : ageSourcetype.getBuckets()) {
System.out.println(agebucket.getKeyAsString() + "::" + agebucket.getDocCount());
}
resultArr.add(result);
}
return BaseResponse.success(resultArr);
}
返回结果:
← RabbitMQ 消息队列 日志 →