一、在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
@NoArgsConstructor
public 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
* ------------------------------
*/
@Service
public 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
@RequestMapping
public 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 查看整体效果: