ES服務的搭建(八)

童話述說我的結局發表於2021-07-05

看下圖的淘寶頁面,可以看到搜尋有多個條件及搜尋產品,並且支援多種排序方式,例如按價格;其實這塊有個特點,就是不管你搜尋哪個商品他都是有分類的,以及他對應的品牌,這兩個是固定的,但其它引數不一定所有商品都具有;這一塊設計就涉及到動態變化資料的載入,設計是比較複雜的,這個可以在後面慢慢說,其實這次想分析的主要是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

相關文章