5.商城业务-检索服务

266 阅读7分钟

本文我们讲解一下项目中的检索服务。

本人采用的配置:阿里云服务器+本地部署nginx+域名,遇到了很多坑!!

1.环境配置

1)引入thymeleaf,针对于热部署可以在IDEA中下载jrebel插件

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

2)关闭thymeleaf缓存

spring:
  thymeleaf:
    cache: false

3)将搜索页的index.html放入到项目中的templates目录下

image-20230109191123519.png

4)引入thymeleaf命名空间

<html lang="en" xmlns:th="http:/www.thymeleaf.org">

5)将对应搜索页的静态资源转移到nginx对应目录下

image-20230109191411074.png

6)将index.html中访问静态资源的前缀修改为static目录下的

2.域名解析设置

针对你购买的域名,是可以设置不同的主机记录的。emmmm,之前没了解过这些,读者有兴趣可以去查一些相关资料。

注意:这里search和www可以理解为不同的前缀,因此在配置区分的时候,一定要指明前缀信息(划重点)

image-20230108234728290.png

解决了域名问题之后,接下来就可以正常配置了。

1)配置本地DNS

image-20230108235210231.png

2)配置nginx监听域名

image-20230108235322663.png

在前面提到,在阿里云域名解析设置的时候,根据前缀的不同产生多个不同的域名,因此这里我们就可以直接使用*.域名信息完成配置。

接下来如果想访问到nginx中存放的关于检索页的静态资源,需要对网关进行配置。

3)配置网关

  • 由网关负载均衡将相关请求转发到各个服务中
image-20230109000138527.png

总流程

image-20230109192050126.png

接下来重启各个服务就可以访问搜索页了

3.跳转页面配置

1)在搜索页配置点击logo回到主页

  • 在search服务中的index.html中配置

    image-20230109193652110.png image-20230109193818602.png

2)主页跳转到搜索页

  • 查看转发路径信息,发现并不是自己的域名信息,以及获取到的搜索页也是错误的

    http://search.gulimall.com/list.html?catalog3Id=225
    
  • 在nginx中product服务中,将catalogLoader.js中的转发信息修改一下

    image-20230109194230243.png

  • 将搜索页index.html修改为list.html

  • 添加页面跳转的控制器

    @Controller
    public class SearchController {
    
        @GetMapping("/list.html")
        public String listPage(){
            return "list";
        }
    }
    

3)主页搜索时,点击按钮完成跳转

image-20230109194821177.png

4.检索业务

1)分析跳转到检索页检索条件

简单筛选条件

  • 输入检索关键字进入检索页:skuTitle->keyword
  • 选择三级分类进入检索页:catalog3Id

复杂筛选条件

  • 排序:saleCount(销量)、hotScore(热度分)、skuPrice(价格)
  • 过滤:hasStock、skuPrice区间、brandId、catalog3Id、attrs
  • 聚合:attrs

可能查询参数举例:

keyword=小米&sort=saleCount_desc/asc&hasStock=0/1&skuPrice=400_1900&brandId=1&catalog3Id=1&attrs=1_3G:4G:5G&attrs=2_骁龙845&attrs=4_高清屏

将上述查询条件封装成VO

@Data
public class SearchParam {
    /**
     * 页面传递过来的全文匹配关键字
     */
    private String keyword;

    /**
     * 三级分类id
     */
    private Long catalog3Id;

    /**
     * 品牌id,可以多选
     */
    private List<Long> brandId;

    /**
     * 排序条件:sort=price_desc/asc/salecount_desc/asc/hotscore_desc/asc
     */
    private String sort;

    /**
     * 是否显示有货
     */
    private Integer hasStock;

    /**
     * 价格区间查询
     */
    private String skuPrice;

    /**
     * 按照属性进行筛选
     */
    private List<String> attrs;

    /**
     * 页码
     */
    private Integer pageNum = 1;
}

2)分析返回给页面的数据

分析一下流程:

  • 页面提交查询的参数,我们需要根据参数去ES中查询相应的数据返回给页面进行渲染。那么我们应该给页面返回什么样的数据呢?

分析需要返回给页面的数据

必需数据:商品信息、当前页码、总记录数、总页码(用于分页)

公共数据:品牌列表用于在品牌栏显示(BrandVo),分类列表用于在分类栏显示(CatalogVo)

特有数据:根据筛选条件查询到的商品信息的属性信息【属性Id、属性名、属性值集合】(AttrVo)

将上述数据信息封装成VO

@Data
public class SearchResult {
    /**
     * 查询到的所有商品信息
     */
    private List<SkuEsModel> product;

    /**
     * 分页信息
     */
    private Integer pageNum;   // 当前页码

    private Long total;   //总记录数

    private Integer totalPages; //总页码

    private List<Integer> pageNavs;

    /**
     * 当前查询到的结果,所有涉及到的品牌
     */
    private List<BrandVo> brands;

    /**
     * 当前查询到的结果,所有涉及到的所有属性
     */
    private List<AttrVo> attrs;

    /**
     * 当前查询到的结果,所有涉及到的所有分类
     */
    private List<CatalogVo> catalogs;


    //===========================以上是返回给页面的所有信息============================//


    @Data
    public static class BrandVo {

        private Long brandId;

        private String brandName;

        private String brandImg;
    }


    @Data
    public static class AttrVo {

        private Long attrId;

        private String attrName;

        private List<String> attrValue;
    }


    @Data
    public static class CatalogVo {

        private Long catalogId;

        private String catalogName;
    }
}

3)检索DSL&检索业务编写

上面我们将检索页请求参数以及返回给页面的数据信息抽取出了两种数据模型SearchParam & SearchResult,下面我们就需要根据页面传过来的SearchParam去ES中检索相应的数据,然后将检索到的数据封装为SearchResult格式并返回给页面进行渲染。

跳转到检索页发出请求

@GetMapping("/list.html")
public String listPage(SearchParam param, Model model) {
	// 1.根据传递来的页面的查询参数,去es中检索商品
    SearchResult result = mallSearchService.search(param);
    model.addAttribute("result", result);
    return "list";
}

实现search()

public SearchResult search(SearchParam param) {
    //1、动态构建出查询需要的DSL语句
    SearchResult result = null;

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

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

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

    return result;
}

根据传递过来的SearchParam数据信息,利用ES的API封装复杂的SearchSourceBuilder,然后向ES指定索引发送请求,获取到请求结果SearchRequest

/**
 * 准备检索请求
 * 模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存),排序,分页,高亮,聚合分析
 * @return
 */
private SearchRequest buildSearchRequest(SearchParam param) {
    SearchSourceBuilder sourceBuilder = 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 -> {
            BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();

            //attrs=1_5寸:8寸&2_16G:8G
            String[] split = item.split("_");
            String attrId = split[0];
            String[] attrValues = split[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(null != param.getSkuPrice()) {
        RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice");
        //skuPrice形式为:1_500或_500或500_
        String skuPrice = param.getSkuPrice();
        String[] price = skuPrice.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);
    }

    //封装所有的查询条件
    sourceBuilder.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;

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

    //分页
    sourceBuilder.from((param.getPageNum()-1)*EsConstant.PRODUCT_PAGESIZE);
    sourceBuilder.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>");

        sourceBuilder.highlighter(highlightBuilder);
    }

    /**
     * 聚合分析
     */
    // TODO 1.聚合商品
    TermsAggregationBuilder brandAgg = AggregationBuilders.terms("brand_agg").field("brandId").size(50);
    // 子聚合商品名称以及商品图片
    brandAgg.subAggregation(AggregationBuilders.terms("brand_name_agg").field("brandName").size(1));
    brandAgg.subAggregation(AggregationBuilders.terms("brand_img_agg").field("brandImg").size(1));
    sourceBuilder.aggregation(brandAgg);

    // TODO 2.聚合分类
    TermsAggregationBuilder catalogAgg = AggregationBuilders.terms("catalog_agg").field("catalogId").size(20);
    catalogAgg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1));
    sourceBuilder.aggregation(catalogAgg);

    // TODO 3.聚合属性
    NestedAggregationBuilder attrAgg = AggregationBuilders.nested("attr_agg", "attrs");
    TermsAggregationBuilder attrIdAgg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId");
    attrIdAgg.subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1));
    attrIdAgg.subAggregation(AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50));
    attrAgg.subAggregation(attrIdAgg);
    sourceBuilder.aggregation(attrAgg);

    System.out.println("构建的DSL语句:" + sourceBuilder.toString());
    // log.debug("构建的DSL语句 {}",sourceBuilder.toString());

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

    return searchRequest;
}

分析响应结果SearchRequest,将其封装为指定格式SearchResult

/**
 * 构建结果数据
 * 模糊匹配,过滤(按照属性、分类、品牌,价格区间,库存),完成排序、分页、高亮,聚合分析功能
 * @param response
 * @return
 */
private SearchResult buildSearchResult(SearchResponse response, SearchParam param) {
    SearchResult searchResult = new SearchResult();
    // 返回的所有查询到的商品
    SearchHits hits = response.getHits();
    List<SkuEsModel> esModels = new ArrayList<>();
    // 遍历所有商品信息
    if(hits.getHits() != null && hits.getHits().length > 0) {
        for (SearchHit searchHit : hits.getHits()) {
            String sourceAsString = searchHit.getSourceAsString();
            SkuEsModel esModel = JSON.parseObject(sourceAsString, SkuEsModel.class);
            //判断是否按关键字检索,若是就显示高亮,否则不显示
            if (!StringUtils.isEmpty(param.getKeyword())) {
                //拿到高亮信息显示标题
                HighlightField skuTitle = searchHit.getHighlightFields().get("skuTitle");
                String skuTitleValue = skuTitle.getFragments()[0].string();
                esModel.setSkuTitle(skuTitleValue);
            }
            esModels.add(esModel);
        }
    }
    // 1.存放进商品信息
    searchResult.setProduct(esModels);

    //2、当前商品涉及到的所有属性信息
    List<SearchResult.AttrVo> attrVos = new ArrayList<>();
    ParsedNested attrAgg = response.getAggregations().get("attr_agg");
    ParsedLongTerms attrIdAgg = attrAgg.getAggregations().get("attr_id_agg");
    for (Terms.Bucket bucket : attrIdAgg.getBuckets()) {
        SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
        // 1)得到属性的id
        attrVo.setAttrId(bucket.getKeyAsNumber().longValue());
        // 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);
    }
    searchResult.setAttrs(attrVos);

    //3、当前商品涉及到的所有分类信息
    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();
        // 1.得到分类id
        catalogVo.setCatalogId(bucket.getKeyAsNumber().longValue());
        // 2.得到分类名称
        ParsedStringTerms catalogNameAgg = bucket.getAggregations().get("catalog_name_agg");
        catalogVo.setCatalogName(catalogNameAgg.getBuckets().get(0).getKeyAsString());
        catalogVos.add(catalogVo);
    }
    searchResult.setCatalogs(catalogVos);

    //4、当前商品涉及到的所有品牌信息
    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);
    }
    searchResult.setBrands(brandVos);
    //===============以上可以从聚合信息中获取====================//
    // 5.分页信息
    //5、1分页信息:总记录数
    long total = hits.getTotalHits().value;
    searchResult.setTotal(total);
    // 5.3分页信息:页码
    searchResult.setPageNum(param.getPageNum());
    //5、2分页信息-总页码-计算
    int totalPages = (int)total % EsConstant.PRODUCT_PAGESIZE == 0 ?
            (int)total / EsConstant.PRODUCT_PAGESIZE : ((int)total / EsConstant.PRODUCT_PAGESIZE + 1);
    searchResult.setTotalPages(totalPages);

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

    return searchResult;
}

将结果可以进行打印,放到kibana中进行册数。接下来将结果数据返回给前端就可以进行渲染了,这里前端内容不再进行描述!