package com.pinyougou.search.service.impl;
import com.alibaba.dubbo.config.annotation.Service;
import com.alibaba.fastjson.JSON;
import com.pinyougou.es.pojo.EsItem;
import com.pinyougou.search.dao.EsItemDao;
import com.pinyougou.service.ItemSearchService;
import org.apache.commons.lang3.StringUtils;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.RangeQueryBuilder;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.SearchResultMapper;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.SearchQuery;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 商品搜素服务实现类
*/
@Service
@Transactional
public class ItemSearchServiceImpl implements ItemSearchService{
//注入elasticsearch模板对象
@Autowired
private ElasticsearchTemplate elasticsearchTemplate;
@Autowired
private EsItemDao esItemDao;
@Override
public Map<String, Object> search(Map<String, Object> params) {
try {
//定义原生搜索构建对象
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
//默认搜索全部数据(查询条件多,不适合用if..else判断,直接设为默认搜索)
builder.withQuery(QueryBuilders.matchAllQuery());
//获取搜索关键字
String keywords = (String) params.get("keywords");
//################## 关键字查询 ##################
//判断搜索关键字是否为空,不为空,根据条件搜索(会覆盖之前的数据)
if (StringUtils.isNoneBlank(keywords)){
// builder.withQuery(QueryBuilders.matchQuery("Keyword",keywords)); 高亮显示不能用这种copy域
builder.withQuery(QueryBuilders.multiMatchQuery(keywords,"title", "category", "brand", "seller"));
//构建高亮对象
HighlightBuilder.Field field = new HighlightBuilder
.Field("title") //需要高亮的字段
.preTags("<font color='red'>") //高亮前缀
.postTags("</font>") //高亮后缀
.fragmentSize(50); //截断字段
//设置高亮字段
builder.withHighlightFields(field);
}
/**
* ################## 过滤条件 ##################
* 页面传参格式:
* {keywords: "华为", category: "手机", brand: "苹果", price: "500-1000", spec: {网络: "移动3G", 机身内存: "32G"}}
* brand: "苹果"
* category: "手机"
* keywords: "华为"
* price: "500-1000"
* spec: {网络: "移动3G", 机身内存: "32G"}
*/
//定义组合查询构建对象
BoolQueryBuilder bqBuilder = QueryBuilders.boolQuery();
//1.商品分类过滤
//获取分类字符串
String category = (String) params.get("category");
//判断是否为空
if (StringUtils.isNoneBlank(category)){
//组合过滤条件(termQuery 词条查询 不分词 / (must 必须的方式))
bqBuilder.must(QueryBuilders.termQuery("category", category));
}
//2.商品品牌过滤
//获取品牌字符串
String brand = (String) params.get("brand");
//判断是否为空
if (StringUtils.isNoneBlank(brand)){
//组合过滤条件
bqBuilder.must(QueryBuilders.termQuery("brand",brand));
}
//3.商品价格过滤
String price = (String) params.get("price");
//如果不为空
if (StringUtils.isNoneBlank(price)){
//将传入的价格字符串转为数组(例:500-1000 -> [500,1000])
String[] split = price.split("-");
//定义区间查询构造对象
RangeQueryBuilder rqBuilder = QueryBuilders.rangeQuery("price");
//如果传入的为最高价格选线(例:3000-*)
if ("*".equals(split[1])){
// 添加条件 :< xxxx
rqBuilder.gt(split[0]);
}else {
//添加条件 :xxx - xxx (默认包含头尾)
rqBuilder.from(split[0],true).to(split[1],true);
}
//组合过滤条件
bqBuilder.must(rqBuilder);
}
/**
* 4.商品规格过滤
* 前端传输格式:spec: {网络: "移动3G", 机身内存: "32G"}
* 到后台转为Map集合(controller接收时设置)
*/
Map<String,String> spec = (Map<String, String>) params.get("spec");
//判断是否为空
if (spec.size() > 0 && spec != null){
//获取Key
for (String key : spec.keySet()) {
//构建嵌套Field
String field = "spec." + key + ".keyword";
/**
* 组合过滤条件(nestedQuer 嵌套查询 不分词 / (must 必须的方式))
* 参数: 嵌套路径 查询构建对象 分词模式
*/
bqBuilder.must(QueryBuilders.nestedQuery("spec",QueryBuilders.termQuery(field,spec.get(key)), ScoreMode.Max));
}
}
//原生搜索查询对象添加过滤查询
builder.withFilter(bqBuilder);
//################## 分页查询 ##################
//创建搜索查询对象
SearchQuery build = builder.build();
//获取当前页页码
Integer page = (Integer) params.get("page");
//如果为空,默认设为第一页
if (page == null){
page = 1;
}
//设置分页数据
build.setPageable(PageRequest.of(page,10));
//################## 排序查询 ##################
//获取传输的‘需要排序的字段’ 和 ‘排序方式’
String sortField = (String) params.get("sortField");
String sortValue = (String) params.get("sortValue");
//如果不为空
if (StringUtils.isNoneBlank(sortField) && StringUtils.isNoneBlank(sortValue)){
//定义排序对象
Sort sort = new Sort("ASC".equalsIgnoreCase(sortValue)? Sort.Direction.ASC : Sort.Direction.DESC,sortField);
build.addSort(sort);
}
/**
* 分页搜索,获得合计分页对象
* build:查询条件
* EsItem:转换类型
* SearchResultMapper:搜索结果转换接口(高亮内容转换)
*/
AggregatedPage<EsItem> esItems = elasticsearchTemplate.queryForPage(build, EsItem.class, new SearchResultMapper() {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> aClass, Pageable pageable) {
//定义集合封装搜索数据
List<EsItem> content = new ArrayList<>();
//迭代合计分页对象(查询到的数据)
for (SearchHit hit : searchResponse.getHits()) {
/**
* 获取JSON字符串,转为对象
* hit.getSourceAsString() : {"sepc":{"网络":"移动4G","机身内存":"64G"},"id":1369356,....
*/
EsItem esItem = JSON.parseObject(hit.getSourceAsString(), EsItem.class);
/**
* 获取标题高亮字段
* [title], fragments[[<font color='red'>华为</font> HUAWEI Mate 10 Pro 移动4G 64G]]
*/
HighlightField highlightField = hit.getHighlightFields().get("title");
//判断高亮对象是否为空
if (highlightField != null){
/**
* 获取高亮字段内容
* ighlightField.getFragments()[0].toString(): <font color='red'>华为</font> HUAWEI Mate 10 Pro 移动4G 64G
*/
String string = highlightField.getFragments()[0].toString();
//设置
esItem.setTitle(string);
}
//封装
content.add(esItem);
}
//返回
return new AggregatedPageImpl(content,pageable,searchResponse.getHits().getTotalHits());
}
});
//定义Map集合 构建返回值
Map<String,Object> map = new HashMap<>();
//设置总记录数
map.put("total",esItems.getTotalElements());
//设置分页数据
map.put("rows",esItems.getContent());
//设置总页数
map.put("totalPages",esItems.getTotalPages());
//返回
return map;
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
搜索分组
/** 商品分类分组搜索 */
private List<String> categoryGroup(String keywords){
// 定义List集合封装分组的商品分类
List<String> categoryList = new ArrayList<>();
// 判断搜索关键是否为空
if (StringUtils.isNoneBlank(keywords)){
// 1. 创建原生的搜索查询构建对象
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
// 2. 添加多条件匹配查询
builder.withQuery(QueryBuilders.multiMatchQuery(keywords,
"category","brand","title","seller"));
// 3. 创建词条分组构建对象
// categoryGroup: 分组名称,用于存储分组后的数据
// category: 根据商品分类分组(分组的Field)
TermsAggregationBuilder aggregationBuilder = AggregationBuilders
.terms("categoryGroup").field("category");
// 4. 添加分组构建对象
builder.addAggregation(aggregationBuilder);
// 5. 搜索,得到多分组封装对象
Aggregations aggregations = esTemplate.query(builder.build(),
new ResultsExtractor<Aggregations>() {
@Override
public Aggregations extract(SearchResponse sr) {
return sr.getAggregations();
}
});
// 6. 获取指定的分组结果
StringTerms terms = aggregations.get("categoryGroup");
// 6.1 迭代分组结果集合
for (StringTerms.Bucket bucket : terms.getBuckets()) {
categoryList.add(bucket.getKeyAsString());
}
}
return categoryList;
}