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

这短短的一生我们最终都会失去,不妨大胆一点,爱一个人,攀一座山,追一个梦
原文地址:https://www.cnblogs.com/xing1/p/14940831.html