【主流技術】ElasticSearch 在 Spring 項目中的實踐

前言

ElasticSearch簡稱es,是一個開源的高擴展的分散式全文檢索引擎。

它可以近乎實時的存儲、檢索數據,其擴展性很好,ElasticSearch是企業級應用中較為常見的技術。

下面和大家分享 ElasticSearch 集成在Spring Boot 項目的一些學習心得。

一、ElasticSearch概述

1.1基本認識

ElasticSearch 是基於 Lucene 實現的開源、分散式、RESTful介面的全文搜索引擎。

Elasticsearch 還是一個分散式文檔資料庫,其中每個欄位均是被索引的數據且可被搜索,它能夠擴展至數以百計的伺服器存儲以及處理PB級的數據。

Elasticsearch 可以通過簡單的 RESTful 風格 API 來隱藏 Lucene 的複雜性,讓搜索變得更加簡單。

1.2核心概念

Elasticsearch 的核心概念是 Elasticsearch 搜索的過程,在搜索的過程中,Elasticsearch 的存儲過程、數據結構都會有所涉及。

  • 對比關係型資料庫

    表1

    關係型資料庫 Elasticsearch
    資料庫(DataBase) 索引(indices)
    表(table) types(已棄用)
    行(rows) documents
    欄位(columns) fields

註:

  1. Elasticsearch (集群)中可以包含多個indices(對應庫),每個索引中可以包含多個types(對應表),每個types下面又包含多個documents(對應行記錄),每個documents中又含有多個fields(對應欄位)。
  2. Elasticsearch 中一切數據的格式都是 JSON。
  • documents
  • fields
  • types(棄用)
  • indices

    Elasticsearch 中的索引是一個非常大的文檔集合,存儲了映射類型的欄位和其它設置,被存儲在各個分片上。

1.3倒排索引

Elasticsearch 使用一種名為倒排索引的結構進行搜索,一個索引由文檔中所有不重複的列表構成,對於每一個詞,都有一個包含它的文檔列表。

傳統資料庫的搜索結構一般以id為主,可以一一對應資料庫中的所有內容,即key-value的形式。

而倒排索引則與之相反,以內容為主,將所有不重複的內容記錄按照匹配的程度(閾值)進行展示,即value-key的形式。

以下舉兩個例子來進行說明。

  • 例一:

    在關係型資料庫中,數據是按照id的順序進行約定的,記錄的id具有唯一性,方便人們使用id去確定內容,如表2所示:

    表2

    id label
    1 java
    2 java
    3 java,python
    4 python
  • 例二:

    在 ElasticSearch 中使用倒排索引:數據是按照不重複的內容進行約定的,不重複的內容具有唯一性,這樣可以快速地找出符合內容的記錄,再根據匹配的閾值去進行展示,如表3所示:

    label id
    java 1,2,3
    python 4,3

1.4了解ELK

ELK 是 ElasticSearch、Logstash、Kibana這三大開源框架首字母大寫簡稱。

其中 Logstash 是中央數據流引擎,用於從不同目標(文件/數據存儲/MQ)中收集不同的數據格式,經過過濾後支援輸送到不同的目的地(文件/MQ/Redis/elasticsearch/kafka等)。

而 Kibana 可以將 ElasticSearch 的數據通過友好的可視化介面展示出來,且提供實時分析的功能。

ELK一般來說是一個日誌分析架構技術棧的總稱,但實際上 ELK 不僅僅適用於日誌分析,它還可以支援任何其它數據分析和收集的場景,日誌的分析和收集只是更具有代表性,並非 ELK 的唯一用途。


二、ElasticSearch(插件)安裝

2.1安裝聲明

  • 適用於JDK1.8及以上版本
  • ElasticSearch客戶端
  • 介面工具
  • ElasticSearch版本與Maven依賴版本對應

2.2 ElasticSearch下載

官網地址://www.elastic.co

下載地址(7.6.1版本)://www.elastic.co/downloads/past-releases/elasticsearch-7-6-1,推薦迅雷下載(速度較快)。

2.3安裝ElasticSearch

將下載好的壓縮包進行安裝即可,解壓後如下圖所示:

解壓安裝

  • 安裝目錄
    • bin 啟動文件

    • config 配置文件

      • log4j2:日誌配置文件
      • jvm.options:Java 虛擬機相關配置
      • elasticsearch.yml:elasticsearch配置文件,默認 9200 埠,解決跨域問題。
    • lib 相關jar包

    • modules 功能模組

    • plugins 插件(如IK分詞器)

2.4啟動ElasticSearch

打開bin文件夾下的elasticsearch.bat文件,雙擊啟動後訪問默認地址:localhost:9200,即可得到以下json格式的數據:

{
  "name" : "ZHUZQC",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "AMdLpCANStmY8kvou9-OtQ",
  "version" : {
    "number" : "7.6.1",
    "build_flavor" : "default",
    "build_type" : "zip",
    "build_hash" : "aa751e09be0a5072e8570670309b1f12348f023b",
    "build_date" : "2020-02-29T00:15:25.529771Z",
    "build_snapshot" : false,
    "lucene_version" : "8.4.0",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "You Know, for Search"
}

2.5可視化介面-head安裝

下載地址://github.com/mobz/elasticsearch-head/

安裝要求:先檢查電腦是否安裝node.js、npm

  • 步驟一:在解壓後的文件目錄下進入cmd,使用 cnpm install 命令安裝鏡像文件;

  • 步驟二:使用 npm run start 命令啟動,得到 //localhost:9100

  • 步驟三:解決跨域問題,打開 elasticsearch.yml 文件,輸入以下程式碼後保存:

    http.cors.enabled: true
    http.cors.allow-origin: "*"
    
  • 再次重啟elasticsearch,進入//localhost:9200 驗證是否啟動成功

  • 最後進入 //localhost:9100,得到以下介面,則head啟動成功:

head啟動

2.6初步創建索引

可以把索引當作一個資料庫來使用,具體的創建如下步驟所示:

  • 步驟一:點擊Indices,在彈出的提示框中填寫索引名稱,點擊確認;

  • 步驟二:可以在head介面中看到該索引,如下圖所示:

創建索引

註:head僅可以當作一個數據可視化的展示工具,對於查詢語句推薦使用Kibana。

2.7安裝Kibana工具

Kibana是一個針對 ElasticSearch 的開源分析、可視化平台,用於搜索、查看交互存儲在ElasticSearch中的數據。

Kibana 操作簡單,基於瀏覽器的的用戶介面可以快速創建儀錶板(dashboard)並實時顯示數據。

官網下載://www.elastic.co/downloads/past-releases/kibana-7-6-1

注意事項:Kibana 版本需要和 ElasticSearch 的版本保持一致。

安裝步驟如下:

  • 步驟一:打開解壓縮後的bin文件夾,雙擊.bat文件;
  • 步驟二:打開 //localhost:5601 進入 Kibana 介面

2.8使用Kibana工具

在開發的過程中,可供數據測試的工具有很多,比如postman、head、Chrome瀏覽器等,這裡推薦使用 Kibana 進行數據測試。

操作介面如下圖所示:

kibana介面


三、IK分詞器

3.1基本介紹

在使用中文進行搜索時,我們會對要搜索的資訊進行分詞:將一段中文分成一個個的詞語或者句子,然後將分出的詞進行搜索。

默認的中文分詞是一個漢字一個詞,如:「你好世界」,會被分成:「你」,「好」,「世」,「界」。但這樣的分詞方式顯然並不全面,比如還可以分成:「你好」,「世界」。

ik分詞器就解決了默認分詞不全面的問題,可以將中文進行不重複的分詞。

ik分詞器提供了兩種2演算法:ik_smart(最少切分)以及ik_max_word(最細顆粒度劃分)。

github下載://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.6.1

  • 步驟一:將解壓後的所有文件放置在 elasticsearch 下的plugins文件夾下;
  • 步驟二:重啟 elasticsearch

3.2使用Kibana測試

  1. 首先測試 ik_smart(最少切分)演算法的分詞效果,具體如圖3-1所示:

    圖3-1
  2. 再測試 ik_max_word(最細顆粒度劃分)演算法的分詞效果,具體如圖3-2所示:

圖3-2

3.3修改本地字典

ik分詞的默認字典並不能完全涵蓋所有的中文分詞,當我們想自定義分詞時,就需要修改ik分詞器的字典配置。

  • 步驟一:打開elasticsearch-7.6.1\plugins\ik\config文件夾,增加自定義dic文件;
  • 步驟二:在同一文件夾中的IKAnalyzer.cfg.xml里,將上述步驟的dic文件寫入自己的擴展字典;

具體效果如下圖3-3所示:

圖3-3


四、Rest風格操作

ElasticSearch 使用 Rest 風格來進行一系列操作,具體的命令如圖4-1所示:

圖4-1

4.1創建索引

PUT /test_1/type/1
{
  "name": "zhuzqc",
  "age": 35364
}

4.2修改索引內容

GET /test_1
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text"
      },
      "age": {
        "type": "long"
      },
      "birthdy": {
        "type": "date"
      }
    }
  }
}

4.3更新索引內容

POST /test_1/_doc/1/_update
{
  "doc": {
  "name": "noone"  
  }
}

4.4刪除索引

DELETE test_2

4.5關於documents的操作

4.5.1基本操作

documents 可以看作是資料庫中的行記錄;

  1. 首先先生產一些 documents 數據:
PUT zhuzqc/user/3
{
  "name": "李四",
  "age": 894,
  "desc": "影流之主",
  "tags": ["劫","刺客","中單"]
}

2.獲取數據:

GET zhuzqc/user/1

3.更新數據

// POST請求對指定內容進行更新
POST zhuzqc/user/1/_update
{
  "doc": {
  "name": "342rfd",
  "age": 243234
  }
}

4.簡單的條件查詢

// 查詢統一GET開頭,_search後接?,q代表query,屬性:內容
GET zhuzqc/user/_search?q=name:李

如:查詢zhuzqc索引中name為李四的資訊,其中李四遵循默認的分詞規則
GET zhuzqc/user/_search?q=name:李四
4.5.2複雜操作

上述的一些簡單查詢操作在企業級應用開發中使用地較少,更多地還是使用查詢實現複雜的業務。

隨著業務的複雜程度增加,查詢的語句也隨之複雜起來,在使用複雜查詢的過程中必然會涉及一些 elasticsearch 的進階語法。

對於複雜查詢的操作在下一章會詳細介紹。


五、查詢詳解

ElasticSearch引擎首先分析需要查詢的字元串,根據分詞器規則對其進行分詞。分詞之後,才會根據查詢條件進行結果返回。

5.1關鍵字介紹

  • query 關鍵字:將需要查詢的 JSON 參數體進行包裹,聲明這是一條查詢語句。
  • bool 關鍵字:表明返回結果類型為布爾類型。
  • keyword 關鍵字:keyword代表一種分詞類型,表明該欄位的值不會被分詞器分詞。
  • must 關鍵字:在 must 中的內容表明都是必須執行的內容,在 must 中可以創建多條語句,多條語句需同時滿足條件才能執行,作用相當於 SQL 語句中的 AND 。
  • should 關鍵字:在 should 關鍵字里的內容只要滿足其中一項就可以執行,作用相當於 SQL 語句中的 OR 。
  • must_not 關鍵字:類似於 Java 中的 != 作用,展示查詢內容之外的內容。
  • match 關鍵字:match 的作用是匹配查詢,首先經過分詞器的分詞,後再執行 match 查詢,默認情況下:欄位內容必須完整地匹配到任意一個詞條(分詞後),才會有返回結果。
  • 註:如果需要查詢的詞有多個,可以用空格隔開。
  • match_all 關鍵字:待補充。
  • match_phrase 關鍵字:待補充。
  • term 關鍵字:精確查詢關鍵字,使用 term 時首先不會對需要查詢的詞條進行分詞,只有精確地匹配到一模一樣的內容才會返回結果。
  • terms 關鍵字:待補充。
  • filter 關鍵字:對查詢的內容進行篩選過濾,常使用 gt(大於)、gte(大於等於)、lt(小於)和 lte (小於等於)來進行篩選。
GET product_cloud/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "bool": {
            "should": [
              {"match": {"product_comment":"持續交付 工程師"}}
            ]
          }
        },
        {
          "bool": {
            "should": [
              {"terms": {"label_ids": [3]}}
            ]
          }
        }
      ],
      "filter": {
        "range": {
          "label_ids": {
            "gte": 0 
          }
        }
      }
    }
  }
  • score 關鍵字:欄位內容與詞條的匹配程度,分數越高,表明匹配度越高,就越符合查詢結果。

  • hits 關鍵字:對應 Java 程式碼中的 hit 對象,包含了索引和文檔資訊,包括查詢結果總數,查詢出來的_doc內容(一串 JSON),分數(score)等。

  • source:需要展示的內容欄位,默認是展示索引的所有欄位,也可以自定義指定需要展示的欄位。

  • sort關鍵字:可以對欄位的展示進行排序;

"_source": ["product_comment","product_name","label_ids","product_solution","company_name"],
  "sort": [
    {
      "label_ids": {
        "order": "desc"
      }
    }
  ],
  "from": 0,
  "size": 3

5.2 highlight 高亮

使用 highlight 關鍵字可以在搜索結果中對需要高亮的欄位進行高亮(可自定義樣式)展示,具體程式碼如下:

GET product_cloud/_search
{
  "query": {
    "term": {
      "product_comment": "世界"
    }
  },
  "highlight": {
    "pre_tags": "<p class='key' style='color:red'>", 
    "post_tags": "</p>",   
    "fields": {
      "product_comment": {}
    }
  }
}

六、Spring Boot集成ElasticSearch

在 Elasticsearch 的官方文檔中有對 Elasticsearch 客戶端使用的詳細介紹: //www.elastic.co/guide/en/elasticsearch/client/java-api-client/8.0/installation.html

6.1添加依賴

<properties>
        <java.version>11</java.version>
        <!-- 自定義 ElasticSearch 依賴版本與安裝的版本一致 -->
        <elasticsearch.verson>7.6.1</elasticsearch.verson>
</properties>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>

6.2創建對象

定義一個客戶端對象:

@Configuration
public class EsConfig {
    @Bean
    public RestHighLevelClient restHighLevelClient(){
        RestHighLevelClient restHighLevelClient = new RestHighLevelClient(
                RestClient.builder(
                        new HttpHost("127.0.0.1",9200,"http")
                )
        );
        return restHighLevelClient;
    }
}

6.3分析類中的方法(索引相關API)

  • 創建索引
     @Autowired
        private RestHighLevelClient restHighLevelClient;
    
        // 測試索引的創建
        @Test
        void testCreateIndex() throws IOException {
            //1、創建索引請求
            CreateIndexRequest request = new CreateIndexRequest("zhu_index");
            //2、執行創建請求,並獲得響應
            CreateIndexResponse createIndexResponse =
                    restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);
            System.out.println(createIndexResponse);
        }
    
  • 獲取索引
    // 測試獲取索引
        @Test
        void testExistIndex() throws IOException {
            GetIndexRequest getIndexRequest = new GetIndexRequest("zhu_index");
            boolean exists = restHighLevelClient.indices().exists(getIndexRequest, RequestOptions.DEFAULT);
            System.out.println(exists);
        }
    
  • 刪除索引
      // 測試刪除索引
        @Test
        void testDeleteIndex() throws IOException {
            DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest();
            AcknowledgedResponse delete = restHighLevelClient.indices().delete(deleteIndexRequest, RequestOptions.DEFAULT);
            System.out.println(delete);
        }
    

七、相關API操作

API 的操作主要是將Spring Boot項目與 Elasticsearch 的 indices 與 docs 相關聯起來,這樣可以做到在 Elasticsearch 中對項目數據進行一系列的操作。

7.1文檔API

  • 新增數據
    // 測試添加文檔
    @Test
    void testAddDocument() throws IOException {
        // 創建對象
        User user = new User("zzz",3);
        // 創建請求
        IndexRequest zhu_index_request = new IndexRequest("zhu_index");
        // 規則:put /zhu_index/_doc/1
        zhu_index_request.id("1");
        zhu_index_request.timeout(TimeValue.timeValueSeconds(1));
        // 將數據放入 ElasticSearch 請求(JSON格式)
        zhu_index_request.source(JSON.toJSONString(user), XContentType.JSON);
        // 客戶端發送請求
        IndexResponse indexResponse = restHighLevelClient.index(zhu_index_request,            
        RequestOptions.DEFAULT);
    }

    // 添加大批量的數據
    @Test
    void testBulkRequest() throws IOException {
        BulkRequest bulkRequest = new BulkRequest();
        bulkRequest.timeout("10s");
        //創建數據集合
        ArrayList<User> userList = new ArrayList<>();
        userList.add(new User("zzz2",22));
        userList.add(new User("zzz3",23));
        userList.add(new User("zzz4",24));
        userList.add(new User("zzz5",25));
        userList.add(new User("zzz6",26));
        //遍曆數據:批量處理
        for (int i = 0; i < userList.size(); i++) {
            // 批量添加(或更新、或刪除)
            bulkRequest.add(
                    new IndexRequest("zhu_index")
                    //.id(""+(i+1))
                    .source(JSON.toJSONString(userList.get(i)), XContentType.JSON));
        }
        BulkResponse bulkResponse = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
    }
  • 獲取_doc資訊
 @Test
    void testGetDocument() throws IOException {
        GetRequest getRequest = new GetRequest("zhu_index","1");
        GetResponse getResponse = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
        // 返回_source的上下文
        getRequest.fetchSourceContext(new FetchSourceContext(true));
    }
  • 更新數據
   // 更新文檔資訊
    @Test
    void testUpdateDocument() throws IOException {
        UpdateRequest updateRequest = new UpdateRequest("zhu_index","1");
        updateRequest.timeout("1s");

        User user = new User("ZhuZhuQC",18);
        updateRequest.doc(JSON.toJSONString(user), XContentType.JSON);
        UpdateResponse updateResponse = restHighLevelClient.update(updateRequest, 
        RequestOptions.DEFAULT);
     
    }
  • 刪除數據

    與添加數據、更新數據類似,創建 DeleteRequest 對象即可。

  • 查詢數據(重點)
    // 查詢數據
    @Test
    void testSearch() throws IOException {
        // 創建查詢對象
        SearchRequest searchRequest = new SearchRequest(EsConst.ES_INDEX);
        // 構建搜索條件(精確查詢、全匹配查詢)
        TermQueryBuilder termQuery = QueryBuilders.termQuery("name","zzz2");
        MatchAllQueryBuilder matchAllQuery = QueryBuilders.matchAllQuery();

        // 執行構造器
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(termQuery);
        sourceBuilder.query(matchAllQuery);

        // 設置查詢時間,3秒內
        sourceBuilder.timeout(new TimeValue(3, TimeUnit.SECONDS));

        // 設置分頁
        sourceBuilder.from(0);
        sourceBuilder.size(3);

        // 最後執行搜索,並返回搜索結果
        searchRequest.source(sourceBuilder);
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, 
        RequestOptions.DEFAULT);
        searchResponse.getHits();

        // 列印結果
        System.out.println(JSON.toJSONString(searchResponse.getHits()));
        for (SearchHit documentFields : searchResponse.getHits().getHits()) {
            System.out.println(documentFields.getSourceAsMap());
        }
    }

八、實戰分析

實戰部分會模擬一個真實的 ElasticSearch 搜索過程:從創建項目開始,到使用爬蟲爬取數據、編寫業務,再到前後端分離交互,最後搜索結果高亮展示。

8.1創建項目

創建項目的步驟可如以下幾步:

  • 步驟一:導入相關依賴
       <properties>
            <java.version>11</java.version>
            <!-- 自定義 ElasticSearch 依賴版本與安裝的版本一致 -->
            <elasticsearch.version>7.6.1</elasticsearch.version>
        </properties>
    
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.83</version>
            </dependency>
    
  • 步驟二:編寫 properties 文件
    server.port=9090
    # 關閉 thymeleaf 快取
    spring.thymeleaf.cache=false
    
    #mysql連接配置
    spring.datasource.username=root
    spring.datasource.password=password123
    spring.datasource.url=jdbc:mysql://localhost:3306/elasticsearch-test?useSSL=false&useUnicode=true&characterEncoding=utf-8
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    
    #mybatis-plus日誌配置
    mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
    
    #mybatis-plus邏輯刪除配置,刪除為1,未刪除為0
    mybatis-plus.global-config.db-config.logic-delete-value = 1
    mybatis-plus.global-config.db-config.logic-not-delete-value = 0
    
  • 步驟三:導入前端樣式

    這個步驟可以在網盤

    地址://pan.baidu.com/s/1yk_yekYoGXCuO0dc5B-Ftg

    密碼: rwpq

    獲取對應的 zip 包,裡面包括了一些前端的靜態資源和樣式,直接放入 resources 文件夾中即可。

  • 步驟四:編寫controller
    @Controller
    public class IndexController {
    
        @GetMapping({"/","/index"})
        public String index(){
            return "index";
        }
    }
    

8.2爬取數據

在真實的項目中,數據可以從資料庫獲得,也可以從MQ(消息隊列)中獲得,也可以通過爬取數據(爬蟲)獲得,在這裡介紹一下使用爬蟲獲取項目所需數據的過程。

1.首先導入網頁解析依賴:
        <!--網頁解析依賴-->
        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>1.11.2</version>
        </dependency>

2.編寫網頁解析工具類(返回爬取到的數據):
@Component
public class HtmlParseUtil {

    public static List<Content> parseJD(String keyword) throws IOException {
        // 1、獲取請求://search.jd.com/Search?keyword=java
        String reqUrl = "//search.jd.com/Search?keyword=" + keyword;

        // 2、解析網頁,返回的document對象就是頁面的 js 對象
        Document document = Jsoup.parse(new URL(reqUrl), 30000);

        // 3、js 中使用的方法獲取頁面資訊
        Element j_goodList = document.getElementById("J_goodsList");

        // 4、獲取所有的 li 元素
        Elements liElements = j_goodList.getElementsByTag("li");

        //5、返回List封裝對象
        ArrayList<Content> goodsList = new ArrayList<>();

        //5、獲取元素中的內容,遍歷的 li 對象就是每一個 li 標籤
        for (Element el : liElements) {
            String price = el.getElementsByClass("p-price").eq(0).text();
            String title = el.getElementsByClass("p-name").eq(0).text();
            String img = el.getElementsByTag("img").eq(0).attr("data-lazy-img");
            // 將爬取的資訊放入 List 對象中
            Content content = new Content();
            content.setTitle(title);
            content.setImg(img);
            content.setPrice(price);
            goodsList.add(content);
        }
        return goodsList;
    }
}

8.3編寫業務

要編寫的業務只有兩部分:1、將上述獲取的數據放入 ElasticSearch 的索引中;2、實現 ElasticSearch 的搜索功能;

步驟一:

1.controller層:

    @Autowired
    private ContentService contentService;

    @GetMapping("/parse/{keyword}")
    public Boolean parse(@PathVariable("keyword") String keyword) throws IOException {
        return contentService.parseContent(keyword);
    }

2.service層:

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    /**
     *  1、將解析後的數據放入 ElasticSearch 的索引中
     * */
    public Boolean parseContent(String keyword) throws IOException {
        List<Content> contents = new HtmlParseUtil().parseJD(keyword);

        //批量插入 es
        BulkRequest bulkRequest = new BulkRequest();
        bulkRequest.timeout("2m");

        for (int i = 0; i < contents.size(); i++) {
            bulkRequest.add(
                    new IndexRequest("jd_goods")
                    .source(JSON.toJSONString(contents.get(i)), XContentType.JSON)
            );
        }
        BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
        return bulk.hasFailures();
    }
步驟二:

1.controller層:

    @GetMapping("/search/{keyword}/{pageNo}/{pageSize}")
    public List<Map<String, Object>> search(@PathVariable("keyword") String keyword,
                                            @PathVariable("pageNo") Integer pageNo,
                                            @PathVariable("pageSize") Integer pageSize) throws IOException {
        return contentService.searchPage(keyword, pageNo, pageSize);
    }

2.service層:

    /**
     * 2、獲取數據後實現搜索功能
     * */
    public List<Map<String,Object>> searchPage(String keyword, Integer pageNo, Integer pageSize) throws IOException {
        if (pageNo <= 1){
            pageNo = 1;
        }
        //條件搜索
        SearchRequest searchRequest = new SearchRequest("jd_goods");
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        //精準匹配
        TermQueryBuilder titleTermQuery = QueryBuilders.termQuery("title", keyword);
        sourceBuilder.query(titleTermQuery);
        sourceBuilder.timeout(new TimeValue(3, TimeUnit.SECONDS));
        //分頁
        sourceBuilder.from(pageNo);
        sourceBuilder.size(10);
        //執行搜索
        searchRequest.source(sourceBuilder);
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest,RequestOptions.DEFAULT);
        //解析結果
        ArrayList<Map<String, Object>> list = new ArrayList<>();
        for (SearchHit documentFields : searchResponse.getHits().getHits()) {
            list.add(documentFields.getSourceAsMap());
        }
        return list;
    }

8.4前後端交互

前後端交互主要是通過介面查詢數據並返回:前端有請求參數(關鍵字、分頁參數)後,後端通過關鍵字去 elasticsearch 索引中進行篩選,最終將結果返回給前端現實的一個過程。

在這裡主要分析一下前端是怎麼獲得後端介面參數的,後端的介面在上述業務編寫中已經包含了。

  • 引入Vue、axios
<!--前端使用Vue-->
<script th:src="@{/js/axios.min.js}"></script>
<script th:src="@{/js/vue.min.js}"></script>
  • 在Vue中綁定
<script>
    new Vue({
        el: '#app',
        data:{
            // 搜索關鍵字
            keyword: '',
            //返回結果
            results: []
        }
    })
</script>
  • 對接後端介面並返回
<script>       
       methods: {
            searchKey(){
                let keyword = this.keyword;
                console.log(keyword);
                //對接後端介面:關鍵字、分頁參數
                axios.get('search/' + keyword + '/1/20').then(response=>{
                      console.log(response);
                      //綁定數據
                      this.results = response.data;
                })
            }
        }
</script>

8.4搜索高亮

關鍵字高亮總結來說,就是將原來搜索內容中的關鍵字置換為加了樣式的關鍵字,進而展示出高亮效果。

主要邏輯在於,獲取到 Hits 對象後,遍歷關鍵字欄位,將高亮的關鍵字重新放入 Hits 集合中。

具體程式碼如下:

        //解析結果
        ArrayList<Map<String, Object>> list = new ArrayList<>();
        for (SearchHit documentFields : searchResponse.getHits().getHits()) {
            //解析高亮欄位,遍歷整個 Hits 對象
            Map<String, HighlightField> highlightFields = documentFields.getHighlightFields();
            //獲取到關鍵字的欄位
            HighlightField title = highlightFields.get("title");
            Map<String, Object> sourceAsMap = documentFields.getSourceAsMap();
            //置換為高亮欄位:將原來的欄位替換為高亮的欄位
            if(title != null){
                Text[] fragments = title.fragments();
                //定義新的高亮欄位
                String new_title = "";
                for (Text text : fragments) {
                    new_title += text;
                }
                //將高亮的欄位放入 Map 集合
                sourceAsMap.put("title",new_title);
            }
            list.add(sourceAsMap);
        }

九、總結

ElasticSearch 作為一個分散式全文檢索引擎,也可以應用在集群當中(K8S、Docker)。

ElasticSearch 實現全文檢索的過程並不複雜,只要在業務需要的地方創建 ElasticSearch 索引,將數據放入索引中,就可以使用 ElasticSearch 集成在各個語言中的搜索對象進行查詢操作了。

而在集成了 ElasticSearch 的 Spring Boot 項目中,無論是創建索引、精準匹配、還是欄位高亮等,都是使用 ElasticSearch 對象在操作,本質上還是一個面向對象的過程。

和 Java 中的其它「對象」一樣,只要靈活運用這些「對象」的使用規則和特性,就可以滿足業務上的需求,對這個過程的把控也是工程師能力 的一種體現。

在 Spring Boot 項目中集成 ElasticSearch 就和大家分享到這裡,如有不足,還望大家不吝賜教!