根据之前设计的页面,效果如下:
搜索功能相对比较繁杂,因为有各种各样的搜索项,返回的结果也乱;所有,我们需要专门为查询条件、查询结果封装成类;
一、检索查询参数的模型抽取SearchParam.java
条件封装类:
@Data public class SearchParam { private String keyword; //页面传递过来的全文匹配关键字 private Long catalog3Id; //三级分类id //排序条件 //saleCount(销量): saleCount_asc/saleCount_desc //hotScore(热度分): hotScore_asc/hotScore_desc //skuPrice(价格): skuPrice_asc/skuPrice_desc private String sort; //过滤条件 //hasStock(仅显示有货): //skuPrice区间 //brandId(品牌Id) //catalog3Id //attrs private Integer hasStock; //hasStock=0/1(0表示无库存,1表示有库存) private String skuPrice; //1_500/_500/500_ private List<Long> brandId; //允许多选 private List<String> attrs; //允许多选,属性attrs=name_其它:安卓 ——属性名name,值为其它、安卓等多个值的 //页码 private Integer pageNum = 1; //只有页码,不需要size,使用默认 }
二、检索返回结果模型抽取SearchResult.java
@Data public class SearchResult { //查询到的所有商品信息 private List<SkuEsModel> products; //分页信息 private Integer pageNum; //当前页码 private Long total; //总记录数 private Integer totalPages; //总页数 //查询到的所有品牌信息 private List<BrandVo> brands; //当前查询到的结果,所有涉及到的品牌——用来缩小查询范围 //查询所涉及到的所有属性 private List<AttrVo> attrs; //当前查询到的结果,所有涉及到的属性 //查询所涉及到的所有分类信息 private List<CatelogVo> catelogs; //当前查询到的结果,所有涉及到的分类 }
其中:
结果涉及的所有品牌:BrandVo.java:
@Data public class BrandVo { private Long brandId; private String brandName; private String brandImg; }
结果涉及的所有分类:CatalogVo.java:
@Data public class CatelogVo { private Long catelogId; private String catelogName; }
结果涉及的所有属性:AttrVo.java:
@Data public class AttrVo { private Long attrId; private String attrName; private List<String> attrValue; }
三、接口调用链路
SearchController.java:
@Controller public class SearchController { @Autowired MallSearchService mallSearchService; /** * 自动将页面提交过来的所有查询参数封装成指定的SearchParam对象 * @param param * @return */ @GetMapping("list.html") public String listPage(SearchParam param, Model model){ SearchResult result = mallSearchService.search(param); model.addAttribute("result", result); return "list"; } }
MallSearchServiceImpl.java:
@Service public class MallSearchServiceImpl implements MallSearchService { @Override public SearchResult search(SearchParam param) { return null; } }
四、检索功能DSL语句测试
# must:模糊查询,要打分,性能略低,适合text全文检索字段 # filter:过滤,不需要打分,性能高 # 聚合分析(难) GET product/_search { "query": { "bool": { "must": [ { "match": { "skuTitle": "华为" } } ], "filter": [ { "term": { "catalogId": "225" } }, { "terms": { "brandId": [ "11", "14" ] } }, { "term": { "hasStock": false } }, { "nested": { "path": "attrs", "query": { "bool": { "must": [ { "term": { "attrs.attrId": { "value": "11" } } }, { "terms": { "attrs.attrValue": [ "海思", "高通" ] } } ] } } } }, { "range": { "skuPrice": { "gte": 5500, "lte": 6500 } } } ] } }, "sort": [ { "skuPrice": { "order": "desc" } } ], "from": 0, "size": 100, "highlight": { "fields": { "skuTitle": {} }, "pre_tags": "<b style='color:red'>", "post_tags": "</b>" }, "aggs": { "brand_agg": { "terms": { "field": "brandId", "size": 10 }, "aggs": { "brand_name_agg": { "terms": { "field": "brandName", "size": 10 } }, "brand_img_agg": { "terms": { "field": "brandImg", "size": 10 } } } }, "catalog_agg": { "terms": { "field": "catalogId", "size": 10 }, "aggs": { "catalog_name_agg": { "terms": { "field": "catalogName", "size": 10 } } } }, "attr_agg": { "nested": { "path": "attrs" }, "aggs": { "attr_id_agg": { "terms": { "field": "attrs.attrId", "size": 10 }, "aggs": { "attr_name_agg": { "terms": { "field": "attrs.attrName", "size": 10 } }, "attr_value_agg": { "terms": { "field": "attrs.attrValue", "size": 10 } } } } } } } }
执行上面的DSL语句,查看结果,是否可用:
结果显示,命中6个目标,并对这六个目标进行了多种聚合分析!
五、Java编写MallSearchServiceImpl.java中的search()方法
1、首先明确方法结构体search():
@Service @Slf4j public class MallSearchServiceImpl implements MallSearchService { @Autowired RestHighLevelClient client; //去es中进行检索 @Override public SearchResult search(SearchParam param) { //动态构建出查询需要的dsl(记录在http://www.jiguiquan.com) SearchResult result=null; //1. 创建检索请求 SearchRequest searchRequest =buildSearchRequest(param); try { //2.执行检索请求 SearchResponse response = client.search(searchRequest, ZidanElasticSearchConfig.COMMON_OPTIONS); //3.分析响应数据封装成为我们想要的格式 result=buildSearchResult(param,response); } catch (IOException e) { } return result; } private SearchRequest buildSearchRequest(SearchParam param) { return null; } private SearchResult buildSearchResult(SearchParam param, SearchResponse response) { return null; } }
2、完成私有方法buildSearchRequest(SearchParam param):
private SearchRequest buildSearchRequest(SearchParam param) { SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); /** * TODO 模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存) */ //1. 构建bool-query BoolQueryBuilder boolQueryBuilder=QueryBuilders.boolQuery(); //1.1 bool-must: 支持模糊匹配的text类型 if(StringUtils.isNotBlank(param.getKeyword())){ boolQueryBuilder.must(QueryBuilders.matchQuery("skuTitle",param.getKeyword())); } //1.2 bool-fiter: 其它不需要模糊匹配的keyword类型 //1.2.1 catelogId if(null != param.getCatalog3Id()){ boolQueryBuilder.filter(QueryBuilders.termQuery("catelogId",param.getCatalog3Id())); } //1.2.2 brandId if(null != param.getBrandId() && param.getBrandId().size() >0){ boolQueryBuilder.filter(QueryBuilders.termsQuery("brandId",param.getBrandId())); } //1.2.3 attrs ———— 按照所有指定的属性进行查询 ———— attrs=1_5寸:8寸&2_16G:8G if(!CollectionUtils.isEmpty(param.getAttrs())){ param.getAttrs().forEach(item -> { //一组: 1_5寸:8寸 BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); 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)); //每一种组合都得生成一个嵌入式nested的Query条件 NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery("attrs",boolQuery, ScoreMode.None); boolQueryBuilder.filter(nestedQueryBuilder); }); } //1.2.4 hasStock if(null != param.getHasStock()){ boolQueryBuilder.filter(QueryBuilders.termQuery("hasStock",param.getHasStock() == 1)); } //1.2.5 skuPrice ——按照价格区间1_500/_500/500_ if(!StringUtils.isEmpty(param.getSkuPrice())){ //skuPrice形式为:1_500或_500或500_ RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice"); String[] price = param.getSkuPrice().split("_"); if(price.length==2){ rangeQueryBuilder.gte(price[0]).lte(price[1]); }else if(price.length == 1){ if(param.getSkuPrice().startsWith("_")){ rangeQueryBuilder.lte(price[1]); } if(param.getSkuPrice().endsWith("_")){ rangeQueryBuilder.gte(price[0]); } } boolQueryBuilder.filter(rangeQueryBuilder); } //封装所有的bool查询条件 searchSourceBuilder.query(boolQueryBuilder); /** * TODO 排序,分页,高亮 */ //排序:形式为sort=hotScore_asc/desc if(!StringUtils.isEmpty(param.getSort())){ String sort = param.getSort(); String[] sortFileds = sort.split("_"); SortOrder sortOrder="asc".equalsIgnoreCase(sortFileds[1])?SortOrder.ASC:SortOrder.DESC; searchSourceBuilder.sort(sortFileds[0],sortOrder); } //分页 searchSourceBuilder.from((param.getPageNum()-1)* EsConstant.PRODUCT_PAGESIZE); searchSourceBuilder.size(EsConstant.PRODUCT_PAGESIZE); //高亮 if(!StringUtils.isEmpty(param.getKeyword())){ HighlightBuilder highlightBuilder = new HighlightBuilder(); highlightBuilder.field("skuTitle"); highlightBuilder.preTags("<b style='color:red'>"); highlightBuilder.postTags("</b>"); searchSourceBuilder.highlighter(highlightBuilder); } /** * TODO 聚合分析 */ //TODO 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); //TODO 2. 分类聚合 TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg"); catalog_agg.field("catalogId").size(20); //2.1 分类聚合的子聚合——分类名称聚合 catalog_agg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1)); searchSourceBuilder.aggregation(catalog_agg); //TODO 3. 按照属性信息进行聚合——嵌入式聚合 NestedAggregationBuilder attr_agg = AggregationBuilders.nested("attr_agg", "attrs"); //3.1 按照属性ID进行聚合————聚合出当前所有的attrId TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId"); attr_agg.subAggregation(attr_id_agg); //3.1.1 在每个attrId下,按照属性名进行聚合 attr_id_agg.subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1)); //3.1.1 在每个attrId下,按照属性值进行聚合 attr_id_agg.subAggregation(AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50)); searchSourceBuilder.aggregation(attr_agg); log.debug("构建的DSL语句 {}",searchSourceBuilder.toString()); SearchRequest searchRequest = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX},searchSourceBuilder); return searchRequest; }
3、完成私有方法buildSearchResult(SearchParam param, SearchResponse response):
/** * 构建结果数据 * @param response * @return */ private SearchResult buildSearchResult(SearchParam param,SearchResponse response) { SearchResult result = new SearchResult(); SearchHits hits = response.getHits(); SearchHit[] subHits = hits.getHits(); List<SkuEsModel> skuEsModels=null; if(subHits != null && subHits.length > 0){ skuEsModels = Arrays.asList(subHits).stream().map(subHit -> { String sourceAsString = subHit.getSourceAsString(); SkuEsModel skuEsModel = JSON.parseObject(sourceAsString, SkuEsModel.class); if (!StringUtils.isEmpty(param.getKeyword())) { //带模糊搜索keyword,我们就进行高亮显示 HighlightField skuTitle = subHit.getHighlightFields().get("skuTitle"); String skuTitleHighLight = skuTitle.getFragments()[0].string(); skuEsModel.setSkuTitle(skuTitleHighLight); } return skuEsModel; }).collect(Collectors.toList()); } //1.返回所查询到的所有商品 result.setProducts(skuEsModels); //2.当前所有商品所涉及到的所有属性信息 ParsedNested attr_agg = response.getAggregations().get("attr_agg"); ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg"); List<AttrVo> attrVos = attr_id_agg.getBuckets().stream().map(item -> { AttrVo attrVo = new AttrVo(); //1.获取属性的id long attrId = item.getKeyAsNumber().longValue(); //2.获取属性名 String attrName = ((ParsedStringTerms) item.getAggregations().get("attr_name_agg")).getBuckets().get(0).getKeyAsString(); //3.获取属性的所有值 List<String> attrValues = ((ParsedStringTerms) item.getAggregations().get("attr_value_agg")).getBuckets() .stream().map(bucket ->bucket.getKeyAsString()).collect(Collectors.toList()); attrVo.setAttrId(attrId); attrVo.setAttrName(attrName); attrVo.setAttrValue(attrValues); return attrVo; }).collect(Collectors.toList()); result.setAttrs(attrVos); //3.当前所有商品所涉及到的所有品牌信息 ParsedLongTerms brand_agg = response.getAggregations().get("brand_agg"); List<BrandVo> brandVos = brand_agg.getBuckets().stream().map(item -> { BrandVo brandVo = new BrandVo(); //1.获取id long brandId = item.getKeyAsNumber().longValue(); //2.获取品牌名 String brandName = ((ParsedStringTerms) item.getAggregations().get("brand_Name_agg")).getBuckets().get(0).getKeyAsString(); //3.获取品牌图片 String brandImag = ((ParsedStringTerms) item.getAggregations().get("brand_img_agg")).getBuckets().get(0).getKeyAsString(); brandVo.setBrandId(brandId); brandVo.setBrandName(brandName); brandVo.setBrandImg(brandImag); return brandVo; }).collect(Collectors.toList()); result.setBrands(brandVos); //4.当前所有商品所涉及到的所有分类信息 ParsedLongTerms catalog_agg = response.getAggregations().get("catalog_agg"); List<CatalogVo> catalogVos = catalog_agg.getBuckets().stream().map(item -> { CatalogVo catalogVo = new CatalogVo(); //获取分类ID String catelogId = item.getKeyAsString(); catalogVo.setCatalogId(Long.parseLong(catelogId)); //获取分类名 ParsedStringTerms catalog_name_agg = item.getAggregations().get("catalog_name_agg"); String catalogName = catalog_name_agg.getBuckets().get(0).getKeyAsString(); catalogVo.setCatalogName(catalogName); return catalogVo; }).collect(Collectors.toList()); result.setCatelogs(catalogVos); //=========以上从聚合信息中获取=========== //5.分页信息-页码 result.setPageNum(param.getPageNum()); //5.分页信息-总记录数 long total = hits.getTotalHits().value; result.setTotal(total); //5.分页信息-总页码 boolean flag=total%EsConstant.PRODUCT_PAGESIZE == 0; int totalPage=flag?((int)total/EsConstant.PRODUCT_PAGESIZE):((int)total/EsConstant.PRODUCT_PAGESIZE+1); result.setTotalPages(totalPage); List<Integer> page = new ArrayList<>(); for (int i=1;i<=totalPage;i++){ page.add(i); } result.setPageNavs(page); return result; }
六、result放入Model后,前端thymeleaf渲染list.html
1、初步效果:
2、动态拼接筛选条件
2.1、动态拼接参数,我们抽象成一个公共方法:
//统一拼接检索商品参数的方法 function searchProducts(name, value){ //原来的url,如果包含"?"则拼接"&",如果不包含"?"则拼接"?" var href = location.href; if (href.indexOf("?") != -1){ location.href = location.href + "&" + name + "=" + value; }else { location.href = location.href + "?" + name + "=" + value; } }
2.2、而在使用过程中:
拼接品牌brandId: <a href="#" th:href="${'javascript:searchProducts("brandId",' + brand.brandId+ ')'}"> </a> 拼接分类catalog3Id: <a href="#" th:href="${'javascript:searchProducts("catalog3Id",' + catalog.catalogId+ ')'}" th:text="${catalog.catalogName}"> </a> 拼接属性attrs <a href="#" th:text="${val}" th:href="${'javascript:searchProducts("attrs","' + attr.attrId + '_' + val + '")'}"> </a>
最终我们得到的url如下:
http://search.zidanmall.com/list.html?brandId=14&catalog3Id=225&attrs=12_MT6765
七、页面搜索框搜索实现
1、html部分:
<div class="header_form"> <input type="text" id="keywordInput" placeholder="手机" th:value="${param.keyword}"/> <a href="javascript:searchByKeyword()">搜索</a> </div>
2、searchByKeyword()方法:
//页面搜索框,按照keyword进行搜索 function searchByKeyword() { var keywordInput = $("#keywordInput").val(); searchProducts("keyword", keywordInput); }
八、实现分页跳转功能
1、html部分:
<div class="filter_page"> <div class="page_wrap"> <span class="page_span1"> <a class="page_a" th:attr="pn=${result.pageNum - 1}" th:if="${result.pageNum > 1}"> < 上一页 </a> <a class="page_a" th:each="nav:${result.pageNavs}" th:attr="pn=${nav}, style=${nav == result.pageNum?'border: 0;color:#ee2222;background: #fff':''}"> [[${nav}]] </a> <a class="page_a" th:attr="pn=${result.pageNum + 1}" th:if="${result.pageNum < result.totalPages}"> 下一页 > </a> </span> <span class="page_span2"> <em>共<b>[[${result.totalPages}]]</b>页 到第</em> <input type="number" value="1"> <em>页</em> <a class="page_submit">确定</a> </span> </div> </div>
2、javascript事件触发方法:
//翻页事件 $(".page_a").click(function () { var pn = $(this).attr("pn"); var href = location.href; console.log("pn",pn); if (href.indexOf("pageNum") != -1){ //替换pageNum的值 location.href = replaceParamVal(location.href, "pageNum", pn); }else { location.href = location.href + "&pageNum=" + pn; } return false; }) function replaceParamVal(url, paramName, replaceVal) { var oUrl = url.toString(); console.log(oUrl); var re = eval('/(' + paramName + '=)([^&]*)/gi'); console.log("re",re); var nUrl = oUrl.replace(re, paramName + '=' + replaceVal); return nUrl; }
九、完成综合排序和回显功能
1、html部分:
<div class="filter_top_left" th:with="p = ${param.sort}"> <!--使用p临时变量获取参数sort的值,供下文使用--> <a th:class="${(!#strings.isEmpty(p)&&(#strings.equals(p, 'hotScore_desc')))?'sort_a desc':'sort_a'}" th:attr="style=${(#strings.isEmpty(p) || #strings.startsWith(p,'hotScore'))?'color: #FFF; border-color: #e4393c; background: #e4393c':'color: #333; border-color: #CCC; background: #FFF'}" sort="hotScore" href="#"> 综合排序[[${(!#strings.isEmpty(p) &&(#strings.equals(p, 'hotScore_desc')))?'↓':'↑'}]]</a> <a th:class="${(!#strings.isEmpty(p)&&(#strings.equals(p,'saleCount_desc')))?'sort_a desc':'sort_a'}" th:attr="style=${(!#strings.isEmpty(p) && #strings.startsWith(p,'saleCount'))?'color: #FFF; border-color: #e4393c; background: #e4393c':'color: #333; border-color: #CCC; background: #FFF'}" sort="saleCount" href="#"> 销量[[${(!#strings.isEmpty(p) &&(#strings.equals(p, 'saleCount_desc')))?'↓':'↑'}]]</a> <a th:class="${(!#strings.isEmpty(p)&&(#strings.equals(p,'skuPrice_desc')))?'sort_a desc':'sort_a'}" th:attr="style=${(!#strings.isEmpty(p) && #strings.startsWith(p,'skuPrice'))?'color: #FFF; border-color: #e4393c; background: #e4393c':'color: #333; border-color: #CCC; background: #FFF'}" sort="skuPrice" href="#"> 价格[[${(!#strings.isEmpty(p) &&(#strings.equals(p, 'skuPrice_desc')))?'↓':'↑'}]]</a> <a class="sort_a" href="#">评论分</a> <a class="sort_a" href="#">上架时间</a> </div>
2、javascript事件方法:
//点击排序改变样式 $(".sort_a").click(function () { //1、改变元素样式 changeStyle(this); //2、拼接参数sort=saleCount_asc/saleCount_desc var sort = $(this).attr("sort"); sort = $(this).hasClass("desc") ? sort + "_desc" : sort + "_asc" location.href = replaceAndAddParamVal(location.href, "sort", sort); return false; }); //改变排序选项样式 function changeStyle(ele) { //1、清除其它兄弟样式 $(".sort_a").css({"color": "#333", "border-color": "#CCC", "background": "#FFF"}) $(".sort_a").each(function () { var text = $(this).text().replace("↓", "").replace("↑", ""); $(this).text(text); }) //2、给自己增加样式 $(ele).css({"color": "#FFF", "border-color": "#e4393c", "background": "#e4393c"}); //3、切换升降序图标 $(ele).toggleClass("desc"); if ($(ele).hasClass("desc")) { var text = $(ele).text().replace("↓", "").replace("↑", ""); $(ele).text(text + "↓"); } else { var text = $(ele).text().replace("↓", "").replace("↑", ""); $(ele).text(text + "↑"); } } //替换or新增参数 function replaceAndAddParamVal(url, paramName, replaceVal) { var oUrl = url.toString(); if (oUrl.indexOf(paramName) !== -1) { var re = eval('/(' + paramName + '=)([^&]*)/gi'); return oUrl.replace(re, paramName + '=' + replaceVal); } else { if (oUrl.indexOf("?") !== -1) { return oUrl + "&" + paramName + "=" + replaceVal; } else { return oUrl + "?" + paramName + "=" + replaceVal; } } }
3、页面效果如下(keyword条件已做了高亮显示):
十、实现价格区间搜索
1、html部分(其中priceRange是定义的临时变量,和上文p一样):
<div class="filter_top_left" th:with="p = ${param.sort}, priceRange = ${param.skuPrice}"> <input id="skuPriceFrom" type="number" style="width: 60px;margin-left: 30px;" th:value="${#strings.isEmpty(priceRange)?'':#strings.substringBefore(priceRange,'_')}" > - <input id="skuPriceTo" type="number" style="width: 60px;margin-right: 10px;" th:value="${#strings.isEmpty(priceRange)?'':#strings.substringAfter(priceRange,'_')}" > <button id="skuPriceSearchBtn">确定</button> </div>
2、javascript事件方法:
//按价格区间查询 $("#skuPriceSearchBtn").click(function () { var from = $("#skuPriceFrom").val(); var to = $("#skuPriceTo").val(); var query = from + "_" + to; location.href = replaceAndAddParamVal(location.href, "skuPrice", query); });
十一、实现“仅显示有货”的checkbox选项
1、html部分:
<li> <a href="#" th:with="check=${param.hasStock}"> <input id="showHasStock" type="checkbox" th:checked="${#strings.equals(check,'1')}" > 仅显示有货 </a> </li>
2、javascript事件方法:
//仅显示有货 $("#showHasStock").change(function () { if ($(this).prop('checked')) { location.href = replaceAndAddParamVal(location.href, "hasStock", 1); } else { //没选中 var re = eval('/(&hasStock=)([^&]*)/gi'); location.href = (location.href + "").replace(re, ''); } return false; });
十二、实现面包屑导航功能
1、在返回结果模型中增加面包屑导航元素List<NavVo>:
@Data public class SearchResult { //查询到的所有商品信息 private List<SkuEsModel> products; //分页信息 private Integer pageNum; //当前页码 private Long total; //总记录数 private Integer totalPages; //总页数 //查询到的所有品牌信息 private List<BrandVo> brands; //当前查询到的结果,所有涉及到的品牌——用来缩小查询范围 //查询所涉及到的所有属性 private List<AttrVo> attrs; //当前查询到的结果,所有涉及到的属性 //查询所涉及到的所有分类信息 private List<CatalogVo> catalogs; //当前查询到的结果,所有涉及到的分类 //===============以上是返回给页面的所有信息================ //可选页码值 private List<Integer> pageNavs; //面包屑导航 private List<NavVo> navs = new ArrayList<>(); private List<Long> attrIds = new ArrayList<>(); //存放那些attrId已经被筛选了,方便做筛选条件联动 @Data public static class NavVo{ private String navName; //导航项的名字 private String navValue; //导航项的值 private String link; //当取消后,应该跳转到的地址 } }
2、MallSearchServiceImpl.java的search()方法的buildSearchResult()方法:
//6、构建面包屑导航功能 if (!CollectionUtils.isEmpty(param.getAttrs())){ List<SearchResult.NavVo> collect = param.getAttrs().stream().map(attr -> { SearchResult.NavVo navVo = new SearchResult.NavVo(); //分析每个attr传递过来的参数值:格式 attrs=2_5寸: String[] s = attr.split("_"); navVo.setNavValue(s[1]); R r = productFeignService.attrInfo(Long.parseLong(s[0])); if (r.getCode() == 0){ AttrResponseVo attrResponse = r.getData("attr", new TypeReference<AttrResponseVo>() {}); navVo.setNavName(attrResponse.getAttrName()); }else { navVo.setNavName(s[0]); } //将条件中已经有的id传递到result中,便于筛选条件联动 result.getAttrIds().add(Long.parseLong(s[0])); //取消这个面包屑后,我们将要跳转的链接————将当前请求地址中的本条件清除掉即可 //获取所有的查询条件————然后去掉自己 String encode = ""; try { encode = URLEncoder.encode(attr, "UTF-8"); encode.replace("+", "20%"); //是因为浏览器对空格的编码和java不一致导致的 } catch (UnsupportedEncodingException e) { e.printStackTrace(); } String replace = param.get_queryString().replace("&attrs=" + encode, "").replace("attrs=" + encode, ""); navVo.setLink("http://search.zidanmall.com/list.html?" + replace); return navVo; }).collect(Collectors.toList()); result.setNavs(collect); } //品牌的面包屑导航功能 if(!CollectionUtils.isEmpty(param.getBrandId())){ List<SearchResult.NavVo> navs = result.getNavs(); SearchResult.NavVo navVo = new SearchResult.NavVo(); navVo.setNavName("品牌"); //TODO 远程查询所有品牌 R r = productFeignService.getBrands(param.getBrandId()); if(r.getCode()==0){ List<BrandShortVo> brands = r.getData("brands", new TypeReference<List<BrandShortVo>>() { }); StringBuffer buffer=new StringBuffer(); String replace=""; for (BrandShortVo brandVo:brands){ buffer.append(brandVo.getName()+";"); String encode = ""; try { encode = URLEncoder.encode(brandVo.getBrandId() + "", "UTF-8"); encode.replace("+", "20%"); //是因为浏览器对空格的编码和java不一致导致的 } catch (UnsupportedEncodingException e) { e.printStackTrace(); } replace = param.get_queryString().replace("&brandId=" + encode, "").replace("brandId=" + encode, ""); } navVo.setNavValue(buffer.toString()); navVo.setLink("http://search.zidanmall.com/list.html?" + replace); } navs.add(navVo); result.setNavs(navs); }
3、html页面遍历使用:
<div class="JD_ipone_one c"> <a th:href="${nav.link}" th:each="nav:${result.navs}"> <span th:text="${nav.navName}"></span>:<span th:text="${nav.navValue}"></span> ×</a> </div>
十三、实现条件筛选联动
(效果——当某个查询条件已经被选择,那么将不再出现它的遍历结果)
1、品牌的条件联动效果:
<div th:if="${#strings.isEmpty(brandId)}" class="JD_nav_wrap"> <div class="sl_key"> <span>品牌:</span> </div> .... </div>
2、其它属性的条件联动效果:上面的后端代码中,已经考虑到属性的联动,而放置了attrIds
<!--遍历所有需要展示的属性--> <div class="JD_pre" th:each="attr:${result.attrs}" th:if="${!#lists.contains(result.attrIds, attr.attrId)}"> <div class="sl_key"> <span th:text="${attr.attrName}">屏幕尺寸:</span> </div> <div class="sl_value"> <ul> <li th:each="val:${attr.attrValue}"> <a href="#" th:text="${val}" th:href="${'javascript:searchProducts("attrs","' + attr.attrId + '_' + val + '")'}"> 以上 </a> </li> </ul> </div> </div>
3、最终效果
到这里,整个商品,基于ElasticSearch的检索功能差不多就做完了,前端花了大量的时间;
以后的章节中,就不会过多得描述前端实现了;重点是架构和后端的技术点;