@Controller
public class SearchController {
@Autowired
MallSearchService mallSearchService;
@GetMapping("/list.html")
public String listPage(SearchParam param, Model model){
SearchResult result = mallSearchService.search(param);
model.addAttribute("result",result);
return "list";
}
}
/**
* 封装页面所有可能传递过来的查询条件
*/
@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=0/1是否有货
* skuPrice 1_500/_500/500_价格区间
* brandId=1 品牌Id
* catalog3Id
* attrs=8寸
*/
private Integer haStock;//是否只显示有货
private String skuPrice;//价格区间
private List<Long> brandId;
private List<String> attrs;//按照属性进行筛选
private Integer pageNum;//页码
}
@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;
}
@Data
public static class AttrVo{
private Long attrId;
private String attrName;
private List<String> attrValues;
}
}
接口
public interface MallSearchService {
/**
* 根据页面传来的检索条件去es中检索出所有商品并过滤聚合出相应的数据
* @param param
* @return
*/
SearchResult search(SearchParam param);
}
实现类
先在kibanna中用es的DSL测试
search.gmall.com/list.html?keyword=华为&catalogId=225
分类、属性、价格这些不需要参与评分的写在filter里。(也可以在must里match后面接着term,但是既然不需要评分,就可以写在filter中)
search.gmall.com/list.html?keyword=华为&catalogId=225&brandId=1&brandId=2&brandId=9
brandId是个数组,一个属性、多个值,用terms
search.gmall.com/list.html?keyword=华为&catalogId=225&brandId=1&brandId=2&brandId=9&attrs=1_5.56&attrs=2_白色:蓝色
attrs是nested的(嵌入式的),查询的时候要用嵌入式的查询语句
如果再查16号属性,还得再写一个nested哦
search.gmall.com/list.html?keyword=华为&catalogId=225&brandId=1&brandId=2&brandId=9&attrs=1_5.56&attrs=2_白色:蓝色&hasStock=true
有无库存继续term
search.gmall.com/list.html?keyword=华为&catalogId=225&brandId=1&brandId=2&brandId=9&attrs=1_5.56&attrs=2_白色:蓝色&hasStock=true&skuPrice=_6000
按价格区间检索用range
search.gmall.com/list.html?keyword=华为&catalogId=225&brandId=1&brandId=2&brandId=9&attrs=1_5.56&attrs=2_白色:蓝色&hasStock=true&skuPrice=_6000&sort=skuPrice_desc
排序是与查询并列的,在query后面写sort
search.gmall.com/list.html?keyword=华为&catalogId=225&brandId=1&brandId=2&brandId=9&attrs=1_5.56&attrs=2_白色:蓝色&hasStock=true&skuPrice=_6000&sort=skuPrice_desc&pageNum=4
分页用from和size,"from":x,"size":y
表示从x开始查y个
9.高亮全文查询关键词
10.聚合分析(从所有查询结果中提取出相关的属性等信息)
使用agg
我们想聚合分析得到属性名字和分类名字应该怎么做呢?可以通过子聚合直接得到,子聚合会用到父聚合的结果再次进行聚合
要想对brandName进行聚合要求它的doc_values
为true
,所以需要对索引进行修改,修改方式:1.新建一个索引 2. 数据迁移
新建索引:
PUT gmall_product
{
"mappings": {
"properties": {
"skuId": {
"type": "long"
},
"spuId": {
"type": "keyword"
},
"skuTitle": {
"type": "text",
"analyzer": "ik_smart"
},
"skuPrice": {
"type": "keyword"
},
"skuImg": {
"type": "keyword"
},
"saleCount": {
"type": "long"
},
"hasStock": {
"type": "boolean"
},
"hotScore": {
"type": "long"
},
"brandId": {
"type": "long"
},
"catalogId": {
"type": "long"
},
"brandName": {
"type": "keyword"
},
"brandImg": {
"type": "keyword"
},
"catalogName": {
"type": "keyword"
},
"attrs": {
"type": "nested",
"properties": {
"attrId": {
"type": "long"
},
"attrName": {
"type": "keyword"
},
"attrValue": {
"type": "keyword"
}
}
}
}
}
}
数据迁移
POST _reindex
{
"source": {
"index": "product"
},
"dest": {
"index": "gmall_product"
}
}
GET gmall_product/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"skuTitle": "华为"
}
}
],
"filter": [
{
"term": {
"catalogId": "225"
}
},
{
"terms": {
"brandId": [
"2"
]
}
},
{
"term": {
"hasStock": "false"
}
},
{
"range": {
"skuPrice": {
"gte": 1000,
"lte": 7000
}
}
},
{
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "6"
}
}
}
]
}
}
}
}
]
}
},
"sort": [
{
"skuPrice": {
"order": "desc"
}
}
],
"from": 0,
"size": 5,
"highlight": {
"fields": {
"skuTitle": {
}
},
"pre_tags": "",
"post_tags": ""
},
"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
}
}
}
}
}
}
}
}
@Service
public class MallSearchServiceImpl implements MallSearchService {
@Autowired
RestHighLevelClient restHighLevelClient;
@Autowired
ProductFeignService productFeignService;
@Override
public SearchResult search(SearchParam param) {
SearchResult searchResult = null;
// 通过请求参数构建检索请求
SearchRequest request = bulidSearchRequest(param);
//使用restHighLevelClient执行检索请求
try {
SearchResponse searchResponse = restHighLevelClient.search(request, GmallElasticSearchConfig.COMMON_OPTIONS);
// 将es响应数据封装成返回给前端的数据
searchResult = bulidSearchResult(param,searchResponse);
} catch (IOException e) {
e.printStackTrace();
}
return searchResult;
}
/**
* 构建检索请求SearchRequest
*
* @param param
* @return
*/
private SearchRequest bulidSearchRequest(SearchParam param) {
// 用于构建DSL语句
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//1. 构建query
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
//1.1 query-bool-must
if (!StringUtils.isEmpty(param.getKeyword())) {
boolQueryBuilder.must(QueryBuilders.matchQuery("skuTitle", param.getKeyword()));
}
//1.2 query-bool-filter
//1.2.1 query-bool-filter-term catalog
if (param.getCatalog3Id() != null) {
boolQueryBuilder.filter(QueryBuilders.termQuery("catalogId", param.getCatalog3Id()));
}
//1.2.2 query-bool-filter-terms brand
if (param.getBrandId() != null && param.getBrandId().size() > 0) {
boolQueryBuilder.filter(QueryBuilders.termsQuery("brandId", param.getBrandId()));
}
//1.2.3 query-bool-filter-term hasStock
if (param.getHasStock() != null) {
boolQueryBuilder.filter(QueryBuilders.termQuery("hasStock", param.getHasStock() == 1));
}
//1.2.4 query-bool-filter-range priceRange
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice");
if (!StringUtils.isEmpty(param.getSkuPrice())) {
String[] prices = param.getSkuPrice().split("_");
if (prices.length == 1) {
if (param.getSkuPrice().startsWith("_")) {
rangeQueryBuilder.lte(Integer.parseInt(prices[0]));
} else {
rangeQueryBuilder.gte(Integer.parseInt(prices[0]));
}
} else if (prices.length == 2) {
//_6000会截取成["","6000"]
if (!prices[0].isEmpty()) {
rangeQueryBuilder.gte(Integer.parseInt(prices[0]));
}
rangeQueryBuilder.lte(Integer.parseInt(prices[1]));
}
boolQueryBuilder.filter(rangeQueryBuilder);
}
//1.2.5 query-bool-filter-nested attrs
//attrs=1_5寸:8寸&attrs=2_16G:8G
if (param.getAttrs() != null && param.getAttrs().size() > 0) {
for (String attrStr : param.getAttrs()) {
BoolQueryBuilder nestedBoolQuery = QueryBuilders.boolQuery();
String[] s = attrStr.split("_");
String attrId = s[0];//检索属性的id
String[] attrValues = s[1].split(":");
nestedBoolQuery.must(QueryBuilders.termQuery("attrs.attrId", attrId));
nestedBoolQuery.must(QueryBuilders.termsQuery("attrs.attrValue", attrValues));
//每一个都得生成一个nested查询
NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery("attrs", nestedBoolQuery, ScoreMode.None);
boolQueryBuilder.filter(nestedQueryBuilder);
}
}
//bool放入searchSourceBuilder
searchSourceBuilder.query(boolQueryBuilder);
//2. sort sort=saleCount_desc/asc
if (!StringUtils.isEmpty(param.getSort())) {
String[] sortSplit = param.getSort().split("_");
searchSourceBuilder.sort(sortSplit[0], sortSplit[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC);
}
//3. 分页 // 是检测结果分页
searchSourceBuilder.from((param.getPageNum() - 1) * EsConstant.PRODUCT_PAGESIZE);
searchSourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);
//4. 高亮highlight(有模糊匹配才需要高亮)
if (!StringUtils.isEmpty(param.getKeyword())) {
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("skuTitle");
highlightBuilder.preTags("");
highlightBuilder.postTags("");
searchSourceBuilder.highlighter(highlightBuilder);
}
//5. 聚合
//5.1 按照brand聚合
TermsAggregationBuilder brandAgg = AggregationBuilders.terms("brandAgg").field("brandId").size(20);
brandAgg.subAggregation(AggregationBuilders.terms("brandNameAgg").field("brandName").size(1));
brandAgg.subAggregation(AggregationBuilders.terms("brandImgAgg").field("brandImg").size(1));//字段存错了
searchSourceBuilder.aggregation(brandAgg);
//5.2 按照catalog聚合
TermsAggregationBuilder catalogAgg = AggregationBuilders.terms("catalogAgg").field("catalogId").size(20);
catalogAgg.subAggregation(AggregationBuilders.terms("catalogNameAgg").field("catalogName").size(1));
searchSourceBuilder.aggregation(catalogAgg);
//5.3 按照attrs聚合
NestedAggregationBuilder nestedAggregationBuilder = new NestedAggregationBuilder("attrs", "attrs");
//按照attrId聚合 //按照attrId聚合之后再按照attrName和attrValue聚合
TermsAggregationBuilder attrIdAgg = AggregationBuilders.terms("attrIdAgg").field("attrs.attrId");
attrIdAgg.subAggregation(AggregationBuilders.terms("attrNameAgg").field("attrs.attrName").size(1));
attrIdAgg.subAggregation(AggregationBuilders.terms("attrValueAgg").field("attrs.attrValue").size(50));
nestedAggregationBuilder.subAggregation(attrIdAgg);
searchSourceBuilder.aggregation(nestedAggregationBuilder);
// log.debug("构建的DSL语句 {}",searchSourceBuilder.toString());
SearchRequest request = new SearchRequest(new String[]{
EsConstant.PRODUCT_INDEX}, searchSourceBuilder);
System.out.println(request);
return request;
}
/**
* 把es响应数据封装为要返回给前端的数据
*
* @param searchResponse
* @return SearchResult
*/
private SearchResult bulidSearchResult(SearchParam searchParam,SearchResponse searchResponse) {
SearchResult result = new SearchResult();
SearchHits hits = searchResponse.getHits();
//1. 封装查询到的所有商品信息
if (hits.getHits() != null && hits.getHits().length > 0) {
List<SkuEsModel> skuEsModels = new ArrayList<>();
for (SearchHit hit : hits) {
String sourceAsString = hit.getSourceAsString();
SkuEsModel skuEsModel = JSON.parseObject(sourceAsString, SkuEsModel.class);
//如果按关键字检索了,拿到高亮的内容
if (!StringUtils.isEmpty(searchParam.getKeyword())) {
HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
String highLight = skuTitle.getFragments()[0].string();
skuEsModel.setSkuTitle(highLight);
}
skuEsModels.add(skuEsModel);
}
result.setProducts(skuEsModels);
}
//2. 封装分页信息
//2.1 当前页码
result.setPageNum(searchParam.getPageNum());
//2.2 总记录数
long total = hits.getTotalHits().value;
result.setTotal(total);
//2.3 总页码
Integer totalPages = (int) total % EsConstant.PRODUCT_PAGESIZE == 0 ?
(int) total / EsConstant.PRODUCT_PAGESIZE : (int) total / EsConstant.PRODUCT_PAGESIZE + 1;
result.setTotalPages(totalPages);
List<Integer> pageNavs = new ArrayList<>();
for (int i = 1; i <= totalPages; i++) {
pageNavs.add(i);
}
result.setPageNavs(pageNavs);
//3. 查询结果涉及到的品牌
List<SearchResult.BrandVo> brandVos = new ArrayList<>();
Aggregations aggregations = searchResponse.getAggregations();
//ParsedLongTerms用于接收terms聚合的结果,并且可以把key转化为Long类型的数据
ParsedLongTerms brandAgg = aggregations.get("brandAgg");
for (Terms.Bucket bucket : brandAgg.getBuckets()) {
//3.1 得到品牌id
Long brandId = bucket.getKeyAsNumber().longValue();
Aggregations subBrandAggs = bucket.getAggregations();
//3.2 得到品牌图片
ParsedStringTerms brandImgAgg = subBrandAggs.get("brandImgAgg");
String brandImg = brandImgAgg.getBuckets().get(0).getKeyAsString();
//3.3 得到品牌名字
Terms brandNameAgg = subBrandAggs.get("brandNameAgg");
String brandName = brandNameAgg.getBuckets().get(0).getKeyAsString();
SearchResult.BrandVo brandVo = new SearchResult.BrandVo();
brandVo.setBrandId(brandId);
brandVo.setBrandImg(brandImg);
brandVo.setBrandName(brandName);
brandVos.add(brandVo);
}
result.setBrands(brandVos);
//4. 查询涉及到的所有分类
List<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
ParsedLongTerms catalogAgg = aggregations.get("catalogAgg");
for (Terms.Bucket bucket : catalogAgg.getBuckets()) {
//4.1 获取分类id
Long catalogId = bucket.getKeyAsNumber().longValue();
Aggregations subcatalogAggs = bucket.getAggregations();
//4.2 获取分类名
ParsedStringTerms catalogNameAgg = subcatalogAggs.get("catalogNameAgg");
String catalogName = catalogNameAgg.getBuckets().get(0).getKeyAsString();
SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo(catalogId,catalogName);
catalogVos.add(catalogVo);
}
result.setCatalogs(catalogVos);
//5 查询涉及到的所有属性
List<SearchResult.AttrVo> attrVos = new ArrayList<>();
//ParsedNested用于接收内置属性的聚合
ParsedNested parsedNested = aggregations.get("attrs");
ParsedLongTerms attrIdAgg = parsedNested.getAggregations().get("attrIdAgg");
for (Terms.Bucket bucket : attrIdAgg.getBuckets()) {
//5.1 查询属性id
Long attrId = bucket.getKeyAsNumber().longValue();
Aggregations subAttrAgg = bucket.getAggregations();
//5.2 查询属性名
ParsedStringTerms attrNameAgg = subAttrAgg.get("attrNameAgg");
String attrName = attrNameAgg.getBuckets().get(0).getKeyAsString();
//5.3 查询属性值
ParsedStringTerms attrValueAgg = subAttrAgg.get("attrValueAgg");
List<String> attrValues = new ArrayList<>();
for (Terms.Bucket attrValueAggBucket : attrValueAgg.getBuckets()) {
String attrValue = attrValueAggBucket.getKeyAsString();
attrValues.add(attrValue);
List<SearchResult.NavVo> navVos = new ArrayList<>();
}
SearchResult.AttrVo attrVo = new SearchResult.AttrVo(attrId, attrName, attrValues);
attrVos.add(attrVo);
}
result.setAttrs(attrVos);
// 6. 构建面包屑导航
List<String> attrs = searchParam.getAttrs();
if (attrs != null && attrs.size() > 0) {
List<SearchResult.NavVo> navVos = attrs.stream().map(attr -> {
String[] split = attr.split("_");
SearchResult.NavVo navVo = new SearchResult.NavVo();
//6.1 设置属性值
navVo.setNavValue(split[1]);
//6.2 查询并设置属性名
try {
R r = productFeignService.attrInfo(Long.parseLong(split[0]));
if (r.getCode() == 0) {
AttrResponseVo attrResponseVo = JSON.parseObject(JSON.toJSONString(r.get("attr")), new TypeReference<AttrResponseVo>() {
});
navVo.setNavName(attrResponseVo.getAttrName());
}
} catch (Exception e) {
// log.error("远程调用商品服务查询属性失败", e);
}
//6.3 设置面包屑跳转链接
String queryString = searchParam.get_queryString();
String replace = queryString.replace("&attrs=" + attr, "").replace("attrs=" + attr + "&", "").replace("attrs=" + attr, "");
navVo.setLink("http://search.gulimall.com/search.html" + (replace.isEmpty() ? "" : "?" + replace));
return navVo;
}).collect(Collectors.toList());
result.setNavs(navVos);
}
System.out.println(result);
return result;
}