一、在search.html文件中组合查询字符串

1.1 JS部分

  1. <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  2. <script>
  3. //1. 向字符串类扩展方法
  4. String.prototype.contains=function(str){
  5. return this.indexOf(str) > -1;
  6. }
  7. //2. 关键字查询
  8. function search(){
  9. location.href = replaceParamVal(location.href,"keywords",$("#keywords").val());
  10. }
  11. //3. 添加分类
  12. function addCategory(v){
  13. //在location.href中添加分类字段
  14. location.href = replaceParamVal(location.href,"category",v);
  15. return false;
  16. }
  17. //4. 显示分类面板,隐藏面包屑导航中的分类
  18. // function hideCategory(v){
  19. // $(v).css("display","none");
  20. // categoryPanel.css("display","block");
  21. // }
  22. //5. 添加品牌
  23. function addBrand(v) {
  24. location.href = replaceParamVal(location.href,"brand",v);
  25. return false;
  26. }
  27. //6. 添加规格
  28. function addSpec(specText,optionName){
  29. let val = (specText + ":" + optionName);
  30. location.href = replaceParamVal(location.href,"spec",val,true);
  31. return false;
  32. }
  33. //7. 添加价格区间
  34. function addPrice(price){
  35. location.href = replaceParamVal(location.href,"price",price);
  36. return false;
  37. }
  38. //8. 排序价格
  39. function addSort(sortField,sortOrder){ //参数1:排序字段 参数2:排序升序(asc)还是降序(desc)
  40. location.href = replaceParamVal(location.href,"sort",sortOrder + ":" + sortField);
  41. return false;
  42. }
  43. /**
  44. * 功能:路径替换函数
  45. * @param url 目前的url
  46. * @param paramName 需要替换的参数属性名
  47. * @param replaceVal 需要替换的参数的新属性值
  48. * @param forceAdd 该参数是否可以重复查询(spec=网络:4G&spec=机身内存:32G),即一次可以查询多个规格
  49. * @returns {string} 替换或添加后的url
  50. */
  51. function replaceParamVal(url, paramName, replaceVal,forceAdd=false) {
  52. let oUrl = decodeURI(url.toString());
  53. let nUrl;
  54. if (oUrl.contains(paramName+"=")) {
  55. let split = replaceVal.split(":");
  56. let str = paramName+"="+split[0];
  57. if( forceAdd && !oUrl.contains(str)) {
  58. if (oUrl.contains("?")) {
  59. nUrl = oUrl + "&" + paramName + "=" + replaceVal;
  60. } else {
  61. nUrl = oUrl + "?" + paramName + "=" + replaceVal;
  62. }
  63. } else {
  64. // /category=手机/ig
  65. if(paramName.startsWith("spec")){
  66. let re = eval('/(' + str + ':)([^&]*)/gi');
  67. nUrl = oUrl.replace(re, str + ':' + split[1]);
  68. }else{
  69. let re = eval('/(' + paramName + '=)([^&]*)/gi');
  70. nUrl = oUrl.replace(re, paramName + '=' + replaceVal);
  71. }
  72. }
  73. } else {
  74. // http://localhost:9001/student/list?username=aaa&pwd=123
  75. if (oUrl.contains("?")) { //包含?
  76. nUrl = oUrl + "&" + paramName + "=" + replaceVal;
  77. } else { //不包含?
  78. nUrl = oUrl + "?" + paramName + "=" + replaceVal;
  79. }
  80. }
  81. return nUrl;
  82. }
  83. </script>

1.2 页面部分:

  1. <!-- 1.关键字搜索 -->
  2. <form class="sui-form form-inline" >
  3. <!--searchAutoComplete-->
  4. <div class="input-append">
  5. <input type="text" th:value="${vo.keywords}" id="keywords" class="input-error input-xxlarge" />
  6. <input class="sui-btn btn-xlarge btn-danger" type="button" onclick="search()" value="搜索">
  7. </div>
  8. </form>
  9. <!-- 2.分类、品牌、规格选项、价格区间、排序一系列查询 -->
  10. <div class="main">
  11. <div class="py-container">
  12. <!--bread-->
  13. <div class="bread">
  14. <ul class="fl sui-breadcrumb">
  15. <li>
  16. <a href="#">全部结果
  17. 总共:<span th:text="${resultMap.total}"/>
  18. </a>
  19. </li>
  20. <li class="active">智能手机</li>
  21. </ul>
  22. <ul class="tags-choose" id="chooseCategory">
  23. <li class="tag">商品分类:<span></span><i class="sui-icon icon-tb-close"
  24. onclick="hideCategory(this.parentNode.parentNode)"></i></li>
  25. </ul>
  26. <div class="clearfix"></div>
  27. </div>
  28. <!--selector-->
  29. <div class="clearfix selector">
  30. <div class="type-wrap">
  31. <div class="fl key">商品分类</div>
  32. <div class="fl value">
  33. <a href="#" th:each="category : ${resultMap.categoryList}"
  34. th:onclick="addCategory([[${category}]])">[[${category}]] </a>
  35. </div>
  36. <div class="fl ext"></div>
  37. </div>
  38. <div class="type-wrap logo">
  39. <div class="fl key brand">品牌</div>
  40. <div class="value logos">
  41. <ul class="logo-list">
  42. <!--1.显示品牌-->
  43. <li th:each="brand : ${resultMap.brandList}" th:align="center">
  44. <a href="#" th:onclick="addBrand([[${brand.text}]])">[[${brand.text}]]</a>
  45. </li>
  46. </ul>
  47. </div>
  48. </div>
  49. <!--2.显示规格列表-->
  50. <div class="type-wrap" th:each="spec,stat : ${resultMap.specList}">
  51. <div class="fl key">[[${spec.text}]]</div>
  52. <div class="fl value">
  53. <ul class="type-list">
  54. <li th:each="option : ${spec.options}">
  55. <a th:onclick="addSpec([[${spec.text}]],[[${option.optionName}]])">
  56. [[${option.optionName}]]</a>
  57. </li>
  58. </ul>
  59. </div>
  60. <div class="fl ext"></div>
  61. </div>
  62. <div class="type-wrap">
  63. <div class="fl key">价格</div>
  64. <div class="fl value">
  65. <ul class="type-list">
  66. <li>
  67. <a onclick="addPrice('0_500')">0-500元</a>
  68. </li>
  69. <li>
  70. <a onclick="addPrice('500_1000')">500-1000元</a>
  71. </li>
  72. <li>
  73. <a onclick="addPrice('1000_1500')">1000-1500元</a>
  74. </li>
  75. <li>
  76. <a onclick="addPrice('1500_2000')">1500-2000元</a>
  77. </li>
  78. <li>
  79. <a onclick="addPrice('2000_3000')">2000-3000元 </a>
  80. </li>
  81. <li>
  82. <a onclick="addPrice('3000_*')">3000元以上</a>
  83. </li>
  84. </ul>
  85. </div>
  86. <div class="fl ext">
  87. </div>
  88. </div>
  89. <div class="type-wrap">
  90. <div class="fl key">更多筛选项</div>
  91. <div class="fl value">
  92. <ul class="type-list">
  93. <li>
  94. <a>特点</a>
  95. </li>
  96. <li>
  97. <a>系统</a>
  98. </li>
  99. <li>
  100. <a>手机内存 </a>
  101. </li>
  102. <li>
  103. <a>单卡双卡</a>
  104. </li>
  105. <li>
  106. <a>其他</a>
  107. </li>
  108. </ul>
  109. </div>
  110. <div class="fl ext">
  111. </div>
  112. </div>
  113. </div>
  114. <!--details-->
  115. <div class="details">
  116. <div class="sui-navbar">
  117. <div class="navbar-inner filter">
  118. <ul class="sui-nav">
  119. <li class="active">
  120. <a href="#">综合</a>
  121. </li>
  122. <li>
  123. <a href="#">销量</a>
  124. </li>
  125. <li>
  126. <a href="#">新品</a>
  127. </li>
  128. <li>
  129. <a href="#">评价</a>
  130. </li>
  131. <li>
  132. <a href="#" onclick="addSort('price','asc')">价格↑</a>
  133. </li>
  134. <li>
  135. <a href="#" onclick="addSort('price','desc')">价格↓</a>
  136. </li>
  137. </ul>
  138. </div>
  139. </div>
  140. <div class="goods-list">
  141. <ul class="yui3-g">
  142. <li class="yui3-u-1-5" th:each="result : ${resultMap.rows}">
  143. <div class="list-wrap">
  144. <div class="p-img">
  145. <a href="#" target="_blank">
  146. <img th:src="${result.image}" /></a>
  147. </div>
  148. <div class="price">
  149. <strong>
  150. <em>¥</em>
  151. <i th:text="${result.price}"></i>
  152. </strong>
  153. </div>
  154. <div class="attr">
  155. <em th:utext="${result.title}"></em>
  156. </div>
  157. <div class="cu">
  158. <em></em>
  159. </div>
  160. <div class="commit">
  161. <i class="command">已有2000人评价</i>
  162. </div>
  163. <div class="operate">
  164. <a href="/static/search/success-cart.html" target="_blank" class="sui-btn btn-bordered btn-danger">加入购物车</a>
  165. <a href="javascript:void(0);" class="sui-btn btn-bordered">对比</a>
  166. <a href="javascript:void(0);" class="sui-btn btn-bordered">关注</a>
  167. </div>
  168. </div>
  169. </li>
  170. </ul>
  171. </div>
  172. <div class="fr page">
  173. <div class="sui-pagination pagination-large">
  174. <ul>
  175. <li class="prev disabled">
  176. <a href="#">«上一页</a>
  177. </li>
  178. <li class="active">
  179. <a href="#">1</a>
  180. </li>
  181. <li>
  182. <a href="#">2</a>
  183. </li>
  184. <li>
  185. <a href="#">3</a>
  186. </li>
  187. <li>
  188. <a href="#">4</a>
  189. </li>
  190. <li>
  191. <a href="#">5</a>
  192. </li>
  193. <li class="dotted"><span>...</span></li>
  194. <li class="next">
  195. <a href="#">下一页»</a>
  196. </li>
  197. </ul>
  198. <div><span>共10页&nbsp;</span><span>
  199. 到第
  200. <input type="text" class="page-num">
  201. <button class="page-confirm" onclick="alert(1)">确定</button></span></div>
  202. </div>
  203. </div>
  204. </div>
  205. <!--hotsale-->
  206. <div class="clearfix hot-sale">
  207. <h4 class="title">热卖商品</h4>
  208. <div class="hot-list">
  209. <ul class="yui3-g">
  210. <li class="yui3-u-1-4">
  211. <div class="list-wrap">
  212. <div class="p-img">
  213. <img src="/static/search/img/like_01.png" />
  214. </div>
  215. <div class="attr">
  216. <em>Apple苹果iPhone 6s (A1699)</em>
  217. </div>
  218. <div class="price">
  219. <strong>
  220. <em>¥</em>
  221. <i>4088.00</i>
  222. </strong>
  223. </div>
  224. <div class="commit">
  225. <i class="command">已有700人评价</i>
  226. </div>
  227. </div>
  228. </li>
  229. <li class="yui3-u-1-4">
  230. <div class="list-wrap">
  231. <div class="p-img">
  232. <img src="/static/search/img/like_03.png" />
  233. </div>
  234. <div class="attr">
  235. <em>金属A面,360°翻转,APP下单省300!</em>
  236. </div>
  237. <div class="price">
  238. <strong>
  239. <em>¥</em>
  240. <i>4088.00</i>
  241. </strong>
  242. </div>
  243. <div class="commit">
  244. <i class="command">已有700人评价</i>
  245. </div>
  246. </div>
  247. </li>
  248. <li class="yui3-u-1-4">
  249. <div class="list-wrap">
  250. <div class="p-img">
  251. <img src="/static/search/img/like_04.png" />
  252. </div>
  253. <div class="attr">
  254. <em>256SSD商务大咖,完爆职场,APP下单立减200</em>
  255. </div>
  256. <div class="price">
  257. <strong>
  258. <em>¥</em>
  259. <i>4068.00</i>
  260. </strong>
  261. </div>
  262. <div class="commit">
  263. <i class="command">已有20人评价</i>
  264. </div>
  265. </div>
  266. </li>
  267. <li class="yui3-u-1-4">
  268. <div class="list-wrap">
  269. <div class="p-img">
  270. <img src="/static/search/img/like_02.png" />
  271. </div>
  272. <div class="attr">
  273. <em>Apple苹果iPhone 6s (A1699)</em>
  274. </div>
  275. <div class="price">
  276. <strong>
  277. <em>¥</em>
  278. <i>4088.00</i>
  279. </strong>
  280. </div>
  281. <div class="commit">
  282. <i class="command">已有700人评价</i>
  283. </div>
  284. </div>
  285. </li>
  286. </ul>
  287. </div>
  288. </div>
  289. </div>
  290. </div>

二、后端部分:

2.1 定义接收查询参数的对象ItemVo:

  1. @Data
  2. @AllArgsConstructor
  3. @NoArgsConstructor
  4. public class ItemVo implements Serializable {
  5. //1. 查询关键字对象
  6. private String keywords;
  7. //2. 商品分类
  8. private String category;
  9. //3. 品牌查询
  10. private String brand;
  11. //4. 规格查询(类似:[网络:4G,机身内存:128G])
  12. private List<String> spec;
  13. //5. 价格区间(price:500_1000)
  14. private String price;
  15. //6. 排序(类似:asc:price)
  16. private String sort;
  17. }

2.2 ItemSearchServiceImpl服务层定义查询功能:

  1. /**
  2. * ------------------------------
  3. * 功能:
  4. * 作者:WF
  5. * 微信:hbxfwf13590332912
  6. * 创建时间:2021/8/2-16:06
  7. * ------------------------------
  8. */
  9. @Service
  10. public class ItemSearchServiceImpl implements ItemSearchService {
  11. @Autowired
  12. private RedisTemplate redisTemplate;
  13. @Autowired
  14. private ElasticsearchRestTemplate restTemplate;
  15. /**
  16. * 功能: 根据查询参数得到查询结果
  17. * 参数:
  18. * 返回值: java.util.Map
  19. * 时间: 2021/8/2 16:06
  20. * @param params
  21. */
  22. @Override
  23. public Map<String, Object> search(ItemVo params) {
  24. System.out.println("ItemVo = " + params);
  25. //1. 得到查询关键字
  26. String keyword = params.getKeywords();
  27. if(StringUtils.isBlank(keyword)){
  28. keyword = "华为";
  29. }
  30. //2. 定义返回结果
  31. Map<String, Object> resultMap = new HashMap<>();
  32. //3. 定义NativeSearchQueryBuilder对象
  33. NativeSearchQueryBuilder searchQueryBuilder = new NativeSearchQueryBuilder()
  34. .withPageable(PageRequest.of(0,10));
  35. //4. 按关键字进行分组查询
  36. searchQueryBuilder.addAggregation(AggregationBuilders.terms("categoryGroup")
  37. .field("category.keyword").size(50));
  38. //5. 添加高亮查询
  39. //5.1 设置高亮查询
  40. searchQueryBuilder.withHighlightBuilder(new HighlightBuilder()
  41. .field("title") //设置高亮字段
  42. .preTags("<span style='color:red'>") //设置高亮显示内容的前缀部分
  43. .postTags("</span>")); //设置高亮显示内容的后缀部分
  44. //6. 添加多字段查询
  45. searchQueryBuilder.withQuery(QueryBuilders.multiMatchQuery( keyword,"title","brand","category"));
  46. //7. 定义过滤查询对象(性能优于must查询),用于组合多个查询
  47. BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
  48. //7.1 分类查询
  49. if(StringUtils.isNotBlank(params.getCategory())){
  50. boolQueryBuilder.filter(QueryBuilders.termQuery("category.keyword",params.getCategory()));
  51. }
  52. //7.2 品牌查询
  53. if(StringUtils.isNotBlank(params.getBrand())){
  54. boolQueryBuilder.filter(QueryBuilders.termQuery("brand.keyword",params.getBrand()));
  55. }
  56. //7.3 添加规格查询
  57. if(params.getSpec() != null && params.getSpec().size() > 0){
  58. for (String spec : params.getSpec()) {
  59. //7.3.1 处理得到的规格数据
  60. String[] split = spec.split(":");
  61. //7.3.2 进行规格的过滤查询
  62. boolQueryBuilder.filter(QueryBuilders.termQuery("specMap."+split[0]+".keyword",split[1]));
  63. }
  64. }
  65. //7.4 价格区间查询
  66. String price = params.getPrice();
  67. if(StringUtils.isNotBlank(price)){
  68. //7.4.1 拆出出两个价格区间
  69. String[] s = price.split("_");
  70. //7.4.2 判断是否结束值带有*号
  71. if(!s[1].equals("*")){ //0-500
  72. boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gt(s[0]).lte(s[1]));
  73. }else{ //3000-*
  74. boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(s[0]));
  75. }
  76. }
  77. //7.5 按照升序、降序排序查询
  78. //7.5.1 得到排序关键字
  79. String sort = params.getSort();
  80. if(StringUtils.isNotBlank(sort)){
  81. //7.5.2 拆分字符串
  82. String[] split = sort.split(":"); //数组第一部分:升序/降序 第二部分:排序字段
  83. //7.5.3 进行排序
  84. searchQueryBuilder.withSort(SortBuilders.fieldSort(split[1]).order(split[0].equals("asc") ? SortOrder.ASC : SortOrder.DESC));
  85. }
  86. //7.6 添加过滤查询
  87. searchQueryBuilder.withFilter(boolQueryBuilder);
  88. //8. 得到查询对象
  89. NativeSearchQuery searchQuery = searchQueryBuilder.build();
  90. //9. 得到命中对象
  91. SearchHits<ItemEntity> searchHits = restTemplate.search(searchQuery, ItemEntity.class, IndexCoordinates.of("item"));
  92. //10. 得到命中的结果
  93. List<SearchHit<ItemEntity>> list = searchHits.getSearchHits();
  94. //11. 得到命中结果的选项
  95. long total = searchHits.getTotalHits(); //总记录数
  96. int totalPage = (int) Math.ceil(total/10.0);//总页数
  97. //12. 处理命中结果(原始的关键字查询)
  98. //List<ItemEntity> collect = list.stream().map(m -> m.getContent()).collect(Collectors.toList());
  99. //13. 得到分组数据
  100. Aggregations aggregations = searchHits.getAggregations();
  101. //14. 得到关于分类的分组
  102. ParsedStringTerms categoryGroupResult = aggregations.get("categoryGroup");
  103. //15. 处理分类分组的结果数据
  104. List<String> categoryList = categoryGroupResult.getBuckets().stream()
  105. .map(m -> m.getKeyAsString()).collect(Collectors.toList());
  106. System.out.println("categoryList = " + categoryList);
  107. //16. 得到高亮查询的结果
  108. //16.1 定义存放高亮字段的内容
  109. List<ItemEntity> highlights = new ArrayList<>();
  110. //16.2 遍历所有命中的数据,从中挑选出高亮数据
  111. for (SearchHit<ItemEntity> searchHit : list) { //list: 所有命中的结果对象
  112. //16.3 可以直接通过高亮字段名称得到此高亮字段的值
  113. List<String> title = searchHit.getHighlightField("title");
  114. //16.4 得到未高亮前的数据
  115. ItemEntity itemEntity = searchHit.getContent();
  116. //16.5 定义存放高亮字段的字符串
  117. StringBuffer buffer = new StringBuffer();
  118. //16.6 组合高亮字段的值
  119. for (String s : title) {
  120. buffer.append(s);
  121. }
  122. //16.7 将高亮字段的值重新设置回原来的对象
  123. itemEntity.setTitle(buffer.toString());
  124. //16.8 将高亮对象添加到集合中
  125. highlights.add(itemEntity);
  126. }
  127. System.out.println("highlights = " + highlights);
  128. //17. 从redis中得到品牌及规格列表
  129. Map brandAndSpecMap = new HashMap();
  130. //18. 得到用户选择的分类
  131. String category = params.getCategory();
  132. if(StringUtils.isBlank(category)){
  133. if(categoryList != null && categoryList.size() > 0) {
  134. category = categoryList.get(0);
  135. }
  136. }
  137. //19. 根据分类进行查询品牌及规格
  138. brandAndSpecMap = findBrandAndSpecList(category);
  139. //20. 将品牌及规格列表放到大集合中
  140. resultMap.putAll(brandAndSpecMap);
  141. //21. 存放高亮数据到大集合
  142. resultMap.put("rows",highlights); //当前分页记录集合
  143. resultMap.put("total",total); //总记录数
  144. resultMap.put("categoryList",categoryList); //分类
  145. resultMap.put("totalPage",totalPage); //总页数
  146. return resultMap;
  147. }
  148. /**
  149. * 功能: 根据分类名称得到品牌列表及规格列表
  150. * 参数:
  151. * 返回值: java.util.Map
  152. * 时间: 2021/8/3 8:39
  153. */
  154. private Map findBrandAndSpecList(String category) {
  155. //1. 根据分类名称得到模板id
  156. Object typeId = redisTemplate.boundHashOps("itemCats").get(category);
  157. //2. 根据模板id得到品牌列表
  158. List<Map> brandList = (List<Map>) redisTemplate.boundHashOps("brandList").get(typeId);
  159. //3. 根据模板id得到规格列表
  160. List<Map> specList = (List<Map>) redisTemplate.boundHashOps("specList").get(typeId);
  161. //4. 定义结果map
  162. Map brandAndSpecmap = new HashMap();
  163. //5. 将上面得到的品牌列表及规格列表都放到map中
  164. brandAndSpecmap.put("brandList",brandList);
  165. brandAndSpecmap.put("specList",specList);
  166. //6. 返回
  167. return brandAndSpecmap;
  168. }
  169. }

2.3 控制层ItemSearchController:

  1. @Controller
  2. @RequestMapping
  3. public class SearchController {
  4. @Reference(timeout = 5000)
  5. private ItemSearchService itemSearchService;
  6. /**
  7. * 功能: 根据条件进行查询
  8. * 参数:
  9. * params: 代表查询参数
  10. * model: 代表存放查询到的结果
  11. * 返回值: java.lang.String
  12. * 时间: 2021/8/2 16:00
  13. */
  14. @RequestMapping({"/","/search.html"})
  15. public String start(ItemVo vo, Model model){
  16. //1. 开始进行查询
  17. Map<String,Object> resultMap = itemSearchService.search(vo);
  18. model.addAttribute("resultMap",resultMap);
  19. model.addAttribute("vo",vo);
  20. return "search";
  21. }

2.4 查看整体效果:

image.png
image.png