商品的搜索(ES)
当天的git地址:ES商品搜索
一、关键字搜索(搜索框)
1、创建一个接口里面传入map(里面封装的是查询条件),返回也是一个map(里面是封装的查询上来的数据)
/**
* @Description:
* @Version: V1.0
*/
public interface SearchService {
/**
* 全文检索 万能型
* @param paramMap
* @return
*/
Map search(Map<String,String> paramMap);
}
-
1、定义一个封装查询结果的map,
-
2、对条件的非空判断
-
3、构建查询条件封装对象
-
4、组合条件对象
-
5、判断搜索框里不为空 根据搜索分词查询
-
6、使用布尔查询查询,must查询。里面拼接的是match(分词查询)根据索引名称(name) 2、分词查询(keywords) 3、交集。
-
7、绑定条件对象
-
8、开启查询:(1)、条件构建对象 (2)、查询操作实体类 (3)、查询结果操作对象
-
9、重写结果操作对象接口:获取ES里面的数据封装到list,并把(1)、ES数据集合 (2)、分页总数据 (3)、数据的条目数 (4)、聚合数据,封装到他的实现类中返回去
-
10、把分页的总条数、分页的总页数、查询上来的结果。封装到定义好的结果集map返回上去
/** * 全文检索 * * @param paramMap * @return */ @Override public Map search(Map<String, String> paramMap) { //接收查询上来的数据 Map<String, Object> resultMap = new HashMap<>(); //条件不为空 if (paramMap != null) { //构建查询条件封装对象 NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder(); //组合条件对象 BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); //1、判断搜索框里不为空 根据搜索分词查询 if (!StringUtils.isEmpty(paramMap.get("keywords"))) { //1、索引名称 2、分词查询 3、交集 boolQuery.must(QueryBuilders.matchQuery("name", paramMap.get("keywords")).operator(Operator.AND)); } //绑定条件对象 nativeSearchQueryBuilder.withQuery(boolQuery); //开启查询 /** * 第一个参数: 条件构建对象 * 第二个参数: 查询操作实体类 * 第三个参数: 查询结果操作对象 */ //封装查询结果 AggregatedPage<SkuInfo> skuInfos = esTemplate.queryForPage(nativeSearchQueryBuilder.build(), SkuInfo.class, new SearchResultMapper() { @Override public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> aClass, Pageable pageable) { ArrayList<T> list = new ArrayList<>(); //获取命中对象 SearchHits hits = searchResponse.getHits(); if (hits != null) { //获取ES每条数据 for (SearchHit hit : hits) { SkuInfo skuInfo = JSON.parseObject(hit.getSourceAsString(), SkuInfo.class); list.add((T) skuInfo); } } //1、ES数据集合 2、分页总数据 3、数据的条目数 4、聚合数据 return new AggregatedPageImpl<>(list, pageable, hits.getTotalHits(), searchResponse.getAggregations()); } }); //11. 总条数 resultMap.put("total", skuInfos.getTotalElements()); //12. 总页数 resultMap.put("totalPages", skuInfos.getTotalPages()); //13. 查询结果集合 resultMap.put("rows", skuInfos.getContent()); return resultMap; } return null; }
二、对品牌查询不分词
-
1、对条件map里的品牌进行非空判断
-
2、在条件查询使用filter连接,对brandName作用域 ,根据brand进行不分词查询
//上面代码一样
//2、对品牌进行查询
if (!StringUtils.isEmpty(paramMap.get("brand"))) {
//不分词查询
boolQuery.filter(QueryBuilders.termQuery("brandName", paramMap.get("brand")));
}
//下面代码都是一样的
三、对品牌进行分组查询
- 1、对品牌进行分组查询,参数1、自己的分组的名称 2、对那个作用域进行分组(注意分组类型必须是keyword不分词)
- 2、从返回的结果集获取分组对象,通过stream流的方式将分组的名称(key),全部封装到了list集合中返回给前端
//3、对品牌进行分组查询 聚合查询只有keyword的类型才可以
String skuBrand="skuBrand"; //参数 1、分组的名称(自己随便起) 2、分组的字段
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(skuBrand).field("brandName"));
//获取品牌分组的对象
StringTerms brandTerms = (StringTerms) skuInfos.getAggregation(skuBrand);
//通过stream流的方式获取品牌的集合
List<String> brandList = brandTerms.getBuckets().stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());
//封装品牌
resultMap.put("brandList", brandList);
四、商品的规格的查询
controller层 由于url路径上传入的参数里有加号,‘+’是特殊字符需要转换
@GetMapping
@ResponseBody
public Map handlerSearchMap(@RequestParam Map<String,String> searchMap){
//特殊符号处理
handleSearchMap(searchMap);
Map resultMap = searchService.search(searchMap);
return resultMap;
}
//处理特殊字符的方法
private void handleSearchMap(Map<String, String> searchMap) {
Set<Map.Entry<String, String>> entries = searchMap.entrySet();
for (Map.Entry<String, String> entry : entries) {
if (entry.getKey().startsWith("spec_")){
searchMap.put(entry.getKey(),entry.getValue().replace("+","%2B"));
}
}
}
service层
-
1、判断map里的key有没有规格条件(spec_)
-
2、把%2B再转化回来
-
3、由于 规格详情为object对象类型需要获取里面的keyword属性值,作为作用域
-
4、将获取keyword作为作用域,和要查询的商品规格名称
//格式 "specMap": { "properties": { "颜色": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "尺码": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } } //展示 "status": "1", "createTime": 1556668800000, "updateTime": 1556668800000, "isDefault": null, "spuId": 2387887412200, "categoryId": 0, "categoryName": "休闲鞋", "brandName": "斯凯奇", "spec": "{'颜色': '粉色', '尺码': '40'}", "specMap": { //规格 "颜色": "粉色", "尺码": "40" }
//4、对商品的规格进行查询
for (String key : paramMap.keySet()) {
if (key.startsWith("spec_")) {
//将特殊字符再转回来
String value = paramMap.get(key).replace("%2B","+");
//获取对象里的keyword的字段。来进行分组查询
boolQuery.filter(QueryBuilders.termQuery(("specMap." + key.substring(5) + ".keyword"), value));
}
}
五、对商品规格进行分组查询
总结:对应的mapping信息,spec 作为字符串,存储所有的信息,以字符串的形式,用于统计.
specMap: 作为object类型,用于查询
-
1、将spec 转换为keyword类型,因为聚合查询必须要keyword类
"spec": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }
-
2、传入参数: 设置名称,分组的作用域
-
3、获取分组的对象,从分组的对象通过stream获取分组数据
-
4、将分组数据转换为前端需要的数据
//规格分组的条件
//5、对商品的规格进行分组查询
String skuSpec="skuSpec";
// 1、分组名称 2、由于spec的类型是text,改为keyword类型,进行统计
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(skuSpec).field("spec.keyword"));
//获取规格的分组对象
StringTerms specTerms = (StringTerms) skuInfos.getAggregation(skuSpec);
//通过stream流的方式获取规格的集合
List<String> specList = specTerms.getBuckets().stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());
//调用方法,将规格分组转换一下。给前端使用
Map<String, Set<String>> specMap = formartSpec(specList);
resultMap.put("specMap", specMap);
/*
[
"{'颜色': '红色', '尺码': '150度'}",
"{'颜色': '黑色', '尺码': '150度'}",
"{'颜色': '黑色'}",
"{'颜色': '红色', '尺码': '100度'}",
"{'颜色': '红色', '尺码': '250度'}",
"{'颜色': '红色', '尺码': '350度'}",
"{'颜色': '黑色', '尺码': '200度'}",
"{'颜色': '黑色', '尺码': '250度'}"
]
转成页面需要的格式:
{
颜色:[黑色,红色],
尺码:[100度,150度,200度,250度,350度]
}
*/
//将规格进行转换
private Map<String, Set<String>> formartSpec(List<String> specList) {
//创建一个map,封装成页面需要的格式
Map<String, Set<String>> resultMap = new HashMap<>();
if (specList.size() > 0 && specList != null) {
for (String spec : specList) {
//将list的每条json字符串转换成map
Map<String,String> specMap= JSON.parseObject(spec, Map.class);
//遍历获取所有的key (颜色,尺码)
for (String key : specMap.keySet()) {
//根据key取出规格相对应的信息 例如(颜色:[黑色,黄色])
Set<String> specSet = resultMap.get(key);
//没有的话就创建一个,set集合
if (specSet == null) {
specSet = new HashSet<>();
}
//将这个key对应value取出,赋值给set
specSet.add(specMap.get(key));
//将这个key,所对应的set封装到map中
resultMap.put(key, specSet);
}
}
}
return resultMap;
}
六、价格区间查询
- 1、判断条件map里有没有这个条件
- 2、对“-”截取价格字符串 (2000-3000)
- 3、判断截取后的数组,如果数组长度大于2,进行区间查询(2000<=x<=3000)
- 4、只有一个的话,将他作为最大区间(x<=3000)
//6、商品的价格区间查询
if (StringUtils.isNotEmpty(paramMap.get("price"))) {
//对价格字符截取
String[] prices = paramMap.get("price").split("-");
//如果价格区间是1000-2000,就会取这之间的
if (prices.length == 2) {
boolQuery.filter(QueryBuilders.rangeQuery("price").lte(Long.parseLong(prices[1])));
}
//只有一个价格,就会取大于等于这个价格
boolQuery.filter(QueryBuilders.rangeQuery("price").gte(Long.parseLong(prices[0])));
}
七、分页查询
- 1、对当前页,一页显示多少条,进行非空判断如果为空的话,就赋一个默认值。
- 2、将分页条件传入,注意springboot的当前页是从0开始的。所以需要减1。
//7、分页查询
String pageNum = paramMap.get("pageNum"); //当前页
String pageSize = paramMap.get("pageSize"); //每页显示多少条
//给当前分页数组默认值
if (StringUtils.isEmpty(pageNum)) {
pageNum = "1";
}
if (StringUtils.isEmpty(pageSize)) {
pageSize = "20";
}
//注意当前页是0开始的,需要减一
nativeSearchQueryBuilder.withPageable(PageRequest.of(Integer.parseInt(pageNum) - 1, Integer.parseInt(pageSize)));
八、价格的排序查询
-
1、价格排序需要两个条件 (1):作用域 (2):排序的规则
-
2、判断这个两个条件不为空
-
3、如果是排序条件是ASC 那就是排序操作
-
4、如果是排序条件是DESC 那就是降序操作
//8、价格排序查询 //参数1、当前域(价格、销量) 2、排序的关键字 (升序ASC,降序DESC) if (StringUtils.isNotEmpty(paramMap.get("sortField")) && StringUtils.isNotEmpty(paramMap.get("sortRule"))){ //升序 if ("ASC".equals(paramMap.get("sortRule"))) { nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort((paramMap.get("sortField"))).order(SortOrder.ASC)); } //降序 if ("DESC".equals(paramMap.get("sortRule"))) { nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort((paramMap.get("sortField"))).order(SortOrder.DESC)); } }
九、高亮查询
-
1、高亮查询的作用域
-
2、高亮的前缀
-
3、高亮的后缀
-
4、取出高亮数据替换原来内容
//9、设置高亮域以及高亮的样式 HighlightBuilder.Field field = new HighlightBuilder.Field("name") .preTags("<span style='color:red'>")//高亮样式的前缀 .postTags("</span>");//高亮样式的后缀 nativeSearchQueryBuilder.withHighlightFields(field); //封装查询结果 AggregatedPage<SkuInfo> skuInfos = esTemplate.queryForPage(nativeSearchQueryBuilder.build(), SkuInfo.class, new SearchResultMapper() { @Override public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> aClass, Pageable pageable) { ArrayList<T> list = new ArrayList<>(); //获取命中对象 SearchHits hits = searchResponse.getHits(); if (hits != null) { //获取ES每条数据 for (SearchHit hit : hits) { SkuInfo skuInfo = JSON.parseObject(hit.getSourceAsString(), SkuInfo.class); //获取高亮对象 Map<String, HighlightField> highlightFields = hit.getHighlightFields(); if (highlightFields != null && highlightFields.size() > 0) { //将高亮的样式替换上原来数据 skuInfo.setName(highlightFields.get("name").getFragments()[0].toString()); } list.add((T) skuInfo); } }
完整代码:
@Service
public class SearchServiceImpl implements SearchService {
@Autowired
private ElasticsearchTemplate esTemplate;
/**
* 全文检索
*
* @param paramMap
* @return
*/
@Override
public Map search(Map<String, String> paramMap) {
//接收查询上来的数据
Map<String, Object> resultMap = new HashMap<>();
//条件不为空
if (paramMap != null) {
//构建查询条件封装对象
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
//组合条件对象
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//1、判断搜索框里不为空 根据搜索分词查询
if (!StringUtils.isEmpty(paramMap.get("keywords"))) {
//1、索引名称 2、分词查询 3、交集
boolQuery.must(QueryBuilders.matchQuery("name", paramMap.get("keywords")).operator(Operator.AND));
}
//2、对品牌进行查询
if (!StringUtils.isEmpty(paramMap.get("brand"))) {
//不分词查询
boolQuery.filter(QueryBuilders.termQuery("brandName", paramMap.get("brand")));
}
//3、对品牌进行分组查询 聚合查询只有keyword的类型才可以
String skuBrand="skuBrand"; //参数 1、分组的名称(自己随便起) 2、分组的字段
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(skuBrand).field("brandName"));
//4、对商品的规格进行查询
for (String key : paramMap.keySet()) {
if (key.startsWith("spec_")) {
//将特殊字符再转回来
String value = paramMap.get(key).replace("%2B","+");
//获取对象里的keyword的字段。来进行分组查询
boolQuery.filter(QueryBuilders.termQuery(("specMap." + key.substring(5) + ".keyword"), value));
}
}
//5、对商品的规格进行分组查询
String skuSpec="skuSpec";
// 1、分组名称 2、由于spec的类型是text,改为keyword类型,进行统计
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(skuSpec).field("spec.keyword"));
//6、商品的价格区间查询
if (StringUtils.isNotEmpty(paramMap.get("price"))) {
//对价格字符截取
String[] prices = paramMap.get("price").split("-");
//如果价格区间是1000-2000,就会取这之间的
if (prices.length == 2) {
boolQuery.filter(QueryBuilders.rangeQuery("price").lte(Long.parseLong(prices[1])));
}
//只有一个价格,就会取大于等于这个价格
boolQuery.filter(QueryBuilders.rangeQuery("price").gte(Long.parseLong(prices[0])));
}
//7、分页查询
String pageNum = paramMap.get("pageNum"); //当前页
String pageSize = paramMap.get("pageSize"); //每页显示多少条
//给当前分页数组默认值
if (StringUtils.isEmpty(pageNum)) {
pageNum = "1";
}
if (StringUtils.isEmpty(pageSize)) {
pageSize = "20";
}
//注意当前页是0开始的,需要减一
nativeSearchQueryBuilder.withPageable(PageRequest.of(Integer.parseInt(pageNum) - 1, Integer.parseInt(pageSize)));
//8、价格排序查询
//参数1、当前域(价格、销量) 2、排序的关键字 (升序ASC,降序DESC)
if (StringUtils.isNotEmpty(paramMap.get("sortField")) && StringUtils.isNotEmpty(paramMap.get("sortRule"))){
//升序
if ("ASC".equals(paramMap.get("sortRule"))) {
nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort((paramMap.get("sortField"))).order(SortOrder.ASC));
}
//降序
if ("DESC".equals(paramMap.get("sortRule"))) {
nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort((paramMap.get("sortField"))).order(SortOrder.DESC));
}
}
//9、设置高亮域以及高亮的样式
HighlightBuilder.Field field = new HighlightBuilder.Field("name")
.preTags("<span style='color:red'>")//高亮样式的前缀
.postTags("</span>");//高亮样式的后缀
nativeSearchQueryBuilder.withHighlightFields(field);
//绑定条件对象
nativeSearchQueryBuilder.withQuery(boolQuery);
//开启查询
/**
* 第一个参数: 条件构建对象
* 第二个参数: 查询操作实体类
* 第三个参数: 查询结果操作对象
*/
//封装查询结果
AggregatedPage<SkuInfo> skuInfos = esTemplate.queryForPage(nativeSearchQueryBuilder.build(), SkuInfo.class, new SearchResultMapper() {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> aClass, Pageable pageable) {
ArrayList<T> list = new ArrayList<>();
//获取命中对象
SearchHits hits = searchResponse.getHits();
if (hits != null) {
//获取ES每条数据
for (SearchHit hit : hits) {
SkuInfo skuInfo = JSON.parseObject(hit.getSourceAsString(), SkuInfo.class);
//获取高亮对象
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if (highlightFields != null && highlightFields.size() > 0) {
//将高亮的样式替换上原来数据
skuInfo.setName(highlightFields.get("name").getFragments()[0].toString());
}
list.add((T) skuInfo);
}
}
//1、ES数据集合 2、分页总数据 3、数据的条目数 4、聚合数据
return new AggregatedPageImpl<>(list, pageable, hits.getTotalHits(), searchResponse.getAggregations());
}
});
//获取品牌分组的对象
StringTerms brandTerms = (StringTerms) skuInfos.getAggregation(skuBrand);
//通过stream流的方式获取品牌的集合
List<String> brandList = brandTerms.getBuckets().stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());
//封装品牌
resultMap.put("brandList", brandList);
//获取规格的分组对象
StringTerms specTerms = (StringTerms) skuInfos.getAggregation(skuSpec);
//通过stream流的方式获取规格的集合
List<String> specList = specTerms.getBuckets().stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());
//封装分组
Map<String, Set<String>> specMap = formartSpec(specList);
resultMap.put("specMap", specMap);
//11. 总条数
resultMap.put("total", skuInfos.getTotalElements());
//12. 总页数
resultMap.put("totalPages", skuInfos.getTotalPages());
//13. 查询结果集合
resultMap.put("rows", skuInfos.getContent());
//返回当前页
resultMap.put("pageNum", pageNum);
return resultMap;
}
return null;
}
/*
[
"{'颜色': '红色', '尺码': '150度'}",
"{'颜色': '黑色', '尺码': '150度'}",
"{'颜色': '黑色'}",
"{'颜色': '红色', '尺码': '100度'}",
"{'颜色': '红色', '尺码': '250度'}",
"{'颜色': '红色', '尺码': '350度'}",
"{'颜色': '黑色', '尺码': '200度'}",
"{'颜色': '黑色', '尺码': '250度'}"
]
转成页面需要的格式:
{
颜色:[黑色,红色],
尺码:[100度,150度,200度,250度,350度]
}
*/
//将规格进行转换
private Map<String, Set<String>> formartSpec(List<String> specList) {
//创建一个map,封装成页面需要的格式
Map<String, Set<String>> resultMap = new HashMap<>();
if (specList.size() > 0 && specList != null) {
for (String spec : specList) {
//将list的每条json字符串转换成map
Map<String,String> specMap= JSON.parseObject(spec, Map.class);
//遍历获取所有的key (颜色,尺码)
for (String key : specMap.keySet()) {
//根据key取出规格相对应的信息 例如(颜色:[黑色,黄色])
Set<String> specSet = resultMap.get(key);
//没有的话就创建一个,set集合
if (specSet == null) {
specSet = new HashSet<>();
}
//将这个key对应value取出,赋值给set
specSet.add(specMap.get(key));
//将这个key,所对应的set封装到map中
resultMap.put(key, specSet);
}
}
}
return resultMap;
}
}