谷粒商城:检索服务
目录
检索服务
构建DSL
DSL
代码实现
封装Result
面包屑导航
条件筛选
检索服务
- 浏览器向http://search.gulimall.com/请求检索页list.html,
- search服务构建DSL语句,从elasticSearch中获取结果
- 将检索结果封装好,装入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
- keyword匹配skuTitle标题,命中的词条高亮显示highLight
- 属性attrs为复杂对象数组,需标明为nested类型
- 聚合分析命中结果中 所有品牌,分类,属性。先依据id聚合,再子聚合获取name与value
- 分页与排序
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:8GString[] 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/descif(!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());//品牌imgTerms 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、得到属性的idlong 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("'+nav.paramName+'","'+nav.navId+'",'+'"'+nav.navValue+'")'}"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;}
其余内容,不做赘述