# 商城检索服务 ![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1648456266000/8ecc8f927e474f39aac038df1b2071a4.png) ## 1.检索页面的搭建   商品检索页面我们放在search服务中处理,首页我们需要在mall-search服务中支持Thymeleaf。添加对应的依赖 ```xml org.springframework.boot spring-boot-starter-thymeleaf ``` 然后我们拷贝模板文件到template目录下,然后不要忘记添加Thymeleaf的名称空间 ![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1648456266000/377ec04c591946f8b45465e03d2a31a3.png) 需要把相关的静态资源文件拷贝到Nginx服务中。目录结构是:/mydata/nginx/html/static/search/ ![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1648456266000/c421808d6bc2482c895ecf9126114fa8.png) 我们需要修改index.html页面中的资源的路径 ![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1648456266000/1e711cb680094d8ab71394eda1b9fd7e.png) 然后我们要通过 `msb.search.com` 来访问我们的检索服务,那么就需要设置对应的host文件 ![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1648456266000/3c2372b09ef144ccbb631836cfbb240c.png) 然后我们就需要修改Nginx的配置 ![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1648456266000/542a3ec0152948ed892f76da0297fd9f.png) 这时我需要在修改网关的服务,根据我们的域名访问,那么需要网关路由到我们的检索服务中 ![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1648456266000/a42fbe4df3b64802833fefb85d3c26f5.png) 然后我们就可以重启相关的服务 ,来测试了 ![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1648456266000/95ad0cb04db54184a2013fe7ee363311.png) ## 2.检索服务 ### 2.1 创建对应VO   我们需要检索数据库中的相关的商品信息,那么我们就需要提交相关的检索条件,为了统一的管理提交的数据,我们需要创建一个VO来封装信息。 ```java /** * 封装页面所有可能提交的查询条件 */ @Data public class SearchParam { private String keyword; // 页面传递的查询全文匹配的关键字 private Long catalog3Id;// 需要根据分类查询的编号 /** * sort=salaCount_asc/desc * sort=skuPrice_asc/desc * sort=hotScore_asc/desc */ private String sort; // 排序条件 // 查询的筛选条件 hasStock=0/1; private Integer hasStock ; // 是否只显示有货 // brandId=1&brandId=2 private List brandId; // 按照品牌来查询,可以多选 // skuPrice=200_300 // skuPrice=_300 // skuPrice=200_ private String skuPrice; // 价格区间查询 // 不同的属性 attrs:1_苹果:6.5寸 private List attrs; // 按照属性信息进行筛选 private Integer pageNum; // 页码 } ```   然后就是检索后的数据我们需要封装的VO对象,定义如下: ```java package com.msb.mall.mallsearch.vo; import com.msb.common.dto.es.SkuESModel; import lombok.Data; import java.util.List; /** * 封装检索后的响应信息 */ @Data public class SearchResult { private List products; // 查询到的所有的商品信息 满足条件 // 分页信息 private Integer pageNum; // 当前页 private Long total; // 总的记录数 private Integer totalPages; // 总页数 // 当前查询的所有的商品涉及到的所有的品牌信息 private List brands; // 当前查询的所有的商品涉及到的所有的属性信息 private List attrs; // 当前查询的所有商品涉及到的所有的类别信息 private List catalogs; @Data public static class CatalogVO{ private Long catalogId; private String catalogName; } /** * 品牌的相关信息 */ @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 attrValue; // 属性的值 } } ``` ### 2.2 构建查询DSL语句   我们需要根据基本的检索条件来封装对应的DSL语句 * 查询关键字 模糊匹配 * 过滤(分类,品牌,属性,价格区间,库存...) * 排序 * 分页 * 高亮 ```json GET /product/_search { "query": { "bool": { "must": [ { "match": { "subTitle": "华为" } } ], "filter": [ { "term": { "catalogId": "225" } }, { "terms": { "brandId": [ "13", "16", "14" ] } }, { "range": { "skuPrice": { "gte": 10, "lte": 12000 } } }, { "nested": { "path": "attrs", "query": { "bool": { "must": [ { "term": { "attrs.attrId": { "value": "9" } } }, { "terms": { "attrs.attrValue": [ "12", "08", "11" ] } } ] } } } } ] } },"sort": [ { "skuPrice": { "order": "desc" } } ],"from": 0 ,"size": 20 ,"highlight": { "fields": {"subTitle": {}} ,"pre_tags": "" ,"post_tags": "" } } ``` ### 2.3 构建SearchRequest对象   根据客户端提交的检索的信息,我们需要封装为对应的SearchRequest对象,然后通过ES的API来检索数据。 ``` /** * 构建检索的请求 * 模糊匹配,关键字匹配 * 过滤(类别,品牌,属性,价格区间,库存) * 排序 * 分页 * 高亮 * 聚合分析 * @param param * @return */ private SearchRequest buildSearchRequest(SearchParam param) { SearchRequest searchRequest = new SearchRequest(); searchRequest.indices(ESConstant.PRODUCT_INDEX); SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); // 构建具体的检索的条件 // 1.构建bool查询 BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); // 1.1 关键字的条件 if(!StringUtils.isEmpty(param.getKeyword())){ boolQuery.must(QueryBuilders.matchQuery("subTitle",param.getKeyword())); } // 1.2 类别的检索条件 if(param.getCatalog3Id() != null){ boolQuery.filter(QueryBuilders.termQuery("catalogId",param.getCatalog3Id())); } // 1.3 品牌的检索条件 if(param.getBrandId() != null && param.getBrandId().size() > 0){ boolQuery.filter(QueryBuilders.termsQuery("brandId",param.getBrandId())); } // 1.4 是否有库存 if(param.getHasStock() != null){ boolQuery.filter(QueryBuilders.termQuery("hasStock",param.getHasStock() == 1)); } // 1.5 根据价格区间来检索 if(!StringUtils.isEmpty(param.getSkuPrice())){ String[] msg = param.getSkuPrice().split("_"); RangeQueryBuilder skuPrice = QueryBuilders.rangeQuery("skuPrice"); if(msg.length == 2){ // 说明是 200_300 skuPrice.gte(msg[0]); skuPrice.lte(msg[1]); }else if(msg.length == 1){ // 说明是 _300 200_ if(param.getSkuPrice().endsWith("_")){ // 说明是 200_ skuPrice.gte(msg[0]); } if(param.getSkuPrice().startsWith("_")){ // 说明是 _300 skuPrice.lte(msg[0]); } } boolQuery.filter(skuPrice); } // 1.6 属性的检索条件 attrs=20_8英寸:10英寸&attrs=19_64GB:32GB if(param.getAttrs() != null && param.getAttrs().size() > 0){ for (String attrStr : param.getAttrs()) { BoolQueryBuilder boolNestedQuery = QueryBuilders.boolQuery(); // attrs=19_64GB:32GB 我们首先需要根据 _ 做分割 String[] attrStrArray = attrStr.split("_"); // 属性的编号 String attrId = attrStrArray[0]; // 64GB:32GB 获取属性的值 String[] values = attrStrArray[1].split(":"); // 拼接组合条件 boolNestedQuery.must(QueryBuilders.termQuery("attrs.attrId",attrId)); boolNestedQuery.must(QueryBuilders.termsQuery("attrs.attrValue",values)); NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("attrs", boolNestedQuery, ScoreMode.None); boolQuery.filter(nestedQuery); } } sourceBuilder.query(boolQuery); // 2.排序 if(!StringUtils.isEmpty(param.getSort())){ // sort=salaCount_asc/desc String[] s = param.getSort().split("_"); SortOrder order = s[1].equalsIgnoreCase("asc")?SortOrder.ASC:SortOrder.DESC; sourceBuilder.sort(s[0], order); } // 3.处理分页 // Integer pageNum; // 页码 if(param.getPageNum() != null){ // 需要做分页处理 pageSize = 5 // pageNum:1 from:0 [0,1,2,3,4] // pageNum:2 from:5 [5,6,7,8,9] // from = ( pageNum - 1 ) * pageSize sourceBuilder.from( (param.getPageNum() - 1 ) * ESConstant.PRODUCT_PAGESIZE); sourceBuilder.size(ESConstant.PRODUCT_PAGESIZE); } // 4. 设置高亮 if(!StringUtils.isEmpty(param.getKeyword())){ // 如果有根据关键字查询那么我们才需要高亮设置 HighlightBuilder highlightBuilder = new HighlightBuilder(); highlightBuilder.field("subTitle"); highlightBuilder.preTags(""); highlightBuilder.postTags(""); sourceBuilder.highlighter(highlightBuilder); } // 5.聚合运算 // 5.1 品牌的聚合 TermsAggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg"); brand_agg.field("brandId"); brand_agg.size(50); // 品牌的子聚合 brand_agg.subAggregation(AggregationBuilders.terms("brand_name_agg").field("brandName").size(10)); brand_agg.subAggregation(AggregationBuilders.terms("brand_img_agg").field("brandImg").size(10)); sourceBuilder.aggregation(brand_agg); // 5.2 类别的聚合 TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg"); catalog_agg.field("catalogId"); catalog_agg.size(10); // 类别的子聚合 catalog_agg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(10)); sourceBuilder.aggregation(catalog_agg); // 5.3 属性的聚合 NestedAggregationBuilder attr_agg = AggregationBuilders.nested("attr_agg", "attrs"); // 属性id聚合 TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg"); attr_id_agg.field("attrs.attrId"); attr_id_agg.size(10); // 属性id下的子聚合 属性名称和属性值 attr_id_agg.subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(10)); attr_id_agg.subAggregation(AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(10)); attr_agg.subAggregation(attr_id_agg); sourceBuilder.aggregation(attr_agg); System.out.println(sourceBuilder.toString()); searchRequest.source(sourceBuilder); return searchRequest; } ``` ### 2.4 构建SearchResult对象   当我们通过封装的SearchRequest对象从ES中检索出了相关的信息后,我们需要将返回的SearchResponse对象封装为前端接收的SearchResult对象。 * 所有的满足条件的商品 * 分页相关的信息 * 当前商品涉及的品牌信息 * 当前商品涉及的类别信息 * 当前商品涉及的属性信息 ``` /** * 根据检索的结果解析封装为SearchResult对象 * @param response * @return */ private SearchResult buildSearchResult(SearchResponse response,SearchParam param){ SearchResult result = new SearchResult(); SearchHits hits = response.getHits(); // 1.检索的所有商品信息 SearchHit[] products = hits.getHits(); List esModels = new ArrayList<>(); if(products != null && products.length > 0){ for (SearchHit product : products) { String sourceAsString = product.getSourceAsString(); // 把json格式的字符串通过fastjson转换为SkuESModel对象 SkuESModel model = JSON.parseObject(sourceAsString, SkuESModel.class); if(!StringUtils.isEmpty(param.getKeyword())){ // 我们需要设置高亮 HighlightField subTitle = product.getHighlightFields().get("subTitle"); String subTitleHighlight = subTitle.getFragments()[0].string(); model.setSubTitle(subTitleHighlight); // 设置高亮 } esModels.add(model); } } result.setProducts(esModels); Aggregations aggregations = response.getAggregations(); // 2.当前商品所涉及到的所有的品牌 ParsedLongTerms brand_agg = aggregations.get("brand_agg"); List buckets = brand_agg.getBuckets(); // 存储所有品牌的容器 List brandVOS = new ArrayList<>(); if(buckets!=null && buckets.size() > 0){ for (Terms.Bucket bucket : buckets) { SearchResult.BrandVO brandVO = new SearchResult.BrandVO(); // 获取品牌的key String keyAsString = bucket.getKeyAsString(); brandVO.setBrandId(Long.parseLong(keyAsString)); // 设置品牌的编号 // 然后我们需要获取品牌的名称和图片的地址 ParsedStringTerms brand_img_agg = bucket.getAggregations().get("brand_img_agg"); List bucketsImg = brand_img_agg.getBuckets(); if(bucketsImg != null && bucketsImg.size() > 0){ String img = bucketsImg.get(0).getKeyAsString(); brandVO.setBrandImg(img); } // 获取品牌名称的信息 ParsedStringTerms brand_name_agg = bucket.getAggregations().get("brand_name_agg"); String breadName = brand_name_agg.getBuckets().get(0).getKeyAsString(); brandVO.setBrandName(breadName); brandVOS.add(brandVO); } } result.setBrands(brandVOS); // 3.当前商品涉及到的所有的类别信息 ParsedLongTerms catalog_agg = aggregations.get("catalog_agg"); List bucketsCatalogs = catalog_agg.getBuckets(); // 创建一个保存所有类别的容器 List catalogVOS = new ArrayList<>(); if(bucketsCatalogs != null && bucketsCatalogs.size() > 0){ for (Terms.Bucket bucket : bucketsCatalogs) { SearchResult.CatalogVO catalogVO = new SearchResult.CatalogVO(); String keyAsString = bucket.getKeyAsString(); // 获取类别的编号 catalogVO.setCatalogId(Long.parseLong(keyAsString)); // 获取类别的名称 ParsedStringTerms catalog_name_agg = bucket.getAggregations().get("catalog_name_agg"); String catalogName = catalog_name_agg.getBuckets().get(0).getKeyAsString(); catalogVO.setCatalogName(catalogName); catalogVOS.add(catalogVO); } } result.setCatalogs(catalogVOS); // 4.当前商品涉及到的所有的属性信息 ParsedNested attr_agg = aggregations.get("attr_agg"); ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg"); List bucketsAttr = attr_id_agg.getBuckets(); List attrVos = new ArrayList<>(); if(bucketsAttr != null && bucketsAttr.size() > 0){ for (Terms.Bucket bucket : bucketsAttr) { SearchResult.AttrVo attrVo = new SearchResult.AttrVo(); // 获取属性的编号 String keyAsString = bucket.getKeyAsString(); attrVo.setAttrId(Long.parseLong(keyAsString)); // 又得分别获取 属性的名称 和 属性的值 ParsedStringTerms attr_name_agg = bucket.getAggregations().get("attr_name_agg"); String attrName = attr_name_agg.getBuckets().get(0).getKeyAsString(); // 属性的名称 attrVo.setAttrName(attrName); ParsedStringTerms attr_value_agg = bucket.getAggregations().get("attr_value_agg"); if(attr_value_agg.getBuckets() != null && attr_value_agg.getBuckets().size() > 0 ){ List values = attr_value_agg.getBuckets().stream().map(item -> { String keyAsString1 = item.getKeyAsString(); return keyAsString1; }).collect(Collectors.toList()); attrVo.setAttrValue(values); } attrVos.add(attrVo); } } result.setAttrs(attrVos); // 5. 分页信息 当前页 总的记录数 总页数 long total = hits.getTotalHits().value; result.setTotal(total);// 设置总记录数 6 /5 1+1 result.setPageNum(param.getPageNum()); // 设置当前页 long totalPage = total % ESConstant.PRODUCT_PAGESIZE == 0 ? total / ESConstant.PRODUCT_PAGESIZE : (total / ESConstant.PRODUCT_PAGESIZE + 1); result.setTotalPages((int)totalPage); // 设置总的页数 return result; } ```