1. 搜索过滤分析
前面我们已经实现了基本搜索,页面上已经可以展示出搜索的结果了。接下来就来实现一下搜索过滤,先来看一下要实现的效果:
搜索过滤有 3 部分组成:
- 顶部的导航,已经选择的过滤条件展示
- 商品分类面包屑,根据用户选择的商品分类变化
- 其它已选择过滤参数
- 过滤条件展示,包含 3 部分
- 商品分类展示
- 品牌展示
- 其它规格参数
- 展开或收起的过滤条件的按钮
实现顺序
顶部导航要展示的内容跟用户选择的过滤条件有关,展开或收起的按钮是否显示也取决于用户选择的过滤条件的多少(如果很少,那么就没必要显示)。看来我们必须先需要实现过滤条件展示。
2. 展示分类和品牌过滤
2.1 实现思路
首先,我们要展示出的分类和品牌信息,肯定不是所用商品的分类和品牌,而是用户进行搜索后的商品的分类和品牌。也就是说无论是分类信息,还是品牌信息,都应该从搜索的商品中进行聚合得到。
2.2 拓展搜索结果
之前我们返回的搜索结果是 PageResult 对象,其中有 total、totalPage、items 这 3 个属性。但现在还要返回商品的分类和品牌信息,我们就需要对返回的结果进行扩展,添加分类和品牌的属性。
那么问题来了,分类和品牌的属性应该使用什么数据类型呢?
- 分类:页面需要分类名称,但背后肯定要保存 id 信息。我们可以以键值对的方式进行存取,那就可以使用
List<Map<String, Object>>
- 品牌:页面需要品牌图片,如果没有图片时,就展示品牌名称,但背后肯定要保存 id 信息。基本上是品牌的完整数据,所以可以使用
List<Brand>
在 leyou-search 工程 pojo 包中创建 SearchResult 类,继承 PageResult
public class SearchResult extends PageResult<Goods> {
private List<Brand> brands;
private List<Map<String,Object>> categories;
public SearchResult() {
}
public SearchResult(Long total, List items, Integer totalPage, List<Brand> brands, List<Map<String, Object>> categories) {
super(total, items, totalPage);
this.brands = brands;
this.categories = categories;
}
public List<Brand> getBrands() {
return brands;
}
public void setBrands(List<Brand> brands) {
this.brands = brands;
}
public List<Map<String, Object>> getCategories() {
return categories;
}
public void setCategories(List<Map<String, Object>> categories) {
this.categories = categories;
}
}
2.3 聚合商品分类和品牌
-
修改 leyou-search 工程 service 包中的 SearchService 中的 search 方法
/**
* 根据搜索条件搜索数据
*
* @param request
* @return
*/
public SearchResult search(SearchRequest request) {
// 获取搜索条件
String key = request.getKey();
// 判断是否有搜索条件,如果没有,直接返回 null。不允许搜索全部商品
if (StringUtils.isBlank(key)) {
return null;
}
// 构建查询条件
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 对 key 进行匹配查询
queryBuilder.withQuery(QueryBuilders.matchQuery("all", key).operator(Operator.AND));
// 通过 sourceFilter 设置返回的结果字段,我们只需要 id、skus、subTitle
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id", "skus", "subTitle"}, null));
// 分页
int page = request.getPage();
int size = request.getSize();
queryBuilder.withPageable(PageRequest.of(page - 1, size));
// 加入请求中的排序条件
String sortBy = request.getSortBy();
Boolean descending = request.getDescending();
if (StringUtils.isNotBlank(sortBy)) {
queryBuilder.withSort(SortBuilders.fieldSort(sortBy).order(descending ? SortOrder.DESC : SortOrder.ASC));
}
// 品牌和分类的聚合名称
String categoryAggName = "categories";
String brandAggName = "brands";
// 添加品牌和分类的聚合
queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3"));
queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId"));
// 执行查询
AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>)this.goodsRepository.search(queryBuilder.build());
// 解析聚合结果集
List<Map<String, Object>> categories = getCategoryAggResult((LongTerms)goodsPage.getAggregation(categoryAggName));
List<Brand> brands = getBrandAggResult((LongTerms)goodsPage.getAggregation(brandAggName));
// 封装结果并返回
return new SearchResult(goodsPage.getTotalElements(), goodsPage.getContent(), goodsPage.getTotalPages(), brands, categories);
}
2.在 SearchService 中添加解析聚合结果集的方法
/**
* 解析品牌聚合结果集
* @param aggregation
* @return
*/
private List<Brand> getBrandAggResult(LongTerms aggregation) {
ArrayList<Brand> brands = new ArrayList<>();
// 获取桶
List<LongTerms.Bucket> buckets = aggregation.getBuckets();
// 遍历桶
for (LongTerms.Bucket bucket : buckets) {
// 根据品牌 id 查询品牌
Brand brand = brandClient.queryBrandById(bucket.getKeyAsNumber().longValue());
brands.add(brand);
}
return brands;
}
/**
* 解析分类聚合结果集
* @param aggregation
* @return
*/
private List<Map<String, Object>> getCategoryAggResult(LongTerms aggregation) {
ArrayList<Map<String, Object>> categories = new ArrayList<>();
// 获取桶
List<LongTerms.Bucket> buckets = aggregation.getBuckets();
// 遍历桶
for (LongTerms.Bucket bucket : buckets) {
HashMap<String, Object> category = new HashMap<>();
// 获取 key
long id = bucket.getKeyAsNumber().longValue();
// 根据商品分类 id,查询商品分类名称
List<String> names = categoryClient.queryNamesByIds(Arrays.asList(id));
category.put("id", id);
category.put("name",names.get(0));
categories.add(category);
}
return categories;
}
2.4 测试
-
重启 leyou-search 工程
-
再次搜索手机,成功返回分类和品牌过滤条件
2.5 页面渲染
略,交给前端吧。最终效果如下:文章来源:https://www.toymoban.com/news/detail-830679.html
3. 展示规格参数过滤
3.1 实现思路
首先思考来下面几个问题:
什么时候需要展示规格参数过滤信息?
如果用户尚未选择商品分类,或者聚合得到的分类数大于 1,那么就没必要进行规格参数的聚合。因为不同分类的商品,其规格参数是不同的。
因此,需要对聚合得到的商品分类数量进行判断,如果等于 1,我们才继续进行规格参数的聚合。
哪些规格参数需要过滤?
我们在设计规格参数时,已经标记了某些规格可搜索。
因此,商品分类确定后,我们就可以根据商品分类查询到其对应的可搜索的规格参数,这些规格参数就是需要过滤的。
如何获取需要过滤规格参数的值?
虽然数据库中有所有该分类下的规格参数值,但是不能都用来给供用户选择,因为有些规格参数值并不在用户的搜索结果中。
应该是从用户搜索得到的结果中聚合规格参数,从而得到规格参数可选值。
3.2 拓展搜索结果
搜索结果中需要增加一个属性,用来保存规格参数过滤条件。
那么问题来了,规格参数过滤条件的属性应该使用什么数据类型呢?
来看看规格参数过滤条件的 JSON 结构,所以我们用 List<Map<String, Object>>
来表示。
[
{
"k":"规格参数名",
"options":["规格参数值","规格参数值"]
}
]
在 SearchResult 类中增加 specs 属性
public class SearchResult extends PageResult<Goods> {
private List<Brand> brands;
private List<Map<String,Object>> categories;
private List<Map<String,Object>> specs;
// Constructor、getter、setter、toString 方法省略
}
3.3 聚合规格参数
-
修改 leyou-search 工程 service 包中的 SearchService 中的 search 方法
/**
* 根据搜索条件搜索数据
*
* @param request
* @return
*/
public SearchResult search(SearchRequest request) {
// 获取搜索条件
String key = request.getKey();
// 判断是否有搜索条件,如果没有,直接返回 null。不允许搜索全部商品
if (StringUtils.isBlank(key)) {
return null;
}
// 构建查询条件
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 添加查询条件
MatchQueryBuilder basicQuery = QueryBuilders.matchQuery("all", key).operator(Operator.AND);
// 对 key 进行匹配查询
queryBuilder.withQuery(basicQuery);
// 通过 sourceFilter 设置返回的结果字段,我们只需要 id、skus、subTitle
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id", "skus", "subTitle"}, null));
// 分页
int page = request.getPage();
int size = request.getSize();
queryBuilder.withPageable(PageRequest.of(page - 1, size));
// 加入请求中的排序条件
String sortBy = request.getSortBy();
Boolean descending = request.getDescending();
if (StringUtils.isNotBlank(sortBy)) {
queryBuilder.withSort(SortBuilders.fieldSort(sortBy).order(descending ? SortOrder.DESC : SortOrder.ASC));
}
// 品牌和分类的聚合名称
String categoryAggName = "categories";
String brandAggName = "brands";
// 添加品牌和分类的聚合
queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3"));
queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId"));
// 执行查询
AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>)this.goodsRepository.search(queryBuilder.build());
// 解析聚合结果集
List<Map<String, Object>> categories = getCategoryAggResult((LongTerms)goodsPage.getAggregation(categoryAggName));
List<Brand> brands = getBrandAggResult((LongTerms)goodsPage.getAggregation(brandAggName));
// 判断分类聚合的结果集大小,等于1则聚合
List<Map<String, Object>> specs = null;
if (categories.size() == 1) {
specs = getParamAggResult((Long)categories.get(0).get("id"), basicQuery);
}
// 封装结果并返回
return new SearchResult(goodsPage.getTotalElements(), goodsPage.getContent(), goodsPage.getTotalPages(), brands, categories, specs);
}
2.在 SearchService 中添加聚合规格参数过滤条件的方法
/**
* 聚合出规格参数过滤条件
* @param id
* @param basicQuery
* @return
*/
private List<Map<String,Object>> getParamAggResult(Long id, QueryBuilder basicQuery) {
// 创建自定义查询构建器
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 基于基本的查询条件,聚合规格参数
queryBuilder.withQuery(basicQuery);
// 查询要聚合的规格参数
List<SpecParam> params = this.specificationClient.querySpecParams(null, id, null, true);
// 添加聚合
params.forEach(param -> {
queryBuilder.addAggregation(AggregationBuilders.terms(param.getName()).field("specs." + param.getName() + ".keyword"));
});
// 只需要聚合结果集,不需要查询结果集
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{}, null));
// 执行聚合查询
AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>)this.goodsRepository.search(queryBuilder.build());
// 定义一个集合,收集聚合结果集
List<Map<String, Object>> paramMapList = new ArrayList<>();
// 解析聚合查询的结果集
Map<String, Aggregation> aggregationMap = goodsPage.getAggregations().asMap();
for (Map.Entry<String, Aggregation> entry : aggregationMap.entrySet()) {
Map<String, Object> map = new HashMap<>();
// 放入规格参数名
map.put("k", entry.getKey());
// 收集规格参数值
List<Object> options = new ArrayList<>();
// 解析每个聚合
StringTerms terms = (StringTerms)entry.getValue();
// 遍历每个聚合中桶,把桶中key放入收集规格参数的集合中
terms.getBuckets().forEach(bucket -> options.add(bucket.getKeyAsString()));
map.put("options", options);
paramMapList.add(map);
}
return paramMapList;
}
3.4 测试
-
重启 leyou-search 工程
-
再次搜索手机,成功返回规格参数过滤条件
3.5 页面渲染
略,交给前端吧。最终效果如下:
4. 实现搜索过滤
前面已经实现了展示过滤条件,接下来就可以实现点击过滤条件实现搜索过滤了。
4.1 前端保存过滤项
当我们点击展示的过滤条件后,应该将这个过滤条件保存起来,然后再作为请求参数发给后台。
我们把已选择的过滤项保存在 search 中:
search.filter 是一个对象,结构如下:
{
"过滤项名":"过滤项值"
}
4.2 后台实现搜索过滤
4.2.1 拓展搜索请求对象
我们需要在 SearchRequest 中添加过滤条件属性,可以使用 Map<String, String>
来表示。
在 SearchRequest 类中增加 filter 属性
public class SearchRequest {
private String key;// 搜索条件
private Integer page;// 当前页
private String sortBy; // 排序字段
private Boolean descending; // 是否降序
private Map<String,String> filter; // 过滤条件
// getter、setter、toString 方法省略
}
4.2.2 添加过滤条件
现在我们的基础查询,如下:
MatchQueryBuilder basicQuery = QueryBuilders.matchQuery("all", key).operator(Operator.AND);
但我们要把页面传递的过滤条件也加入进去,就不能在使用普通的查询,而是要用到 BooleanQuery。
BoolQueryBuilder basicQuery = buildBoolQueryBuilder(request);
修改 leyou-search 工程 service 包中的 SearchService 中的 search 方法,将基本查询改为布尔查询
/**
* 根据搜索条件搜索数据
*
* @param request
* @return
*/
public SearchResult search(SearchRequest request) {
// 获取搜索条件
String key = request.getKey();
// 判断是否有搜索条件,如果没有,直接返回 null。不允许搜索全部商品
if (StringUtils.isBlank(key)) {
return null;
}
// 构建查询条件
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 添加布尔查询条件
BoolQueryBuilder basicQuery = buildBoolQueryBuilder(request);
// 对 key 进行匹配查询
queryBuilder.withQuery(basicQuery);
// 通过 sourceFilter 设置返回的结果字段,我们只需要 id、skus、subTitle
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id", "skus", "subTitle"}, null));
// 分页
int page = request.getPage();
int size = request.getSize();
queryBuilder.withPageable(PageRequest.of(page - 1, size));
// 加入请求中的排序条件
String sortBy = request.getSortBy();
Boolean descending = request.getDescending();
if (StringUtils.isNotBlank(sortBy)) {
queryBuilder.withSort(SortBuilders.fieldSort(sortBy).order(descending ? SortOrder.DESC : SortOrder.ASC));
}
// 品牌和分类的聚合名称
String categoryAggName = "categories";
String brandAggName = "brands";
// 添加品牌和分类的聚合
queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3"));
queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId"));
// 执行查询
AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>) this.goodsRepository.search(queryBuilder.build());
// 解析聚合结果集
List<Map<String, Object>> categories = getCategoryAggResult((LongTerms) goodsPage.getAggregation(categoryAggName));
List<Brand> brands = getBrandAggResult((LongTerms) goodsPage.getAggregation(brandAggName));
// 判断分类聚合的结果集大小,等于1则聚合
List<Map<String, Object>> specs = null;
if (categories.size() == 1) {
specs = getParamAggResult((Long) categories.get(0).get("id"), basicQuery);
}
// 封装结果并返回
return new SearchResult(goodsPage.getTotalElements(), goodsPage.getContent(), goodsPage.getTotalPages(), brands, categories, specs);
}
2.添加布尔查询构建器方法
/**
* 布尔查询构建器
* @param request
* @return
*/
private BoolQueryBuilder buildBoolQueryBuilder(SearchRequest request) {
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 添加基本查询条件
boolQueryBuilder.must(QueryBuilders.matchQuery("all", request.getKey()).operator(Operator.AND));
// 判断过滤条件是否为空
if (CollectionUtils.isEmpty(request.getFilter())){
return boolQueryBuilder;
}
// 添加过滤条件
for (Map.Entry<String, String> entry : request.getFilter().entrySet()) {
String key = entry.getKey();
// 如果过滤条件是品牌, 过滤的字段名:brandId
if (StringUtils.equals("品牌", key)) {
key = "brandId";
} else if (StringUtils.equals("分类", key)) {
// 如果是分类,过滤字段名:cid3
key = "cid3";
} else {
// 如果是规格参数名,过滤字段名:specs.key.keyword
key = "specs." + key + ".keyword";
}
boolQueryBuilder.filter(QueryBuilders.termQuery(key, entry.getValue()));
}
return boolQueryBuilder;
}
4.3 测试
-
重启 leyou-search 工程
-
再次搜索手机,并选择品牌 “小米”
5. 展示选择过滤项
5.1 展示商品分类面包屑
当用户选择一个商品分类以后,我们应该在过滤模块的上方展示一个面包屑,把三级商品分类都显示出来。
用户选择的商品分类就存放在 search.filter 中,但是里面只有第三级分类的 id:cid3。
因此,我们需要根据它查询出所有三级分类的 id 及名称。
5.1.1 后台提供查询分类接口
-
在 leyou-item-service 项目中的 CategoryController 中添加方法 queryAllByCid3
/**
* 根据第 3 级分类 id,查询 1~3 级的分类
* @param id
* @return
*/
@GetMapping("/all/level")
public ResponseEntity<List<Category>> queryAllByCid3(@RequestParam("id") Long id){
List<Category> categories = this.categoryService.queryAllByCid3(id);
if (CollectionUtils.isEmpty(categories)) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(categories);
}
2.在 leyou-item-service 项目中的 CategoryService 中添加方法 queryAllByCid3
/**
* 根据第 3 级分类 id,查询 1~3 级的分类
* @param id
* @return
*/
public List<Category> queryAllByCid3(Long id) {
Category category3 = categoryMapper.selectByPrimaryKey(id);
Category category2 = categoryMapper.selectByPrimaryKey(category3.getParentId());
Category category1 = categoryMapper.selectByPrimaryKey(category2.getParentId());
return Arrays.asList(category1,category2,category3);
}
3.打开浏览器,输入以下地址,测试一下
http://api.leyou.com/api/item-service/category/all//level?id=242
5.1.2 页面渲染
略,交给前端吧。最终效果如下:
5.2 展示其他过滤项
略,交给前端吧。最终效果如下:
文章来源地址https://www.toymoban.com/news/detail-830679.html
到了这里,关于乐优商城(七)搜索过滤的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!