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);
    }
}

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