当我们在京东上搜索智能手机的时候,会将相关品牌罗列出来供用户选择
我们要实现的也是这个功能,就是将搜索结果中的品牌进行分类统计
这个功能和上一篇文章中提到的分类统计是一毛一样的,所以添加几行代码就搞定了。
但是我在搜索“智能手机”的时候,品牌只出现了两个,这显然和实际不符。原因很简单,就是“ik_smart”并没有将“智能手机”拆分成“智能”和“手机”,所以将分词模式改成“ik_max_word”就好了。将SkuInfo中name字段从“ik_smart”改成“ik_max_word”,然后重新导入数据,再来测试一下:
这样就对了。
当我们在京东上面搜索的时候,会将规格信息罗列出来用户选择。在我们的ES中也有规格信息,但这些信息都是json字符串,我们要做的就是把这些json字符串转换成Map集合从而实现和京东相同的功能。
在SkuEsServiceImpl的searchByKeywords方法中添加以下代码:
从代码中可以看出,先是添加搜索条件,然后从搜索结果中取出spec的集合,遍历存放进specMap里面。因为搜索结果是一条条的json字符串,所以每次将json字符串转换成map集合,再遍历map,从map中依次取出数据放入specSet。如果specMap中没有对应的specSet就直接new一个并存入进去,有的话就直接从specMap取出。最后将specMap放入SearchEntity返回给前端即可。
当我们把品牌或分类作为条件搜索的时候,就不用去处理品牌和分类统计了。之前只有一个keywords参数我就直接写在了地址栏里,但现在参数多了,还是封装进SearchEntity吧,然后参数写在请求体里面。
private String keywords; //前端传过来的关键词信息 private String brand; //前端传过来的品牌信息 private String category; //前端传过来的分类信息
然后 在SkuEsServiceImpl的searchByKeywords方法中添加代码 ,我现在的searchByKeywords已经写得很臃肿了,先不管这个问题,最后再去优化代码。
如果category或brand参数不为空就作为条件筛选,不进行统计,否则还是进行统计。需要注意的是这里使用了withFilter()方法,其实withQuery()也是可以的,但是用withQuery()的话高亮搜索就不行了,所以要用withFilter()。
withFilter()
可以看到,现在指定了品牌信息但不指定分类信息,品牌就不会进行统计,分类还是会进行统计,达到了我们预期的效果。
和上面一个一样,当我们把规格作为参数传给后端,同样也不会去进行规格统计。要实现这个功能,首先还得在SearchEntity中加一个字段用来接收规格参数。
private List<String> searchSpec; //前端传过来的规格信息
然后 在SkuEsServiceImpl的searchByKeywords方法中添加代码 :
…… Map<String,String> searchSpec = searchEntity.getSearchSpec(); if (searchSpec != null && searchSpec.size() > 0) { for (String key:searchSpec.keySet()){ //格式:specMap.规格名字.keyword keyword表示不分词 boolQueryBuilder.filter(QueryBuilders.termQuery("specMap."+key+".keyword",searchSpec.get(key))); } } ……
获取前端传过来的searchSpec,然后遍历取出规格的内容,再用boolQueryBuilder.filter()进行过滤。
从图中可以看出,当我们指定颜色为蓝色,版本为“6GB+64GB”,结果中就都是我们筛选过的结果。
当我们在京东上面搜索产品的时候,可以指定一个价格区间。现在要实现的也是同样的功能。首先,还是要在SearchEntity中添加一个字段用于接收间隔区间参数。
private String price; //前端穿过来的价格区间字符串 300-500元 3000元以上
然后添加实现的代码:
…… if (!StringUtils.isEmpty(searchEntity.getPrice())) { String[] price = searchEntity.getPrice().replace("元","") .replace("以上","").split("-"); boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(Integer.parseInt(price[0]))); if (price.length>1){ boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").lt(Integer.parseInt(price[1]))); } } ……
这样就可以实现对价格的过滤了。
分页功能就比较简单了,接收前端传进来的pageNum参数,然后调用nativeSearchQueryBuilder.withPageable()方法就可以实现分页。
int pageNum = 1; if (!StringUtils.isEmpty(searchEntity.getPageNum())) { pageNum = Integer.parseInt(searchEntity.getPageNum()); } nativeSearchQueryBuilder.withPageable(PageRequest.of(pageNum-1,SEARCH_PAGE_SIZE));
代码很简单,给pageNum一个默认值,如果前端没有传参数过来就显示第一页,每页的条数是10条。我定义的一个常量SEARCH_PAGE_SIZE去表示10。
可以看到,一共搜索到22条数据,10条一页的话就是3页,分页是正确的。
排序功能比较简单,就是将要排序的域和排序方式传到对应的方法里面即可。
String sortField = searchEntity.getSortField(); String sortRule = searchEntity.getSortRule(); if (!StringUtils.isEmpty(sortField) && !StringUtils.isEmpty(sortRule)) { nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(sortField).order(SortOrder.valueOf(sortRule))); }
sortField是要排序的域,sortRule是要排序的方式,如果这两个参数都不为空的话就调用withSort方法添加搜索条件即可。这样就可以了。
我们平时在京东或者淘宝搜索的时候,关键词都会高亮显示。
其实实现起来也很简单,只要将搜索词用html包裹起来,样式改为红色即可。
首先需要 配置高亮 ,也就是指定高亮的域:
HighlightBuilder.Field field = new HighlightBuilder.Field("name"); //指定域 field.preTags("<em style=\"color:red;\">"); //指定前缀 field.postTags("</em>"); //指定后缀 nativeSearchQueryBuilder.withHighlightFields(field);
在这段代码中,我们指定了要高亮搜索的域,并为其添加了高亮显示的html代码。
指定完需要高亮显示的域后,就需要 进行高亮搜索,将非高亮数据替换成高亮数据 。之前搜索的时候用的是queryForPage(SearchQuery query, Class<T> clazz)方法,现在改成queryForPage(SearchQuery query, Class<T> clazz, SearchResultMapper mapper)方法。
queryForPage(SearchQuery query, Class<T> clazz)
queryForPage(SearchQuery query, Class<T> clazz, SearchResultMapper mapper)
AggregatedPage<SkuInfo> skuInfos = elasticsearchTemplate .queryForPage(nativeSearchQuery, SkuInfo.class, new SearchResultMapper() { @Override public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) { List<T> list = new ArrayList<>(); for (SearchHit hit : response.getHits()) { //遍历所有数据 SkuInfo skuInfo = JSON.parseObject(hit.getSourceAsString(), SkuInfo.class);//非高亮数据 HighlightField highlightField = hit.getHighlightFields().get("name"); //高亮数据 //将非高亮数据替换成高亮数据 if (highlightField != null && highlightField.getFragments() != null) { Text[] fragments = highlightField.getFragments(); StringBuilder builder = new StringBuilder(); for (Text fragment : fragments) { builder.append(fragment.toString()); } skuInfo.setName(builder.toString()); list.add((T) skuInfo); } } return new AggregatedPageImpl<T>(list, pageable, response.getHits().getTotalHits(),response.getAggregations()); } });
我运行的时候发现过滤搜索的skuInfos.getAggregations()总是报空指针异常,我还以为是高亮搜索和过滤搜索不能在一起用,最后才发现我把这里的response.getAggregations()写丢了,少了一个参数。所以这里要注意一下。
skuInfos.getAggregations()
response.getAggregations()
之前看视频的时候发现他写得不太好,其实只要查一次就好了,但是却查询了很多次,不过视频的最后也提到了这个问题并改进了代码。所以我之前并没有按照视频中的写,而是挤在一个方法里想着最后再去优化,最后一个方法写了有一百多行,简直太臃肿了,而且《阿里巴巴Java开发手册》里也提到过一个方法不要超过80行。现在搜索功能都已经实现了,可以去优化一下了。下面是我优化过的代码👇
@Override public SearchEntity searchByKeywords(SearchEntity searchEntity) { if (searchEntity != null && !StringUtils.isEmpty(searchEntity.getKeywords())) { NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder(); //配置高亮 HighlightBuilder.Field field = new HighlightBuilder.Field("name"); field.preTags("<em style=\"color:red;\">"); field.postTags("</em>"); nativeSearchQueryBuilder.withHighlightFields(field); //条件筛选或者分组统计 BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); if (!StringUtils.isEmpty(searchEntity.getCategory())) { //分类过滤 boolQueryBuilder.filter(QueryBuilders.termQuery("categoryName", searchEntity.getCategory())); } else { nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms ("categories_grouping").field("categoryName").size(10000)); } if (!StringUtils.isEmpty(searchEntity.getBrand())) { //品牌过滤 boolQueryBuilder.filter(QueryBuilders.termQuery("brandName", searchEntity.getBrand())); } else { nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms ("brands_grouping").field("brandName").size(10000)); } nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms ("spec_grouping").field("spec.keyword").size(10000)); if (!StringUtils.isEmpty(searchEntity.getPrice())) { //价格过滤 String[] price = searchEntity.getPrice().replace("元", "") .replace("以上", "").split("-"); boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(Integer.parseInt(price[0]))); if (price.length > 1) { boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").lt(Integer.parseInt(price[1]))); } } Map<String, String> searchSpec = searchEntity.getSearchSpec(); if (searchSpec != null && searchSpec.size() > 0) { for (String key : searchSpec.keySet()) { boolQueryBuilder.filter(QueryBuilders.termQuery("specMap." + key + ".keyword", searchSpec.get(key))); } } //分页 int pageNum = (!StringUtils.isEmpty(searchEntity.getPageNum())) ? (Integer.parseInt(searchEntity.getPageNum())) : 1; nativeSearchQueryBuilder.withPageable(PageRequest.of(pageNum - 1, SEARCH_PAGE_SIZE)); //排序 String sortField = searchEntity.getSortField(); String sortRule = searchEntity.getSortRule(); if (!StringUtils.isEmpty(sortField) && !StringUtils.isEmpty(sortRule)) { nativeSearchQueryBuilder .withSort(SortBuilders.fieldSort(sortField).order(SortOrder.valueOf(sortRule))); } nativeSearchQueryBuilder .withQuery(QueryBuilders.queryStringQuery(searchEntity.getKeywords()).field("name")) .withFilter(boolQueryBuilder); //这两行顺序不能颠倒 AggregatedPage<SkuInfo> skuInfos = elasticsearchTemplate .queryForPage(nativeSearchQueryBuilder.build(), SkuInfo.class, new SearchResultMapperImpl()); Aggregations aggregations = skuInfos.getAggregations(); List<String> categoryList = buildGroupList(aggregations.get("categories_grouping")); List<String> brandList = buildGroupList(aggregations.get("brands_grouping")); Map<String, Set<String>> specMap = specGroup(aggregations.get("spec_grouping")); searchEntity.setTotal(skuInfos.getTotalElements()); searchEntity.setTotalPages(skuInfos.getTotalPages()); searchEntity.setCategoryList(categoryList); searchEntity.setBrandList(brandList); searchEntity.setSpecMap(specMap); searchEntity.setRows(skuInfos.getContent()); return searchEntity; } return null; } //将过滤搜索出来的StringTerms转换成List集合 private List<String> buildGroupList(StringTerms stringTerms) { List<String> list = new ArrayList<>(); if (stringTerms != null) { for (StringTerms.Bucket bucket : stringTerms.getBuckets()) { list.add(bucket.getKeyAsString()); } } return list; } //规格统计 private Map<String, Set<String>> specGroup(StringTerms specTerms) { Map<String, Set<String>> specMap = new HashMap<>(16); for (StringTerms.Bucket bucket : specTerms.getBuckets()) { Map<String, String> map = JSON.parseObject(bucket.getKeyAsString(), Map.class); for (String key : map.keySet()) { Set<String> specSet; if (!specMap.containsKey(key)) { specSet = new HashSet<>(); specMap.put(key, specSet); } else { specSet = specMap.get(key); } specSet.add(map.get(key)); } } return specMap; }
queryForPage里的SearchResultMapper参数被我单独拎了出来:
public class SearchResultMapperImpl implements SearchResultMapper { @Override public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) { List<T> list = new ArrayList<>(); for (SearchHit hit : response.getHits()) { //遍历所有数据 SkuInfo skuInfo = JSON.parseObject(hit.getSourceAsString(), SkuInfo.class);//非高亮数据 HighlightField highlightField = hit.getHighlightFields().get("name"); //高亮数据 //将非高亮数据替换成高亮数据 if (highlightField != null && highlightField.getFragments() != null) { Text[] fragments = highlightField.getFragments(); StringBuilder builder = new StringBuilder(); for (Text fragment : fragments) { builder.append(fragment.toString()); } skuInfo.setName(builder.toString()); list.add((T) skuInfo); } } return new AggregatedPageImpl<T>(list, pageable, response.getHits().getTotalHits(),response.getAggregations()); } }
虽然代码还是挺多的,但是逻辑清晰了很多。虽然还可以优化一下,但是我觉得没这个必要。现在测试一下:
好了,所有功能都正常运行了。
原文链接:https://blog.csdn.net/weixin_43461520/article/details/107579868