谷粒商城:检索服务

发布于:2025-08-09 ⋅ 阅读:(17) ⋅ 点赞:(0)

目录

检索服务

构建DSL

DSL

代码实现

封装Result

面包屑导航

条件筛选


检索服务

  1. 浏览器向http://search.gulimall.com/请求检索页list.html,
  2. search服务构建DSL语句,从elasticSearch中获取结果
  3. 将检索结果封装好,装入model,页面由themleaf渲染。
    @RequestMapping("/list.html")
    public String listPage(SearchParam searchParam, Model model){
        SearchResult searchResult = mallSearchSearchService.search(searchParam);
        model.addAttribute("result",searchResult);
        return "list";
    }

构建DSL

DSL

依据请求参数,构建DSL检索语句

keyword=Huawei&brandId=1&catalog3Id=225&attrs=2_麒麟9000&attrs=5_1080p

  1. keyword匹配skuTitle标题,命中的词条高亮显示highLight
  2. 属性attrs为复杂对象数组,需标明为nested类型
  3. 聚合分析命中结果中 所有品牌,分类,属性。先依据id聚合,再子聚合获取name与value
  4. 分页与排序
GET /gulimall_product/_search
{
  "from": 0,
  "size": 4,
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "skuTitle":"Huawei"
          }
        }
      ],
      "filter": [
        {
          "term": {
            "catalogId": 225
          }
        },
        {
          "term": {
            "brandId": 1
          }
        },
        {
          "nested": {
            "path": "attrs",
            "query": {
              "bool": {
                "must": [
                  {
                    "term": {
                      "attrs.attrId": {
                        "value": "5"
                      }
                    }
                  },
                  {
                    "terms": {
                      "attrs.attrValue": [
                        "1080p"
                      ]
                    }
                  }
                ]
              }
            }
          }
        },
        {
          "nested": {
            "path": "attrs",
            "query": {
              "bool": {
                "must": [
                  {
                    "term": {
                      "attrs.attrId": {
                        "value": "2"
                      }
                    }
                  },
                  {
                    "terms": {
                      "attrs.attrValue": [
                        "麒麟9000"
                      ]
                    }
                  }
                ]
              }
            }
          }
        }
      ]
    }
  },
  "aggregations": {
    "brand_agg": {
      "terms": {
        "field": "brandId"
      },
      "aggregations": {
        "brand_name_agg": {
          "terms": {
            "field": "brandName"
          }
        },
        "brand_img_agg": {
          "terms": {
            "field": "brandImg"
          }
        }
      }
    },
    "catalog_agg": {
      "terms": {
        "field": "catalogId"
      },
      "aggregations": {
        "catalog_name_agg": {
          "terms": {
            "field": "catalogName"
          }
        }
      }
    },
    "attr_agg": {
      "nested": {
        "path": "attrs"
      },
      "aggregations": {
        "attr_id_agg": {
          "terms": {
            "field": "attrs.attrId"
          },
          "aggregations": {
            "attr_name_agg": {
              "terms": {
                "field": "attrs.attrName"
              }
            },
            "attr_value_agg": {
              "terms": {
                "field": "attrs.attrValue"
              }
            }
          }
        }
      }
    }
  },
  "highlight": {
    "pre_tags": [
      "<b style='color:red'>"
    ],
    "post_tags": [
      "</b>"
    ],
    "fields": {
      "skuTitle": {}
    }
  }
}

代码实现

            /**
             * 构建DSL语句
             * 模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存),排序,分页,高亮,聚合分析
             * @param searchParam
             * @return
             */
            private SearchRequest buildSearchRequest(SearchParam searchParam) {
                SearchSourceBuilder searchSourceBuilder =new SearchSourceBuilder();
                BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
                /**
                 * 模糊匹配
                 */
                if(!StringUtils.isEmpty(searchParam.getKeyword())){
                    boolQueryBuilder.must(QueryBuilders.matchQuery("skuTitle",searchParam.getKeyword()));
                }
                if(searchParam.getCatalog3Id()!=null){
                    boolQueryBuilder.filter(QueryBuilders.termQuery("catalogId",searchParam.getCatalog3Id()));
                }
                if(searchParam.getBrandId()!=null&& !searchParam.getBrandId().isEmpty()){
                    boolQueryBuilder.filter(QueryBuilders.termsQuery("brandId",searchParam.getBrandId()));
                }
                if(searchParam.getHasStock()!=null){
                    boolQueryBuilder.filter(QueryBuilders.termQuery("hasStock",searchParam.getHasStock()==1));
                }
                /**
                 * 价格区间
                 */
                if(!StringUtils.isEmpty(searchParam.getSkuPrice())){
                    RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice");
                    String[] price = searchParam.getSkuPrice().trim().split("_");
                    if(price.length==2){
                        //这里 skuPrice=_1000 会传 空字符串
                        rangeQueryBuilder.gte(StringUtils.isEmpty(price[0])? null:price[0]).lte(price[1]);
                    }else if(price.length == 1){
                        if(searchParam.getSkuPrice().startsWith("_")){
                            rangeQueryBuilder.lte(price[0]);
                        }
                        if(searchParam.getSkuPrice().endsWith("_")){
                            rangeQueryBuilder.gte(price[0]);
                        }
                    }
                    boolQueryBuilder.filter(rangeQueryBuilder);
                }
                /**
                 * 属性
                 * 使用nested嵌套查询
                 */
                if(searchParam.getAttrs() != null && !searchParam.getAttrs().isEmpty()){
                    searchParam.getAttrs().forEach(item -> {
                        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
                        //attrs=1_5寸:8寸&2_16G:8G
                        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);
                    });
                }
                searchSourceBuilder.query(boolQueryBuilder);
                /**
                 * 排序,分页,高亮
                 */
                //排序 形式为sort=hotScore_asc/desc
                if(!StringUtils.isEmpty(searchParam.getSort())){
                    String sort = searchParam.getSort();
                    String[] sortFields = sort.split("_");
        
                    SortOrder sortOrder="asc".equalsIgnoreCase(sortFields[1])? SortOrder.ASC:SortOrder.DESC;
        
                    searchSourceBuilder.sort(sortFields[0],sortOrder);
                }
        
                //分页
                searchSourceBuilder.from((searchParam.getPageNum()-1) *EsConstant.PRODUCT_PAGESIZE);
                searchSourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);
        
                //高亮
                if(!StringUtils.isEmpty(searchParam.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);
        
                System.out.println("DSL:");
                System.out.println(searchSourceBuilder.toString());
                return new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX},searchSourceBuilder);
            }

        封装Result

        将elasticsearch返回的结果封装成Result

            /**
             * 构建响应数据
             * 1.商品.标题高亮
             * 2.品牌,分类,页码
             * @param searchResponse
             * @return
             */
            private SearchResult buildSearchResult(SearchResponse searchResponse,SearchParam searchParam) {
                SearchResult result = new SearchResult();
                /**
                 * 1.将source转为sku
                 * 2.替换标题,高亮
                 * 3.将sku商品集合装入result
                 */
                SearchHits hits = searchResponse.getHits();
                List<SkuEsModel> products =  new ArrayList<>();
                for (SearchHit hit:hits.getHits()) {
                    SkuEsModel skuEsModel = JSON.parseObject(hit.getSourceAsString(), SkuEsModel.class);
                    if(!StringUtils.isEmpty(searchParam.getKeyword())){
                        String skuTitle = hit.getHighlightFields().get("skuTitle").getFragments()[0].string();
                        skuEsModel.setSkuTitle(skuTitle);
                    }
                    products.add(skuEsModel);
                }
                result.setProduct(products);
                /**
                 * 页码
                 */
                result.setTotal(hits.getTotalHits().value);
                result.setTotalPages((int)hits.getTotalHits().value%EsConstant.PRODUCT_PAGESIZE == 0?
                        (int)hits.getTotalHits().value/EsConstant.PRODUCT_PAGESIZE:(int)hits.getTotalHits().value/EsConstant.PRODUCT_PAGESIZE+1);
                result.setPageNum(searchParam.getPageNum());
        
                List<Integer> pageNavs = new ArrayList<>();
                for (int i = 1; i <= result.getTotalPages(); i++) {
                    pageNavs.add(i);
                }
                result.setPageNavs(pageNavs);
        
                Aggregations aggregations = searchResponse.getAggregations();
                /**
                 * 品牌
                 */
                HashMap<Long, SearchResult.BrandVo> brandVoHashMap = new HashMap<>();
                Terms brand_agg =  aggregations.get("brand_agg");
                List<SearchResult.BrandVo> brands =new ArrayList<>();
                for(Terms.Bucket bucket:brand_agg.getBuckets()){
                    SearchResult.BrandVo brand = new SearchResult.BrandVo();
                    brand.setBrandId(bucket.getKeyAsNumber().longValue());
                    //品牌名
                    Terms brand_name_agg = bucket.getAggregations().get("brand_name_agg");
                    brand.setBrandName(brand_name_agg.getBuckets().get(0).getKeyAsString());
                    //品牌img
                    Terms brand_img_agg = bucket.getAggregations().get("brand_img_agg");
                    brand.setBrandImg(brand_img_agg.getBuckets().get(0).getKeyAsString());
                    brands.add(brand);
                    brandVoHashMap.put(brand.getBrandId(),brand);
                }
                result.setBrands(brands);
                /**
                 * 分类
                 */
                Terms catalog_agg =  aggregations.get("catalog_agg");
                List<SearchResult.CatalogVo> catalogs =new ArrayList<>();
                for(Terms.Bucket bucket:catalog_agg.getBuckets()){
                    SearchResult.CatalogVo catalog = new SearchResult.CatalogVo();
                    catalog.setCatalogId(bucket.getKeyAsNumber().longValue());
                    //分类名
                    Terms catalog_name_agg = bucket.getAggregations().get("catalog_name_agg");
                    catalog.setCatalogName(catalog_name_agg.getBuckets().get(0).getKeyAsString());
                    catalogs.add(catalog);
                }
                result.setCatalogs(catalogs);
                /**
                 * 当前商品涉及到的所有属性信息
                 */
                List<SearchResult.AttrVo> attrVos = new ArrayList<>();
                HashMap<Long,SearchResult.AttrVo> attrVoHashMap = new HashMap<>();
                //获取属性信息的聚合
                Nested attr_agg = aggregations.get("attr_agg");
                Terms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg");
                for (Terms.Bucket bucket : attr_id_agg.getBuckets()) {
                    SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
                    //1、得到属性的id
                    long attrId = bucket.getKeyAsNumber().longValue();
                    attrVo.setAttrId(attrId);
                    //2、得到属性的名字
                    Terms attr_name_agg = bucket.getAggregations().get("attr_name_agg");
                    String attrName = attr_name_agg.getBuckets().get(0).getKeyAsString();
                    attrVo.setAttrName(attrName);
                    //3、得到属性的所有值
                    Terms attr_value_agg = bucket.getAggregations().get("attr_value_agg");
                    List<String> attrValues = attr_value_agg.getBuckets().stream().map(item -> item.getKeyAsString()).collect(Collectors.toList());
                    attrVo.setAttrValue(attrValues);
                    attrVos.add(attrVo);
                    /*
                    这里用hashMap存一下属性,就不用远程调用了,太麻烦了
                     */
                    attrVoHashMap.put(attrId,attrVo);
                }
                result.setAttrs(attrVos);
        
                //6、构建面包屑导航
                if (searchParam.getAttrs() != null && !searchParam.getAttrs().isEmpty()) {
                    List<SearchResult.NavVo> collect = searchParam.getAttrs().stream().map(attr -> {
                        //1、分析每一个attrs传过来的参数值
                        SearchResult.NavVo navVo = new SearchResult.NavVo();
                        String[] s = attr.split("_");
                        navVo.setNavValue(s[1]);
                        navVo.setNavId(Long.parseLong(s[0]));
                        navVo.setParamName("attrs");
                        String navName;
                        if (!StringUtils.isEmpty(navName=attrVoHashMap.get(Long.parseLong(s[0])).getAttrName())) {
                            navVo.setNavName(navName);
                        } else {
                            navVo.setNavName(s[0]);
                        }
                        return navVo;
                    }).collect(Collectors.toList());
                    result.setNavs(collect);
                }
        
                if(searchParam.getBrandId()!=null && !searchParam.getBrandId().isEmpty()){
                    List<SearchResult.NavVo> navs = result.getNavs();
                    for (Long brandId:searchParam.getBrandId()) {
                        SearchResult.NavVo navVo = new SearchResult.NavVo();
                        navVo.setParamName("brandId");
                        navVo.setNavId(brandId);
                        navVo.setNavName("品牌");
                        String navValue;
                        if (!StringUtils.isEmpty(navValue=brandVoHashMap.get(brandId).getBrandName())) {
                            navVo.setNavValue(navValue);
                        } else {
                            navVo.setNavValue(String.valueOf(brandId));
                        }
                        navs.add(0,navVo);
                    }
                }
        
                System.out.println("searchResult:");
                System.out.println(result);
                return result;
            }

        面包屑导航

        <!-- 遍历面包屑功能 -->
        <a href="/static/search/#"
           th:href="${'javascript:removeNav(&quot;'+nav.paramName+'&quot;,&quot;'+nav.navId+'&quot;,'+'&quot;'+nav.navValue+'&quot;)'}"
           th:each="nav:${result.navs}">
            <span th:text="${nav.navName}"></span>:<span th:text="${nav.navValue}"></span> x</a>

        result中封装了navs,前端依据navs来渲染,可以直观地看到筛选条件。

        (不封装navs,前端通过解析url中的param,应该也能实现)

        为了封装navs,在聚合分析时,顺便将聚合结果存储在hashMap中,以便取值,避免调用远程服务

        移除一个筛选条件时,将url中对应的param移除即可

        url中带有中文时,浏览器url会解码中文。因此 进行正则匹配时,也要解码paramVal

            function removeNav(paramName,navId,navValue){
                if(paramName==="attrs"){
                    location.href = removeParamVal(paramName,navId+"_"+navValue);
                }else{
                    location.href = removeParamVal(paramName,navId);
                }
            }
        
            function removeParamVal(paramName,paramVal) {
                var oUrl = location.href.toString();
                //处理url编码中文
                const encodedVal = encodeURIComponent(paramVal);
                var re = eval('/&?'+'('+ paramName +'='+ encodedVal+ ')/gi');
                var nUrl = oUrl.replace(re,"");
                console.log(nUrl);
                return nUrl?nUrl:oUrl;
            }

        条件筛选

        将选中的条件 进行拼串或替换

            function searchByKeyword() {
                //搜索时,清除拼串
                location.href = "http://search.gulimall.com/list.html?" + "keyword=" + $("#keyword_input").val();
            }
        
            function searchProducts(name, value,forceAdd) {
                //原來的页面
                location.href = replaceParamVal(location.href,name,value,forceAdd)
            }
        
            function replaceParamVal(url, paramName, replaceVal,forceAdd) {
                var oUrl = url.toString();
                var nUrl;
                if (oUrl.indexOf(paramName) != -1) {
                    if( forceAdd ) {
                        if (oUrl.indexOf("?") != -1) {
                            nUrl = oUrl + "&" + paramName + "=" + replaceVal;
                        } else {
                            nUrl = oUrl + "?" + paramName + "=" + replaceVal;
                        }
                    } else {
                        var re = eval('/(' + paramName + '=)([^&]*)/gi');
                        nUrl = oUrl.replace(re, paramName + '=' + replaceVal);
                    }
                } else {
                    if (oUrl.indexOf("?") != -1) {
                        nUrl = oUrl + "&" + paramName + "=" + replaceVal;
                    } else {
                        nUrl = oUrl + "?" + paramName + "=" + replaceVal;
                    }
                }
                return nUrl;
            }

        其余内容,不做赘述


        网站公告

        今日签到

        点亮在社区的每一天
        去签到