es实现桶聚合

打印 上一主题 下一主题

主题 704|帖子 704|积分 2112

目录

聚合
聚合的分类
DSL实现桶聚合
 dsl语句
 结果
聚合结果排序 
限定聚合范围 
总结
聚合必须的三要素:
聚合可设置的属性
 DSL实现metric聚合
例如:我们必要获取每个品牌的用户评分的min,max,avg等值
只求socre的max
利用RestHighLevelClient实现聚合
业务需求
dsl语句 
java代码
结果
​编辑


聚合

聚合是根据查询后的结果来聚合的,如果没有写query查询条件,就是对索引库的全部文档进行聚合
聚合的分类

聚合(aggregations)可以实现对文档数据的统计,分析,运算。聚合常见的有三类:
1.桶(Bucket)聚合:用来对文档进行分组,相当于mysql的group by


  • TermAggregation:按照文档字段值进行分组,留意:这个文档字段不可分词
  • Date Histogram:按照日期门路分组,例如一周为一组,或者一个月为一组
2.度量(Metric)聚合:用来计算一些值,好比:最大值,最小值,平均值 
3.管道(pipeline)聚合:其他聚合的结果为底子做聚合
参与聚合的字段范例必须是:


  • keyword
  • 数值
  • 日期
  • 布尔 
DSL实现桶聚合

现在,我们要统计全部数据中的酒店品牌有几种,实在就是按照品牌对数据分组。此时可以根据酒店品牌的名称做聚合,也就是Bucket聚合。
 dsl语句

  1. GET /hotel/_search
  2. {
  3.     "size":0, //返回命中文档的详细信息的数量,(默认执行match_all),这里设置为0就是不返回文档的详细信息
  4.     "aggs":{ //聚合查询关键字
  5.         "brandSum":{ //桶名字
  6.             "trems":{ //聚合的类型,这里使用brand字段聚合,所以使用terms
  7.                 "field":"brand",
  8.                 "size":20 //返回最多的桶数量,如果设置为1,就只返回一个桶的信息
  9.             }
  10.         }
  11.     }
  12. }
复制代码

 结果

可以望见hits数组里的值为空,因为我们设置了size=0,不返回文档的具体信息
brandSum就是这个聚合的名字,buckets桶数组最大的巨细为20,默认通过桶里的文档数降序排序

聚合结果排序 

默认情况下,Bucket聚合会统计Bucket内的文档数量,记为_count,并且按照_count降序排序。
我们可以指定order属性,自定义聚合的排序方式:
  1. GET /hotel/_search
  2. {
  3.     "size":0,
  4.     "aggs":{
  5.         "brandSum":{
  6.             "terms":{
  7.                 "field":"brand",
  8.                 "size":20,
  9.                 "order":{ #自定义排序规则
  10.                     "_count":asc #使用桶内的文档数进行升序排序
  11.                 }
  12.             }
  13.         }
  14.     }
  15. }
复制代码
 


限定聚合范围 

默认情况下,Bucket聚合是对索引库的全部文档做聚合,但真实场景下,用户会输入搜索条件,因此聚合必须是对搜索结果聚合。那么聚合必须添加限定条件。
我们可以限定要聚合的文档范围,只要添加query条件即可:
只聚合价格大于500的文档
 

总结

聚合必须的三要素:



  • 聚合名称
  • 聚合范例
  • 聚合字段
聚合可设置的属性



  • size:指定聚合结果(即桶的最大数量)的最大数量 
  • order:指定聚合结果排序的方式
  • field:指定聚合的字段
 DSL实现metric聚合

例如:我们必要获取每个品牌的用户评分的min,max,avg等值

  1. GET /hotel/_search
  2. {
  3.     "aggs":{
  4.         "brandSum":{
  5.             "terms":{
  6.                 "field":"brand",
  7.                 "size":20
  8.             },
  9.          "aggs":{ //brandSum聚合下的子聚合
  10.                 "scoreStats":{//子聚合名字
  11.                     "stats":{ //聚合的类型·。stats会把max,min,avg,sum,count都算出来
  12.                         "field":"score"
  13.                     }
  14.                 }
  15.             }
  16.         }
  17.     }
  18. }
复制代码


只求socre的max

 


利用RestHighLevelClient实现聚合



  1. @SpringBootTest
  2. public class TestAggregation {
  3.     @Autowired
  4.     private RestHighLevelClient restHighLevelClient;
  5.     @Test
  6.     public void test01() throws IOException {
  7.         //构建 查询对象
  8.         SearchRequest request = new SearchRequest("hotel");
  9.         //设置dsl语句
  10.         request.source().size(0);
  11.         request.source().aggregation(AggregationBuilders
  12.                 .terms("brandSum")//指定聚合的名字为brandSum,且聚合的类型是terms
  13.                 .field("brand")//指定聚合的字段是brand
  14.                 .size(20));
  15.         //发送请求
  16.         SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
  17.         //解析响应数据
  18.         Aggregations aggregations = response.getAggregations();
  19.         Terms brandSum= aggregations.get("brandSum");
  20.         List<? extends Terms.Bucket> buckets = brandSum.getBuckets();
  21.         for (Terms.Bucket bucket : buckets) {
  22.             String key = bucket.getKeyAsString();
  23.             System.out.println(key);
  24.         }
  25.     }
  26. }
复制代码
业务需求

需求:搜索页面的品牌、都会等信息不应该是在页面写死,而是通过聚合索引库中的酒店数据得来的:

分析:
目前,页面的都会列表、星级列表、品牌列表都是写死的,并不会随着搜索结果的变革而变革。但是用户搜索条件改变时,搜索结果会跟着变革。
例如:用户搜索“东方明珠”,那搜索的酒店肯定是在上海东方明珠附近,因此,都会只能是上海,此时都会列表中就不应该显示北京、深圳、杭州这些信息了。
也就是说,搜索结果中包罗哪些都会,页面就应该列出哪些都会;搜索结果中包罗哪些品牌,页面就应该列出哪些品牌。
如何得知搜索结果中包罗哪些品牌?如何得知搜索结果中包罗哪些都会?
使用聚合功能,利用Bucket聚合,对搜索结果中的文档基于品牌分组、基于都会分组,就能得知包罗哪些品牌、哪些都会了。
因为是对搜索结果聚合,因此聚合是限定范围的聚合,也就是说聚合的限定条件跟搜索文档的条件一致。
检察浏览器可以发现,前端实在已经发出了这样的一个请求:

请求参数与搜索文档的参数完全一致
返回值范例就是页面要展示的终极结果:

结果是一个Map结构:


  • key是字符串,都会、星级、品牌、价格
  • value是集合,例如多个都会的名称
dsl语句 


结果会有三个桶聚合的结果,可以发现因为查询条件match全文匹配了上海,所以citySum聚合桶内只有一种都会就是上海,这种情况是正确的 ,因为聚合是根据查询后的结果来聚合的,如果没有query查询条件,就是对索引库的全部文档进行聚合
这里有三种聚合,都是独立的 ,city聚合,brand聚合,starName聚合
 
java代码

前端封装类
  1. package com.hhh.hotel.pojo;
  2. import lombok.Data;
  3. @Data
  4. public class RequestParams {
  5.     private String key;
  6.     private Integer page;
  7.     private Integer size;
  8.     private String sortBy;
  9.     // 下面是新增的过滤条件参数
  10.     private String city;
  11.     private String brand;
  12.     private String starName;
  13.     private Integer minPrice;
  14.     private Integer maxPrice;
  15.     //坐标
  16.     private String location;
  17. }
复制代码
controller层
  1.   @RestController
  2. @RequestMapping("/hotel")
  3. public class HotelController {
  4.     @Autowired
  5.     private HotelService hotelService;
  6.     @PostMapping("/list")
  7.     public PageResult getPageResult(@RequestBody RequestParams params){
  8.         return hotelService.getPageResult(params);
  9.     }
  10.     /**
  11.      * 获取传入条件过滤出 星级,城市,品牌
  12.      */
  13.     @PostMapping("/filters")
  14.     public Map<String, List<String>> getFilters(@RequestBody RequestParams params){
  15.         return hotelService.getFilters(params);
  16.     }
  17. }
复制代码
service服务层 
前端进行搜索时,会发送两个请求,一个请求时/hotel/list获取匹配到的hotel信息,还有一个请求时/hotel/filters获取根据查询条件得到的结果去聚合过滤出的星级,都会和品牌 
  1. @Service
  2. public class HotelServiceImpl extends ServiceImpl<HotelMapper, Hotel>
  3.     implements HotelService {
  4.     @Autowired
  5.     private RestHighLevelClient restHighLevelClient;
  6.     @Override
  7.     public PageResult getPageResult(RequestParams params) {
  8.         //1.构建 查询请求对象
  9.         SearchRequest request = new SearchRequest("hotel");
  10.         //2.编写dsl语句
  11.        /* //2.1如果key为空,即搜索的内容为空,就全文查询
  12.         if(StringUtils.isBlank(params.getKey())){
  13.             request.source().query(QueryBuilders.matchAllQuery());
  14.         }else {
  15.             request.source().query(QueryBuilders.matchQuery("all",params.getKey()));
  16.         }*/
  17.         assembleBasicQuery(params,request);
  18.         //构建高亮显示
  19.         request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
  20.         //3.构建分页信息
  21.         Integer pageNum=params.getPage()==null?1:params.getPage();//默认是第一页
  22.         Integer pageSize=params.getSize()==null?5:params.getSize();//默认每页大小为5
  23.         request.source().from((pageNum-1)*pageSize).size(pageSize);
  24.         //TODO:维护距离排序
  25.         if(StringUtils.isNotBlank(params.getLocation())) {
  26.             GeoDistanceSortBuilder geoDistanceSortBuilder = new GeoDistanceSortBuilder("location", new GeoPoint(params.getLocation()))
  27.                     .order(SortOrder.ASC) // 升序排序
  28.                     .unit(DistanceUnit.KILOMETERS); // 单位为千米
  29.             request.source().sort(geoDistanceSortBuilder);
  30.         }
  31.         if(params.getSortBy().equals("price")){
  32.             request.source().sort("price",SortOrder.ASC);
  33.         } else if (params.getSortBy().equals("score")) {
  34.             request.source().sort("score",SortOrder.DESC);
  35.         }
  36.         //4.发送请求
  37.         SearchResponse response = null;
  38.         try {
  39.             response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
  40.         } catch (IOException e) {
  41.             throw new RuntimeException("查询异常");
  42.         }
  43.         //5.解析数据
  44.         Boolean isEnableKm=false;
  45.         if(params.getLocation()!=null){
  46.             isEnableKm=true;//location字段不为null,才设置为true,然后获取排序值
  47.         }
  48.         return ParseResponse(response,isEnableKm);
  49.     }
  50.     /**
  51.      * 获取传入条件过滤出 星级,城市,品牌
  52.      */
  53.     @Override
  54.     public Map<String, List<String>> getFilters(RequestParams params) {
  55.         SearchRequest request = new SearchRequest("hotel");
  56.         request.source().size(0);
  57.         //设置query DSL语句
  58.         assembleBasicQuery(params,request);
  59.         //构建桶聚合
  60.         request.source().aggregation(AggregationBuilders
  61.                 .terms("brandSum")
  62.                 .field("brand")
  63.                 .size(20));
  64.         request.source().aggregation(AggregationBuilders
  65.                 .terms("citySum")
  66.                 .field("city")
  67.                 .size(20));
  68.         request.source().aggregation(AggregationBuilders
  69.                 .terms("starNameSum")
  70.                 .field("starName")
  71.                 .size(20));
  72.         //发起请求
  73.         SearchResponse response = null;
  74.         try {
  75.             response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
  76.         } catch (IOException e) {
  77.             throw new RuntimeException(e);
  78.         }
  79.         //解析返回数据
  80.         List<String>brands=parseAggreData(response,"brandSum");
  81.         List<String>citys=parseAggreData(response,"citySum");
  82.         List<String>startNames=parseAggreData(response,"starNameSum");
  83.         Map<String, List<String>> info = new HashMap<>();
  84.         info.put("brand",brands);
  85.         info.put("city",citys);
  86.         info.put("starName",startNames);
  87.         return info;
  88.     }
  89.     /**
  90.      * 解析桶数据
  91.      * @return
  92.      */
  93.     private List<String> parseAggreData(SearchResponse response, String sum) {
  94.         Aggregations aggregations = response.getAggregations();
  95.         Terms aggregation = aggregations.get(sum);
  96.         if(aggregation==null||CollectionUtils.isEmpty(aggregation.getBuckets())){
  97.             return null;
  98.         }
  99.         ArrayList<String> list = new ArrayList<>();
  100.         for (Terms.Bucket bucket : aggregation.getBuckets()) {
  101.             String key = bucket.getKeyAsString();
  102.             list.add(key);
  103.         }
  104.         return list;
  105.     }
  106.     /**
  107.      * 组装dsl查询语句
  108.      * @param params 前端封装类
  109.      * @param request 查询请求对象
  110.      */
  111.     private void assembleBasicQuery(RequestParams params,SearchRequest request){
  112.         BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
  113.         //1.判断key是否为空
  114.         if(StringUtils.isBlank(params.getKey())){
  115.             boolQuery.must(QueryBuilders.matchAllQuery());
  116.         }else{
  117.             boolQuery.must(QueryBuilders.matchQuery("all",params.getKey()));
  118.         }
  119.         //品牌名不为空,对品牌过滤
  120.         if(StringUtils.isNotBlank(params.getBrand())){
  121.             boolQuery.filter(QueryBuilders.termQuery("brand",params.getBrand()));
  122.         }
  123.         //城市
  124.         if(StringUtils.isNotBlank(params.getCity())){
  125.             boolQuery.filter(QueryBuilders.termQuery("city",params.getCity()));
  126.         }
  127.         //星级
  128.         if(StringUtils.isNotBlank(params.getStarName())){
  129.             boolQuery.filter(QueryBuilders.termQuery("startName",params.getStarName()));
  130.         }
  131.         //价格范围
  132.         if(params.getMaxPrice()!=null&&params.getMinPrice()!=null){
  133.             boolQuery.filter(QueryBuilders.rangeQuery("price").gte(params.getMinPrice()).lte(params.getMaxPrice()));
  134.         }
  135.         //定义算分函数
  136.         //设置排名,根据算分设置level排名
  137.         FieldValueFactorFunctionBuilder functionBuilder = ScoreFunctionBuilders.fieldValueFactorFunction("value").modifier(FieldValueFactorFunction.Modifier.NONE).factor(1.5F).missing(1);
  138.         FunctionScoreQueryBuilder.FilterFunctionBuilder[] functions = new FunctionScoreQueryBuilder.FilterFunctionBuilder[] {
  139.                 new FunctionScoreQueryBuilder.FilterFunctionBuilder(
  140.                         QueryBuilders.termQuery("isAD",true),
  141.                         //ScoreFunctionBuilders.weightFactorFunction(10)  // 权重因子,乘以基础得分
  142.                         functionBuilder
  143.                 )
  144.         };
  145.         FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders.functionScoreQuery(boolQuery, functions).boostMode(CombineFunction.SUM);
  146.         request.source().query(functionScoreQuery);
  147.     }
  148.     /**
  149.      * 解析es响应的数据
  150.      */
  151.     private PageResult ParseResponse(SearchResponse response,Boolean isEnableKm) {
  152.         SearchHits hits = response.getHits();
  153.         //1.获取总命中文档数
  154.         long size = hits.getTotalHits().value;
  155.         SearchHit[] hits1 = hits.getHits();
  156.         ArrayList<HotelDoc> docs = new ArrayList<>();
  157.         if(ArrayUtils.isNotEmpty(hits1)){
  158.             for (SearchHit searchHit : hits1) {
  159.                 String jsonData = searchHit.getSourceAsString();
  160.                 HotelDoc hotelDoc = JSON.parseObject(jsonData, HotelDoc.class);
  161.                 if(isEnableKm) {
  162.                     //获取排序后的距离
  163.                     Object[] sortValues = searchHit.getSortValues();
  164.                     if (ArrayUtils.isNotEmpty(sortValues)) {
  165.                         hotelDoc.setDistance(sortValues[0]);
  166.                     }
  167.                 }
  168.                 //获取高亮
  169.                 Map<String, HighlightField> fieldMap = searchHit.getHighlightFields();
  170.                 if(!CollectionUtils.isEmpty(fieldMap)){
  171.                     HighlightField highlightField = fieldMap.get("name");
  172.                     String highName = highlightField.getFragments()[0].string();
  173.                     //替换hotel实体类的name属性
  174.                     hotelDoc.setName(highName);
  175.                 }
  176.                 docs.add(hotelDoc);
  177.             }
  178.         }
  179.         return new PageResult(size,docs);
  180.     }
  181. }
复制代码
结果

输入上海进行搜索时,会根据hotel/list获取全文检索匹配的文档信息,然后根据/hotel/filters获取过滤出的city,brand,price,但是根据上海匹配出来的文档,颠末city字段进行聚合时,citySum桶内只有一个数据就是上海,只有一个数据时,直接在前端不显示其他都会的选择即可,因为没必要
然后brandSum桶内有全部品牌的名字,和 starSum桶内全部的星级都会显示出来(因为桶内的数量大于1)



免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

北冰洋以北

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表