1、商品搜索流程
商品搜索,包含了数据实时同步和搜索过程,这2个过程必须清楚才能做出更符合工作需要的搜索。
1)数据同步流程如下:
- 1、商家通过程序将Sku添加到数据库
- 2、后台通过 MQ 或者 Canal 实现实时将数据同步到ES搜索引擎
- 3、用户可以在前台使用不同条件进行搜索,关键词、分类、价格区间、动态属性
2)搜索引擎流程实现
@Data
public class SkuEs {
@Id
private String id;
private String name;
private Integer price;
private Integer num;
private String image;
private String images;
private Date createTime;
private Date updateTime;
private String spuId;
private Integer categoryId;
//Keyword:不分词
@Field(type=FieldType.Keyword)
private String categoryName;
private Integer brandId;
@Field(type=FieldType.Keyword)
private String brandName;
private String skuAttribute;
private Integer status;
//属性映射
private Map<String,String> attrMap;
}
2、商品关键词搜索功能实现
2.1 关键字查询
用户输入关键词keywords后,将keywords关键词一起传入后台,需要根据商品名字进行搜索。以后也有可能根据别的条件查询,所以传入后台的数据可以用Map接收,响应页面的数据包含列表、分页等信息,可以用Map封装。
/***
* 商品搜索
* @param searchMap
* @return
*/
@Override
public Map<String, Object> search(Map<String, Object> searchMap) {
//条件封装
NativeSearchQueryBuilder queryBuilder = queryBuilder(searchMap);
//执行搜索
Page<SkuEs> result = skuSearchMapper.search(queryBuilder.build());
//结果集
List<SkuEs> list = result.getContent();
//中记录数
long totalElements = result.getTotalElements();
Map<String,Object> resultMap = new HashMap<String,Object>();
resultMap.put("list",list);
resultMap.put("totalElements",totalElements);
return resultMap;
}
/***
* 搜索条件组装
*/
public NativeSearchQueryBuilder queryBuilder(Map<String,Object> searchMap){
//QueryBuilder构建
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//条件判断
if(searchMap!=null && searchMap.size()>0){
//关键词
Object keywords =searchMap.get("keywords");
if(!StringUtils.isEmpty(keywords)){
queryBuilder.withQuery(QueryBuilders.termQuery("name",keywords.toString()));
}
}
return queryBuilder;
}
2.2 商品条件回显分析
我们每次执行搜索的时候,页面会显示不同搜索条件,例如:品牌、分类、属性,这些搜索条件都不是固定的,其实他们是没执行搜索的时候,符合搜索条件的商品所有品牌和所有分类,以及所有属性,把他们查询出来,然后页面显示。但是这些条件都没有重复的,也就是说要去重,去重一般采用分组查询即可,所以我们要想动态获取这样的搜索条件,我们需要在后台进行分组查询。
比如返回的结果如下,之后按照分类,品牌,动态属性列表在前端进行展示:
{
"code": 200,
"message": "操作成功",
"data": {
"categoryList": [
"家电",
"儿童",
"图书"
],
"brandList": [
"艾尔利",
"华为",
"OPPO"
],
"attrMap": {
"网络": [
"电信5G",
"电信4G",
"移动4G",
"联通4G"
],
"手机屏尺寸": [
"5.0-5.9英寸",
"6.0-6.2英寸"
],
"像素": [
"3000万以上",
"800-3000万",
"1200-1599万",
"1600万以上"
],
"价格范围": [
"0-500元",
"500-1000元",
"1000-1500元",
"1500-2000元",
"2000-3000元",
"3000元以上"
]
},
"list": [
{
"brandId": 11,
"brandName": "华为",
"categoryId": 11159,
"categoryName": "软件研发",
"createTime": 1603184062000,
"id": "1318596430398562305",
"image": "...",
"images": "...",
"name": "华为Mate40 Pro 128G",
"num": 2,
"price": 3,
"spuId": "1318596430293704706",
"status": 1,
"updateTime": 1603184062000
}
],
"totalElements": 3
}
}
2.3 品牌、分类条件查询
/***
* 分组查询
*/
public void group(NativeSearchQueryBuilder queryBuilder,Map<String, Object> searchMap){
//用户如果没有输入分类条件,则需要将分类搜索出来,作为条件提供给用户
if(StringUtils.isEmpty(searchMap.get("category"))){
queryBuilder.addAggregation(
AggregationBuilders
.terms("categoryList")//别名,类似Map的key
.field("categoryName")//根据categoryName域进行分组
.size(100) //分组结果显示100个
);
}
//用户如果没有输入品牌条件,则需要将品牌搜索出来,作为条件提供给用户
if(StringUtils.isEmpty(searchMap.get("brand"))){
queryBuilder.addAggregation(
AggregationBuilders
.terms("brandList")//别名,类似Map的key
.field("brandName")//根据brandName域进行分组
.size(100) //分组结果显示100个
);
}
//属性分组查询
queryBuilder.addAggregation(
AggregationBuilders
.terms("attrMap")//别名,类似Map的key
.field("skuAttribute")//根据skuAttribute域进行分组
.size(100000) //分组结果显示100000个
);
}
2.4 品牌、分类分组结果解析
/***
* 分组结果解析
*/
public void parseGroup(Aggregations aggregations,Map<String,Object> resultMap){
if(aggregations!=null){
for (Aggregation aggregation : aggregations) {
//强转ParsedStringTerms
ParsedStringTerms terms = (ParsedStringTerms) aggregation;
//循环结果集对象
List<String> values = new ArrayList<String>();
for (Terms.Bucket bucket : terms.getBuckets()) {
values.add(bucket.getKeyAsString());
}
//名字
String key = aggregation.getName();
resultMap.put(key,values);
}
}
}
/****
* 将属性信息合并成Map对象
*/
public void attrParse(Map<String,Object> searchMap){
//先获取attrmaps
Object attrmaps = searchMap.get("attrMap");
if(attrmaps!=null){
//集合数据
List<String> groupList= (List<String>) attrmaps;
//定义一个集合Map<String,Set<String>>,存储所有汇总数据
Map<String,Set<String>> allMaps = new HashMap<String,Set<String>>();
//循环集合
for (String attr : groupList) {
Map<String,String> attrMap = JSON.parseObject(attr,Map.class);
for (Map.Entry<String, String> entry : attrMap.entrySet()) {
//获取每条记录,将记录转成Map 就业薪资 学习费用
String key = entry.getKey();
Set<String> values = allMaps.get(key);
if(values==null){
values = new HashSet<String>();
}
values.add(entry.getValue());
//覆盖之前的数据
allMaps.put(key,values);
}
}
//覆盖之前的attrmaps
searchMap.put("attrMap",allMaps);
}
}
2.5 调用实现分组查询
/****
* 关键词搜索
* @param searchMap
* 关键词:keywords->name
* @return
*/
@Override
public Map<String, Object> search(Map<String, Object> searchMap) {
//QueryBuilder->构建搜索条件
NativeSearchQueryBuilder queryBuilder =queryBuilder(searchMap);
//分组搜索调用
group(queryBuilder,searchMap);
//1.设置高亮信息 关键词前(后)面的标签、设置高亮域
HighlightBuilder.Field field = new HighlightBuilder
.Field("name") //根据指定的域进行高亮查询
.preTags("<span style=\"color:red;\">") //关键词高亮前缀
.postTags("</span>") //高亮关键词后缀
.fragmentSize(100); //碎片长度
queryBuilder.withHighlightFields(field);
//2.将非高亮数据替换成高亮数据
AggregatedPage<SkuEs> page = elasticsearchRestTemplate.queryForPage(queryBuilder.build(), SkuEs.class,null);
//获取结果集:集合列表、总记录数
Map<String,Object> resultMap = new HashMap<String,Object>();
//分组数据解析
parseGroup(page.getAggregations(),resultMap);
//动态属性解析
attrParse(resultMap);
List<SkuEs> list = page.getContent();
resultMap.put("list",list);
resultMap.put("totalElements",page.getTotalElements());
return resultMap;
}
3.商品搜索条件筛选
3.1 条件搜索分析
用户在前端执行条件搜索的时候,有可能会选择分类、品牌、价格、属性,每次选择条件传入后台,后台按照指定参数进行条件查询,我们这里约定一个传参数的规则:
1、分类参数:category
2、品牌参数:brand
3、价格参数:price
4、属性参数:attr_属性名:属性值
5、分页参数:page
3.2 分类/品牌/价格查询
/***
* 搜索条件组装
*/
public NativeSearchQueryBuilder queryBuilder(Map<String,Object> searchMap){
//QueryBuilder构建
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//多条件组合查询对象
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//条件判断
if(searchMap!=null && searchMap.size()>0){
//关键词
Object keywords =searchMap.get("keywords");
if(!StringUtils.isEmpty(keywords)){
boolQuery.must(QueryBuilders.termQuery("name",keywords.toString()));
}
//分类
Object category =searchMap.get("category");
if(!StringUtils.isEmpty(category)){
boolQuery.must(QueryBuilders.termQuery("categoryName",category.toString()));
}
//品牌
Object brand =searchMap.get("brand");
if(!StringUtils.isEmpty(brand)){
boolQuery.must(QueryBuilders.termQuery("brandName",brand.toString()));
}
//价格
Object price =searchMap.get("price");
if(!StringUtils.isEmpty(price)){
//去掉元和以上,并按-分割
String[] prices = price.toString()
.replace("元","")
.replace("以上","")
.split("-");
//price>x
boolQuery.must(QueryBuilders.rangeQuery("price").gt(Integer.valueOf(prices[0])));
//price<=y
if(prices.length==2){
boolQuery.must(QueryBuilders.rangeQuery("price").lte(Integer.valueOf(prices[1])));
}
}
}
return queryBuilder.withQuery(boolQuery);
}
3.3 分页查询
编写分页实现,先获取当前页,再在SkuSearchServiceImpl#queryBuilder
调用该方法。
1)获取分页方法
/***
* 当前页
*/
public int currentPage(Map<String,Object> searchMap){
try {
Object currentPage = searchMap.get("page");
return Integer.valueOf(currentPage.toString())> 0 ? Integer.valueOf(currentPage.toString()) -1 : 0;
} catch (NumberFormatException e) {
return 0;
}
}
2)分页调用
//分页查询
builder.withPageable(PageRequest.of(currentPage(searchMap),10));
3.4 属性查询
属性查询,每次传到后台的参数都是以attr_
开始,我们可以遍历传过来的参数searchMap,判断是否是以attr_
开始的参数,如果是,则查询属性。
在SkuSearchServiceImpl#queryBuilder
中添加如下代码 即可:
上图代码如下:
//属性查询
for (Map.Entry<String, Object> entry : searchMap.entrySet()) {
if(entry.getKey().startsWith("attr_")){
//获取结果
String key = entry.getKey().replaceFirst("attr_","");
String value = entry.getValue().toString();
//执行查询
boolQuery.must(QueryBuilders.termQuery("attrMap."+key+".keyword",value));
}
}
4、商品搜索排序实现
4.1 排序搜索分析
排序搜索有多种排序方式,我们可以把排序升序、降序当做一个参数,把排序的域当做一个参数,无论是哪种排序方式,只需要把这两个参数传到后端服务即可。
排序规则有 销量、新品、评价、价格,但是这里排序字段只有单个一种排序,不会出现销量升序和价格升序这种组合排序。
我们定义一下传参数规则:
1、排序域:sfield
2、排序方式:sm
例如:根据价格升序
sfield=price
sm=ASC
例如:新品排序
sfield=updateTime
sm=DESC
4.2 排序实现
在SkuSearchServiceImpl#queryBuilder
中添加如下daima 即可:
//排序
Object sfield = searchMap.get("sfield");
Object sm = searchMap.get("sm");
if(!StringUtils.isEmpty(sfield) && !StringUtils.isEmpty(sm)){
queryBuilder.withSort(
SortBuilders.fieldSort(sfield.toString()) //排序域
.order(SortOrder.valueOf(sm.toString()))); //排序方式
}
5、商品搜索高亮实现
5.1 高亮原理分析
高亮是指搜索商品的时候,商品列表中如何和你搜索的关键词相同,那么它会高亮展示,也就是变色展示,如下京东搜索,其实就是给关键词增加了样式,所以是红色,ES搜索引擎也是一样,也可以实现关键词高亮展示,原理和京东搜索高亮原理一样。
5.2 高亮实现
高亮搜索实现有2个步骤:
1、配置高亮域以及对应的样式
2、从结果集中取出高亮数据,并将非高亮数据换成高亮数据
1)高亮配置
在SkuSearchServiceImpl#search
中添加如下高亮代码:
//高亮配置
HighlightBuilder.Field field = new HighlightBuilder.
Field("name"). //指定的高亮域
preTags("<span style=\"color:red\">"). //前缀
postTags("</span>"). //后缀
fragmentSize(100);
//添加高亮域
queryBuilder.withHighlightFields(field);
2)结果映射转换
创建一个结果映射转换对象com.gupaoedu.vip.mall.search.es.HighlightResultMapper
,其实主要是将非高亮转换成高亮数据,代码如下:
public class HighlightResultMapper extends DefaultResultMapper{
/***
* 处理结果集
*/
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
//所有数据
for (SearchHit hit : response.getHits()) {
//当前单条数据
Map<String, Object> sourceMap = hit.getSourceAsMap();
//高亮数据
for (Map.Entry<String, HighlightField> entry : hit.getHighlightFields().entrySet()) {
String key = entry.getKey();
if (sourceMap.containsKey(key)) {
Text[] fragments = entry.getValue().getFragments();
sourceMap.put(key, transTextArrayToString(fragments));
}
}
hit.sourceRef(new ByteBufferReference(ByteBuffer.wrap(JSONObject.toJSONString(sourceMap).getBytes())));
}
return super.mapResults(response, clazz, pageable);
}
/***
* 拼接数据碎片
*/
private String transTextArrayToString(Text[] fragments) {
if (null == fragments) {
return "";
}
StringBuffer buffer = new StringBuffer();
for (Text fragment : fragments) {
buffer.append(fragment.string());
}
return buffer.toString();
}
}
评论区