# 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:
      # 服务地址,可设置多个
      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.IntegerFieldType.LongFieldType.DoubleFieldType.DateFieldType.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/

elasticsearch_k_1.png

这里主要介绍关于使用kibana查询数据以及操作数据,使用Dev Tools打开,如下图

elasticsearch_k_2.png

# 删除数据

elasticsearch_k_4.png

# 添加/更新数据

elasticsearch_k_5.png

# 查询数据

# 查询所有数据

elasticsearch_k_3.png

# 复合查询

elasticsearch_k_6.png

# 聚合

elasticsearch_k_7.png

# 使用示例

# 复杂查询

/**
 * 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请求:

elasticsearch_1.png

返回结果:

elasticsearch_2.png

# 聚合


/**
 * 聚合示例
 * 根据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);
}


返回结果:

elasticsearch_3.png