根据之前设计的页面,效果如下:

搜索功能相对比较繁杂,因为有各种各样的搜索项,返回的结果也乱;所有,我们需要专门为查询条件、查询结果封装成类;
一、检索查询参数的模型抽取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的检索功能差不多就做完了,前端花了大量的时间;
以后的章节中,就不会过多得描述前端实现了;重点是架构和后端的技术点;



