商品检索——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;//当前查询到的结果,所有涉及到的属性//=====================以上是返给页面的信息==========================@Datapublic static class BrandVo{private Long brandId;private String brandName;private String brandImg;}@Datapublic static class CatalogVo{private Long catalogId;private String catalogName;private String brandImg;}@Datapublic 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
@Overridepublic 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-queryBoolQueryBuilder 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-catalogIdLong catalog3Id = param.getCatalog3Id();if(catalog3Id != null){boolQuery.filter(QueryBuilders.termQuery("catalogId", catalog3Id));}// 1.2.2.bool-filter-brandIdList<Long> brandIds = param.getBrandId();if(brandIds != null && brandIds.size() > 0){boolQuery.filter(QueryBuilders.termsQuery("brandId", brandIds));}// 1.2.3.bool-filter-hasStock = 0/1Integer 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_500lt = split[1];// case2: _500if(StringUtils.isNotBlank(split[0])){gt = split[0];}}else {// case3: 500_gt = split[0];}// 构建rangeQueryBuilderRangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice");if(StringUtils.isNotEmpty(lt)){rangeQueryBuilder.lt(Long.parseLong(lt));}if(StringUtils.isNotEmpty(gt)){rangeQueryBuilder.gt(Long.parseLong(gt));}// 放入boolQueryboolQuery.filter(rangeQueryBuilder);}// 1.2.5.bool-filter-nested-attrs = 2_5寸:6寸&2_16G:8GList<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/descString 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.聚合brandAggTermsAggregationBuilder brandAgg = AggregationBuilders.terms("brandAgg");brandAgg.field("brandId").size(50);// 3.1.1.子聚合brandNameAggbrandAgg.subAggregation(AggregationBuilders.terms("brandNameAgg").field("brandName").size(1));// 3.1.2.子聚合brandImgAggbrandAgg.subAggregation(AggregationBuilders.terms("brandImgAgg").field("brandImg").size(1));// 3.1.3.整合brandAggbuilder.aggregation(brandAgg);// 3.2.聚合catalogAggTermsAggregationBuilder catalogAgg = AggregationBuilders.terms("catalogAgg");// 3.2.1.聚合catalogIdcatalogAgg.field("catalogId").size(20);// 3.2.2.子聚合catalogNameAggcatalogAgg.subAggregation(AggregationBuilders.terms("catalogNameAgg").field("catalogName").size(1));// 3.2.3.整合catalogAggbuilder.aggregation(catalogAgg);// 3.3.聚合attrsAggTermsAggregationBuilder attrIdAgg = AggregationBuilders.terms("attrIdAgg").field("attrs.attrId");// attrIdAgg的子聚合attrNameAggTermsAggregationBuilder attrNameAgg = AggregationBuilders.terms("attrNameAgg").field("attrs.attrName").size(1);attrIdAgg.subAggregation(attrNameAgg);// attrIdAgg的子聚合attrValueAggTermsAggregationBuilder attrValueAgg = AggregationBuilders.terms("attrValueAgg").field("attrs.attrValue").size(50);attrIdAgg.subAggregation(attrValueAgg);// attrsAgg的子聚合attrIdAggNestedAggregationBuilder attrsAgg = AggregationBuilders.nested("attrsAgg", "attrs");attrsAgg.subAggregation(attrIdAgg);// 整合attrsAggbuilder.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.productsSearchHits 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.brandsParsedLongTerms brandAgg = aggregations.get("brandAgg");List<BrandVo> brandVos = brandAgg.getBuckets().stream().map(bucket -> {BrandVo brandVo = new BrandVo();// brandIdbrandVo.setBrandId(bucket.getKeyAsNumber().longValue());Aggregations bucketAggs = bucket.getAggregations();// brandNameParsedStringTerms brandName = bucketAggs.get("brandNameAgg");brandVo.setBrandName(brandName.getBuckets().get(0).getKeyAsString());// brandImgParsedStringTerms brandImg = bucketAggs.get("brandImgAgg");brandVo.setBrandImg(brandImg.getBuckets().get(0).getKeyAsString());return brandVo;}).collect(Collectors.toList());result.setBrands(brandVos);// 3.catalogsParsedLongTerms 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.attrsParsedNested attrsAgg = aggregations.get("attrsAgg");ParsedLongTerms attrIdAgg = attrsAgg.getAggregations().get("attrIdAgg");List<AttrVo> attrVos = attrIdAgg.getBuckets().stream().map(bucket -> {AttrVo attrVo = new AttrVo();// attrIdattrVo.setAttrId(Long.parseLong(bucket.getKeyAsString()));Aggregations bucketAggregations = bucket.getAggregations();// attrNameParsedStringTerms attrNameAgg = bucketAggregations.get("attrNameAgg");attrVo.setAttrName(attrNameAgg.getBuckets().get(0).getKeyAsString());// attrValuesParsedStringTerms 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, totalPageslong total = hits.getTotalHits().value;result.setTotal(total);// 计算totalPagesint 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获取attrNametry {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改为目标urlString 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, "");}