1. 检索
根据DSL语句构建检索条件
1.1 DSL语句
GET gulimall_product/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"skuTitle": "华为"
}
}
],
"filter": [
{
"term": {
"catalogId": "1111133"
}
},
{
"terms": {
"brandId": [
"3",
"5"
]
}
},
{
"term": {
"hasStock": true
}
},
{
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "15"
}
}
},
{
"terms": {
"attrs.attrValue": [
"aaa",
"白色",
"OCE-AN10"
]
}
}
]
}
}
}
}
]
}
},
"sort": [
{
"skuPrice": {
"order": "desc"
}
}
],
"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": {
"terms": {
"field": "attrs.attrId",
"size": 10
},
"aggs": {
"attr_name_id": {
"terms": {
"field": "attrs.attrName",
"size": 10
}
}
}
}
}
}
},
"from": 0,
"size": 2,
"highlight": {
"fields": {"skuTitle": {}},
"pre_tags": "<b style='color:red'>",
"post_tags": "</b>"
}
}
1.2 java代码
在java中使用es检索,比较重要的一个对象是SearchSourceBuilder
1.2.1 不带聚合条件
import com.bjc.gulimall.search.config.GulimallElasticSearchConfig;
import com.bjc.gulimall.search.constant.EsConstant;
import com.bjc.gulimall.search.service.MallSearchService;
import com.bjc.gulimall.search.vo.SearchParam;
import com.bjc.gulimall.search.vo.SearchResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
@Service
@Slf4j
public class MallSearchServiceImpl implements MallSearchService {
@Autowired
private RestHighLevelClient client;
/*
* 根据检索参数返回检索结果
* SearchResult 包含页面需要的所有信息
* */
@Override
public SearchResult search(SearchParam searchParam) {
SearchResult result = null;
/*
* 1. 动态构建出查询需要的DSL语句
* */
// 1.1 创建检索请求对象
// SearchRequest searchRequest = new SearchRequest();
SearchRequest searchRequest = buildSearchRequest(searchParam);
try {
/* 2. 执行检索请求 */
SearchResponse response = client.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
/* 3. 分析响应数据,封装成指定的数据格式 */
result = buildSearchResult(response);
} catch (Exception e) {
log.error("动态构建出查询需要的DSL语句出错,原因:",e);
}
return null;
}
/* 根据响应构建返回结果对象 */
private SearchResult buildSearchResult(SearchResponse response) {
return null;
}
/* 准备检索请求
* 关键字模糊匹配、过滤(按照属性、分类、品牌、价格区间),排序,分页,高亮,聚合分析
* */
private SearchRequest buildSearchRequest(SearchParam searchParam) {
// 用于构建DSL语句
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
/*
* 1. 查询部分DSL构建
* */
// 1.1 构建queryBuilder对象。复杂的query是通过bool组合检索的,因此需要构建BoolQueryBuilder对象
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 1.2 构建全文检索条件must,关键字模糊匹配. 如果页面有关键字搜索,才进行全文模糊检索
if(StringUtils.isNotEmpty(searchParam.getKeyword())){
// 将按照skuTitle全文检索的条件封装到boolQuery中
boolQuery.must(QueryBuilders.matchQuery("skuTitle",searchParam.getKeyword()));
}
// 1.3 构建过滤filter条件
// 1.3.1 按照三级分类id过滤
if(null != searchParam.getCatalog3Id()){
// 三级分类id是精确匹配,用term
boolQuery.filter(QueryBuilders.termQuery("catalogId",searchParam.getCatalog3Id()));
}
// 1.3.2 按照品牌ID过滤(支持多选)
if(!CollectionUtils.isEmpty(searchParam.getBrandId())){
boolQuery.filter(QueryBuilders.termsQuery("brandId",searchParam.getBrandId()));
}
// 1.3.3 按照所有指定的属性进行查询
/*
* {
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "15"
}
}
},
{
"terms": {
"attrs.attrValue": [
"aaa",
"白色",
"OCE-AN10"
]
}
}
]
}
}
}
}
* */
if(!CollectionUtils.isEmpty(searchParam.getAttrs())){
searchParam.getAttrs().forEach(attrStr -> {
BoolQueryBuilder nestedBoolQuery = QueryBuilders.boolQuery();
// attrs=1_5寸:8寸&attrs=2_8G:16G
String[] s = attrStr.split("_");
// 检索的属性ID
String attrId = s[0];
// 检索的属性ID对应的属性值的数组
String[] attrValues = s[1].split(":");
nestedBoolQuery.must(QueryBuilders.termQuery("attrs.attrId",attrId));
nestedBoolQuery.must(QueryBuilders.termsQuery("attrs.attrValue",attrValues));
// 每一个属性都需要生成一个嵌入式查询
// ScoreMode.None 表示不参与评分
NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery("attrs", nestedBoolQuery, ScoreMode.None);
boolQuery.filter(nestedQueryBuilder);
});
}
// 1.3.4 按照库存是否有进行查询
boolQuery.filter(QueryBuilders.termQuery("hasStock",searchParam.getHasStock() == 1));
// 1.3.5 按照价格区间进行查询
if(StringUtils.isNotEmpty(searchParam.getSkuPrice())){
// 1_500/_500/500_
/*
* "range": {
"skuPrice": {
"gte": 0.0,
"lte": 20000.0
}
}
* */
int g = 0;
int l = 0;
// 1)构建rangeQueryBuilder对象
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice");
// 2)解析查询参数
String[] s = searchParam.getSkuPrice().split("_");
if(s.length == 2){
rangeQueryBuilder.gte(s[0]).lte(s[1]);
} else if(s.length == 1){ // 否则就是单值
if(searchParam.getSkuPrice().startsWith("_")){
rangeQueryBuilder.lte(s[0]);
} else {
rangeQueryBuilder.gte(s[0]);
}
}
boolQuery.filter(rangeQueryBuilder);
}
// 将所有的查询条件封装到sourceBuilder
sourceBuilder.query(boolQuery);
/*
* 2. 排序、分页、高亮
* */
// 2.1 排序
if(StringUtils.isNotEmpty(searchParam.getSort())){
String sort = searchParam.getSort(); // &sort = hotScore_asc/desc
String[] s = sort.split("_");
// SortOrder sortOrder = s[1].equalsIgnoreCase("asc")?SortOrder.ASC:SortOrder.DESC;
sourceBuilder.sort(s[0], SortOrder.fromString(s[1]));
}
// 2.2 分页 from = (当前页-1) * 每页显示条数
sourceBuilder.from((searchParam.getPageNum()-1) * EsConstant.PRODUCT_PAGESIZE) // 第几页
.size(EsConstant.PRODUCT_PAGESIZE); // 每页显示记录数
// 2.3 高亮 只有关键字查询才高亮
if(StringUtils.isNotEmpty(searchParam.getKeyword())){
HighlightBuilder highlight = new HighlightBuilder();
// 指定需要高亮的字段是哪个
highlight.field("skuTitle");
// 指定高亮前置标签
highlight.preTags("<b style='color:red'>");
// 指定后缀标签
highlight.postTags("</b>");
sourceBuilder.highlighter(highlight);
}
/*
* 3. 聚合分析
* */
System.out.println("构建的DSL:" + sourceBuilder.toString());
SearchRequest searchRequest = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX}, sourceBuilder);
return searchRequest;
}
}
测试:
1)带keyword
控制台结果:
将该结果复制到Kibana上检索,如图:
2)添加属性和分类查询条件
1.2.2 聚合分析的构建
/*
* 3. 聚合分析
* */
// 3.1 品牌聚合
AggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg") // 参数为聚合的名称
.field("brandId") // 要聚合的字段
.size(50); // 查询并显示多少条记录
// 3.1.1 子聚合
brand_agg.subAggregation(AggregationBuilders.terms("brand_name_agg")
.field("brandName").size(1));
// 3.1.2 子聚合
brand_agg.subAggregation(AggregationBuilders.terms("brand_img_agg")
.field("brandImg").size(1));
// 3.1.3 添加聚合条件到sourceBuilder
sourceBuilder.aggregation(brand_agg);
// 3.2 分类聚合
AggregationBuilder catalog_agg = AggregationBuilders.terms("catalogAgg").field("catalogId").size(50);
catalog_agg.subAggregation(AggregationBuilders.terms("catalogNameAgg").field("catalogName").size(1));
sourceBuilder.aggregation(catalog_agg);
// 3.3 属性聚合(嵌入式聚合)
AggregationBuilder attrAgg = AggregationBuilders.nested("attrAgg", "attrs");
// 3.3.1 聚合出当前所有的AttrId
AggregationBuilder attrIdAggregation = AggregationBuilders.terms("attrIdAgg").field("attrs.attrId").size(100);
// 3.3.2 聚合分析出当前attrId对应的名称
attrIdAggregation.subAggregation(AggregationBuilders.terms("attrNameIdAgg").field("attrs.attrName").size(1));
// 3.3.3 聚合分析出当前attrId对应的所有可能的属性值attrValue
attrIdAggregation.subAggregation(AggregationBuilders.terms("attrValueAgg").field("attrs.attrValue").size(50));
// 3.3.4 将attrId子聚合添加到嵌入式聚合attrAgg中
attrAgg.subAggregation(attrIdAggregation);
// 3.3.4 将嵌入式聚合attrAgg聚合条件添加到sourceBuilder中
sourceBuilder.aggregation(attrAgg);
2. 结果封装
/* 根据响应构建返回结果对象 */
private SearchResult buildSearchResult(SearchResponse response,SearchParam searchParam) {
SearchResult result = new SearchResult();
// 获取命中结果
SearchHits hits = response.getHits();
// 从名字结果中获取命中记录
SearchHit[] resultHits = hits.getHits();
// 1. 封装所有查询到的商品
List<SkuEsModel> esModels = new ArrayList<>();
if(ArrayUtils.isNotEmpty(resultHits)){
for(SearchHit hit : resultHits){
String sourceAsString = hit.getSourceAsString();
SkuEsModel esModel = JSONObject.parseObject(sourceAsString, SkuEsModel.class);
// 设置高亮
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if(!CollectionUtils.isEmpty(highlightFields)){
HighlightField skuTitle = highlightFields.get("skuTitle");
String title = skuTitle.getFragments()[0].string();
esModel.setSkuTitle(title);
}
esModels.add(esModel);
}
}
result.setProducts(esModels);
// 获取所有的聚合信息
Aggregations aggregations = response.getAggregations();
// 2. 封装当前所有商品涉及到的所有属性信息
List<SearchResult.AttrVo> attrs = new ArrayList<>();
// 获取嵌套聚合
ParsedNested parsedNested = aggregations.get("attrAgg");
// 获取属性id聚合
ParsedLongTerms attrIdAgg = parsedNested.getAggregations().get("attrIdAgg");
// 遍历属性id聚合,得到每一个属性下的属性值
attrIdAgg.getBuckets().forEach(item -> {
SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
// 获取属性id
String key = item.getKeyAsString();
attrVo.setAttrId(Long.parseLong(key));
// 获取属性名称
ParsedStringTerms attrNameTerms = item.getAggregations().get("attrNameIdAgg");
String name = attrNameTerms.getBuckets().get(0).getKeyAsString();
attrVo.setAttrName(name);
// 获取属性值
ParsedStringTerms attrValueTerms = item.getAggregations().get("attrValueAgg");
List<String> attrValues = attrValueTerms.getBuckets().stream().map(Terms.Bucket::getKeyAsString).collect(Collectors.toList());
attrVo.setAttrValue(attrValues);
// 将每一个属性对象添加到属性集合
attrs.add(attrVo);
});
result.setAttrs(attrs);
// 3. 封装当前商品所涉及到的品牌信息
List<SearchResult.BrandVo> brandVos = new ArrayList<>();
ParsedLongTerms brandAgg = aggregations.get("brand_agg");
brandAgg.getBuckets().forEach(bucket -> {
SearchResult.BrandVo brandVo = new SearchResult.BrandVo();
// 获取品牌id
String key = bucket.getKeyAsString();
brandVo.setBrandId(Long.parseLong(key));
// 获取品牌名称
ParsedStringTerms stringTerms = bucket.getAggregations().get("brand_name_agg");
String name = stringTerms.getBuckets().get(0).getKeyAsString();
brandVo.setBrandName(name);
// 获取品牌图片
ParsedStringTerms imgTerms = bucket.getAggregations().get("brand_img_agg");
String img = imgTerms.getBuckets().get(0).getKeyAsString();
brandVo.setBrandImg(img);
brandVos.add(brandVo);
});
result.setBrandVos(brandVos);
// 4. 封装当前商品所属的分类
List<SearchResult.CatalogVo> catalogs = new ArrayList<>();
// 获取分类聚合信息(因为分类id是long类型的,所以用Aggregations接口的实现类ParsedLongTerms)
ParsedLongTerms catalogAgg = aggregations.get("catalogAgg");
List<? extends Terms.Bucket> buckets = catalogAgg.getBuckets();
buckets.forEach(bucket -> {
SearchResult.CatalogVo cVo = new SearchResult.CatalogVo();
// bucket的key就是分类的id
String key = bucket.getKeyAsString();
cVo.setCatalogId(Long.parseLong(key));
// 获取分类名称 分类的名称是分类id的子聚合
ParsedStringTerms nameAgg = bucket.getAggregations().get("catalogNameAgg");
String name = nameAgg.getBuckets().get(0).getKeyAsString();
cVo.setCatalogName(name);
catalogs.add(cVo);
});
result.setCatalogs(catalogs);
// 5. 设置分页信息
long total = hits.getTotalHits().value;
// 5.1 页码
result.setPageNum(searchParam.getPageNum());
// 5.2 总记录数
result.setTotal(total);
// 5.3 总页码
int pages = (int)(total/EsConstant.PRODUCT_PAGESIZE + (total%EsConstant.PRODUCT_PAGESIZE > 0 ? 1 : 0));
result.setTotalPages(pages);
return result;
}