8.检索服务

一、添加模板页面

<!-- 模板引擎 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

将资料中的前端页面放到 search 服务模块下的 resource/templates 下;

二、配置请求跳转

2.1 配置 Nginx 转发

配置 Windows hosts 文件:

192.168.163.131		search.gulimall.com

找到 Nginx 的配置文件,编辑 gulimall.conf,将所有 *.gulimall.com 的请求都经由 Nginx 转发给网关;

server {
    listen       80;
    server_name  gulimall.com *.gulimall.com;
		...
 }

然后重启 Nginx

docker restart nginx

2.2 配置网关服务转发到 search 服务

- id: mall_search_route
  uri: lb://mall-search
  predicates:
  - Host=search.gulimall.com

三、配置页面跳转

配置 /list.html 请求转发到 list 模板

/**
 * 自动将页面提交过来的所有请求参数封装成我们指定的对象
 *
 * @param param
 * @return
 */
@GetMapping(value = "/list.html")
public String listPage(SearchParam param, Model model, HttpServletRequest request) {
    return "list";
}

四、检索功能

 @Override
    public SearchResult search(SearchParam param) {

        //1、动态构建出查询需要的DSL语句
        SearchResult result = null;

        //1、准备检索请求
        SearchRequest searchRequest = buildSearchRequest(param);

        try {
            //2、执行检索请求
            SearchResponse response = esRestClient.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);

            //3、分析响应数据,封装成我们需要的格式
            result = buildSearchResult(response,param);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return result;
    }
/**
 * 准备检索请求
 * 模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存),排序,分页,高亮,聚合分析
 * @return
 */
private SearchRequest buildSearchRequest(SearchParam param) {

    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

    /**
     * 模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存)
     */
    //1. 构建bool-query
    BoolQueryBuilder boolQueryBuilder=new BoolQueryBuilder();

    //1.1 bool-must
    if(!StringUtils.isEmpty(param.getKeyword())){
        boolQueryBuilder.must(QueryBuilders.matchQuery("skuTitle",param.getKeyword()));
    }

    //1.2 bool-fiter
    //1.2.1 catelogId
    if(null != param.getCatalog3Id()){
        boolQueryBuilder.filter(QueryBuilders.termQuery("catalogId",param.getCatalog3Id()));
    }

    //1.2.2 brandId
    if(null != param.getBrandId() && param.getBrandId().size() >0){
        boolQueryBuilder.filter(QueryBuilders.termsQuery("brandId",param.getBrandId()));
    }

    //1.2.3 attrs
    if(param.getAttrs() != null && param.getAttrs().size() > 0){

        param.getAttrs().forEach(item -> {
            //attrs=1_5寸:8寸&2_16G:8G
            BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();


            //attrs=1_5寸:8寸
            String[] s = item.split("_");
            String attrId=s[0];
            String[] attrValues = s[1].split(":");//这个属性检索用的值
            boolQuery.must(QueryBuilders.termQuery("attrs.attrId",attrId));
            boolQuery.must(QueryBuilders.termsQuery("attrs.attrValue",attrValues));

            NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery("attrs",boolQuery, ScoreMode.None);
            boolQueryBuilder.filter(nestedQueryBuilder);
        });

    }

    //1.2.4 hasStock
    if(null != param.getHasStock()){
        boolQueryBuilder.filter(QueryBuilders.termQuery("hasStock",param.getHasStock() == 1));
    }


    //1.2.5 skuPrice
    if(!StringUtils.isEmpty(param.getSkuPrice())){
        //skuPrice形式为:1_500或_500或500_
        RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice");
        String[] price = param.getSkuPrice().split("_");
        if(price.length==2){
            rangeQueryBuilder.gte(price[0]).lte(price[1]);
        }else if(price.length == 1){
            if(param.getSkuPrice().startsWith("_")){
                rangeQueryBuilder.lte(price[1]);
            }
            if(param.getSkuPrice().endsWith("_")){
                rangeQueryBuilder.gte(price[0]);
            }
        }
        boolQueryBuilder.filter(rangeQueryBuilder);
    }

    //封装所有的查询条件
    searchSourceBuilder.query(boolQueryBuilder);


    /**
     * 排序,分页,高亮
     */

    //排序
    //形式为sort=hotScore_asc/desc
    if(!StringUtils.isEmpty(param.getSort())){
        String sort = param.getSort();
        String[] sortFileds = sort.split("_");

        SortOrder sortOrder="asc".equalsIgnoreCase(sortFileds[1])?SortOrder.ASC:SortOrder.DESC;

        searchSourceBuilder.sort(sortFileds[0],sortOrder);
    }

    //分页
    searchSourceBuilder.from((param.getPageNum()-1)*EsConstant.PRODUCT_PAGESIZE);
    searchSourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);

    //高亮
    if(!StringUtils.isEmpty(param.getKeyword())){

        HighlightBuilder highlightBuilder = new HighlightBuilder();
        highlightBuilder.field("skuTitle");
        highlightBuilder.preTags("<b style='color:red'>");
        highlightBuilder.postTags("</b>");

        searchSourceBuilder.highlighter(highlightBuilder);
    }



    /**
     * 聚合分析
     */
    //1. 按照品牌进行聚合
    TermsAggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg");
    brand_agg.field("brandId").size(50);


    //1.1 品牌的子聚合-品牌名聚合
    brand_agg.subAggregation(AggregationBuilders.terms("brand_name_agg")
            .field("brandName").size(1));
    //1.2 品牌的子聚合-品牌图片聚合
    brand_agg.subAggregation(AggregationBuilders.terms("brand_img_agg")
            .field("brandImg").size(1));

    searchSourceBuilder.aggregation(brand_agg);

    //2. 按照分类信息进行聚合
    TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg");
    catalog_agg.field("catalogId").size(20);

    catalog_agg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1));

    searchSourceBuilder.aggregation(catalog_agg);

    //2. 按照属性信息进行聚合
    NestedAggregationBuilder attr_agg = AggregationBuilders.nested("attr_agg", "attrs");
    //2.1 按照属性ID进行聚合
    TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId");
    attr_agg.subAggregation(attr_id_agg);
    //2.1.1 在每个属性ID下,按照属性名进行聚合
    attr_id_agg.subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1));
    //2.1.1 在每个属性ID下,按照属性值进行聚合
    attr_id_agg.subAggregation(AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50));
    searchSourceBuilder.aggregation(attr_agg);

    log.debug("构建的DSL语句 {}",searchSourceBuilder.toString());

    SearchRequest searchRequest = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX},searchSourceBuilder);

    return searchRequest;
}

 

/**
 * 构建结果数据
 * 模糊匹配,过滤(按照属性、分类、品牌,价格区间,库存),完成排序、分页、高亮,聚合分析功能
 * @param response
 * @return
 */
private SearchResult buildSearchResult(SearchResponse response,SearchParam param) {

    SearchResult result = new SearchResult();

    //1、返回的所有查询到的商品
    SearchHits hits = response.getHits();

    List<SkuEsModel> esModels = new ArrayList<>();
    //遍历所有商品信息
    if (hits.getHits() != null && hits.getHits().length > 0) {
        for (SearchHit hit : hits.getHits()) {
            String sourceAsString = hit.getSourceAsString();
            SkuEsModel esModel = JSON.parseObject(sourceAsString, SkuEsModel.class);

            //判断是否按关键字检索,若是就显示高亮,否则不显示
            if (!StringUtils.isEmpty(param.getKeyword())) {
                //拿到高亮信息显示标题
                HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
                String skuTitleValue = skuTitle.getFragments()[0].string();
                esModel.setSkuTitle(skuTitleValue);
            }
            esModels.add(esModel);
        }
    }
    result.setProduct(esModels);

    //2、当前商品涉及到的所有属性信息
    List<SearchResult.AttrVo> attrVos = new ArrayList<>();
    //获取属性信息的聚合
    ParsedNested attrsAgg = response.getAggregations().get("attr_agg");
    ParsedLongTerms attrIdAgg = attrsAgg.getAggregations().get("attr_id_agg");
    for (Terms.Bucket bucket : attrIdAgg.getBuckets()) {
        SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
        //1、得到属性的id
        long attrId = bucket.getKeyAsNumber().longValue();
        attrVo.setAttrId(attrId);

        //2、得到属性的名字
        ParsedStringTerms attrNameAgg = bucket.getAggregations().get("attr_name_agg");
        String attrName = attrNameAgg.getBuckets().get(0).getKeyAsString();
        attrVo.setAttrName(attrName);

        //3、得到属性的所有值
        ParsedStringTerms attrValueAgg = bucket.getAggregations().get("attr_value_agg");
        List<String> attrValues = attrValueAgg.getBuckets().stream().map(item -> item.getKeyAsString()).collect(Collectors.toList());
        attrVo.setAttrValue(attrValues);

        attrVos.add(attrVo);
    }

    result.setAttrs(attrVos);

    //3、当前商品涉及到的所有品牌信息
    List<SearchResult.BrandVo> brandVos = new ArrayList<>();
    //获取到品牌的聚合
    ParsedLongTerms brandAgg = response.getAggregations().get("brand_agg");
    for (Terms.Bucket bucket : brandAgg.getBuckets()) {
        SearchResult.BrandVo brandVo = new SearchResult.BrandVo();

        //1、得到品牌的id
        long brandId = bucket.getKeyAsNumber().longValue();
        brandVo.setBrandId(brandId);

        //2、得到品牌的名字
        ParsedStringTerms brandNameAgg = bucket.getAggregations().get("brand_name_agg");
        String brandName = brandNameAgg.getBuckets().get(0).getKeyAsString();
        brandVo.setBrandName(brandName);

        //3、得到品牌的图片
        ParsedStringTerms brandImgAgg = bucket.getAggregations().get("brand_img_agg");
        String brandImg = brandImgAgg.getBuckets().get(0).getKeyAsString();
        brandVo.setBrandImg(brandImg);

        brandVos.add(brandVo);
    }
    result.setBrands(brandVos);

    //4、当前商品涉及到的所有分类信息
    //获取到分类的聚合
    List<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
    ParsedLongTerms catalogAgg = response.getAggregations().get("catalog_agg");
    for (Terms.Bucket bucket : catalogAgg.getBuckets()) {
        SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
        //得到分类id
        String keyAsString = bucket.getKeyAsString();
        catalogVo.setCatalogId(Long.parseLong(keyAsString));

        //得到分类名
        ParsedStringTerms catalogNameAgg = bucket.getAggregations().get("catalog_name_agg");
        String catalogName = catalogNameAgg.getBuckets().get(0).getKeyAsString();
        catalogVo.setCatalogName(catalogName);
        catalogVos.add(catalogVo);
    }

    result.setCatalogs(catalogVos);
    //===============以上可以从聚合信息中获取====================//
    //5、分页信息-页码
    result.setPageNum(param.getPageNum());
    //5、1分页信息、总记录数
    long total = hits.getTotalHits().value;
    result.setTotal(total);

    //5、2分页信息-总页码-计算
    int totalPages = (int)total % EsConstant.PRODUCT_PAGESIZE == 0 ?
            (int)total / EsConstant.PRODUCT_PAGESIZE : ((int)total / EsConstant.PRODUCT_PAGESIZE + 1);
    result.setTotalPages(totalPages);

    List<Integer> pageNavs = new ArrayList<>();
    for (int i = 1; i <= totalPages; i++) {
        pageNavs.add(i);
    }
    result.setPageNavs(pageNavs);


    //6、构建面包屑导航
    if (param.getAttrs() != null && param.getAttrs().size() > 0) {
        List<SearchResult.NavVo> collect = param.getAttrs().stream().map(attr -> {
            //1、分析每一个attrs传过来的参数值
            SearchResult.NavVo navVo = new SearchResult.NavVo();
            String[] s = attr.split("_");
            navVo.setNavValue(s[1]);
            R r = productFeignService.attrInfo(Long.parseLong(s[0]));
            if (r.getCode() == 0) {
                AttrResponseVo data = r.getData("attr", new TypeReference<AttrResponseVo>() {
                });
                navVo.setNavName(data.getAttrName());
            } else {
                navVo.setNavName(s[0]);
            }

            //2、取消了这个面包屑以后,我们要跳转到哪个地方,将请求的地址url里面的当前置空
            //拿到所有的查询条件,去掉当前
            String encode = null;
            try {
                encode = URLEncoder.encode(attr,"UTF-8");
                encode.replace("+","%20");  //浏览器对空格的编码和Java不一样,差异化处理
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            String replace = param.get_queryString().replace("&attrs=" + attr, "");
            navVo.setLink("http://search.gulimall.com/list.html?" + replace);

            return navVo;
        }).collect(Collectors.toList());

        result.setNavs(collect);
    }


    return result;
}

4.1 ES mall-product 映射 mapping修改

这里由于之前设置的映射 设置一些字段的 doc_value 为 false,导致后面聚合查询时报错!

修改完映射 mapping 要同步修改检索服务中的常量类中的 es 索引常量,二者要求对应。

# 查看原来的映射规则
GET gulimall_product/_mapping
# 修改为新的映射 并创建新的索引,下面进行数据迁移
PUT /mall_product
{
  "mappings": {
    "properties": {
      "skuId": {
        "type": "long"
      },
      "spuId": {
        "type": "long"
      },
      "skuTitle": {
        "type": "text",
        "analyzer": "ik_smart"
      },
      "skuPrice": {
        "type": "keyword"
      },
      "skuImg": {
        "type": "keyword"
      },
      "saleCount": {
        "type": "long"
      },
      "hosStock": {
        "type": "boolean"
      },
      "hotScore": {
        "type": "long"
      },
      "brandId": {
        "type": "long"
      },
      "catalogId": {
        "type": "long"
      },
      "brandName": {
        "type": "keyword"
      },
      "brandImg": {
        "type": "keyword"
      },
      "catalogName": {
        "type": "keyword"
      },
      "attrs": {
        "type": "nested",
        "properties": {
          "attrId": {
            "type": "long"
          },
          "attrName": {
            "type": "keyword"
          },
          "attrValue": {
            "type": "keyword"
          }
        }
      }
    }
  }
}
# 数据迁移
POST _reindex
{
  "source": {
    "index": "gulimall_product"
  },
  "dest": {
    "index": "mall_product"
  }
}

4.2 ES 检索 DSL语句分析

GET mall_product/_search
{
  "query": {
    "bool": {
      "must": [ {"match": {  "skuTitle": "华为" }} ], # 检索出华为
      "filter": [ # 过滤
        { "term": { "catalogId": "225" } },
        { "terms": {"brandId": [ "2"] } }, 
        { "term": { "hasStock": "false"} },
        {
          "range": {
            "skuPrice": { # 价格1K~7K
              "gte": 1000,
              "lte": 7000
            }
          }
        },
        {
          "nested": {
            "path": "attrs", # 聚合名字
            "query": {
              "bool": {
                "must": [
                  {
                    "term": { "attrs.attrId": { "value": "6"} }
                  }
                ]
              }
            }
          }
        }
      ]
    }
  },
  "sort": [ {"skuPrice": {"order": "desc" } } ],
  "from": 0,
  "size": 5,
  "highlight": {  
    "fields": {"skuTitle": {}}, # 高亮的字段
    "pre_tags": "<b style='color:red'>",  # 前缀
    "post_tags": "</b>"
  },
  "aggs": { # 查完后聚合
    "brandAgg": {
      "terms": {
        "field": "brandId",
        "size": 10
      },
      "aggs": { # 子聚合
        "brandNameAgg": {  # 每个商品id的品牌
          "terms": {
            "field": "brandName",
            "size": 10
          }
        },
      
        "brandImgAgg": {
          "terms": {
            "field": "brandImg",
            "size": 10
          }
        }
        
      }
    },
    "catalogAgg":{
      "terms": {
        "field": "catalogId",
        "size": 10
      },
      "aggs": {
        "catalogNameAgg": {
          "terms": {
            "field": "catalogName",
            "size": 10
          }
        }
      }
    },
    "attrs":{
      "nested": {"path": "attrs" },
      "aggs": {
        "attrIdAgg": {
          "terms": {
            "field": "attrs.attrId",
            "size": 10
          },
          "aggs": {
            "attrNameAgg": {
              "terms": {
                "field": "attrs.attrName",
                "size": 10
              }
            }
          }
        }
      }
    }
  }
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
posted @ 2022-03-17 10:37  随遇而安==  阅读(67)  评论(0编辑  收藏  举报