商品检索------ElasticSearch(搜索)
1. 检索条件&排序条件分析
- 全文检索:skuTitle -> keyword
- 排序:saleCount(销量)、hotScore(热度分)、skuPrice(价格)
- 过滤:hasStock、skuPrice区间、brandId、catalog3Id、attrs
- 聚合:attrs
- 完整查询参数
- keyword=小米&sort=saleCount_desc/asc&hasStock=0/1&skuPrice=400_1900&brandId=1&catalog3Id=1&attrs=1_3G:4G:5G&attrs=2_骁龙845&attrs=4_高清屏
2. DSL分析
检索时需要进行:模糊匹配、过滤(按照属性,分类,品牌,价格区间,库存)、排序、分页、高亮、聚合分析 。
GET gulimall_product/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"skuTitle": "华为"
}
}
],
"filter": [
{
"term": {
"catalogId": "225"
}
},
{
"terms": {
"brandId": [
"4"
]
}
},
{
"term": {
"hasStock": "false"
}
},
{
"range": {
"skuPrice": {
"gte": 1000,
"lte": 7000
}
}
},
{
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "1"
}
}
}
]
}
}
}
}
]
}
},
"sort": [
{
"skuPrice": {
"order": "desc"
}
}
],
"from": 0,
"size": 5,
"highlight": {
"fields": {"skuTitle": {}},
"pre_tags": "<b style='color:red'>",
"post_tags": "</b>"
},
"aggs": {
"brandAgg": {
"terms": {
"field": "brandId",
"size": 10
},
"aggs": {
"brandNameAgg": {
"terms": {
"field": "brandName",
"size": 10
}
},
"brandImgAgg": {
"terms": {
"field": "brandImg",
"size": 10
}
}
}
},
"catalogAgg":{
"terms": {
"field": "catalogId",
"size": 10
},
"aggs": {
"catalogNameAgg": {
"terms": {
"field": "catalogName",
"size": 10
}
}
}
},
"attrs":{
"nested": {
"path": "attrs"
},
"aggs": {
"attrIdAgg": {
"terms": {
"field": "attrs.attrId",
"size": 10
},
"aggs": {
"attrNameAgg": {
"terms": {
"field": "attrs.attrName",
"size": 10
}
}
}
}
}
}
}
}
3. 检索代码
3.1. 请求参数和返回结果封装
package com.atguigu.gulimall.search.vo;
import lombok.Data;
import java.util.List;
/**
* 封装页面所有可能传递过来的查询条件
* catalog3Id=225&keyword=小米&sort=saleCount_asc
*/
@Data
public class SearchParam {
private String keyword;//页面传递过来的全文匹配关键字
private Long catalog3Id;//三级分类id
/**
* sort=saleCount_asc/desc
* sort=skuPrice_asc/desc
* sort=hotScore_asc/desc
*/
private String sort;//排序条件
/**
* 好多的过滤条件
* hasStock(是否有货)、skuPrice区间、brandId、catalog3Id、attrs
* hasStock=0/1
* skuPrice=1_500
*/
private Integer hasStock;//是否只显示有货
private String skuPrice;//价格区间查询
private List<Long> brandId;//按照品牌进行查询,可以多选
private List<String> attrs;//按照属性进行筛选
private Integer pageNum = 1;//页码
}
package com.atguigu.gulimall.search.vo;
import com.atguigu.common.to.es.SkuEsModel;
import lombok.Data;
import java.util.List;
@Data
public class SearchResult {
/**
* 查询到的商品信息
*/
private List<SkuEsModel> products;
private Integer pageNum;//当前页码
private Long total;//总记录数
private Integer totalPages;//总页码
private List<BrandVo> brands;//当前查询到的结果,所有涉及到的品牌
private List<CatalogVo> catalogs;//当前查询到的结果,所有涉及到的分类
private List<AttrVo> attrs;//当前查询到的结果,所有涉及到的属性
//=====================以上是返给页面的信息==========================
@Data
public static class BrandVo{
private Long brandId;
private String brandName;
private String brandImg;
}
@Data
public static class CatalogVo{
private Long catalogId;
private String catalogName;
private String brandImg;
}
@Data
public static class AttrVo{
private Long attrId;
private String attrName;
private List<String> attrValue;
}
}
3.2. 业务逻辑
3.2.1. SearchController
/**
* 自动将页面提交过来的所有请求查询参数封装成指定的对象
*
* @return
*/
@GetMapping("/list.html")
public String listPage(SearchParam searchParam, Model model) {
SearchResult result = mallSearchService.search(searchParam);
System.out.println("====================" + result);
model.addAttribute("result", result);
return "list";
}
3.2.2. MallSearchServiceImpl
@Override
public SearchResult search(SearchParam param) {
SearchResult result = null;
// 1.准备检索请求
SearchRequest searchRequest = buildSearchRequest(param);
try {
// 2.执行检索请求
SearchResponse response = esClient.search(searchRequest, COMMON_OPTIONS);
// 3.解析响应数据并封装需要返回的页面数据
result = buildSearchResult(param, response);
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
/**
* 准备检索请求
* @return
*/
private SearchRequest buildSearchRequest(SearchParam param) {
// 构建DSL语句
SearchSourceBuilder builder = new SearchSourceBuilder();
/**
* 查询:模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存)
*/
// 1.构建bool-query
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 1.1.bool-must 模糊匹配
String keyword = param.getKeyword();
if(StringUtils.isNotEmpty(keyword)){
boolQuery.must(QueryBuilders.matchQuery("skuTitle", keyword));
}
// 1.2.bool-filter 过滤条件
// 1.2.1.bool-filter-catalogId
Long catalog3Id = param.getCatalog3Id();
if(catalog3Id != null){
boolQuery.filter(QueryBuilders.termQuery("catalogId", catalog3Id));
}
// 1.2.2.bool-filter-brandId
List<Long> brandIds = param.getBrandId();
if(brandIds != null && brandIds.size() > 0){
boolQuery.filter(QueryBuilders.termsQuery("brandId", brandIds));
}
// 1.2.3.bool-filter-hasStock = 0/1
Integer hasStock = param.getHasStock();
if(hasStock != null){
boolQuery.filter(QueryBuilders.termQuery("hasStock", hasStock));
}
// 1.2.4.bool-filter-skuPrice = 1_500/_500/500_
String skuPrice = param.getSkuPrice();
if(StringUtils.isNotEmpty(skuPrice)){
// 将skuPrice以_分隔
String[] split = skuPrice.split("_");
String lt = null, gt = null;
if(split.length == 2){
// case1: 1_500
lt = split[1];
// case2: _500
if(StringUtils.isNotBlank(split[0])){
gt = split[0];
}
}else {
// case3: 500_
gt = split[0];
}
// 构建rangeQueryBuilder
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice");
if(StringUtils.isNotEmpty(lt)){
rangeQueryBuilder.lt(Long.parseLong(lt));
}
if(StringUtils.isNotEmpty(gt)){
rangeQueryBuilder.gt(Long.parseLong(gt));
}
// 放入boolQuery
boolQuery.filter(rangeQueryBuilder);
}
// 1.2.5.bool-filter-nested-attrs = 2_5寸:6寸&2_16G:8G
List<String> attrs = param.getAttrs();
if(attrs != null && attrs.size() > 0){
for (String attrStr : attrs) {
BoolQueryBuilder nestedBoolQuery = QueryBuilders.boolQuery();
// attrStr = 2_5寸:6寸
String[] s = attrStr.split("_");
String attrId = s[0];
// s[1] = 5寸:6寸
String[] attrValues = s[1].split(":");
nestedBoolQuery.must(QueryBuilders.termQuery("attrs.attrId", attrId));
nestedBoolQuery.must(QueryBuilders.termsQuery("attrs.attrValue", attrValues));
// 每一个必须都得生成nested查询
NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("attrs", nestedBoolQuery, ScoreMode.None);
boolQuery.filter(nestedQuery);
}
}
// 把以上所有查询条件封装
builder.query(boolQuery);
/**
* 处理:排序,分页,高亮
*/
// 2.1.排序:sort = hotScore_asc/desc
String sort = param.getSort();
if(StringUtils.isNotEmpty(sort)){
String[] s = sort.split("_");
builder.sort(s[0], s[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC);
}
// 2.2.分页
builder.from((param.getPageNum() - 1) * EsConstant.PRODUCT_PAGE_SIZE);
builder.size(EsConstant.PRODUCT_PAGE_SIZE);
// 2.3.高亮
if(StringUtils.isNotEmpty(keyword)){
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("skuTitle");
highlightBuilder.preTags("<b style='color:red'>");
highlightBuilder.postTags("</b>");
builder.highlighter(highlightBuilder);
}
/**
* 响应:聚合分析
*/
// 3.1.聚合brandAgg
TermsAggregationBuilder brandAgg = AggregationBuilders.terms("brandAgg");
brandAgg.field("brandId").size(50);
// 3.1.1.子聚合brandNameAgg
brandAgg.subAggregation(AggregationBuilders.terms("brandNameAgg").field("brandName").size(1));
// 3.1.2.子聚合brandImgAgg
brandAgg.subAggregation(AggregationBuilders.terms("brandImgAgg").field("brandImg").size(1));
// 3.1.3.整合brandAgg
builder.aggregation(brandAgg);
// 3.2.聚合catalogAgg
TermsAggregationBuilder catalogAgg = AggregationBuilders.terms("catalogAgg");
// 3.2.1.聚合catalogId
catalogAgg.field("catalogId").size(20);
// 3.2.2.子聚合catalogNameAgg
catalogAgg.subAggregation(AggregationBuilders.terms("catalogNameAgg").field("catalogName").size(1));
// 3.2.3.整合catalogAgg
builder.aggregation(catalogAgg);
// 3.3.聚合attrsAgg
TermsAggregationBuilder attrIdAgg = AggregationBuilders.terms("attrIdAgg").field("attrs.attrId");
// attrIdAgg的子聚合attrNameAgg
TermsAggregationBuilder attrNameAgg = AggregationBuilders.terms("attrNameAgg").field("attrs.attrName").size(1);
attrIdAgg.subAggregation(attrNameAgg);
// attrIdAgg的子聚合attrValueAgg
TermsAggregationBuilder attrValueAgg = AggregationBuilders.terms("attrValueAgg").field("attrs.attrValue").size(50);
attrIdAgg.subAggregation(attrValueAgg);
// attrsAgg的子聚合attrIdAgg
NestedAggregationBuilder attrsAgg = AggregationBuilders.nested("attrsAgg", "attrs");
attrsAgg.subAggregation(attrIdAgg);
// 整合attrsAgg
builder.aggregation(attrsAgg);
System.out.println("DSL:" + builder.toString());
SearchRequest request = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX}, builder);
return request;
}
/**
* 构建页面响应数据
* @param response
* @return
*/
private SearchResult buildSearchResult(SearchParam param, SearchResponse response) {
SearchResult result = new SearchResult();
// 1.products
SearchHits hits = response.getHits();
SearchHit[] searchHits = hits.getHits();
List<SkuEsModel> esModels = Collections.emptyList();
if (searchHits != null && searchHits.length > 0) {
esModels = Arrays.stream(searchHits).map(searchHit -> {
String source = searchHit.getSourceAsString();
SkuEsModel esModel = JSON.parseObject(source, SkuEsModel.class);
if (StringUtils.isNotEmpty(param.getKeyword())) {
HighlightField skuTitle = searchHit.getHighlightFields().get("skuTitle");
String string = skuTitle.getFragments()[0].string();
esModel.setSkuTitle(string);
}
return esModel;
}).collect(Collectors.toList());
}
result.setProducts(esModels);
Aggregations aggregations = response.getAggregations();
// 2.brands
ParsedLongTerms brandAgg = aggregations.get("brandAgg");
List<BrandVo> brandVos = brandAgg.getBuckets().stream().map(bucket -> {
BrandVo brandVo = new BrandVo();
// brandId
brandVo.setBrandId(bucket.getKeyAsNumber().longValue());
Aggregations bucketAggs = bucket.getAggregations();
// brandName
ParsedStringTerms brandName = bucketAggs.get("brandNameAgg");
brandVo.setBrandName(brandName.getBuckets().get(0).getKeyAsString());
// brandImg
ParsedStringTerms brandImg = bucketAggs.get("brandImgAgg");
brandVo.setBrandImg(brandImg.getBuckets().get(0).getKeyAsString());
return brandVo;
}).collect(Collectors.toList());
result.setBrands(brandVos);
// 3.catalogs
ParsedLongTerms catalogAgg = aggregations.get("catalogAgg");
List<CatalogVo> catalogVos = catalogAgg.getBuckets().stream().map(bucket -> {
CatalogVo catalogVo = new CatalogVo();
catalogVo.setCatalogId(Long.parseLong(bucket.getKeyAsString()));
ParsedStringTerms catalogNameAgg = bucket.getAggregations().get("catalogNameAgg");
catalogVo.setCatalogName(catalogNameAgg.getBuckets().get(0).getKeyAsString());
return catalogVo;
}).collect(Collectors.toList());
result.setCatalogs(catalogVos);
// 4.attrs
ParsedNested attrsAgg = aggregations.get("attrsAgg");
ParsedLongTerms attrIdAgg = attrsAgg.getAggregations().get("attrIdAgg");
List<AttrVo> attrVos = attrIdAgg.getBuckets().stream().map(bucket -> {
AttrVo attrVo = new AttrVo();
// attrId
attrVo.setAttrId(Long.parseLong(bucket.getKeyAsString()));
Aggregations bucketAggregations = bucket.getAggregations();
// attrName
ParsedStringTerms attrNameAgg = bucketAggregations.get("attrNameAgg");
attrVo.setAttrName(attrNameAgg.getBuckets().get(0).getKeyAsString());
// attrValues
ParsedStringTerms attrValueAgg = bucketAggregations.get("attrValueAgg");
List<String> attrValue = attrValueAgg.getBuckets().stream()
.map(MultiBucketsAggregation.Bucket::getKeyAsString).collect(Collectors.toList());
attrVo.setAttrValues(attrValue);
return attrVo;
}).collect(Collectors.toList());
result.setAttrs(attrVos);
// 5.分页信息:pageNum, total, totalPages
long total = hits.getTotalHits().value;
result.setTotal(total);
// 计算totalPages
int totalPages = (int) (total / EsConstant.PRODUCT_PAGE_SIZE + (total % EsConstant.PRODUCT_PAGE_SIZE == 0 ? 0 : 1));
result.setTotalPages(totalPages);
result.setPageNum(param.getPageNum());
// 6.面包屑导航功能
List<String> _attrs = param.getAttrs();
if(_attrs != null && _attrs.size() > 0){
List<NavVo> navVos = _attrs.stream().map(attr -> {
// attrs = 2_5寸:6存
String[] split = attr.split("_");
// 设置属性值
NavVo navVo = new NavVo();
navVo.setNavValue(split[1]);
// 远程调用通过attrId获取attrName
try {
R r = productFeignService.getAttrsInfo(Long.parseLong(split[0]));
result.getAttrIds().add(Long.valueOf(split[0]));
if(r.getCode() == 0){
AttrResponseVo attrResponseVo = r.getData("attr", new TypeReference<AttrResponseVo>() {
});
navVo.setNavName(attrResponseVo.getAttrName());
}else {
navVo.setNavName(split[0]);
}
} catch (NumberFormatException e) {
e.printStackTrace();
}
// 取消面包屑后,跳转到取消后的地方,将原url改为目标url
String replace = replaceQueryString(param, attr, "attrs");
navVo.setLink("http://search.gulimall.com/list.html" + (replace.isEmpty() ? "" : "?" + replace));
return navVo;
}).collect(Collectors.toList());
result.setNavs(navVos);
}
// 品牌 条件筛选
List<Long> brandIds = param.getBrandId();
if(brandIds != null && brandIds.size() > 0){
List<NavVo> navs = result.getNavs();
NavVo navVo = new NavVo();
navVo.setNavName("品牌");
// 远程查询所有品牌
R r = productFeignService.brandsInfo(brandIds);
if(r.getCode() == 0){
List<com.cwh.search.vo.BrandVo> brand = r.getData("brand", new TypeReference<List<com.cwh.search.vo.BrandVo>>() {
});
StringBuffer buffer = new StringBuffer();
String replace = "";
for (com.cwh.search.vo.BrandVo brandVo : brand) {
buffer.append(brandVo.getName() + ";");
replace = replaceQueryString(param, brandVo.getBrandId().toString(), "brandId");
}
navVo.setNavValue(buffer.toString());
navVo.setLink("http://search.gulimall.com/list.html?" + (replace.isEmpty() ? "" : "?" + replace));
}
navs.add(navVo);
}
// TODO 分类 条件筛选
return result;
}
private String replaceQueryString(SearchParam param, String value, String key) {
String queryString = param.get_queryString();
String encode = null;
try {
encode = URLEncoder.encode(value, "UTF-8");
encode = encode.replace("+", "%20");//浏览器对空格编码和java不一样
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return queryString.replace("&" + key + "=" + encode, "");
}