有四个问题需要先思考清楚:
什么情况下显示有关规格参数的过滤?
如果用户尚未选择商品分类,或者聚合得到的分类数大于1,那么就没必要进行规格参数的聚合。因为不同分类的商品,其规格是不同的。
因此,我们在后台需要对聚合得到的商品分类数量进行判断,如果等于1,我们才继续进行规格参数的聚合。
如何知道哪些规格需要过滤?
我们不能把数据库中的所有规格参数都拿来过滤。因为并不是所有的规格参数都可以用来过滤,参数的值是不确定的。
值的庆幸的是,我们在设计规格参数时,已经标记了某些规格可搜索,某些不可搜索。
因此,一旦商品分类确定,我们就可以根据商品分类查询到其对应的规格,从而知道哪些规格要进行搜索。
要过滤的参数,其可选值是如何获取的?
虽然数据库中有所有的规格参数,但是不能把一切数据都用来供用户选择。
与商品分类和品牌一样,应该是从用户搜索得到的结果中聚合,得到与结果品牌的规格参数可选值。
规格过滤的可选值,其数据格式怎样的?
我们直接看页面效果:
我们之前存储时已经将数据分段,恰好符合这里的需求
接下来,我们就用代码实现刚才的思路。
总结一下,应该是以下几步:
返回结果中需要增加新数据,用来保存规格参数过滤条件。这里与前面的品牌和分类过滤的json结构类似:
[
{
"k":"规格参数名",
"options":["规格参数值","规格参数值"]
}
]
因此,在java中我们用List<Map<String, String>>来表示。
public class SearchResult extends PageResult<Goods>{
private List<Category> categories;// 分类过滤条件
private List<Brand> brands; // 品牌过滤条件
private List<Map<String,String>> specs; // 规格参数过滤条件
public SearchResult(Long total, Integer totalPage, List<Goods> items,
List<Category> categories, List<Brand> brands,
List<Map<String,String>> specs) {
super(total, totalPage, items);
this.categories = categories;
this.brands = brands;
this.specs = specs;
}
}
首先,在聚合得到商品分类后,判断分类的个数,如果是1个则进行规格聚合:
我们将聚合的代码抽取到了一个getSpecs
方法中。
然后,我们需要根据商品分类,查询所有可用于搜索的规格参数:
要注意的是,这里我们需要根据id查询规格,而规格参数接口需要从商品微服务提供
因为规格参数保存时不做分词,因此其名称会自动带上一个.keyword后缀:
@Service
public class SearchService {
@Autowired
private CategoryClient categoryClient;
@Autowired
private GoodsClient goodsClient;
@Autowired
private SpecificationClient specificationClient;
@Autowired
private GoodsRepository goodsRepository;
@Autowired
private BrandClient brandClient;
@Autowired
private ElasticsearchTemplate elasticsearchTemplate;
private ObjectMapper mapper = new ObjectMapper();
private static final Logger logger = LoggerFactory.getLogger(SearchService.class);
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);
queryBuilder.withQuery(basicQuery);
// 通过sourceFilter设置返回的结果字段,我们只需要id、skus、subTitle
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id", "skus", "subTitle"}, null));
// 分页
searchWithPageAndSort(queryBuilder, request);
// 聚合
queryBuilder.addAggregation(AggregationBuilders.terms("brands").field("cid3"));
queryBuilder.addAggregation(AggregationBuilders.terms("category").field("brandId"));
// 执行查询获取结果集
AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>) this.goodsRepository.search(queryBuilder.build());
// 获取聚合结果集
// 商品分类的聚合结果
List<Category> categories =
getCategoryAggResult(goodsPage.getAggregation("brands"));
// 品牌的聚合结果
List<Brand> brands = getBrandAggResult(goodsPage.getAggregation("category"));
// 根据商品分类判断是否需要聚合
List<Map<String, Object>> specs = new ArrayList<>();
if (categories.size() == 1) {
// 如果商品分类只有一个才进行聚合,并根据分类与基本查询条件聚合
specs = getSpec(categories.get(0).getId(), basicQuery);
}
return new SearchResult(goodsPage.getTotalElements(), goodsPage.getTotalPages(), goodsPage.getContent(), categories, brands, specs);
}
/**
* 聚合出规格参数
*
* @param cid
* @param query
* @return
*/
private List<Map<String, Object>> getSpec(Long cid, QueryBuilder query) {
try {
// 不管是全局参数还是sku参数,只要是搜索参数,都根据分类id查询出来
List<SpecParam> params = this.specificationClient.querySpecParam(null, cid, true, null);
List<Map<String, Object>> specs = new ArrayList<>();
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
queryBuilder.withQuery(query);
// 聚合规格参数
params.forEach(p -> {
String key = p.getName();
queryBuilder.addAggregation(AggregationBuilders.terms(key).field("specs." + key + ".keyword"));
});
// 查询
Map<String, Aggregation> aggs = this.elasticsearchTemplate.query(queryBuilder.build(),
SearchResponse::getAggregations).asMap();
// 解析聚合结果
params.forEach(param -> {
Map<String, Object> spec = new HashMap<>();
String key = param.getName();
spec.put("k", key);
StringTerms terms = (StringTerms) aggs.get(key);
spec.put("options", terms.getBuckets().stream().map(StringTerms.Bucket::getKeyAsString));
specs.add(spec);
});
return specs;
} catch (
Exception e)
{
logger.error("规格聚合出现异常:", e);
return null;
}
}
// 构建基本查询条件
private void searchWithPageAndSort(NativeSearchQueryBuilder queryBuilder, SearchRequest request) {
// 准备分页参数
int page = request.getPage();
int size = request.getSize();
// 1、分页
queryBuilder.withPageable(PageRequest.of(page - 1, size));
// 2、排序
String sortBy = request.getSortBy();
Boolean desc = request.getDescending();
if (StringUtils.isNotBlank(sortBy)) {
// 如果不为空,则进行排序
queryBuilder.withSort(SortBuilders.fieldSort(sortBy).order(desc ? SortOrder.DESC : SortOrder.ASC));
}
}
// 解析品牌聚合结果
private List<Brand> getBrandAggResult(Aggregation aggregation) {
try {
LongTerms brandAgg = (LongTerms) aggregation;
List<Long> bids = new ArrayList<>();
for (LongTerms.Bucket bucket : brandAgg.getBuckets()) {
bids.add(bucket.getKeyAsNumber().longValue());
}
// 根据id查询品牌
return this.brandClient.queryBrandByIds(bids);
} catch (Exception e) {
logger.error("品牌聚合出现异常:", e);
return null;
}
}
// 解析商品分类聚合结果
private List<Category> getCategoryAggResult(Aggregation aggregation) {
try {
List<Category> categories = new ArrayList<>();
LongTerms categoryAgg = (LongTerms) aggregation;
List<Long> cids = new ArrayList<>();
for (LongTerms.Bucket bucket : categoryAgg.getBuckets()) {
cids.add(bucket.getKeyAsNumber().longValue());
}
// 根据id查询分类名称
List<String> names = this.categoryClient.queryNameByIds(cids);
for (int i = 0; i < names.size(); i++) {
Category c = new Category();
c.setId(cids.get(i));
c.setName(names.get(i));
categories.add(c);
}
return categories;
} catch (Exception e) {
logger.error("分类聚合出现异常:", e);
return null;
}
}
}
首先把后台传递过来的specs添加到filters数组:
要注意:分类、品牌的option选项是对象,里面有name属性,而specs中的option是简单的字符串,所以需要进行封装,变为相同的结构:
最后的结果:
是不是感觉显示的太多了,我们可以通过按钮点击来展开和隐藏部分内容:
我们在data中定义变量,记录展开或隐藏的状态:
然后在按钮绑定点击事件,以改变show的取值:
在展示规格时,对show进行判断:
OK!