目录
检索服务
- 浏览器向http://search.gulimall.com/请求检索页list.html,
- search服务构建DSL语句,从elasticSearch中获取结果
- 将检索结果封装好,装入model,页面由themleaf渲染。
@RequestMapping("/list.html")
public String listPage(SearchParam searchParam, Model model){
SearchResult searchResult = mallSearchSearchService.search(searchParam);
model.addAttribute("result",searchResult);
return "list";
}
构建DSL
DSL
依据请求参数,构建DSL检索语句
keyword=Huawei&brandId=1&catalog3Id=225&attrs=2_麒麟9000&attrs=5_1080p
- keyword匹配skuTitle标题,命中的词条高亮显示highLight
- 属性attrs为复杂对象数组,需标明为nested类型
- 聚合分析命中结果中 所有品牌,分类,属性。先依据id聚合,再子聚合获取name与value
- 分页与排序
GET /gulimall_product/_search
{
"from": 0,
"size": 4,
"query": {
"bool": {
"must": [
{
"match": {
"skuTitle":"Huawei"
}
}
],
"filter": [
{
"term": {
"catalogId": 225
}
},
{
"term": {
"brandId": 1
}
},
{
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "5"
}
}
},
{
"terms": {
"attrs.attrValue": [
"1080p"
]
}
}
]
}
}
}
},
{
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "2"
}
}
},
{
"terms": {
"attrs.attrValue": [
"麒麟9000"
]
}
}
]
}
}
}
}
]
}
},
"aggregations": {
"brand_agg": {
"terms": {
"field": "brandId"
},
"aggregations": {
"brand_name_agg": {
"terms": {
"field": "brandName"
}
},
"brand_img_agg": {
"terms": {
"field": "brandImg"
}
}
}
},
"catalog_agg": {
"terms": {
"field": "catalogId"
},
"aggregations": {
"catalog_name_agg": {
"terms": {
"field": "catalogName"
}
}
}
},
"attr_agg": {
"nested": {
"path": "attrs"
},
"aggregations": {
"attr_id_agg": {
"terms": {
"field": "attrs.attrId"
},
"aggregations": {
"attr_name_agg": {
"terms": {
"field": "attrs.attrName"
}
},
"attr_value_agg": {
"terms": {
"field": "attrs.attrValue"
}
}
}
}
}
}
},
"highlight": {
"pre_tags": [
"<b style='color:red'>"
],
"post_tags": [
"</b>"
],
"fields": {
"skuTitle": {}
}
}
}
代码实现
/**
* 构建DSL语句
* 模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存),排序,分页,高亮,聚合分析
* @param searchParam
* @return
*/
private SearchRequest buildSearchRequest(SearchParam searchParam) {
SearchSourceBuilder searchSourceBuilder =new SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
/**
* 模糊匹配
*/
if(!StringUtils.isEmpty(searchParam.getKeyword())){
boolQueryBuilder.must(QueryBuilders.matchQuery("skuTitle",searchParam.getKeyword()));
}
if(searchParam.getCatalog3Id()!=null){
boolQueryBuilder.filter(QueryBuilders.termQuery("catalogId",searchParam.getCatalog3Id()));
}
if(searchParam.getBrandId()!=null&& !searchParam.getBrandId().isEmpty()){
boolQueryBuilder.filter(QueryBuilders.termsQuery("brandId",searchParam.getBrandId()));
}
if(searchParam.getHasStock()!=null){
boolQueryBuilder.filter(QueryBuilders.termQuery("hasStock",searchParam.getHasStock()==1));
}
/**
* 价格区间
*/
if(!StringUtils.isEmpty(searchParam.getSkuPrice())){
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice");
String[] price = searchParam.getSkuPrice().trim().split("_");
if(price.length==2){
//这里 skuPrice=_1000 会传 空字符串
rangeQueryBuilder.gte(StringUtils.isEmpty(price[0])? null:price[0]).lte(price[1]);
}else if(price.length == 1){
if(searchParam.getSkuPrice().startsWith("_")){
rangeQueryBuilder.lte(price[0]);
}
if(searchParam.getSkuPrice().endsWith("_")){
rangeQueryBuilder.gte(price[0]);
}
}
boolQueryBuilder.filter(rangeQueryBuilder);
}
/**
* 属性
* 使用nested嵌套查询
*/
if(searchParam.getAttrs() != null && !searchParam.getAttrs().isEmpty()){
searchParam.getAttrs().forEach(item -> {
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//attrs=1_5寸:8寸&2_16G:8G
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));
NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery("attrs",boolQuery, ScoreMode.None);
boolQueryBuilder.filter(nestedQueryBuilder);
});
}
searchSourceBuilder.query(boolQueryBuilder);
/**
* 排序,分页,高亮
*/
//排序 形式为sort=hotScore_asc/desc
if(!StringUtils.isEmpty(searchParam.getSort())){
String sort = searchParam.getSort();
String[] sortFields = sort.split("_");
SortOrder sortOrder="asc".equalsIgnoreCase(sortFields[1])? SortOrder.ASC:SortOrder.DESC;
searchSourceBuilder.sort(sortFields[0],sortOrder);
}
//分页
searchSourceBuilder.from((searchParam.getPageNum()-1) *EsConstant.PRODUCT_PAGESIZE);
searchSourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);
//高亮
if(!StringUtils.isEmpty(searchParam.getKeyword())){
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("skuTitle");
highlightBuilder.preTags("<b style='color:red'>");
highlightBuilder.postTags("</b>");
searchSourceBuilder.highlighter(highlightBuilder);
}
/**
* 聚合分析
*/
//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);
//2. 按照分类信息进行聚合
TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg");
catalog_agg.field("catalogId").size(20);
catalog_agg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1));
searchSourceBuilder.aggregation(catalog_agg);
//2. 按照属性信息进行聚合
NestedAggregationBuilder attr_agg = AggregationBuilders.nested("attr_agg", "attrs");
//2.1 按照属性ID进行聚合
TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId");
attr_agg.subAggregation(attr_id_agg);
//2.1.1 在每个属性ID下,按照属性名进行聚合
attr_id_agg.subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1));
//2.1.1 在每个属性ID下,按照属性值进行聚合
attr_id_agg.subAggregation(AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50));
searchSourceBuilder.aggregation(attr_agg);
System.out.println("DSL:");
System.out.println(searchSourceBuilder.toString());
return new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX},searchSourceBuilder);
}
封装Result
将elasticsearch返回的结果封装成Result
/**
* 构建响应数据
* 1.商品.标题高亮
* 2.品牌,分类,页码
* @param searchResponse
* @return
*/
private SearchResult buildSearchResult(SearchResponse searchResponse,SearchParam searchParam) {
SearchResult result = new SearchResult();
/**
* 1.将source转为sku
* 2.替换标题,高亮
* 3.将sku商品集合装入result
*/
SearchHits hits = searchResponse.getHits();
List<SkuEsModel> products = new ArrayList<>();
for (SearchHit hit:hits.getHits()) {
SkuEsModel skuEsModel = JSON.parseObject(hit.getSourceAsString(), SkuEsModel.class);
if(!StringUtils.isEmpty(searchParam.getKeyword())){
String skuTitle = hit.getHighlightFields().get("skuTitle").getFragments()[0].string();
skuEsModel.setSkuTitle(skuTitle);
}
products.add(skuEsModel);
}
result.setProduct(products);
/**
* 页码
*/
result.setTotal(hits.getTotalHits().value);
result.setTotalPages((int)hits.getTotalHits().value%EsConstant.PRODUCT_PAGESIZE == 0?
(int)hits.getTotalHits().value/EsConstant.PRODUCT_PAGESIZE:(int)hits.getTotalHits().value/EsConstant.PRODUCT_PAGESIZE+1);
result.setPageNum(searchParam.getPageNum());
List<Integer> pageNavs = new ArrayList<>();
for (int i = 1; i <= result.getTotalPages(); i++) {
pageNavs.add(i);
}
result.setPageNavs(pageNavs);
Aggregations aggregations = searchResponse.getAggregations();
/**
* 品牌
*/
HashMap<Long, SearchResult.BrandVo> brandVoHashMap = new HashMap<>();
Terms brand_agg = aggregations.get("brand_agg");
List<SearchResult.BrandVo> brands =new ArrayList<>();
for(Terms.Bucket bucket:brand_agg.getBuckets()){
SearchResult.BrandVo brand = new SearchResult.BrandVo();
brand.setBrandId(bucket.getKeyAsNumber().longValue());
//品牌名
Terms brand_name_agg = bucket.getAggregations().get("brand_name_agg");
brand.setBrandName(brand_name_agg.getBuckets().get(0).getKeyAsString());
//品牌img
Terms brand_img_agg = bucket.getAggregations().get("brand_img_agg");
brand.setBrandImg(brand_img_agg.getBuckets().get(0).getKeyAsString());
brands.add(brand);
brandVoHashMap.put(brand.getBrandId(),brand);
}
result.setBrands(brands);
/**
* 分类
*/
Terms catalog_agg = aggregations.get("catalog_agg");
List<SearchResult.CatalogVo> catalogs =new ArrayList<>();
for(Terms.Bucket bucket:catalog_agg.getBuckets()){
SearchResult.CatalogVo catalog = new SearchResult.CatalogVo();
catalog.setCatalogId(bucket.getKeyAsNumber().longValue());
//分类名
Terms catalog_name_agg = bucket.getAggregations().get("catalog_name_agg");
catalog.setCatalogName(catalog_name_agg.getBuckets().get(0).getKeyAsString());
catalogs.add(catalog);
}
result.setCatalogs(catalogs);
/**
* 当前商品涉及到的所有属性信息
*/
List<SearchResult.AttrVo> attrVos = new ArrayList<>();
HashMap<Long,SearchResult.AttrVo> attrVoHashMap = new HashMap<>();
//获取属性信息的聚合
Nested attr_agg = aggregations.get("attr_agg");
Terms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg");
for (Terms.Bucket bucket : attr_id_agg.getBuckets()) {
SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
//1、得到属性的id
long attrId = bucket.getKeyAsNumber().longValue();
attrVo.setAttrId(attrId);
//2、得到属性的名字
Terms attr_name_agg = bucket.getAggregations().get("attr_name_agg");
String attrName = attr_name_agg.getBuckets().get(0).getKeyAsString();
attrVo.setAttrName(attrName);
//3、得到属性的所有值
Terms attr_value_agg = bucket.getAggregations().get("attr_value_agg");
List<String> attrValues = attr_value_agg.getBuckets().stream().map(item -> item.getKeyAsString()).collect(Collectors.toList());
attrVo.setAttrValue(attrValues);
attrVos.add(attrVo);
/*
这里用hashMap存一下属性,就不用远程调用了,太麻烦了
*/
attrVoHashMap.put(attrId,attrVo);
}
result.setAttrs(attrVos);
//6、构建面包屑导航
if (searchParam.getAttrs() != null && !searchParam.getAttrs().isEmpty()) {
List<SearchResult.NavVo> collect = searchParam.getAttrs().stream().map(attr -> {
//1、分析每一个attrs传过来的参数值
SearchResult.NavVo navVo = new SearchResult.NavVo();
String[] s = attr.split("_");
navVo.setNavValue(s[1]);
navVo.setNavId(Long.parseLong(s[0]));
navVo.setParamName("attrs");
String navName;
if (!StringUtils.isEmpty(navName=attrVoHashMap.get(Long.parseLong(s[0])).getAttrName())) {
navVo.setNavName(navName);
} else {
navVo.setNavName(s[0]);
}
return navVo;
}).collect(Collectors.toList());
result.setNavs(collect);
}
if(searchParam.getBrandId()!=null && !searchParam.getBrandId().isEmpty()){
List<SearchResult.NavVo> navs = result.getNavs();
for (Long brandId:searchParam.getBrandId()) {
SearchResult.NavVo navVo = new SearchResult.NavVo();
navVo.setParamName("brandId");
navVo.setNavId(brandId);
navVo.setNavName("品牌");
String navValue;
if (!StringUtils.isEmpty(navValue=brandVoHashMap.get(brandId).getBrandName())) {
navVo.setNavValue(navValue);
} else {
navVo.setNavValue(String.valueOf(brandId));
}
navs.add(0,navVo);
}
}
System.out.println("searchResult:");
System.out.println(result);
return result;
}
面包屑导航
<!-- 遍历面包屑功能 -->
<a href="/static/search/#"
th:href="${'javascript:removeNav("'+nav.paramName+'","'+nav.navId+'",'+'"'+nav.navValue+'")'}"
th:each="nav:${result.navs}">
<span th:text="${nav.navName}"></span>:<span th:text="${nav.navValue}"></span> x</a>
result中封装了navs,前端依据navs来渲染,可以直观地看到筛选条件。
(不封装navs,前端通过解析url中的param,应该也能实现)
为了封装navs,在聚合分析时,顺便将聚合结果存储在hashMap中,以便取值,避免调用远程服务
移除一个筛选条件时,将url中对应的param移除即可
url中带有中文时,浏览器url会解码中文。因此 进行正则匹配时,也要解码paramVal
function removeNav(paramName,navId,navValue){
if(paramName==="attrs"){
location.href = removeParamVal(paramName,navId+"_"+navValue);
}else{
location.href = removeParamVal(paramName,navId);
}
}
function removeParamVal(paramName,paramVal) {
var oUrl = location.href.toString();
//处理url编码中文
const encodedVal = encodeURIComponent(paramVal);
var re = eval('/&?'+'('+ paramName +'='+ encodedVal+ ')/gi');
var nUrl = oUrl.replace(re,"");
console.log(nUrl);
return nUrl?nUrl:oUrl;
}
条件筛选
将选中的条件 进行拼串或替换
function searchByKeyword() {
//搜索时,清除拼串
location.href = "http://search.gulimall.com/list.html?" + "keyword=" + $("#keyword_input").val();
}
function searchProducts(name, value,forceAdd) {
//原來的页面
location.href = replaceParamVal(location.href,name,value,forceAdd)
}
function replaceParamVal(url, paramName, replaceVal,forceAdd) {
var oUrl = url.toString();
var nUrl;
if (oUrl.indexOf(paramName) != -1) {
if( forceAdd ) {
if (oUrl.indexOf("?") != -1) {
nUrl = oUrl + "&" + paramName + "=" + replaceVal;
} else {
nUrl = oUrl + "?" + paramName + "=" + replaceVal;
}
} else {
var re = eval('/(' + paramName + '=)([^&]*)/gi');
nUrl = oUrl.replace(re, paramName + '=' + replaceVal);
}
} else {
if (oUrl.indexOf("?") != -1) {
nUrl = oUrl + "&" + paramName + "=" + replaceVal;
} else {
nUrl = oUrl + "?" + paramName + "=" + replaceVal;
}
}
return nUrl;
}
其余内容,不做赘述