第七章
商品搜索
优就业.JAVA教研室
学习目标
- 条件筛选
- 多条件搜索
[品牌、规格条件搜索]
- 规格过滤
- 价格区间搜索
- 搜索分页
- 搜索排序
- 搜索高亮
1. 品牌统计
用户搜索的时候,除了使用分类搜索外,还有可能使用品牌搜索,所以我们还需要显示品牌数据和规格数据,品牌数据和规格数据的显示比较容易,都可以考虑使用分类统计的方式进行分组实现。
1.1 品牌统计分析
看下面的SQL语句,我们在执行搜索的时候,第1条SQL语句是执行搜,第2条语句是根据品牌名字分组查看有多少品牌,大概执行了2个步骤就可以获取数据结果以及品牌统计,我们可以发现他们的搜索条件完全一样。
-- 查询所有
SELECT * FROM tb_item WHERE title LIKE '%手机%';
-- 根据品牌名字分组查询
SELECT brand FROM tb_item WHERE title LIKE '%手机%' GROUP BY brand;
我们每次执行搜索的时候,需要显示商品品牌名称,这里要显示的品牌名称其实就是符合搜素条件的所有商品的品牌集合,我们可以按照上面的实现思路,使用ES根据分组名称做一次分组查询即可实现。
1.2 品牌分组统计实现
修改search微服务的com.offcn.search.service.impl.SkuServiceImpl类,添加一个品牌分组搜索,如图:
添加的代码如下:
//设置分组条件 商品品牌
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuBrandgroup").field("brand.keyword").size(50));
执行获取分组结果:
整体代码如下:
public Map search(Map<String, String> searchMap) {
//1.获取关键字的值
String keywords = searchMap.get("keywords");
if (StringUtils.isEmpty(keywords)) {
keywords = "华为";//赋值给一个默认的值
}
//2.创建查询对象 的构建对象
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
//3.设置查询的条件
//设置分组条件 商品分类
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuCategorygroup").field("category.keyword").size(50));
//设置分组条件 商品品牌
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuBrandgroup").field("brand.keyword").size(50));
nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("title", keywords));
//4.构建查询对象
NativeSearchQuery query = nativeSearchQueryBuilder.build();
//5.执行查询
AggregatedPage<SkuInfo> skuPage = esTemplate.queryForPage(query, SkuInfo.class);
//获取分组结果 商品分类
Terms TermsCategory = skuPage.getAggregation("skuCategorygroup");
//获取分组结果 商品品牌
Terms TermsBrand = skuPage.getAggregation("skuBrandgroup");
List<String> categoryList = getStringsCategoryList(stringTermsCategory);
List<String> brandList = getStringsBrandList(stringTermsBrand);
//6.返回结果
Map resultMap = new HashMap<>();
resultMap.put("categoryList", categoryList);
resultMap.put("brandList", brandList);
resultMap.put("rows", skuPage.getContent());
resultMap.put("total", skuPage.getTotalElements());
resultMap.put("totalPages", skuPage.getTotalPages());
return resultMap;
}
/**
* 获取品牌列表
*
* @param terms
* @return
*/
private List<String> getStringBrandList(Terms terms){
//创建一个集合存放全部的品牌分组结果
List<String> brandList=new ArrayList<>();
if(terms!=null){
for (Terms.Bucket bucket : terms.getBuckets()) {
String keyAsString = bucket.getKeyAsString();
brandList.add(keyAsString);
}
}
return brandList;
}
/**
* 获取分类列表数据
*
* @param stringTerms
* @return
*/
private List<String> getStringCategorList(Terms terms){
//创建一个集合存放全部的分组结果
List<String> categoryList=new ArrayList<>();
//遍历分组结果
if(terms!=null){
for (Terms.Bucket bucket : terms.getBuckets()) {
//获取分组的值
String keyAsString = bucket.getKeyAsString();
//把分组的值加入到集合
categoryList.add(keyAsString);
}
}
return categoryList;
}
1.3 测试
使用PostMan请求http://localhost:9004/search
2. 规格统计
用户搜索的时候,除了使用分类、品牌搜索外,还有可能使用规格搜索,所以我们还需要显示规格数据,规格数据的显示相比上面2种实现略微较难一些,需要对数据进行处理,我们也可以考虑使用分类统计和品牌统计的方式进行分组实现。
2.1 规格统计分析
看下面的SQL语句,我们在执行搜索的时候,第1条SQL语句是执行搜,第2条语句是根据规格分组查看有多少规格,大概执行了2个步骤就可以获取数据结果以及规格统计,我们可以发现他们的搜索条件完全一样。
-- 查询所有
SELECT * FROM tb_item WHERE name LIKE '%手机%';
-- 根据规格名字分组查询
SELECT spec FROM tb_item WHERE title LIKE '%手机%' GROUP BY spec;
上述SQL语句执行后的结果如下图:
获取到的规格数据我们发现有重复,不过也可以解决,解决思路如下:
1.获取所有规格数据
2.将所有规格数据转换成Map
3.定义一个Map<String,Set>,key是规格名字,防止重复所以用Map,valu是规格值,规格值有多个,所以用集合,为了防止规格重复,用Set去除重复
4.循环规格的Map,将数据填充到定义的Map<String,Set>中
我们每次执行搜索的时候,需要显示商品规格数据,这里要显示的规格数据其实就是符合搜素条件的所有商品的规格集合,我们可以按照上面的实现思路,使用ES根据分组名称做一次分组查询,并去除重复数据即可实现。
2.2 规格统计分组实现
修改search微服务的com.offcn.search.service.impl.SkuServiceImpl类,添加一个规格分组搜索
如图:添加规格分组条件 上图代码如下:
//设置分组条件 商品的规格
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuSpecgroup").field("spec.keyword").size(100));
如图:获取规格分组结果:
封装调用分组结果的方法,代码如下:
/**
* 获取规格列表数据
*
* @param stringTermsSpec
* @return
*/
//获取规格列表数据
public Map<String, Set<String>> getStringSpecMap(Terms terms){
//创建一个返回的Map
Map<String,Set<String>> specMap=new HashMap<>();
//创建一个set存储规格选项
Set<String> specSet=new HashSet<>();
//判断查询结果不为空
if (terms != null) {
for (Terms.Bucket bucket : terms.getBuckets()) {
//把查询到规格名称加入到set自动去重
specSet.add(bucket.getKeyAsString());
}
}
//遍历set
for (String specJson : specSet) {
//解析转换规格的json字符串为map集合
Map<String,String> map = JSON.parseObject(specJson, Map.class);
//遍历map
for (Map.Entry<String, String> entry : map.entrySet()) {
//提取规格名称
String key = entry.getKey();
String value = entry.getValue();
//根据规格名称提取对应的规格选项数据
Set<String> specValues=specMap.get(key);
//判断规格选项数据是否为空
if(specValues==null){
specValues=new HashSet<>();
}
//把当前规格选项加入到规格选项数据集合
specValues.add(value);
//把当前的数据加入到返回map
specMap.put(key,specValues);
}
}
return specMap;
}
整体代码如下:
public Map search(Map<String, String> searchMap) {
//1.获取关键字的值
String keywords = searchMap.get("keywords");
if (StringUtils.isEmpty(keywords)) {
keywords = "华为";//赋值给一个默认的值
}
//2.创建查询对象 的构建对象
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
//3.设置查询的条件
//设置分组条件 商品分类
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuCategorygroup").field("category.keyword").size(50));
//设置分组条件 商品品牌
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuBrandgroup").field("brand.keyword").size(50));
//设置分组条件 商品的规格
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuSpecgroup").field("spec.keyword").size(100));
nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("title", keywords));
//4.构建查询对象
NativeSearchQuery query = nativeSearchQueryBuilder.build();
//5.执行查询
AggregatedPage<SkuInfo> skuPage = esTemplate.queryForPage(query, SkuInfo.class);
//获取分组结果 商品分类
StringTerms stringTermsCategory = (StringTerms) skuPage.getAggregation("skuCategorygroup");
//获取分组结果 商品品牌
StringTerms stringTermsBrand = (StringTerms) skuPage.getAggregation("skuBrandgroup");
//获取分组结果 商品规格数据
StringTerms stringTermsSpec = (StringTerms) skuPage.getAggregation("skuSpecgroup");
List<String> categoryList = getStringsCategoryList(stringTermsCategory);
List<String> brandList = getStringsBrandList(stringTermsBrand);
Map<String, Set<String>> specMap = getStringSetMap(stringTermsSpec);
//6.返回结果
Map resultMap = new HashMap<>();
resultMap.put("specMap", specMap);
resultMap.put("categoryList", categoryList);
resultMap.put("brandList", brandList);
resultMap.put("rows", skuPage.getContent());
resultMap.put("total", skuPage.getTotalElements());
resultMap.put("totalPages", skuPage.getTotalPages());
return resultMap;
}
/**
* 获取品牌列表
*
* @param stringTermsBrand
* @return
*/
private List<String> getStringsBrandList(StringTerms stringTermsBrand) {
List<String> brandList = new ArrayList<>();
if (stringTermsBrand != null) {
for (StringTerms.Bucket bucket : stringTermsBrand.getBuckets()) {
brandList.add(bucket.getKeyAsString());
}
}
return brandList;
}
/**
* 获取分类列表数据
*
* @param stringTerms
* @return
*/
private List<String> getStringsCategoryList(StringTerms stringTerms) {
List<String> categoryList = new ArrayList<>();
if (stringTerms != null) {
for (StringTerms.Bucket bucket : stringTerms.getBuckets()) {
String keyAsString = bucket.getKeyAsString();//分组的值
categoryList.add(keyAsString);
}
}
return categoryList;
}
/**
* 获取规格列表数据
*
* @param stringTermsSpec
* @return
*/
private Map<String, Set<String>> getStringSetMap(StringTerms stringTermsSpec) {
Map<String, Set<String>> specMap = new HashMap<String, Set<String>>();
Set<String> specList = new HashSet<>();
if (stringTermsSpec != null) {
for (StringTerms.Bucket bucket : stringTermsSpec.getBuckets()) {
specList.add(bucket.getKeyAsString());
}
}
for (String specjson : specList) {
Map<String, String> map = JSON.parseObject(specjson, Map.class);
for (Map.Entry<String, String> entry : map.entrySet()) {//
String key = entry.getKey(); //规格名字
String value = entry.getValue(); //规格选项值
//获取当前规格名字对应的规格数据
Set<String> specValues = specMap.get(key);
if (specValues == null) {
specValues = new HashSet<String>();
}
//将当前规格加入到集合中
specValues.add(value);
//将数据存入到specMap中
specMap.put(key, specValues);
}
}
return specMap;
}
2.3 测试
使用Postman测试访问http://localhost:9004/search 效果如下:
3 条件筛选
用户有可能会根据分类搜索、品牌搜索,还有可能根据规格搜索,以及价格搜索和排序操作。根据分类和品牌搜索的时候,可以直接根据指定域搜索,而规格搜索的域数据是不确定的,价格是一个区间搜索,所以我们可以分为三段时间,先实现分类、品牌搜素,再实现规格搜索,然后实现价格区间搜索。
3.1 分类、品牌筛选
3.1.1 需求分析
页面每次向后台传入对应的分类和品牌,后台据分类和品牌进行条件过滤即可。
3.1.2 代码实现
修改搜索微服务com.offcn.search.service.impl.SkuServiceImpl的search方法,添加分类和品牌过滤,
添加过滤条件如下:
PS说明: 以上,我们建议使用filter ,它的搜索效率要优于must.可以参考官方文档说明:
https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html
上图整体代码如下:
@Override
public Map search(Map<String, String> searchMap) {
//1.获取关键字的值
String keywords = searchMap.get("keywords");
if (StringUtils.isEmpty(keywords)) {
keywords = "华为";//赋值给一个默认的值
}
//2.创建查询对象 的构建对象
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
//3.设置查询的条件
//设置分组条件 商品分类
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuCategorygroup").field("category.keyword").size(50));
//设置分组条件 商品品牌
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuBrandgroup").field("brand.keyword").size(50));
//设置分组条件 商品的规格
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuSpecgroup").field("spec.keyword").size(1000));
//设置主关键字查询
nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("title", keywords));
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
if (!StringUtils.isEmpty(searchMap.get("brand"))) {
boolQueryBuilder.filter(QueryBuilders.termQuery("brand.keyword", searchMap.get("brand")));
}
if (!StringUtils.isEmpty(searchMap.get("category"))) {
boolQueryBuilder.filter(QueryBuilders.termQuery("category.keyword", searchMap.get("category")));
}
//构建过滤查询
nativeSearchQueryBuilder.withFilter(boolQueryBuilder);
//4.构建查询对象
NativeSearchQuery query = nativeSearchQueryBuilder.build();
//5.执行查询
AggregatedPage<SkuInfo> skuPage = esTemplate.queryForPage(query, SkuInfo.class);
//获取分组结果 商品分类
StringTerms stringTermsCategory = (StringTerms) skuPage.getAggregation("skuCategorygroup");
//获取分组结果 商品品牌
StringTerms stringTermsBrand = (StringTerms) skuPage.getAggregation("skuBrandgroup");
//获取分组结果 商品规格数据
StringTerms stringTermsSpec = (StringTerms) skuPage.getAggregation("skuSpecgroup");
List<String> categoryList = getStringsCategoryList(stringTermsCategory);
List<String> brandList = getStringsBrandList(stringTermsBrand);
Map<String, Set<String>> specMap = getStringSetMap(stringTermsSpec);
//6.返回结果
Map resultMap = new HashMap<>();
resultMap.put("specMap", specMap);
resultMap.put("categoryList", categoryList);
resultMap.put("brandList", brandList);
resultMap.put("rows", skuPage.getContent());
resultMap.put("total", skuPage.getTotalElements());
resultMap.put("totalPages", skuPage.getTotalPages());
return resultMap;
}
3.1.3 测试
测试效果如下:
访问地址:http://localhost:9004/search
{"keywords":"黑色","category":"手机","brand":"三星"}
此时只能搜到TCL手机
3.2 规格过滤
3.2.1 需求分析
格这一块,需要向后台发送规格名字以及规格值,我们可以按照一定要求来发送数据,例如规格名字以特殊前缀提交到后台:spec_网络制式:电信4G、spec_显示屏尺寸:4.0-4.9英寸
后台接到数据后,可以根据前缀spec_来区分是否是规格,如果以spec_xxx
开始的数据则为规格数据,需要根据指定规格找信息。
上图是规格的索引存储格式,真实数据在spechMap.规格名字.keyword中,所以找数据也是按照如下格式去找:
spechMap.规格名字.keyword
使用命令可以查看存储格式:
GET /skuinfo/_mapping
3.2.2 代码实现
修改com.offcn.search.service.impl.SkuServiceImpl的search方法,增加规格查询操作,代码如下:
//规格过滤查询
if (searchMap != null) {
for (String key : searchMap.keySet()) {
if (key.startsWith("spec_")) {
boolQueryBuilder.filter(QueryBuilders.termQuery("specMap." + key.substring(5) + ".keyword", searchMap.get(key)));
}
}
}
3.2.3 测试
访问地址:http://localhost:9004/search
{"keywords":"黑色","spec_网络":"双卡","spec_机身内存":"32G"}
3.3 价格区间查询
3.3.1 需求分析
价格区间查询,每次需要将价格传入到后台,前端传入后台的价格大概是price=0-500
或者price=500-1000
依次类推,最后一个是price=3000
,后台可以根据-分割,如果分割得到的结果最多有2个,第1个表示x<price
,第2个表示price<=y
。
3.3.2 代码实现
修改com.offcn.search.service.impl.SkuServiceImpl的search方法,增加价格区间查询操作,代码如下:
//价格过滤查询
String price = searchMap.get("price");
if (!StringUtils.isEmpty(price)) {
String[] split = price.split("-");
if (!split[1].equalsIgnoreCase("*")) {
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").from(split[0], true).to(split[1], true));
} else {
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(split[0]));
}
}
3.3.3 测试
访问地址:http://localhost:9004/search
{"keywords":"黑色","price":"1-500"}
效果如下(部分数据):
{
"total": 1,
"categoryList": [
"手机"
],
"totalPages": 1,
"specMap": {
"网络": [
"电信3G",
"移动3G",
"联通3G",
"联通2G",
"移动4G",
"联通4G",
"移动2G",
"电信4G",
"双卡"
],
"机身内存": [
"16G",
"128G",
"32G",
"64G"
]
},
"brandList": [
"三星",
"苹果",
"中国移动",
"联想",
"华为",
"酷派",
"诺基亚",
"飞利浦",
"OPPO",
"HTC",
"TCL",
"中兴",
"索尼",
"金立",
"小米",
"朵唯",
"百加",
"海信",
"微软",
"阿尔卡特",
"魅族",
"派信"
],
"rows": [
{
"id": 1260714,
"title": "TCL (S850L) 16GB 耀目红 电信4G手机",
"price": 759.00,
"num": 99999,
"image": "http://img11.360buyimg.com/n1/s450x450_jfs/t2278/328/1482029120/347965/7755349f/565e97aaN5710a07d.jpg",
"status": "1",
"createTime": "2015-03-08T13:30:44.000+0000",
"updateTime": "2015-03-08T13:30:44.000+0000",
"isDefault": null,
"goodsId": 1,
"categoryId": 560,
"category": "手机",
"brand": "TCL",
"spec": "{\"机身内存\":\"16G\",\"网络\":\"电信4G\"}",
"specMap": {
"网络": "电信4G",
"机身内存": "16G"
}
}
]
}
4 搜索分页
4.1 分页分析
页面需要实现分页搜索,所以我们后台每次查询的时候,需要实现分页。用户页面每次会传入当前页和每页查询多少条数据,当然如果不传入每页显示多少条数据,默认查询30条即可。
4.2 分页实现
分页使用PageRequest.of( pageNo- 1, pageSize);实现,第1个参数表示第N页,从0开始,第2个参数表示每页显示多少条,实现代码如下:
上图代码如下:
//略
//构建过滤查询
nativeSearchQueryBuilder.withFilter(boolQueryBuilder);
//构建分页查询
Integer pageNum = 1;
if (!StringUtils.isEmpty(searchMap.get("pageNum"))) {
try {
pageNum = Integer.valueOf(searchMap.get("pageNum"));
} catch (NumberFormatException e) {
e.printStackTrace();
pageNum=1;
}
}
Integer pageSize = 30;
//设置分页条件
nativeSearchQueryBuilder.withPageable(PageRequest.of(pageNum - 1, pageSize));
//略
//4.构建查询对象
NativeSearchQuery query = nativeSearchQueryBuilder.build();
//略
测试如下:
5 搜索排序
5.1 排序分析
排序这里总共有根据价格排序、根据评价排序、根据新品排序、根据销量排序,排序要想实现非常简单,只需要告知排序的域以及排序方式即可实现。
价格排序:只需要根据价格高低排序即可,降序价格高->低,升序价格低->高
评价排序:评价分为好评、中评、差评,可以在数据库中设计3个列,用来记录好评、中评、差评的量,每次排序的时候,好评的比例来排序,当然还要有条数限制,评价条数需要超过N条。
新品排序:直接根据商品的发布时间或者更新时间排序。
销量排序:销量排序除了销售数量外,还应该要有时间段限制。
5.2 排序实现
这里我们不单独针对某个功能实现排序,我们只需要在后台接收2个参数,分别是排序域名字和排序方式,代码如下:
解释: 前端页面传递要排序的字段(field)和要排序的类型(ASC,DESC),后台接收.
上图代码如下:
//构建排序查询
String sortRule = searchMap.get("sortRule");
String sortField = searchMap.get("sortField");
if (!StringUtils.isEmpty(sortRule) && !StringUtils.isEmpty(sortField)) {
nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(sortField).order(sortRule.equals("DESC") ? SortOrder.DESC : SortOrder.ASC));
}
测试
根据价格降序:
{"keywords":"手机","pageNum":"1","sortRule":"DESC","sortField":"price"}
根据价格升序:
{"keywords":"手机","pageNum":"1","sortRule":"ASC","sortField":"price"}
6 高亮显示
6.1 高亮分析
高亮显示是指根据商品关键字搜索商品的时候,显示的页面对关键字给定了特殊样式,让它显示更加突出,如上图商品搜索中,关键字变成了红色,其实就是给定了红色样式。
6.2 高亮搜索实现步骤解析
将之前的搜索换掉,换成高亮搜索,我们需要做3个步骤:
1.指定高亮域,也就是设置哪个域需要高亮显示
设置高亮域的时候,需要指定前缀和后缀,也就是关键词用什么html标签包裹,再给该标签样式
2.高亮搜索实现
3.将非高亮数据替换成高亮数据
第1点,例如在百度中搜索数据的时候,会有2个地方高亮显示,分别是标题和描述,商城搜索的时候,只是商品名称高亮显示了。而高亮显示其实就是添加了样式,例如<span style="color:red;">笔记本</span>
,而其中span开始标签可以称为前缀,span结束标签可以称为后缀。
第2点,高亮搜索使用ElasticsearchTemplate实现。
第3点,高亮搜索后,会搜出非高亮数据和高亮数据,高亮数据会加上第1点中的高亮样式,此时我们需要将非高亮数据换成高亮数据即可。例如非高亮:华为笔记本性能超强悍
高亮数据:华为<span style="color:red;"笔记本</span>性能超强悍
,将非高亮的换成高亮的,到页面就能显示样式了。
6.3 高亮代码实现
修改com.offcn.search.service.impl.SkuServiceImpl的search方法搜索代码,添加高亮显示的域:
上图代码如下:
//设置高亮条件
nativeSearchQueryBuilder.withHighlightFields(new HighlightBuilder.Field("title"));
nativeSearchQueryBuilder.withHighlightBuilder(new HighlightBuilder().preTags("<em style=\"color:red\">").postTags("</em>"));
//设置主关键字查询,修改为多字段的搜索条件
nativeSearchQueryBuilder.withQuery(QueryBuilders.multiMatchQuery(keywords,"title","brand","category"));
处理查询高亮结果:
//....执行查询
//获取高亮结果
for (SearchHit<SkuInfo> searchHit : searchHits) {
//获取高亮的内容
Map<String, List<String>> highlightFields = searchHit.getHighlightFields();
//获取标题的高亮结果
searchHit.getContent().setTitle(highlightFields.get("title")==null?searchHit.getContent().getTitle():highlightFields.get("title").get(0));
}
//...获取查询结果数据集合
注意:放置高亮结果处理代码的顺序,一定要放在获取查询结果数据集合上面。
6.4 测试
效果如下:
"name": "HTC M8Sd (E8) 波尔多红 电信4G<span style=\"color:red\">手机</span> 双卡双待双通",
整体代码如下:
@Service
public class SkuServiceImpl implements SkuService {
@Autowired
private SkuEsMapper skuEsMapper;
@Autowired
private ItemFeign itemFeign;
@Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;
@Override
public void importSku() {
//调用商品微服务,获取审核状态为1,审核通过的商品
Result<List<Item>> result = itemFeign.findByStatus("1");
//算法很好,把接收过来的数据,转换为json字符串,在重新转换为集合
List<SkuInfo> skuInfoList = JSON.parseArray(JSON.toJSONString(result.getData()), SkuInfo.class);
//遍历sku集合
for (SkuInfo skuInfo : skuInfoList) {
//获取规格数据{"网络":"移动4G"},转换为map
Map<String, Object> specMap= JSON.parseObject(skuInfo.getSpec());
//关联到动态域
skuInfo.setSpecMap(specMap);
}
//保存sku到ES
skuEsMapper.saveAll(skuInfoList);
}
@Override
public Map search(Map<String, String> searchMap) {
//获取搜索关键字
String keywords=searchMap.get("keywords");
//判断关键字是否为空,如果为空设置默认值
if(StringUtils.isEmpty(keywords)){
keywords="手机";
}
//创建查询对象构建器对象
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
//设置分组条件
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuCategorygroup").field("category.keyword"));
//设置分组条件 商品品牌
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuBrandgroup").field("brand.keyword").size(50));
//设置分组条件 规格
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuSpecgroup").field("spec.keyword").size(50));
//设置查询条件
nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("title",keywords));
//创建过滤器对象
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
//判断品牌查询条件是否为空
if(!StringUtils.isEmpty(searchMap.get("brand"))){
boolQueryBuilder.filter(QueryBuilders.termQuery("brand",searchMap.get("brand.keyword")));
}
//判断分类查询条件是否为空
if(!StringUtils.isEmpty(searchMap.get("category"))){
boolQueryBuilder.filter(QueryBuilders.termQuery("category",searchMap.get("category.keyword")));
}
//规格过滤
if(searchMap!=null){
//遍历searchMap
for (String key : searchMap.keySet()) {
//判断key是不是spec_开头
if(key.startsWith("spec_")){
boolQueryBuilder.filter(QueryBuilders.termQuery("specMap."+key.substring(5)+".keyword",searchMap.get(key)));
}
}
}
//价格区间过滤
String price= searchMap.get("price");
//判断价格不为空
if(!StringUtils.isEmpty(price)){
//切开价格
String[] split = price.split("-");
//判断结束价格不等于*,就设置价格上限 和下限
if(!split[1].equalsIgnoreCase("*")){
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").from(split[0],true).to(split[1],true));
}else {
//只设置价格下限 >=
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(split[0]));
}
}
//关联查询器对象和过滤器
nativeSearchQueryBuilder.withFilter(boolQueryBuilder);
//构建分页查询
//获取要跳转到的页面
Integer pageNum=1;
//判断前端传递过来的页码
if(!StringUtils.isEmpty(searchMap.get("pageNum"))){
//把页码转换为整数
try {
pageNum= Integer.valueOf(searchMap.get("pageNum"));
} catch (NumberFormatException e) {
e.printStackTrace();
//转换失败,赋初始化值1
pageNum=1;
}
}
//获取每页显示的记录数
Integer pageSize=5;
//判断前端传递过来的每页显示记录数
if(!StringUtils.isEmpty(searchMap.get("pageSize"))){
try {
pageSize=Integer.valueOf(searchMap.get("pageSize"));
} catch (NumberFormatException e) {
e.printStackTrace();
pageSize=5;
}
}
//查询器对象,设置分页参数
//注意页码要当前页码-1
nativeSearchQueryBuilder.withPageable(PageRequest.of(pageNum-1,pageSize));
//获取排序方式
String sortRule = searchMap.get("sortRule");
String sortField = searchMap.get("sortField");
//判断不为空,设置排序条件
if(!StringUtils.isEmpty(sortRule)&&!StringUtils.isEmpty(sortField)){
nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(sortField).order(sortRule.equals("DESC")? SortOrder.DESC:SortOrder.ASC));
}
//设置高亮字段
nativeSearchQueryBuilder.withHighlightFields(new HighlightBuilder.Field("title"));
//设置高亮选项 高亮前缀、高亮后缀
nativeSearchQueryBuilder.withHighlightBuilder(new HighlightBuilder().preTags("<span style='color:red'>").postTags("</span>"));
//设置多关键词搜索
nativeSearchQueryBuilder.withQuery(QueryBuilders.multiMatchQuery(keywords,"title","brand","category"));
//构建查询器对象
NativeSearchQuery searchQuery = nativeSearchQueryBuilder.build();
//执行查询
SearchHits<SkuInfo> searchHits = elasticsearchRestTemplate.search(searchQuery, SkuInfo.class);
//获取高亮结果
for (SearchHit<SkuInfo> searchHit : searchHits) {
//获取高亮的内容
Map<String, List<String>> highlightFields = searchHit.getHighlightFields();
//获取标题的高亮结果
searchHit.getContent().setTitle(highlightFields.get("title")==null?searchHit.getContent().getTitle():highlightFields.get("title").get(0));
}
//对搜索结果进行分页封装
SearchPage<SkuInfo> skuPage = SearchHitSupport.searchPageFor(searchHits, searchQuery.getPageable());
//创建集合存储商品信息
List<SkuInfo> skuInfoList=new ArrayList<>();
for (SearchHit<SkuInfo> searchHit : skuPage.getContent()) {
SkuInfo skuInfo= searchHit.getContent();
skuInfoList.add(skuInfo);
}
//获取分组结果
//获取分组结果
Terms terms = searchHits.getAggregations().get("skuCategorygroup");
// 获取分类名称集合
List<String> categoryList = getStringsCategoryList(terms);
//获取品牌分组结果
Terms termsBrand=searchHits.getAggregations().get("skuBrandgroup");
//获取品牌集合
List<String> brandList = getStringBrandList(termsBrand);
//获取规格分组结果
Terms termsSpec= searchHits.getAggregations().get("skuSpecgroup");
Map<String, Set<String>> specMap = getStringSetMap(termsSpec);
Map resultMap = new HashMap<>();
resultMap.put("rows", skuInfoList);//获取所需SkuInfo集合数据内容
resultMap.put("total",searchHits.getTotalHits());//总记录数
resultMap.put("totalPages", skuPage.getTotalPages());//总页数
resultMap.put("categoryList",categoryList);
resultMap.put("brandList",brandList);
resultMap.put("specMap",specMap);
return resultMap;
}
/**
* 获取分类列表数据
* @param terms
* @return
**/
private List<String> getStringsCategoryList(Terms terms) {
List<String> categoryList = new ArrayList<>();
if (terms != null) {
for (Terms.Bucket bucket : terms.getBuckets()) {
String keyAsString = bucket.getKeyAsString();// 分组的值(分类名称)
categoryList.add(keyAsString);
}
}
return categoryList;
}
//获取品牌分组结果
private List<String> getStringBrandList(Terms terms){
List<String> brandList=new ArrayList<>();
if(terms!=null){
for (Terms.Bucket bucket : terms.getBuckets()) {
String keyAsString = bucket.getKeyAsString();
brandList.add(keyAsString);
}
}
return brandList;
}
//获取规格分组结果
private Map<String, Set<String>> getStringSetMap(Terms terms){
Map<String, Set<String>> specMap=new HashMap<>();
Set<String> specList=new HashSet<>();
if(terms!=null){
for (Terms.Bucket bucket : terms.getBuckets()) {
specList.add(bucket.getKeyAsString());
}
}
//循环遍历
for (String specJson : specList) {
//把规格的json字符串转换为map
Map<String, String> map= JSON.parseObject(specJson,Map.class);
//遍历map
for (Map.Entry<String, String> entry : map.entrySet()) {
String key=entry.getKey();
String value=entry.getValue();
//去specMap查找指定的key看是否存在
Set<String> specValues = specMap.get(key);
//判断value如果为空
if(specValues==null){
//初始化
specValues=new HashSet<String>();
}
//把当前的值添加到set
specValues.add(value);
//把数据添加到map
specMap.put(key, specValues);
}
}
return specMap;
}
}