一、在search.html文件中组合查询字符串
1.1 JS部分
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script> //1. 向字符串类扩展方法 String.prototype.contains=function(str){ return this.indexOf(str) > -1; } //2. 关键字查询 function search(){ location.href = replaceParamVal(location.href,"keywords",$("#keywords").val()); } //3. 添加分类 function addCategory(v){ //在location.href中添加分类字段 location.href = replaceParamVal(location.href,"category",v); return false; } //4. 显示分类面板,隐藏面包屑导航中的分类 // function hideCategory(v){ // $(v).css("display","none"); // categoryPanel.css("display","block"); // } //5. 添加品牌 function addBrand(v) { location.href = replaceParamVal(location.href,"brand",v); return false; } //6. 添加规格 function addSpec(specText,optionName){ let val = (specText + ":" + optionName); location.href = replaceParamVal(location.href,"spec",val,true); return false; } //7. 添加价格区间 function addPrice(price){ location.href = replaceParamVal(location.href,"price",price); return false; } //8. 排序价格 function addSort(sortField,sortOrder){ //参数1:排序字段 参数2:排序升序(asc)还是降序(desc) location.href = replaceParamVal(location.href,"sort",sortOrder + ":" + sortField); return false; } /** * 功能:路径替换函数 * @param url 目前的url * @param paramName 需要替换的参数属性名 * @param replaceVal 需要替换的参数的新属性值 * @param forceAdd 该参数是否可以重复查询(spec=网络:4G&spec=机身内存:32G),即一次可以查询多个规格 * @returns {string} 替换或添加后的url */ function replaceParamVal(url, paramName, replaceVal,forceAdd=false) { let oUrl = decodeURI(url.toString()); let nUrl; if (oUrl.contains(paramName+"=")) { let split = replaceVal.split(":"); let str = paramName+"="+split[0]; if( forceAdd && !oUrl.contains(str)) { if (oUrl.contains("?")) { nUrl = oUrl + "&" + paramName + "=" + replaceVal; } else { nUrl = oUrl + "?" + paramName + "=" + replaceVal; } } else { // /category=手机/ig if(paramName.startsWith("spec")){ let re = eval('/(' + str + ':)([^&]*)/gi'); nUrl = oUrl.replace(re, str + ':' + split[1]); }else{ let re = eval('/(' + paramName + '=)([^&]*)/gi'); nUrl = oUrl.replace(re, paramName + '=' + replaceVal); } } } else { // http://localhost:9001/student/list?username=aaa&pwd=123 if (oUrl.contains("?")) { //包含? nUrl = oUrl + "&" + paramName + "=" + replaceVal; } else { //不包含? nUrl = oUrl + "?" + paramName + "=" + replaceVal; } } return nUrl; } </script>
1.2 页面部分:
<!-- 1.关键字搜索 --><form class="sui-form form-inline" > <!--searchAutoComplete--> <div class="input-append"> <input type="text" th:value="${vo.keywords}" id="keywords" class="input-error input-xxlarge" /> <input class="sui-btn btn-xlarge btn-danger" type="button" onclick="search()" value="搜索"> </div></form><!-- 2.分类、品牌、规格选项、价格区间、排序一系列查询 --><div class="main"> <div class="py-container"> <!--bread--> <div class="bread"> <ul class="fl sui-breadcrumb"> <li> <a href="#">全部结果 总共:<span th:text="${resultMap.total}"/>条 </a> </li> <li class="active">智能手机</li> </ul> <ul class="tags-choose" id="chooseCategory"> <li class="tag">商品分类:<span></span><i class="sui-icon icon-tb-close" onclick="hideCategory(this.parentNode.parentNode)"></i></li> </ul> <div class="clearfix"></div> </div> <!--selector--> <div class="clearfix selector"> <div class="type-wrap"> <div class="fl key">商品分类</div> <div class="fl value"> <a href="#" th:each="category : ${resultMap.categoryList}" th:onclick="addCategory([[${category}]])">[[${category}]] </a> </div> <div class="fl ext"></div> </div> <div class="type-wrap logo"> <div class="fl key brand">品牌</div> <div class="value logos"> <ul class="logo-list"> <!--1.显示品牌--> <li th:each="brand : ${resultMap.brandList}" th:align="center"> <a href="#" th:onclick="addBrand([[${brand.text}]])">[[${brand.text}]]</a> </li> </ul> </div> </div> <!--2.显示规格列表--> <div class="type-wrap" th:each="spec,stat : ${resultMap.specList}"> <div class="fl key">[[${spec.text}]]</div> <div class="fl value"> <ul class="type-list"> <li th:each="option : ${spec.options}"> <a th:onclick="addSpec([[${spec.text}]],[[${option.optionName}]])"> [[${option.optionName}]]</a> </li> </ul> </div> <div class="fl ext"></div> </div> <div class="type-wrap"> <div class="fl key">价格</div> <div class="fl value"> <ul class="type-list"> <li> <a onclick="addPrice('0_500')">0-500元</a> </li> <li> <a onclick="addPrice('500_1000')">500-1000元</a> </li> <li> <a onclick="addPrice('1000_1500')">1000-1500元</a> </li> <li> <a onclick="addPrice('1500_2000')">1500-2000元</a> </li> <li> <a onclick="addPrice('2000_3000')">2000-3000元 </a> </li> <li> <a onclick="addPrice('3000_*')">3000元以上</a> </li> </ul> </div> <div class="fl ext"> </div> </div> <div class="type-wrap"> <div class="fl key">更多筛选项</div> <div class="fl value"> <ul class="type-list"> <li> <a>特点</a> </li> <li> <a>系统</a> </li> <li> <a>手机内存 </a> </li> <li> <a>单卡双卡</a> </li> <li> <a>其他</a> </li> </ul> </div> <div class="fl ext"> </div> </div> </div> <!--details--> <div class="details"> <div class="sui-navbar"> <div class="navbar-inner filter"> <ul class="sui-nav"> <li class="active"> <a href="#">综合</a> </li> <li> <a href="#">销量</a> </li> <li> <a href="#">新品</a> </li> <li> <a href="#">评价</a> </li> <li> <a href="#" onclick="addSort('price','asc')">价格↑</a> </li> <li> <a href="#" onclick="addSort('price','desc')">价格↓</a> </li> </ul> </div> </div> <div class="goods-list"> <ul class="yui3-g"> <li class="yui3-u-1-5" th:each="result : ${resultMap.rows}"> <div class="list-wrap"> <div class="p-img"> <a href="#" target="_blank"> <img th:src="${result.image}" /></a> </div> <div class="price"> <strong> <em>¥</em> <i th:text="${result.price}"></i> </strong> </div> <div class="attr"> <em th:utext="${result.title}"></em> </div> <div class="cu"> <em></em> </div> <div class="commit"> <i class="command">已有2000人评价</i> </div> <div class="operate"> <a href="/static/search/success-cart.html" target="_blank" class="sui-btn btn-bordered btn-danger">加入购物车</a> <a href="javascript:void(0);" class="sui-btn btn-bordered">对比</a> <a href="javascript:void(0);" class="sui-btn btn-bordered">关注</a> </div> </div> </li> </ul> </div> <div class="fr page"> <div class="sui-pagination pagination-large"> <ul> <li class="prev disabled"> <a href="#">«上一页</a> </li> <li class="active"> <a href="#">1</a> </li> <li> <a href="#">2</a> </li> <li> <a href="#">3</a> </li> <li> <a href="#">4</a> </li> <li> <a href="#">5</a> </li> <li class="dotted"><span>...</span></li> <li class="next"> <a href="#">下一页»</a> </li> </ul> <div><span>共10页 </span><span> 到第 <input type="text" class="page-num"> 页 <button class="page-confirm" onclick="alert(1)">确定</button></span></div> </div> </div> </div> <!--hotsale--> <div class="clearfix hot-sale"> <h4 class="title">热卖商品</h4> <div class="hot-list"> <ul class="yui3-g"> <li class="yui3-u-1-4"> <div class="list-wrap"> <div class="p-img"> <img src="/static/search/img/like_01.png" /> </div> <div class="attr"> <em>Apple苹果iPhone 6s (A1699)</em> </div> <div class="price"> <strong> <em>¥</em> <i>4088.00</i> </strong> </div> <div class="commit"> <i class="command">已有700人评价</i> </div> </div> </li> <li class="yui3-u-1-4"> <div class="list-wrap"> <div class="p-img"> <img src="/static/search/img/like_03.png" /> </div> <div class="attr"> <em>金属A面,360°翻转,APP下单省300!</em> </div> <div class="price"> <strong> <em>¥</em> <i>4088.00</i> </strong> </div> <div class="commit"> <i class="command">已有700人评价</i> </div> </div> </li> <li class="yui3-u-1-4"> <div class="list-wrap"> <div class="p-img"> <img src="/static/search/img/like_04.png" /> </div> <div class="attr"> <em>256SSD商务大咖,完爆职场,APP下单立减200</em> </div> <div class="price"> <strong> <em>¥</em> <i>4068.00</i> </strong> </div> <div class="commit"> <i class="command">已有20人评价</i> </div> </div> </li> <li class="yui3-u-1-4"> <div class="list-wrap"> <div class="p-img"> <img src="/static/search/img/like_02.png" /> </div> <div class="attr"> <em>Apple苹果iPhone 6s (A1699)</em> </div> <div class="price"> <strong> <em>¥</em> <i>4088.00</i> </strong> </div> <div class="commit"> <i class="command">已有700人评价</i> </div> </div> </li> </ul> </div> </div> </div> </div>
二、后端部分:
2.1 定义接收查询参数的对象ItemVo:
@Data@AllArgsConstructor@NoArgsConstructorpublic class ItemVo implements Serializable { //1. 查询关键字对象 private String keywords; //2. 商品分类 private String category; //3. 品牌查询 private String brand; //4. 规格查询(类似:[网络:4G,机身内存:128G]) private List<String> spec; //5. 价格区间(price:500_1000) private String price; //6. 排序(类似:asc:price) private String sort;}
2.2 ItemSearchServiceImpl服务层定义查询功能:
/** * ------------------------------ * 功能: * 作者:WF * 微信:hbxfwf13590332912 * 创建时间:2021/8/2-16:06 * ------------------------------ */@Servicepublic class ItemSearchServiceImpl implements ItemSearchService { @Autowired private RedisTemplate redisTemplate; @Autowired private ElasticsearchRestTemplate restTemplate; /** * 功能: 根据查询参数得到查询结果 * 参数: * 返回值: java.util.Map * 时间: 2021/8/2 16:06 * @param params */ @Override public Map<String, Object> search(ItemVo params) { System.out.println("ItemVo = " + params); //1. 得到查询关键字 String keyword = params.getKeywords(); if(StringUtils.isBlank(keyword)){ keyword = "华为"; } //2. 定义返回结果 Map<String, Object> resultMap = new HashMap<>(); //3. 定义NativeSearchQueryBuilder对象 NativeSearchQueryBuilder searchQueryBuilder = new NativeSearchQueryBuilder() .withPageable(PageRequest.of(0,10)); //4. 按关键字进行分组查询 searchQueryBuilder.addAggregation(AggregationBuilders.terms("categoryGroup") .field("category.keyword").size(50)); //5. 添加高亮查询 //5.1 设置高亮查询 searchQueryBuilder.withHighlightBuilder(new HighlightBuilder() .field("title") //设置高亮字段 .preTags("<span style='color:red'>") //设置高亮显示内容的前缀部分 .postTags("</span>")); //设置高亮显示内容的后缀部分 //6. 添加多字段查询 searchQueryBuilder.withQuery(QueryBuilders.multiMatchQuery( keyword,"title","brand","category")); //7. 定义过滤查询对象(性能优于must查询),用于组合多个查询 BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); //7.1 分类查询 if(StringUtils.isNotBlank(params.getCategory())){ boolQueryBuilder.filter(QueryBuilders.termQuery("category.keyword",params.getCategory())); } //7.2 品牌查询 if(StringUtils.isNotBlank(params.getBrand())){ boolQueryBuilder.filter(QueryBuilders.termQuery("brand.keyword",params.getBrand())); } //7.3 添加规格查询 if(params.getSpec() != null && params.getSpec().size() > 0){ for (String spec : params.getSpec()) { //7.3.1 处理得到的规格数据 String[] split = spec.split(":"); //7.3.2 进行规格的过滤查询 boolQueryBuilder.filter(QueryBuilders.termQuery("specMap."+split[0]+".keyword",split[1])); } } //7.4 价格区间查询 String price = params.getPrice(); if(StringUtils.isNotBlank(price)){ //7.4.1 拆出出两个价格区间 String[] s = price.split("_"); //7.4.2 判断是否结束值带有*号 if(!s[1].equals("*")){ //0-500 boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gt(s[0]).lte(s[1])); }else{ //3000-* boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(s[0])); } } //7.5 按照升序、降序排序查询 //7.5.1 得到排序关键字 String sort = params.getSort(); if(StringUtils.isNotBlank(sort)){ //7.5.2 拆分字符串 String[] split = sort.split(":"); //数组第一部分:升序/降序 第二部分:排序字段 //7.5.3 进行排序 searchQueryBuilder.withSort(SortBuilders.fieldSort(split[1]).order(split[0].equals("asc") ? SortOrder.ASC : SortOrder.DESC)); } //7.6 添加过滤查询 searchQueryBuilder.withFilter(boolQueryBuilder); //8. 得到查询对象 NativeSearchQuery searchQuery = searchQueryBuilder.build(); //9. 得到命中对象 SearchHits<ItemEntity> searchHits = restTemplate.search(searchQuery, ItemEntity.class, IndexCoordinates.of("item")); //10. 得到命中的结果 List<SearchHit<ItemEntity>> list = searchHits.getSearchHits(); //11. 得到命中结果的选项 long total = searchHits.getTotalHits(); //总记录数 int totalPage = (int) Math.ceil(total/10.0);//总页数 //12. 处理命中结果(原始的关键字查询) //List<ItemEntity> collect = list.stream().map(m -> m.getContent()).collect(Collectors.toList()); //13. 得到分组数据 Aggregations aggregations = searchHits.getAggregations(); //14. 得到关于分类的分组 ParsedStringTerms categoryGroupResult = aggregations.get("categoryGroup"); //15. 处理分类分组的结果数据 List<String> categoryList = categoryGroupResult.getBuckets().stream() .map(m -> m.getKeyAsString()).collect(Collectors.toList()); System.out.println("categoryList = " + categoryList); //16. 得到高亮查询的结果 //16.1 定义存放高亮字段的内容 List<ItemEntity> highlights = new ArrayList<>(); //16.2 遍历所有命中的数据,从中挑选出高亮数据 for (SearchHit<ItemEntity> searchHit : list) { //list: 所有命中的结果对象 //16.3 可以直接通过高亮字段名称得到此高亮字段的值 List<String> title = searchHit.getHighlightField("title"); //16.4 得到未高亮前的数据 ItemEntity itemEntity = searchHit.getContent(); //16.5 定义存放高亮字段的字符串 StringBuffer buffer = new StringBuffer(); //16.6 组合高亮字段的值 for (String s : title) { buffer.append(s); } //16.7 将高亮字段的值重新设置回原来的对象 itemEntity.setTitle(buffer.toString()); //16.8 将高亮对象添加到集合中 highlights.add(itemEntity); } System.out.println("highlights = " + highlights); //17. 从redis中得到品牌及规格列表 Map brandAndSpecMap = new HashMap(); //18. 得到用户选择的分类 String category = params.getCategory(); if(StringUtils.isBlank(category)){ if(categoryList != null && categoryList.size() > 0) { category = categoryList.get(0); } } //19. 根据分类进行查询品牌及规格 brandAndSpecMap = findBrandAndSpecList(category); //20. 将品牌及规格列表放到大集合中 resultMap.putAll(brandAndSpecMap); //21. 存放高亮数据到大集合 resultMap.put("rows",highlights); //当前分页记录集合 resultMap.put("total",total); //总记录数 resultMap.put("categoryList",categoryList); //分类 resultMap.put("totalPage",totalPage); //总页数 return resultMap; } /** * 功能: 根据分类名称得到品牌列表及规格列表 * 参数: * 返回值: java.util.Map * 时间: 2021/8/3 8:39 */ private Map findBrandAndSpecList(String category) { //1. 根据分类名称得到模板id Object typeId = redisTemplate.boundHashOps("itemCats").get(category); //2. 根据模板id得到品牌列表 List<Map> brandList = (List<Map>) redisTemplate.boundHashOps("brandList").get(typeId); //3. 根据模板id得到规格列表 List<Map> specList = (List<Map>) redisTemplate.boundHashOps("specList").get(typeId); //4. 定义结果map Map brandAndSpecmap = new HashMap(); //5. 将上面得到的品牌列表及规格列表都放到map中 brandAndSpecmap.put("brandList",brandList); brandAndSpecmap.put("specList",specList); //6. 返回 return brandAndSpecmap; }}
2.3 控制层ItemSearchController:
@Controller@RequestMappingpublic class SearchController { @Reference(timeout = 5000) private ItemSearchService itemSearchService; /** * 功能: 根据条件进行查询 * 参数: * params: 代表查询参数 * model: 代表存放查询到的结果 * 返回值: java.lang.String * 时间: 2021/8/2 16:00 */ @RequestMapping({"/","/search.html"}) public String start(ItemVo vo, Model model){ //1. 开始进行查询 Map<String,Object> resultMap = itemSearchService.search(vo); model.addAttribute("resultMap",resultMap); model.addAttribute("vo",vo); return "search"; }
2.4 查看整体效果:

