微服务实战——ElasticSearch(搜索)

商品检索------ElasticSearch(搜索)

1. 检索条件&排序条件分析

  • 全文检索:skuTitle -> keyword
  • 排序: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_高清屏

2. DSL分析

检索时需要进行:模糊匹配、过滤(按照属性,分类,品牌,价格区间,库存)、排序、分页、高亮、聚合分析 。

GET gulimall_product/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "skuTitle": "华为"
          }
        }
      ],
      "filter": [
        {
          "term": {
            "catalogId": "225"
          }
        },
        {
          "terms": {
            "brandId": [
              "4"
            ]
          }
        },
        {
          "term": {
            "hasStock": "false"
          }
        },
        {
          "range": {
            "skuPrice": {
              "gte": 1000,
              "lte": 7000
            }
          }
        },
        {
          "nested": {
            "path": "attrs",
            "query": {
              "bool": {
                "must": [
                  {
                    "term": {
                      "attrs.attrId": {
                        "value": "1"
                      }
                    }
                  }
                ]
              }
            }
          }
        }
      ]
    }
  },
  "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": {
          "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
              }
            }
          }
        }
      }
    }
  }
}

3. 检索代码

3.1. 请求参数和返回结果封装

package com.atguigu.gulimall.search.vo;

import lombok.Data;

import java.util.List;

/**
 * 封装页面所有可能传递过来的查询条件
 * catalog3Id=225&keyword=小米&sort=saleCount_asc
 */
@Data
public class SearchParam {
    private String keyword;//页面传递过来的全文匹配关键字

    private Long catalog3Id;//三级分类id

    /**
     * sort=saleCount_asc/desc
     * sort=skuPrice_asc/desc
     * sort=hotScore_asc/desc
     */
    private String sort;//排序条件

    /**
     * 好多的过滤条件
     * hasStock(是否有货)、skuPrice区间、brandId、catalog3Id、attrs
     * hasStock=0/1
     * skuPrice=1_500
     */
    private Integer hasStock;//是否只显示有货

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

    private List<Long> brandId;//按照品牌进行查询,可以多选

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

    private Integer pageNum = 1;//页码
}

package com.atguigu.gulimall.search.vo;
 
import com.atguigu.common.to.es.SkuEsModel;
import lombok.Data;
 
import java.util.List;
 
@Data
public class SearchResult {
 
    /**
     * 查询到的商品信息
     */
    private List<SkuEsModel> products;
 
    private Integer pageNum;//当前页码
 
    private Long total;//总记录数
 
    private Integer totalPages;//总页码
 
    private List<BrandVo> brands;//当前查询到的结果,所有涉及到的品牌
 
    private List<CatalogVo> catalogs;//当前查询到的结果,所有涉及到的分类
 
    private List<AttrVo> attrs;//当前查询到的结果,所有涉及到的属性
 
    //=====================以上是返给页面的信息==========================
 
    @Data
    public static class BrandVo{
        private Long brandId;
 
        private String brandName;
 
        private String brandImg;
    }
 
    @Data
    public static class CatalogVo{
        private Long catalogId;
 
        private String catalogName;
 
        private String brandImg;
    }
 
    @Data
    public static class AttrVo{
        private Long attrId;
 
        private String attrName;
 
        private List<String> attrValue;
    }
}

3.2. 业务逻辑

3.2.1. SearchController
    /**
     * 自动将页面提交过来的所有请求查询参数封装成指定的对象
     *
     * @return
     */
    @GetMapping("/list.html")
    public String listPage(SearchParam searchParam, Model model) {
        SearchResult result = mallSearchService.search(searchParam);
        System.out.println("====================" + result);
        model.addAttribute("result", result);
        return "list";
    }
3.2.2. MallSearchServiceImpl
@Override
    public SearchResult search(SearchParam param) {

        SearchResult result = null;

        // 1.准备检索请求
        SearchRequest searchRequest = buildSearchRequest(param);
        try {
            // 2.执行检索请求
            SearchResponse response = esClient.search(searchRequest, COMMON_OPTIONS);

            // 3.解析响应数据并封装需要返回的页面数据
            result = buildSearchResult(param, response);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 准备检索请求
     * @return
     */
    private SearchRequest buildSearchRequest(SearchParam param) {
        // 构建DSL语句
        SearchSourceBuilder builder = new SearchSourceBuilder();

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

        // 1.1.bool-must 模糊匹配
        String keyword = param.getKeyword();
        if(StringUtils.isNotEmpty(keyword)){
            boolQuery.must(QueryBuilders.matchQuery("skuTitle", keyword));
        }

        // 1.2.bool-filter 过滤条件
        // 1.2.1.bool-filter-catalogId
        Long catalog3Id = param.getCatalog3Id();
        if(catalog3Id != null){
            boolQuery.filter(QueryBuilders.termQuery("catalogId", catalog3Id));
        }

        // 1.2.2.bool-filter-brandId
        List<Long> brandIds = param.getBrandId();
        if(brandIds != null && brandIds.size() > 0){
            boolQuery.filter(QueryBuilders.termsQuery("brandId", brandIds));
        }

        // 1.2.3.bool-filter-hasStock = 0/1
        Integer hasStock = param.getHasStock();
        if(hasStock != null){
            boolQuery.filter(QueryBuilders.termQuery("hasStock", hasStock));
        }

        // 1.2.4.bool-filter-skuPrice = 1_500/_500/500_
        String skuPrice = param.getSkuPrice();
        if(StringUtils.isNotEmpty(skuPrice)){
            // 将skuPrice以_分隔
            String[] split = skuPrice.split("_");
            String lt = null, gt = null;
            if(split.length == 2){
                // case1: 1_500
                lt = split[1];
                // case2: _500
                if(StringUtils.isNotBlank(split[0])){
                    gt = split[0];
                }
            }else {
                // case3: 500_
                gt = split[0];
            }
            // 构建rangeQueryBuilder
            RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice");
            if(StringUtils.isNotEmpty(lt)){
                rangeQueryBuilder.lt(Long.parseLong(lt));
            }
            if(StringUtils.isNotEmpty(gt)){
                rangeQueryBuilder.gt(Long.parseLong(gt));
            }
            // 放入boolQuery
            boolQuery.filter(rangeQueryBuilder);
        }

        // 1.2.5.bool-filter-nested-attrs = 2_5寸:6寸&2_16G:8G
        List<String> attrs = param.getAttrs();
        if(attrs != null && attrs.size() > 0){
            for (String attrStr : attrs) {
                BoolQueryBuilder nestedBoolQuery = QueryBuilders.boolQuery();
                // attrStr = 2_5寸:6寸
                String[] s = attrStr.split("_");
                String attrId = s[0];

                // s[1] = 5寸:6寸
                String[] attrValues = s[1].split(":");
                nestedBoolQuery.must(QueryBuilders.termQuery("attrs.attrId", attrId));
                nestedBoolQuery.must(QueryBuilders.termsQuery("attrs.attrValue", attrValues));

                // 每一个必须都得生成nested查询
                NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("attrs", nestedBoolQuery, ScoreMode.None);
                boolQuery.filter(nestedQuery);
            }
        }

        // 把以上所有查询条件封装
        builder.query(boolQuery);

        /**
         * 处理:排序,分页,高亮
         */
        // 2.1.排序:sort = hotScore_asc/desc
        String sort = param.getSort();
        if(StringUtils.isNotEmpty(sort)){
            String[] s = sort.split("_");
            builder.sort(s[0], s[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC);
        }

        // 2.2.分页
        builder.from((param.getPageNum() - 1) * EsConstant.PRODUCT_PAGE_SIZE);
        builder.size(EsConstant.PRODUCT_PAGE_SIZE);

        // 2.3.高亮
        if(StringUtils.isNotEmpty(keyword)){
            HighlightBuilder highlightBuilder = new HighlightBuilder();
            highlightBuilder.field("skuTitle");
            highlightBuilder.preTags("<b style='color:red'>");
            highlightBuilder.postTags("</b>");
            builder.highlighter(highlightBuilder);
        }

        /**
         * 响应:聚合分析
         */
        // 3.1.聚合brandAgg
        TermsAggregationBuilder brandAgg = AggregationBuilders.terms("brandAgg");
        brandAgg.field("brandId").size(50);
        // 3.1.1.子聚合brandNameAgg
        brandAgg.subAggregation(AggregationBuilders.terms("brandNameAgg").field("brandName").size(1));
        // 3.1.2.子聚合brandImgAgg
        brandAgg.subAggregation(AggregationBuilders.terms("brandImgAgg").field("brandImg").size(1));
        // 3.1.3.整合brandAgg
        builder.aggregation(brandAgg);

        // 3.2.聚合catalogAgg
        TermsAggregationBuilder catalogAgg = AggregationBuilders.terms("catalogAgg");
        // 3.2.1.聚合catalogId
        catalogAgg.field("catalogId").size(20);
        // 3.2.2.子聚合catalogNameAgg
        catalogAgg.subAggregation(AggregationBuilders.terms("catalogNameAgg").field("catalogName").size(1));
        // 3.2.3.整合catalogAgg
        builder.aggregation(catalogAgg);

        // 3.3.聚合attrsAgg
        TermsAggregationBuilder attrIdAgg = AggregationBuilders.terms("attrIdAgg").field("attrs.attrId");
        // attrIdAgg的子聚合attrNameAgg
        TermsAggregationBuilder attrNameAgg = AggregationBuilders.terms("attrNameAgg").field("attrs.attrName").size(1);
        attrIdAgg.subAggregation(attrNameAgg);

        // attrIdAgg的子聚合attrValueAgg
        TermsAggregationBuilder attrValueAgg = AggregationBuilders.terms("attrValueAgg").field("attrs.attrValue").size(50);
        attrIdAgg.subAggregation(attrValueAgg);

        // attrsAgg的子聚合attrIdAgg
        NestedAggregationBuilder attrsAgg = AggregationBuilders.nested("attrsAgg", "attrs");
        attrsAgg.subAggregation(attrIdAgg);

        // 整合attrsAgg
        builder.aggregation(attrsAgg);

        System.out.println("DSL:" + builder.toString());

        SearchRequest request = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX}, builder);
        return request;
    }

    /**
     * 构建页面响应数据
     * @param response
     * @return
     */
    private SearchResult buildSearchResult(SearchParam param, SearchResponse response) {
        SearchResult result = new SearchResult();

        // 1.products
        SearchHits hits = response.getHits();
        SearchHit[] searchHits = hits.getHits();
        List<SkuEsModel> esModels = Collections.emptyList();
        if (searchHits != null && searchHits.length > 0) {
            esModels = Arrays.stream(searchHits).map(searchHit -> {
                String source = searchHit.getSourceAsString();
                SkuEsModel esModel = JSON.parseObject(source, SkuEsModel.class);
                if (StringUtils.isNotEmpty(param.getKeyword())) {
                    HighlightField skuTitle = searchHit.getHighlightFields().get("skuTitle");
                    String string = skuTitle.getFragments()[0].string();
                    esModel.setSkuTitle(string);
                }
                return esModel;
            }).collect(Collectors.toList());
        }
        result.setProducts(esModels);

        Aggregations aggregations = response.getAggregations();
        // 2.brands
        ParsedLongTerms brandAgg = aggregations.get("brandAgg");
        List<BrandVo> brandVos = brandAgg.getBuckets().stream().map(bucket -> {
            BrandVo brandVo = new BrandVo();
            // brandId
            brandVo.setBrandId(bucket.getKeyAsNumber().longValue());

            Aggregations bucketAggs = bucket.getAggregations();
            // brandName
            ParsedStringTerms brandName = bucketAggs.get("brandNameAgg");
            brandVo.setBrandName(brandName.getBuckets().get(0).getKeyAsString());

            // brandImg
            ParsedStringTerms brandImg = bucketAggs.get("brandImgAgg");
            brandVo.setBrandImg(brandImg.getBuckets().get(0).getKeyAsString());

            return brandVo;
        }).collect(Collectors.toList());
        result.setBrands(brandVos);

        // 3.catalogs
        ParsedLongTerms catalogAgg = aggregations.get("catalogAgg");
        List<CatalogVo> catalogVos = catalogAgg.getBuckets().stream().map(bucket -> {
            CatalogVo catalogVo = new CatalogVo();
            catalogVo.setCatalogId(Long.parseLong(bucket.getKeyAsString()));
            ParsedStringTerms catalogNameAgg = bucket.getAggregations().get("catalogNameAgg");
            catalogVo.setCatalogName(catalogNameAgg.getBuckets().get(0).getKeyAsString());
            return catalogVo;
        }).collect(Collectors.toList());
        result.setCatalogs(catalogVos);

        // 4.attrs
        ParsedNested attrsAgg = aggregations.get("attrsAgg");
        ParsedLongTerms attrIdAgg = attrsAgg.getAggregations().get("attrIdAgg");
        List<AttrVo> attrVos = attrIdAgg.getBuckets().stream().map(bucket -> {
            AttrVo attrVo = new AttrVo();
            // attrId
            attrVo.setAttrId(Long.parseLong(bucket.getKeyAsString()));

            Aggregations bucketAggregations = bucket.getAggregations();
            // attrName
            ParsedStringTerms attrNameAgg = bucketAggregations.get("attrNameAgg");
            attrVo.setAttrName(attrNameAgg.getBuckets().get(0).getKeyAsString());
            // attrValues
            ParsedStringTerms attrValueAgg = bucketAggregations.get("attrValueAgg");
            List<String> attrValue = attrValueAgg.getBuckets().stream()
                    .map(MultiBucketsAggregation.Bucket::getKeyAsString).collect(Collectors.toList());
            attrVo.setAttrValues(attrValue);

            return attrVo;
        }).collect(Collectors.toList());
        result.setAttrs(attrVos);

        // 5.分页信息:pageNum, total, totalPages
        long total = hits.getTotalHits().value;
        result.setTotal(total);
        // 计算totalPages
        int totalPages = (int) (total / EsConstant.PRODUCT_PAGE_SIZE + (total % EsConstant.PRODUCT_PAGE_SIZE == 0 ? 0 : 1));
        result.setTotalPages(totalPages);
        result.setPageNum(param.getPageNum());

        // 6.面包屑导航功能
        List<String> _attrs = param.getAttrs();
        if(_attrs != null && _attrs.size() > 0){
            List<NavVo> navVos = _attrs.stream().map(attr -> {
                // attrs = 2_5寸:6存
                String[] split = attr.split("_");
                // 设置属性值
                NavVo navVo = new NavVo();
                navVo.setNavValue(split[1]);
                // 远程调用通过attrId获取attrName
                try {
                    R r = productFeignService.getAttrsInfo(Long.parseLong(split[0]));
                    result.getAttrIds().add(Long.valueOf(split[0]));
                    if(r.getCode() == 0){
                        AttrResponseVo attrResponseVo = r.getData("attr", new TypeReference<AttrResponseVo>() {
                        });
                        navVo.setNavName(attrResponseVo.getAttrName());
                    }else {
                        navVo.setNavName(split[0]);
                    }
                } catch (NumberFormatException e) {
                    e.printStackTrace();
                }
                // 取消面包屑后,跳转到取消后的地方,将原url改为目标url
                String replace = replaceQueryString(param, attr, "attrs");
                navVo.setLink("http://search.gulimall.com/list.html" + (replace.isEmpty() ? "" : "?" + replace));
                return navVo;
            }).collect(Collectors.toList());
            result.setNavs(navVos);
        }

        // 品牌 条件筛选
        List<Long> brandIds = param.getBrandId();
        if(brandIds != null && brandIds.size() > 0){
            List<NavVo> navs = result.getNavs();
            NavVo navVo = new NavVo();
            navVo.setNavName("品牌");
            // 远程查询所有品牌
            R r = productFeignService.brandsInfo(brandIds);
            if(r.getCode() == 0){
                List<com.cwh.search.vo.BrandVo> brand = r.getData("brand", new TypeReference<List<com.cwh.search.vo.BrandVo>>() {
                });
                StringBuffer buffer = new StringBuffer();
                String replace = "";
                for (com.cwh.search.vo.BrandVo brandVo : brand) {
                    buffer.append(brandVo.getName() + ";");
                    replace = replaceQueryString(param, brandVo.getBrandId().toString(), "brandId");
                }
                navVo.setNavValue(buffer.toString());
                navVo.setLink("http://search.gulimall.com/list.html?" + (replace.isEmpty() ? "" : "?" + replace));
            }
            navs.add(navVo);
        }

        // TODO 分类 条件筛选

        return result;
    }

    private String replaceQueryString(SearchParam param, String value, String key) {
        String queryString = param.get_queryString();
        String encode = null;
        try {
            encode = URLEncoder.encode(value, "UTF-8");
            encode = encode.replace("+", "%20");//浏览器对空格编码和java不一样
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return queryString.replace("&" + key + "=" + encode, "");
    }
相关推荐
金刚猿3 分钟前
简单理解下基于 Redisson 库的分布式锁机制
分布式·分布式锁·redisson
郑祎亦19 分钟前
Spring Boot 项目 myblog 整理
spring boot·后端·java-ee·maven·mybatis
不是二师兄的八戒19 分钟前
本地 PHP 和 Java 开发环境 Docker 化与配置开机自启
java·docker·php
我一直在流浪26 分钟前
Kafka - 消费者程序仅消费一半分区消息的问题
分布式·kafka
爱编程的小生31 分钟前
Easyexcel(2-文件读取)
java·excel
本当迷ya32 分钟前
💖2025年不会Stream流被同事排挤了┭┮﹏┭┮(强烈建议实操)
后端·程序员
带多刺的玫瑰1 小时前
Leecode刷题C语言之统计不是特殊数字的数字数量
java·c语言·算法
计算机毕设指导61 小时前
基于 SpringBoot 的作业管理系统【附源码】
java·vue.js·spring boot·后端·mysql·spring·intellij-idea
Gu Gu Study1 小时前
枚举与lambda表达式,枚举实现单例模式为什么是安全的,lambda表达式与函数式接口的小九九~
java·开发语言
Chris _data2 小时前
二叉树oj题解析
java·数据结构