搜索过滤
过滤功能分析
整个过滤部分有3块:
- 顶部的导航,已经选择的过滤条件展示:
- 商品分类面包屑,根据用户选择的商品分类变化
- 其它已选择过滤参数
- 过滤条件展示,又包含3部分
- 商品分类展示
- 品牌展示
- 其它规格参数
- 展开或收起的过滤条件的按钮
顶部导航要展示的内容跟用户选择的过滤条件有关。
- 比如用户选择了某个商品分类,则面包屑中才会展示具体的分类
- 比如用户选择了某个品牌,列表中才会有品牌信息。
所以,这部分需要依赖第二部分:过滤条件的展示和选择。
展开或收起的按钮是否显示,取决于过滤条件现在有多少,如果有很多,那么就没必要展示。
生成分类和品牌过滤
先来看分类和品牌。在我们的数据库中已经有所有的分类和品牌信息。用户搜索的条件会对商品进行过滤,而在搜索结果中,不一定包含所有的分类和品牌,直接展示出所有商品分类,让用户选择显然是不合适的。无论是分类信息,还是品牌信息,都应该从搜索的结果商品中进行聚合得到。
扩展返回的结果
原来,我们返回的结果是PageResult对象,里面只有total、totalPage、items3个属性。但是现在要对商品分类和品牌进行聚合,数据显然不够用,我们需要对返回的结果进行扩展,添加分类和品牌的数据。
分类:页面显示了分类名称,但背后肯定要保存id信息。所以至少要有id和name
品牌:页面展示的有logo,有文字,当然肯定有id,基本上是品牌的完整数据
新建一个类,继承PageResult,然后扩展两个新的属性:分类集合和品牌集合:
public class SearchResult extends PageResult<Goods>{
private List<Category> categories;
private List<Brand> brands;
public SearchResult(Long total, Integer totalPage, List<Goods> items, List<Category> categories, List<Brand> brands) {
super(total, totalPage, items);
this.categories = categories;
this.brands = brands;
}
}
聚合商品分类和品牌
修改搜索的业务逻辑,对分类和品牌聚合。
因为索引库中只有id,所以根据id聚合,然后再根据id去查询完整数据。
所以,商品微服务需要提供一个接口:根据品牌id集合,批量查询品牌。
提供查询品牌接口
BrandApi
@RequestMapping("brand")
public interface BrandApi {
@GetMapping("list")
List<Brand> queryBrandByIds(@RequestParam("ids") List<Long> ids);
}
BrandController
/**
* 根据多个id查询品牌
* @param ids
* @return
*/
@GetMapping("list")
public ResponseEntity<List<Brand>> queryBrandByIds(@RequestParam("ids") List<Long> ids){
List<Brand> list = this.brandService.queryBrandByIds(ids);
if(list == null){
new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return ResponseEntity.ok(list);
}
BrandService
public List<Brand> queryBrandByIds(List<Long> ids) {
return this.brandMapper.selectByIdList(ids);
}
BrandMapper
继承通用mapper的 SelectByIdListMapper
即可
public interface BrandMapper extends Mapper<Brand>, SelectByIdListMapper<Brand,Long> {}
搜索功能改造
添加BrandClient
@FeignClient("item-service")
public interface BrandClient extends BrandApi {
}
修改SearchService:
@Service
public class SearchService {
@Autowired
private GoodsRepository goodsRepository;
@Autowired
private CategoryClient categoryClient;
@Autowired
private BrandClient brandClient;
private static final Logger logger = LoggerFactory.getLogger(SearchService.class);
public PageResult<Goods> search(SearchRequest request) {
// 判断是否有搜索条件,如果没有,直接返回null。不允许搜索全部商品
if (StringUtils.isBlank(request.getKey())) {
return null;
}
// 1、构建查询条件
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 通过sourceFilter设置返回的结果字段,我们只需要id、skus、subTitle
queryBuilder.withSourceFilter(new FetchSourceFilter(
new String[]{"id", "skus", "subTitle"}, null));
// 1.1、基本查询
queryBuilder.withQuery(QueryBuilders.matchQuery("all", request.getKey()));
// 1.2.分页排序
searchWithPageAndSort(queryBuilder,request);
// 1.3、聚合
String categoryAggName = "category"; // 商品分类聚合名称
String brandAggName = "brand"; // 品牌聚合名称
// 对商品分类进行聚合
queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3"));
// 对品牌进行聚合
queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId"));
// 2、查询,获取结果
AggregatedPage<Goods> pageInfo = (AggregatedPage<Goods>) this.goodsRepository.search(queryBuilder.build());
// 3、解析查询结果
// 3.1、分页信息
Long total = pageInfo.getTotalElements();
int totalPage = (total.intValue() + request.getSize() - 1) / request.getSize();
// 3.2、商品分类的聚合结果
List<Category> categories =
getCategoryAggResult(pageInfo.getAggregation(categoryAggName));
// 3.3、品牌的聚合结果
List<Brand> brands = getBrandAggResult(pageInfo.getAggregation(brandAggName));
// 返回结果
return new SearchResult(total, totalPage, pageInfo.getContent(), categories, brands);
}
// 解析品牌聚合结果
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;
}
}
// 构建基本查询条件
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));
}
}
}
List<Map<String,Object>>
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个则进行规格聚合:
// 判断商品分类数量,看是否需要对规格参数进行聚合
List<Map<String,Object>> specs=null;
if (categories.size()==1){
specs=getSpecs(categories.get(0).getId(),query);
}
获取需要聚合的规格参数
然后,需要根据商品分类,查询所有可用于搜索的规格参数:
List<SpecParam> specParams = this.specificationClient.querySpecParams(null, id, true, null);
要注意的是,这里需要根据id查询规格,而规格参数接口需要从商品微服务提供
商品微服务:tt-item-interface中提供接口:
@RequestMapping("spec")
public interface SpecificationApi {
@GetMapping("/params")
List<SpecParam> querySpecParam(SpecParam specParam);
}
@FeignClient("item-service")
public interface SpecificationClient extends SpecificationApi {
}
聚合规格参数
因为规格参数保存时不做分词,因此其名称会自动带上一个.keyword后缀:
specParams.forEach(p->{
String key = p.getName();
queryBuilder.addAggregation(AggregationBuilders.terms(key).field("specs." + key + ".keyword"));
});
最终的完整代码
@Service
public class IndexService {
@Autowired
private GoodsRepository goodsRepository;
@Autowired
private ElasticsearchTemplate esTemplate;
@Autowired
private CategoryClient categoryClient;
@Autowired
private BrandClient brandClient;
@Autowired
private GoodsClient goodsClient;
@Autowired
private SpecificationClient specificationClient;
private static final Logger logger = LoggerFactory.getLogger(IndexService.class);
private static final ObjectMapper mapper = new ObjectMapper();
public PageResult<Goods> search(SearchRequest request) {
// 判断是否有搜索条件,如果没有,直接返回null。不允许搜索全部商品
if (StringUtils.isBlank(request.getKey())) {
return null;
}
// 1、构建查询条件
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 通过sourceFilter设置返回的结果字段,我们只需要id、skus、subTitle
queryBuilder.withSourceFilter(new FetchSourceFilter(
new String[]{"id", "skus", "subTitle"}, null));
// 1.1、基本查询
queryBuilder.withQuery(QueryBuilders.matchQuery("all", key).operator(Operator.AND));
// 1.2.分页排序
searchWithPageAndSort(queryBuilder, request);
// 1.3、聚合
String categoryAggName = "category"; // 商品分类聚合名称
String brandAggName = "brand"; // 品牌聚合名称
// 对商品分类进行聚合
queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3"));
// 对品牌进行聚合
queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId"));
// 对规格参数聚合
// 2、查询,获取结果
AggregatedPage<Goods> pageInfo = (AggregatedPage<Goods>) this.goodsRepository.search(queryBuilder.build());
// 3、解析查询结果
// 3.1、分页信息
Long total = pageInfo.getTotalElements();
int totalPage = (total.intValue() + request.getSize() - 1) / request.getSize();
// 3.2、商品分类的聚合结果
List<Category> categories = getCategoryAggResult(pageInfo.getAggregation(categoryAggName));
// 3.3、品牌的聚合结果
List<Brand> brands = getBrandAggResult(pageInfo.getAggregation(brandAggName));
// 判断商品分类数量,看是否需要对规格参数进行聚合
List<Map<String, Object>> specs = null;
if (categories.size() == 1) {
// 如果分类只剩下一个,才进行规格参数过滤
specs = getSpecs(categories.get(0).getId(), query);
}
// 返回结果
return new SearchResult(total, totalPage, pageInfo.getContent(), categories, brands, specs);
}
// 聚合规格参数
private List<Map<String, Object>> getSpecs(Long cid, QueryBuilder query) {
try {
// 根据分类查询规格
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.esTemplate.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 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;
}
}
// 构建基本查询条件
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));
}
}
// ...
}
后台添加过滤条件
拓展请求对象
在请求类:SearchRequest
中添加属性,接收过滤属性。过滤属性都是键值对格式,但是key不确定,所以用一个map来接收即可。
public class SearchRequest {
@Autowired
private SearchConfig searchConfig;
private String sortBy;
private Boolean descending;
private String key;// 搜索条件
private Integer page;// 当前页
private Map<String,String> filter;
// private static final Integer DEFAULT_SIZE = 20;// 每页大小,不从页面接收,而是固定大小
private static final Integer DEFAULT_PAGE = 1;// 默认页
public Integer getPage() {
if(page == null){
return DEFAULT_PAGE;
}
// 获取页码时做一些校验,不能小于1
return Math.max(DEFAULT_PAGE, page);
}
}
添加过滤条件
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id","skus","subTitle"}, null));
QueryBuilder query = buildBasicQueryWithFilter(request);
queryBuilder.withQuery(query);
// 2.2、基本查询
// queryBuilder.withQuery(QueryBuilders.matchQuery("all", key));
private QueryBuilder buildBasicQueryWithFilter(SearchRequest request) {
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
// 基本查询条件
queryBuilder.must(QueryBuilders.matchQuery("all", request.getKey()).operator(Operator.AND));
// 过滤条件构建器
BoolQueryBuilder filterQueryBuilder = QueryBuilders.boolQuery();
// 整理过滤条件
Map<String, String> filter = request.getFilter();
for (Map.Entry<String, String> entry : filter.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
// 商品分类和品牌要特殊处理
if (key != "cid3" && key != "brandId") {
key = "specs." + key + ".keyword";
}
// 字符串类型,进行term查询
filterQueryBuilder.must(QueryBuilders.termQuery(key, value));
}
// 添加过滤条件
queryBuilder.filter(filterQueryBuilder);
return queryBuilder;
}