19.检索服务

Search模块

1.整理前端内容

导入依赖

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

html放入template目录下,静态资源放到nginx/search目录下

 配置域名转发

 

 

 想实现的效果是查询search.gulimall能访问到检索页面,在nginx中配置,不管是gulimall.com还是前面有前缀的都应该交给网关处理

 

 

 

 网关配置:

 

 

 配置热部署依赖并且配置缓存为false

  <!--  热部署依赖,使页面实时生效-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
spring: 
thymeleaf: cache: false

以后ctrl+f9就可以重新构建了

2.后台的实现(前端内容不介绍了)

实现点击三级分类跳转到list页面--检索页面

@Controller
public class SearchController {
    //转交给检索页面
    @GetMapping("/list.html")
    public String listPage(){
        return "list";
    }
}

之前映射的有错误,现在在es处迁移映射

PUT gulimall_product
{
    "mappings":{
        "properties": {
            "skuId":{ "type": "long" },
            "spuId":{ "type": "keyword" },  
            "skuTitle": {
                "type": "text",
                "analyzer": "ik_smart"  
            },
            "skuPrice": { "type": "keyword" },  
            "skuImg"  : { "type": "keyword" }, 
            "saleCount":{ "type":"long" },
            "hasStock": { "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": "product"
  },
  "dest": {
    "index": "gulimall_product"
  }
}

构建的查询条件

GET gulimall_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
              }
            }
          }
        }
      }
    }
  }
}

 请求的构建

   private SearchRequest buildSearchRequest(SearchPara searchPara) {
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        //1.构建bool的query---》检索
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        //1.1)must模糊匹配 对应语句  "must": [ {"match": {  "skuTitle": "华为" }} ]
        if (!StringUtils.isEmpty(searchPara.getKeyword())) {
            boolQuery.must(QueryBuilders.matchQuery("skuTitle", searchPara.getKeyword()));
        }
        //1.2)filter 与must的区别是filter的项目不参与评分
        //"filter": [ # 过滤{ "term": { "catalogId": "225" } },
        if (searchPara.getCatalog3Id() != null) {
            boolQuery.filter(QueryBuilders.termQuery("catalogId", searchPara.getCatalog3Id()));
        }
        //1.3) { "terms": {"brandId": [ "2"] } },  { "term": { "hasStock": "false"} }
        if (searchPara.getBrandId() != null && searchPara.getBrandId().size() > 0) {
            boolQuery.filter(QueryBuilders.termQuery("brandId", searchPara.getBrandId()));
        }
        boolQuery.filter(QueryBuilders.termQuery("hasStock", searchPara.getHasStock() == 1));
        //1.4) "range": {"skuPrice": { # 价格1K~7K"gte": 1000, "lte": 7000}} 1_500表示大于1小于500
        RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("skuPrice");
        if (!StringUtils.isEmpty(searchPara.getSkuPrice())) {
            String[] split = searchPara.getSkuPrice().split("_");
            if (split.length == 2) {//表示是一个区间
                rangeQuery.gte(split[0]).lte(split[1]);
            } else {
                if (searchPara.getSkuPrice().startsWith("_")) {
                    rangeQuery.lte(split[0]);
                } else {
                    rangeQuery.gte(split[0]);
                }
            }
            boolQuery.filter(rangeQuery);
        }
        //传入的值 attrs=1_5寸:8存 attrs=2_16G:8G  前面的1_ 2_表示几号属性
        //1.5) {"nested": {"path": "attrs", # 聚合名字"query": {"bool": {"must": [{"term": { "attrs.attrId": { "value": "6"
        if (searchPara.getAttrs() != null && searchPara.getAttrs().size() > 0) {

            for (String attr : searchPara.getAttrs()) {
                BoolQueryBuilder boolQuery1 = QueryBuilders.boolQuery();
                String[] s = attr.split("_");
                String attrId = s[0];//检索的属性id
                String[] attrValue = s[1].split(":");//检索的值
                boolQuery1.must(QueryBuilders.termQuery("attrs.attrId", attrId));
                boolQuery1.must(QueryBuilders.termsQuery("attrs.attrValue", attrValue));
                NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("attrs", boolQuery1, ScoreMode.None);//none表示不参与评分
                boolQuery.filter(nestedQuery);//每个属性都要生成一个嵌入式的查询条件
            }
        }
        sourceBuilder.query(boolQuery);
        //2.sort--》排序
        //sort=hostScore_asc/desc
        String sort = searchPara.getSort();
        if (!StringUtils.isEmpty(sort)) {
            String[] s = sort.split("_");
            SortOrder order = s[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC;
            sourceBuilder.sort(s[0], order);
        }
        //3.分页
        //pageNum:1 from 0 size 5 [0,1,2,3,4]
        //pageNum:2 from 5 size 5 [5,6,7,8,9]
        //from=(pageNum-1)*size
        if (searchPara.getPageNum() != null) {
            sourceBuilder.from((searchPara.getPageNum() - 1) * EsConstant.PRODUCT_PAGESIZE);
            sourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);
        }
        //4.高亮
        /*
        "highlight": {
            "fields": {"skuTitle": {}}, # 高亮的字段
            "pre_tags": "<b style='color:red'>",  # 前缀
            "post_tags": "</b>"
        },
         */
        if (!StringUtils.isEmpty(searchPara.getKeyword())) {
            HighlightBuilder highlightBuilder = new HighlightBuilder();
            highlightBuilder.field("skuTitle");
            highlightBuilder.preTags("<b style='color:red>");
            highlightBuilder.postTags("</b>");
            sourceBuilder.highlighter(highlightBuilder);
        }
        //5.聚合分析
        //5.1)聚合品牌
        /*
        "aggs": { "brandAgg": {"terms": {"field": "brandId","size": 10
         */
        TermsAggregationBuilder brandAgg = AggregationBuilders.terms("brand_agg");
        brandAgg.field("brandId").size(10);
        //5.2) 品牌子聚合
        /*
        "aggs": {
        "brandNameAgg": {
          "terms": {
            "field": "brandName",
            "size": 10

        "brandImgAgg": {
          "terms": {
            "field": "brandImg",
            "size": 10
         */
        brandAgg.subAggregation(AggregationBuilders.terms("brandNameAgg").field("brandName").size(10));
        brandAgg.subAggregation(AggregationBuilders.terms("brandImgAgg").field("brandImg").size(10));
        sourceBuilder.aggregation(brandAgg);
        //5.3)分类聚合
        /*
        "catalogAgg":{
        "terms": {
        "field": "catalogId",
        "size": 10
      },
      "aggs": {
        "catalogNameAgg": {
          "terms": {
            "field": "catalogName",
            "size": 10
         */
        TermsAggregationBuilder catalogAgg = AggregationBuilders.terms("catalogAgg").field("catalogId").size(10);
        catalogAgg.subAggregation(AggregationBuilders.terms("catalogNameAgg").field("catalogName").size(1));
        sourceBuilder.aggregation(catalogAgg);
        //5.4)属性聚合
        /*
        "attrs":{
      "nested": {"path": "attrs" },
      "aggs": {
        "attrIdAgg": {
          "terms": {
            "field": "attrs.attrId",
            "size": 10
          },
          "aggs": {
            "attrNameAgg": {
              "terms": {
                "field": "attrs.attrName",
                "size": 10

           "aggs": {
            "attrValueAgg": {
              "terms": {
                "field": "attrs.attrValue",
                "size": 10
         */
        NestedAggregationBuilder attrsAgg = AggregationBuilders.nested("attr_agg", "attrs");
        //聚合出id
        TermsAggregationBuilder attrIdAgg = AggregationBuilders.terms("attrIdAgg").field("attrs.attrId").size(10);
        //id里聚合出名字
        attrIdAgg.subAggregation(AggregationBuilders.terms("attrNameAgg").field("attrs.attrName").size(10));
        attrIdAgg.subAggregation(AggregationBuilders.terms("attrValueAgg").field("attrs.attrValue").size(10));
        //小聚合放入大聚合
        attrsAgg.subAggregation(attrIdAgg);
        sourceBuilder.aggregation(attrsAgg);
        //客户端打印
        String s = sourceBuilder.toString();
        System.out.println(s);
        SearchRequest searchRequest = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX}, sourceBuilder);
        return searchRequest;
    }

结果的返回

    private SearchResult buildSearchResult(SearchResponse response, SearchPara para) {
        SearchResult result = new SearchResult();
        SearchHits hits = response.getHits();
        //1.返回查询到的所有商品
        List<SkuEsModel> list = new ArrayList<>();
        SearchHit[] searchHits = hits.getHits();
        if(searchHits!=null&&searchHits.length>0){
            for (SearchHit hit : searchHits) {
                String sourceAsString = hit.getSourceAsString();
                //解析数据
                SkuEsModel esModel = JSON.parseObject(sourceAsString, SkuEsModel.class);
                //如果有高亮的信息
                if(!StringUtils.isEmpty(para.getKeyword())){//按关键字检索了就设置高亮的字段
                    HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
                    String s = skuTitle.getFragments()[0].toString();
                    esModel.setSkuTitle(s);
                }
                list.add(esModel);
            }
        }
        result.setProducts(list);
        //2.商品分类信息
        ParsedLongTerms catalogAgg = response.getAggregations().get("catalogAgg");//ParsedLongTerms就是Long类型的Term聚合
        List<? extends Terms.Bucket> buckets = catalogAgg.getBuckets();
        ArrayList<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
        for (Terms.Bucket bucket : buckets) {
            SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
            catalogVo.setCatalogId(Long.parseLong(bucket.getKeyAsString()));
            //名字在子聚合里面
            ParsedStringTerms catalogNameAgg = bucket.getAggregations().get("catalogNameAgg");//ParsedStringTerms就是String类型的Term聚合
            String catalogName = catalogAgg.getBuckets().get(0).getKeyAsString();
            catalogVo.setCatalogName(catalogName);
            catalogVos.add(catalogVo);
        }
        //3.当前商品的品牌信息
        List<SearchResult.BrandVo> brandVos = new ArrayList<>();
        Aggregations aggregations = response.getAggregations();
        //ParsedLongTerms用于接收terms聚合的结果,并且可以把key转化为Long类型的数据
        ParsedLongTerms brandAgg = aggregations.get("brand_agg");
        for (Terms.Bucket bucket : brandAgg.getBuckets()) {
            // 得到品牌id
            Long brandId = bucket.getKeyAsNumber().longValue();

            Aggregations subBrandAggs = bucket.getAggregations();
            //得到品牌图片
            ParsedStringTerms brandImgAgg=subBrandAggs.get("brandImgAgg");
            String brandImg = brandImgAgg.getBuckets().get(0).getKeyAsString();
            // 得到品牌名字
            Terms brandNameAgg=subBrandAggs.get("brandNameAgg");
            String brandName = brandNameAgg.getBuckets().get(0).getKeyAsString();
            SearchResult.BrandVo brandVo = new SearchResult.BrandVo(brandId, brandName, brandImg);
            brandVos.add(brandVo);
        }
        result.setBrands(brandVos);
        //4.属性信息
        List<SearchResult.AttrVo> attrVos = new ArrayList<>();
        //ParsedNested用于接收内置属性的聚合
        ParsedNested parsedNested=aggregations.get("attr_agg");
        ParsedLongTerms attrIdAgg=parsedNested.getAggregations().get("attrIdAgg");
        for (Terms.Bucket bucket : attrIdAgg.getBuckets()) {
            //查询属性id
            Long attrId = bucket.getKeyAsNumber().longValue();

            Aggregations subAttrAgg = bucket.getAggregations();
            //查询属性名
            ParsedStringTerms attrNameAgg=subAttrAgg.get("attrNameAgg");
            String attrName = attrNameAgg.getBuckets().get(0).getKeyAsString();
            //查询属性值
            ParsedStringTerms attrValueAgg = subAttrAgg.get("attrValueAgg");
            List<String> attrValues = new ArrayList<>();
            for (Terms.Bucket attrValueAggBucket : attrValueAgg.getBuckets()) {
                String attrValue = attrValueAggBucket.getKeyAsString();
                attrValues.add(attrValue);
                List<SearchResult.NavVo> navVos = new ArrayList<>();
            }
            SearchResult.AttrVo attrVo = new SearchResult.AttrVo(attrId, attrName, attrValues);
            attrVos.add(attrVo);
        }
        result.setAttrs(attrVos);

        //5.分页页码
        result.setPageNum(para.getPageNum());
        //6.总记录数,总页码
        long totalRecord = hits.getTotalHits().value;
        result.setTotal(totalRecord);
        int total=(int)totalRecord;
        int totalPages=total%EsConstant.PRODUCT_PAGESIZE==0?total/EsConstant.PRODUCT_PAGESIZE:(total/EsConstant.PRODUCT_PAGESIZE)+1;
        result.setTotalPages(totalPages);
        return result;
    }

整体方法

    @Override
    public SearchResult search(SearchPara searchPara) {
        //构建检索请求
        SearchRequest request = buildSearchRequest(searchPara);
        SearchResult result =null;
        try {
            //执行检索请求
            SearchResponse search = client.search(request, ESConfig.COMMON_OPTIONS);
            //分析响应数据封装成我们需要的格式
            result = buildSearchResult(search,searchPara);
        } catch (IOException e) {
            e.printStackTrace();

        }
        return result;
    }

Controller 

    //转交给检索页面
    @GetMapping("/list.html")
    public String listPage(SearchPara searchPara, Model model){
        SearchResult result=mallSearchService.search(searchPara);
        model.addAttribute("result",result);
        return "list";
    }

页面渲染前端内容略

面包屑导航功能

search模块导入远程调用依赖,同时要导入springcloud的依赖管理

 <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

开启远程调用功能

 

 search模块的远程接口

@FeignClient("gulimall-product")
public interface ProductFeignService {
    @GetMapping("/product/attr/info/{attrId}")
    public R attrInfo(@PathVariable("attrId") Long attrId);//通过id获得att的信息
}

新增common模块utils的R方法

    //利用fastJson进行逆转
    public<T> T getData(String key,TypeReference<T> typeReference){
        Object data=get(key);
        String s = JSON.toJSONString(data);
        T t=JSON.parseObject(s,typeReference);
        return t;
    }

@GetMapping("/list.html")
    public String listPage(SearchPara searchPara, Model model, HttpServletRequest request){
        String queryString=request.getQueryString();
        searchPara.set_queryString(queryString);//设置url
        SearchResult result=mallSearchService.search(searchPara);
        model.addAttribute("result",result);
        return "list";
    }

private SearchResult buildSearchResult(SearchResponse response, SearchPara para) {
        SearchResult result = new SearchResult();
        SearchHits hits = response.getHits();
        //1.返回查询到的所有商品
        List<SkuEsModel> list = new ArrayList<>();
        SearchHit[] searchHits = hits.getHits();
        if (searchHits != null && searchHits.length > 0) {
            for (SearchHit hit : searchHits) {
                String sourceAsString = hit.getSourceAsString();
                //解析数据
                SkuEsModel esModel = JSON.parseObject(sourceAsString, SkuEsModel.class);
                //如果有高亮的信息
                if (!StringUtils.isEmpty(para.getKeyword())) {//按关键字检索了就设置高亮的字段
                    HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
                    String s = skuTitle.getFragments()[0].toString();
                    esModel.setSkuTitle(s);
                }
                list.add(esModel);
            }
        }
        result.setProducts(list);
        //2.商品分类信息
        ParsedLongTerms catalogAgg = response.getAggregations().get("catalogAgg");//ParsedLongTerms就是Long类型的Term聚合
        List<? extends Terms.Bucket> buckets = catalogAgg.getBuckets();
        ArrayList<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
        for (Terms.Bucket bucket : buckets) {
            SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
            catalogVo.setCatalogId(Long.parseLong(bucket.getKeyAsString()));
            //名字在子聚合里面
            ParsedStringTerms catalogNameAgg = bucket.getAggregations().get("catalogNameAgg");//ParsedStringTerms就是String类型的Term聚合
            String catalogName = catalogAgg.getBuckets().get(0).getKeyAsString();
            catalogVo.setCatalogName(catalogName);
            catalogVos.add(catalogVo);
        }
        //3.当前商品的品牌信息
        List<SearchResult.BrandVo> brandVos = new ArrayList<>();
        Aggregations aggregations = response.getAggregations();
        //ParsedLongTerms用于接收terms聚合的结果,并且可以把key转化为Long类型的数据
        ParsedLongTerms brandAgg = aggregations.get("brand_agg");
        for (Terms.Bucket bucket : brandAgg.getBuckets()) {
            // 得到品牌id
            Long brandId = bucket.getKeyAsNumber().longValue();

            Aggregations subBrandAggs = bucket.getAggregations();
            //得到品牌图片
            ParsedStringTerms brandImgAgg = subBrandAggs.get("brandImgAgg");
            String brandImg = brandImgAgg.getBuckets().get(0).getKeyAsString();
            // 得到品牌名字
            Terms brandNameAgg = subBrandAggs.get("brandNameAgg");
            String brandName = brandNameAgg.getBuckets().get(0).getKeyAsString();
            SearchResult.BrandVo brandVo = new SearchResult.BrandVo(brandId, brandName, brandImg);
            brandVos.add(brandVo);
        }
        result.setBrands(brandVos);
        //4.属性信息
        List<SearchResult.AttrVo> attrVos = new ArrayList<>();
        //ParsedNested用于接收内置属性的聚合
        ParsedNested parsedNested = aggregations.get("attr_agg");
        ParsedLongTerms attrIdAgg = parsedNested.getAggregations().get("attrIdAgg");
        for (Terms.Bucket bucket : attrIdAgg.getBuckets()) {
            //查询属性id
            Long attrId = bucket.getKeyAsNumber().longValue();

            Aggregations subAttrAgg = bucket.getAggregations();
            //查询属性名
            ParsedStringTerms attrNameAgg = subAttrAgg.get("attrNameAgg");
            String attrName = attrNameAgg.getBuckets().get(0).getKeyAsString();
            //查询属性值
            ParsedStringTerms attrValueAgg = subAttrAgg.get("attrValueAgg");
            List<String> attrValues = new ArrayList<>();
            for (Terms.Bucket attrValueAggBucket : attrValueAgg.getBuckets()) {
                String attrValue = attrValueAggBucket.getKeyAsString();
                attrValues.add(attrValue);
                List<SearchResult.NavVo> navVos = new ArrayList<>();
            }
            SearchResult.AttrVo attrVo = new SearchResult.AttrVo(attrId, attrName, attrValues);
            attrVos.add(attrVo);
        }
        result.setAttrs(attrVos);

        //5.分页页码
        result.setPageNum(para.getPageNum());
        //6.总记录数,总页码
        long totalRecord = hits.getTotalHits().value;
        result.setTotal(totalRecord);
        int total = (int) totalRecord;
        int totalPages = total % EsConstant.PRODUCT_PAGESIZE == 0 ? total / EsConstant.PRODUCT_PAGESIZE : (total / EsConstant.PRODUCT_PAGESIZE) + 1;
        result.setTotalPages(totalPages);

        //7.构建面包屑 导航功能
        if (para.getAttrs() != null && para.getAttrs().size() > 0) {
            List<SearchResult.NavVo> navVos = para.getAttrs().stream().map(attr -> {
                SearchResult.NavVo navVo = new SearchResult.NavVo();
                //attrs=2_5寸:6寸
                //分析每一个传过来的查询参数值
                String[] s = attr.split("_");
                navVo.setNavValue(s[1]);
                R r = productFeignService.attrInfo(Long.parseLong(s[0]));
                if (r.getCode() == 0) {
                    new TypeReference<SearchResult.AttrVo>() {
                    };
                    AttrResponseVo vo = r.getData("attr", new TypeReference<AttrResponseVo>() {
                    });
                    navVo.setNavValue(vo.getAttrName());//拿到属性的名字
                } else {
                    navVo.setName(s[0]);
                }
                //取消了面包屑以后我们要跳转到哪--》将请求地址的url里面的当前置空
                //拿到所有查询条件,去掉当前--》在controller里拿到值
                String encode = "";
                try {
                    encode = URLEncoder.encode(attr, "UTF-8");//前端的数据是编码过的
                    encode.replace("+", "%20");//空格会被浏览器解析为%20然后被java解析成+
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
                String replace = para.get_queryString().replace("&attrs=" + attr, "").replace("attrs=" + attr + "&", "").replace("attrs=" + attr, "");
                navVo.setLink("http://search.gulimall.com/list.html?" + replace);

                return navVo;
            }).collect(Collectors.toList());
            result.setNavs(navVos);
        }
        return result;
    }

 

posted @ 2021-08-17 18:54  一拳超人的逆袭  阅读(135)  评论(0编辑  收藏  举报