• ES服务的搭建(八)


    看下图的淘宝页面,可以看到搜索有多个条件及搜索产品,并且支持多种排序方式,例如按价格;其实这块有个特点,就是不管你搜索哪个商品他都是有分类的,以及他对应的品牌,这两个是固定的,但其它参数不一定所有商品都具有;这一块设计就涉及到动态变化数据的加载,设计是比较复杂的,这个可以在后面慢慢说,其实这次想分析的主要是es的搜索服务使用

    一、es的搜索服务使用

    1. 完成关键字的搜索功能
    2. 完成商品分类过滤功能
    3. 完成品牌、规格过滤功能
    4. 完成价格区间过滤功能

    二、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>
    bootstrap.yml 
    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);
        }
    搜索高亮实现:
    高亮是指搜索商品的时候,商品列表中如何和你搜索的关键词相同,那么它会高亮展示,也就是变色展示,京东搜索其实就是给关键词增加了样式,所以是红色,ES搜索引擎也是一样,也可以实现关键词高亮展示,原理和京东搜索高亮原理一样。高亮搜索实现有2个步骤:
    • 配置高亮域以及对应的样式 
    • 从结果集中取出高亮数据,并将非高亮数据换成高亮数据

    接下来按这个思路来玩下,在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;
        }
    }
    注入对象 ElasticsearchRestTemplate
    @Autowired private ElasticsearchRestTemplate 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);
        }
    }

    源码:https://gitee.com/TongHuaShuShuoWoDeJieJu/spring-cloud-alibaba1.git

    这短短的一生我们最终都会失去,不妨大胆一点,爱一个人,攀一座山,追一个梦
  • 相关阅读:
    02SpringMvc_springmvc快速入门小案例(XML版本)
    01SpringMvc_初识工作流程
    07JavaIO详解_字符流
    06JavaIO详解_IO流中的设计模式-装饰者模式
    05JavaIO详解_仿照IO源码自己去实现一个IO流(为了加深印象,本身没有价值)
    作为程序员,我到底在恐慌什么
    android:layout_weight详解
    Android 遍历界面控件
    一个Activity中使用两个layout实例
    Android获取文件的MD5值
  • 原文地址:https://www.cnblogs.com/xing1/p/14940831.html
Copyright © 2020-2023  润新知