ES服务的搭建(八)
看下图的淘宝页面,可以看到搜索有多个条件及搜索产品,并且支持多种排序方式,例如按价格;其实这块有个特点,就是不管你搜索哪个商品他都是有分类的,以及他对应的品牌,这两个是固定的,但其它参数不一定所有商品都具有;这一块设计就涉及到动态变化数据的加载,设计是比较复杂的,这个可以在后面慢慢说,其实这次想分析的主要是es的搜索服务使用
一、es的搜索服务使用
- 完成关键字的搜索功能
- 完成商品分类过滤功能
- 完成品牌、规格过滤功能
- 完成价格区间过滤功能
二、ES服务的搭建
在搭建服务前先理下流程,其实流程也很简单,前台服务对数据库进行了操作后,canal会同步变化的数据,将数据发到ES搜索引擎上去,用户就可以在前台使用不同条件进行搜索,关键词、分类、价格区间、动态属性;因为搜索功能在很多模块会被调用,所以先在api模块下建一个子服务spring-cloud-search-api,然后导入包
<dependencies> <!--ElasticSearch--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency> </dependencies>
在接下来写前看下上图片,现在需要将数据库数据查询出来,再存入ES中,但中间需要有一个和ES索引库对应的JavaBean,为了不影响原来程序对象,所以会创建一个新的 JavaBean 对象
/** * indexName是索引库中对应的索引名称 * type是当前实体类中对应的一个类型,可以它理解一个表的名字 */ @Data @Document(indexName = "shopsearch",type = "skues") public class SkuEs { @Id private String id; //这里是因为要对商品进行模糊查询,要对它进行分词查找,所以要选择分词器,这里选择的是IK分词器 @Field(type = FieldType.Text,analyzer = "ik_smart",searchAnalyzer = "ik_smart") 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; @Field(type=FieldType.Keyword) private String skuAttribute; private Integer status; //属性映射(动态创建域信息) private Map<String,String> attrMap; }
这一步搞定后就是要搭建搜索工程了,接下来在spring-cloud-service下面搭建子服务spring-cloud-search-service
<dependency>
<groupId>com.ghy</groupId>
<artifactId>spring-cloud-search-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
server: port: 8084 spring: application: name: spring-cloud-search-service cloud: nacos: config: file-extension: yaml server-addr: 192.168.32.135:8848 discovery: #Nacos的注册地址 server-addr: 192.168.32.135:8848 #Elasticsearch服务配置 6.8.12 elasticsearch: rest: uris: http://192.168.32.135:9200 #日志配置 logging: pattern: console: "%msg%n"
上面配置工作做完后下面要的事就是写业务代码了,在业务场景中当数据库sku数据变更的时候,需要做的操作就是通过Canal微服务调用当前搜索微服务实现数据实时更新,原因在上面也画图说明了。接下来先先Mapper代码
public interface SkuSearchMapper extends ElasticsearchRepository<SkuEs,String> { }
public interface SkuSearchService { //增加索引 void add(SkuEs skuEs); //删除索引 void del(String id); }
@Service public class SkuSearchServiceImpl implements SkuSearchService { @Autowired private SkuSearchMapper skuSearchMapper; /*** * 增加索引 * @param skuEs */ @Override public void add(SkuEs skuEs) { //获取属性 String attrMap = skuEs.getSkuAttribute(); if(!StringUtils.isEmpty(attrMap)){ //将属性添加到attrMap中 skuEs.setAttrMap(JSON.parseObject(attrMap, Map.class)); } skuSearchMapper.save(skuEs); } /*** * 根据主键删除索引 * @param id */ @Override public void del(String id) { skuSearchMapper.deleteById(id); } }
@RestController @RequestMapping(value = "/search") public class SkuSearchController { @Autowired private SkuSearchService skuSearchService; /***** * 增加索引 */ @PostMapping(value = "/add") public RespResult add(@RequestBody SkuEs skuEs){ skuSearchService.add(skuEs); return RespResult.ok(); } /*** * 删除索引 */ @DeleteMapping(value = "/del/{id}") public RespResult del(@PathVariable(value = "id")String id){ skuSearchService.del(id); return RespResult.ok(); } }
和上一篇一样,这个搜索功能在很多模块会被调用,所以要在对应的API中写上feign接口
@FeignClient(value = "spring-cloud-search-service") public interface SkuSearchFeign { /***** * 增加索引 */ @PostMapping(value = "/search/add") RespResult add(@RequestBody SkuEs skuEs); /*** * 删除索引 */ @DeleteMapping(value = "/search/del/{id}") RespResult del(@PathVariable(value = "id")String id); }
索引服务的删除和添加功能做好了,但是这样还没完,前面说过ES的更新是由Canal推过来的,所以需要在Canal服务调用刚刚上面写的两个接口,在spring-cloud-canal-service引入search的api
<dependency> <groupId>com.ghy</groupId> <artifactId>spring-cloud-search-api</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
然后和上一篇一样在canal服务中写一个监听事件
@CanalTable(value = "sku") @Component public class Search implements EntryHandler<Sku> { @Resource private SkuSearchFeign skuSearchFeign; /*** * 增加数据监听 * @param sku */ @Override public void insert(Sku sku) { if(sku.getStatus().intValue()==1){ //将Sku转成JSON,再将JSON转成SkuEs skuSearchFeign.add(JSON.parseObject(JSON.toJSONString(sku), SkuEs.class)); } } /**** * 修改数据监听 * @param before * @param after */ @Override public void update(Sku before, Sku after) { if(after.getStatus().intValue()==2){ //删除索引 skuSearchFeign.del(after.getId()); }else{ //更新 skuSearchFeign.add(JSON.parseObject(JSON.toJSONString(after), SkuEs.class)); } } /*** * 删除数据监听 * @param sku */ @Override public void delete(Sku sku) { skuSearchFeign.del(sku.getId()); } }
现在看似功能做好了,数据也能监听推送到es了,但是还有一个问题,启动程序测试一下就可以发现,由于实体类与数据库映射关系问题导致,所以需要在api中导入以下包
<!--JPA-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
然后在对应的实体类上加上@Column注解就解决了
然后打开es控制面板,在数据库随便操作一条数据会发现控制面板有更新,做到这一步就说明实时更新已经完成
添加和删除搞定后,接下来就来搞下查询功能,也就是关键词搜索功能,实现也很简单,就是用户输入关键词后,将关键词一起传入后台,需要根据商品名字进行搜索。以后也有可能根据别的条件查询,所以传入后台的数据可以用Map接收,响应页面的数据包含列表、分页等信息,可以用Map封装。
public interface SkuSearchService { /**** * 搜索数据 */ Map<String,Object> search(Map<String,Object> searchMap); //增加索引 void add(SkuEs skuEs); //删除索引 void del(String id); }
/**** * 关键词搜索 * @param searchMap * 关键词:keywords->name * @return */ @Override public Map<String, Object> search(Map<String, Object> searchMap) { //QueryBuilder->构建搜索条件 NativeSearchQueryBuilder queryBuilder =queryBuilder(searchMap); //skuSearchMapper进行搜索 Page<SkuEs> page = skuSearchMapper.search(queryBuilder.build()); //获取结果集:集合列表、总记录数 Map<String,Object> resultMap = new HashMap<String,Object>(); List<SkuEs> list = page.getContent(); resultMap.put("list",list); resultMap.put("totalElements",page.getTotalElements()); return resultMap; } /**** * 搜索条件构建 * @param searchMap * @return */ public NativeSearchQueryBuilder queryBuilder(Map<String, Object> searchMap){ NativeSearchQueryBuilder builder= new NativeSearchQueryBuilder(); //判断关键词是否为空,不为空,则设置条件 if(searchMap!=null && searchMap.size()>0){ //关键词条件,关键词前后台要统一 Object keywords = searchMap.get("keywords"); if(!StringUtils.isEmpty(keywords)){ builder.withQuery(QueryBuilders.termQuery("name",keywords.toString())); } return builder; }
@RestController @RequestMapping(value = "/search") public class SkuSearchController { @Autowired private SkuSearchService skuSearchService; /*** * 商品搜索 */ @GetMapping public RespResult<Map<String,Object>> search(@RequestParam(required = false)Map<String,Object> searchMap){ Map<String, Object> resultMap = skuSearchService.search(searchMap); return RespResult.ok(resultMap); } /***** * 增加索引 */ @PostMapping(value = "/add") public RespResult add(@RequestBody SkuEs skuEs){ skuSearchService.add(skuEs); return RespResult.ok(); } /*** * 删除索引 */ @DeleteMapping(value = "/del/{id}") public RespResult del(@PathVariable(value = "id")String id){ skuSearchService.del(id); return RespResult.ok(); } }
条件回显问题:

看上图可知,当每次执行搜索的时候,页面会显示不同搜索条件,例如:品牌,这些搜索条件都不是固定的,其实他们是没执行搜索的时候,符合搜索条件的商品所有品牌和所有分类,以及所有属性,把他们查询出来,然后页面显示。但是这些条件都没有重复的,也就是说要去重,去重一般采用分组查询即可,所以我们要想动态获取这样的搜索条件,需要在后台进行分组查询。 这个也很简单,只用修改上面写的search方法的业务层代码就好。
/**** * 关键词搜索 * @param searchMap * 关键词:keywords->name * @return */ @Override public Map<String, Object> search(Map<String, Object> searchMap) { //QueryBuilder->构建搜索条件 NativeSearchQueryBuilder queryBuilder =queryBuilder(searchMap); //分组搜索调用 group(queryBuilder,searchMap); //skuSearchMapper进行搜索 //Page<SkuEs> page = skuSearchMapper.search(queryBuilder.build()); AggregatedPage<SkuEs> page = (AggregatedPage<SkuEs>) skuSearchMapper.search(queryBuilder.build()); //获取结果集:集合列表、总记录数 Map<String,Object> resultMap = new HashMap<String,Object>(); //分组数据解析 parseGroup(page.getAggregations(),resultMap); List<SkuEs> list = page.getContent(); resultMap.put("list",list); resultMap.put("totalElements",page.getTotalElements()); return resultMap; } /*** * 分组结果解析 */ 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); } } } /*** * 分组查询 */ 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("attrmaps")//别名,类似Map的key .field("skuAttribute")//根据skuAttribute域进行分组 .size(100000) //分组结果显示100000个 ); } /**** * 搜索条件构建 * @param searchMap * @return */ public NativeSearchQueryBuilder queryBuilder(Map<String, Object> searchMap){ NativeSearchQueryBuilder builder= new NativeSearchQueryBuilder(); //判断关键词是否为空,不为空,则设置条件 if(searchMap!=null && searchMap.size()>0){ //关键词条件,关键词前后台要统一 Object keywords = searchMap.get("keywords"); if(!StringUtils.isEmpty(keywords)){ builder.withQuery(QueryBuilders.termQuery("name",keywords.toString())); } return builder; }
经过上面的步骤就完成了搜索功能中的分类和品牌的操作,这两块相对来说还是比较简单的,因为他们是固定的,但接下来的什么价格呀、款式呀什么的不是固定的,是动态的。下面就说下这块属性回显的做法;属性条件其实就是当前搜索的所有商品属性信息,所以我们可以把所有属性信息全部查询出来,然后把属性名作为key,属性值用集合存起来,就是我们页面要的属性条件了。
/**** * 关键词搜索 * @param searchMap * 关键词:keywords->name * @return */ @Override public Map<String, Object> search(Map<String, Object> searchMap) { //QueryBuilder->构建搜索条件 NativeSearchQueryBuilder queryBuilder =queryBuilder(searchMap); //分组搜索调用 group(queryBuilder,searchMap); //skuSearchMapper进行搜索 //Page<SkuEs> page = skuSearchMapper.search(queryBuilder.build()); AggregatedPage<SkuEs> page = (AggregatedPage<SkuEs>) skuSearchMapper.search(queryBuilder.build()); //获取结果集:集合列表、总记录数 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; } /**** * 将属性信息合并成Map对象 */ public void attrParse(Map<String,Object> searchMap){ //先获取attrmaps Object attrmaps = searchMap.get("attrmaps"); 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("attrmaps",allMaps); } } /*** * 分组结果解析 */ 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); } } } /*** * 分组查询 */ 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("attrmaps")//别名,类似Map的key .field("skuAttribute")//根据skuAttribute域进行分组 .size(100000) //分组结果显示100000个 ); } /**** * 搜索条件构建 * @param searchMap * @return */ public NativeSearchQueryBuilder queryBuilder(Map<String, Object> searchMap){ NativeSearchQueryBuilder builder= new NativeSearchQueryBuilder(); //判断关键词是否为空,不为空,则设置条件 if(searchMap!=null && searchMap.size()>0){ //关键词条件,关键词前后台要统一 Object keywords = searchMap.get("keywords"); if(!StringUtils.isEmpty(keywords)){ builder.withQuery(QueryBuilders.termQuery("name",keywords.toString())); } return builder; }
前面的做法还停留在单条件,但用户在前端执行条件搜索的时候,有可能会选择分类、品牌、价格、属性,每次选择条件传入后台,后台按照指定参数进行条件查询,这里制定一个传参数的规则:
1、分类参数:category 2、品牌参数:brand 3、价格参数:price 4、属性参数:attr_属性名:属性值 5、分页参数:page
现在来做的是获取category,brand,price的值,并根据这三个只分别实现分类过滤、品牌过滤、价格过滤,其中价格过滤传入的数据以-分割,修改的实现代码如下:
public NativeSearchQueryBuilder queryBuilder(Map<String, Object> searchMap){ NativeSearchQueryBuilder builder= new NativeSearchQueryBuilder(); //组合查询对象 BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); //判断关键词是否为空,不为空,则设置条件 if(searchMap!=null && searchMap.size()>0){ //关键词条件 Object keywords = searchMap.get("keywords"); if(!StringUtils.isEmpty(keywords)){ //builder.withQuery(QueryBuilders.termQuery("name",keywords.toString())); boolQueryBuilder.must(QueryBuilders.termQuery("name",keywords.toString())); } //分类查询 Object category = searchMap.get("category"); if(!StringUtils.isEmpty(category)){ boolQueryBuilder.must(QueryBuilders.termQuery("categoryName",category.toString())); } //品牌查询 Object brand = searchMap.get("brand"); if(!StringUtils.isEmpty(brand)){ boolQueryBuilder.must(QueryBuilders.termQuery("brandName",brand.toString())); } //价格区间查询 price=0-500元 500-1000元 1000元以上 Object price = searchMap.get("price"); if(!StringUtils.isEmpty(price)){ //价格区间 String[] prices = price.toString().replace("元","").replace("以上","").split("-"); //price>x boolQueryBuilder.must(QueryBuilders.rangeQuery("price").gt(Integer.valueOf(prices[0]))); //price<=y if(prices.length==2){ boolQueryBuilder.must(QueryBuilders.rangeQuery("price").lte(Integer.valueOf(prices[1]))); } } //动态属性查询 for (Map.Entry<String, Object> entry : searchMap.entrySet()) { //以attr_开始,动态属性 attr_网络:移动5G if(entry.getKey().startsWith("attr_")){ String key = "attrMap."+entry.getKey().replaceFirst("attr_","")+".keyword"; boolQueryBuilder.must(QueryBuilders.termQuery(key,entry.getValue().toString())); } } } return builder; }
上面查询搞完了准备收尾工作了,加上前面说的排序问题和分页代码
public NativeSearchQueryBuilder queryBuilder(Map<String, Object> searchMap){ NativeSearchQueryBuilder builder= new NativeSearchQueryBuilder(); //组合查询对象 BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); //判断关键词是否为空,不为空,则设置条件 if(searchMap!=null && searchMap.size()>0){ //关键词条件 Object keywords = searchMap.get("keywords"); if(!StringUtils.isEmpty(keywords)){ //builder.withQuery(QueryBuilders.termQuery("name",keywords.toString())); boolQueryBuilder.must(QueryBuilders.termQuery("name",keywords.toString())); } //分类查询 Object category = searchMap.get("category"); if(!StringUtils.isEmpty(category)){ boolQueryBuilder.must(QueryBuilders.termQuery("categoryName",category.toString())); } //品牌查询 Object brand = searchMap.get("brand"); if(!StringUtils.isEmpty(brand)){ boolQueryBuilder.must(QueryBuilders.termQuery("brandName",brand.toString())); } //价格区间查询 price=0-500元 500-1000元 1000元以上 Object price = searchMap.get("price"); if(!StringUtils.isEmpty(price)){ //价格区间 String[] prices = price.toString().replace("元","").replace("以上","").split("-"); //price>x boolQueryBuilder.must(QueryBuilders.rangeQuery("price").gt(Integer.valueOf(prices[0]))); //price<=y if(prices.length==2){ boolQueryBuilder.must(QueryBuilders.rangeQuery("price").lte(Integer.valueOf(prices[1]))); } } //动态属性查询 for (Map.Entry<String, Object> entry : searchMap.entrySet()) { //以attr_开始,动态属性 attr_网络:移动5G if(entry.getKey().startsWith("attr_")){ String key = "attrMap."+entry.getKey().replaceFirst("attr_","")+".keyword"; boolQueryBuilder.must(QueryBuilders.termQuery(key,entry.getValue().toString())); } } //排序 Object sfield = searchMap.get("sfield"); Object sm = searchMap.get("sm"); if(!StringUtils.isEmpty(sfield) && !StringUtils.isEmpty(sm)){ builder.withSort( SortBuilders.fieldSort(sfield.toString()) //指定排序域 .order(SortOrder.valueOf(sm.toString())) //排序方式 ); } } //分页查询 builder.withPageable(PageRequest.of(currentPage(searchMap),5)); return builder.withQuery(boolQueryBuilder); }
- 配置高亮域以及对应的样式
- 从结果集中取出高亮数据,并将非高亮数据换成高亮数据
接下来按这个思路来玩下,在search方法中加入下面一段代码就好了
//1.设置高亮信息 关键词前(后)面的标签、设置高亮域 HighlightBuilder.Field field = new HighlightBuilder .Field("name") //根据指定的域进行高亮查询 .preTags("<span style=\"color:red;\">") //关键词高亮前缀 .postTags("</span>") //高亮关键词后缀 .fragmentSize(100); //碎片长度 queryBuilder.withHighlightFields(field);
public class HighlightResultMapper extends DefaultResultMapper { /*** * 映射转换,将非高亮数据替换成高亮数据 * @param response * @param clazz * @param pageable * @param <T> * @return */ @Override public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) { //1、获取所有非高亮数据 SearchHits hits = response.getHits(); //2、循环非高亮数据集合 for (SearchHit hit : hits) { //非高亮数据 Map<String, Object> sourceAsMap = hit.getSourceAsMap(); //3、获取高亮数据 for (Map.Entry<String, HighlightField> entry : hit.getHighlightFields().entrySet()) { //4、将非高亮数据替换成高亮数据 String key = entry.getKey(); //如果当前非高亮对象中有该高亮数据对应的非高亮对象,则进行替换 if(sourceAsMap.containsKey(key)){ //高亮碎片 String hlresult = transTxtToArrayToString(entry.getValue().getFragments()); if(!StringUtils.isEmpty(hlresult)){ //替换高亮 sourceAsMap.put(key,hlresult); } } } //更新hit的数据 hit.sourceRef(new ByteBufferReference(ByteBuffer.wrap(JSONObject.toJSONString(sourceAsMap).getBytes()))); } return super.mapResults(response, clazz, pageable); } /*** * Text转成字符串 * @param fragments * @return */ public String transTxtToArrayToString(Text[] fragments){ if(fragments!=null){ StringBuffer buffer = new StringBuffer(); for (Text fragment : fragments) { buffer.append(fragment.toString()); } return buffer.toString(); } return null; } }
@Autowired private ElasticsearchRestTemplate elasticsearchRestTemplate;
AggregatedPage<SkuEs> page = elasticsearchRestTemplate.queryForPage(queryBuilder.build(), SkuEs.class,new HighlightResultMapper());
完整类代码
@Service public class SkuSearchServiceImpl implements SkuSearchService { @Autowired private SkuSearchMapper skuSearchMapper; @Autowired private ElasticsearchRestTemplate elasticsearchRestTemplate; /**** * 关键词搜索 * @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.将非高亮数据替换成高亮数据 //skuSearchMapper进行搜索 //Page<SkuEs> page = skuSearchMapper.search(queryBuilder.build()); //AggregatedPage<SkuEs> page = (AggregatedPage<SkuEs>) skuSearchMapper.search(queryBuilder.build()); AggregatedPage<SkuEs> page = elasticsearchRestTemplate.queryForPage(queryBuilder.build(), SkuEs.class,new HighlightResultMapper()); //获取结果集:集合列表、总记录数 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; } /**** * 将属性信息合并成Map对象 */ public void attrParse(Map<String,Object> searchMap){ //先获取attrmaps Object attrmaps = searchMap.get("attrmaps"); 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("attrmaps",allMaps); } } /*** * 分组结果解析 */ 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); } } } /*** * 分组查询 */ 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("attrmaps")//别名,类似Map的key .field("skuAttribute")//根据skuAttribute域进行分组 .size(100000) //分组结果显示100000个 ); } /**** * 搜索条件构建 * @param searchMap * @return */ /**** * 搜索条件构建 * @param searchMap * @return */ public NativeSearchQueryBuilder queryBuilder(Map<String, Object> searchMap){ NativeSearchQueryBuilder builder= new NativeSearchQueryBuilder(); //组合查询对象 BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); //判断关键词是否为空,不为空,则设置条件 if(searchMap!=null && searchMap.size()>0){ //关键词条件 Object keywords = searchMap.get("keywords"); if(!StringUtils.isEmpty(keywords)){ //builder.withQuery(QueryBuilders.termQuery("name",keywords.toString())); boolQueryBuilder.must(QueryBuilders.termQuery("name",keywords.toString())); } //分类查询 Object category = searchMap.get("category"); if(!StringUtils.isEmpty(category)){ boolQueryBuilder.must(QueryBuilders.termQuery("categoryName",category.toString())); } //品牌查询 Object brand = searchMap.get("brand"); if(!StringUtils.isEmpty(brand)){ boolQueryBuilder.must(QueryBuilders.termQuery("brandName",brand.toString())); } //价格区间查询 price=0-500元 500-1000元 1000元以上 Object price = searchMap.get("price"); if(!StringUtils.isEmpty(price)){ //价格区间 String[] prices = price.toString().replace("元","").replace("以上","").split("-"); //price>x boolQueryBuilder.must(QueryBuilders.rangeQuery("price").gt(Integer.valueOf(prices[0]))); //price<=y if(prices.length==2){ boolQueryBuilder.must(QueryBuilders.rangeQuery("price").lte(Integer.valueOf(prices[1]))); } } //动态属性查询 for (Map.Entry<String, Object> entry : searchMap.entrySet()) { //以attr_开始,动态属性 attr_网络:移动5G if(entry.getKey().startsWith("attr_")){ String key = "attrMap."+entry.getKey().replaceFirst("attr_","")+".keyword"; boolQueryBuilder.must(QueryBuilders.termQuery(key,entry.getValue().toString())); } } //排序 Object sfield = searchMap.get("sfield"); Object sm = searchMap.get("sm"); if(!StringUtils.isEmpty(sfield) && !StringUtils.isEmpty(sm)){ builder.withSort( SortBuilders.fieldSort(sfield.toString()) //指定排序域 .order(SortOrder.valueOf(sm.toString())) //排序方式 ); } } //分页查询 builder.withPageable(PageRequest.of(currentPage(searchMap),5)); return builder.withQuery(boolQueryBuilder); } /*** * 分页参数 */ public int currentPage(Map<String,Object> searchMap){ try { Object page = searchMap.get("page"); return Integer.valueOf(page.toString())-1; } catch (Exception e) { return 0; } } /*** * 增加索引 * @param skuEs */ @Override public void add(SkuEs skuEs) { //获取属性 String attrMap = skuEs.getSkuAttribute(); if(!StringUtils.isEmpty(attrMap)){ //将属性添加到attrMap中 skuEs.setAttrMap(JSON.parseObject(attrMap, Map.class)); } skuSearchMapper.save(skuEs); } /*** * 根据主键删除索引 * @param id */ @Override public void del(String id) { skuSearchMapper.deleteById(id); } }
源码://gitee.com/TongHuaShuShuoWoDeJieJu/spring-cloud-alibaba1.git