Spring Boot + Elasticsearch 搜索功能,看完這篇你也會

在這個教學裏,我們會使用到以下關鍵技術棧:

  • Spring Boot
  • Elasticsearch
  • Logstash
  • MySQL

Spring Boot 主要作爲我們對外的 API 接口,Elasticsearch 則是作爲我們的搜索引擎。假設我們本來已經在使用 MySQL 作爲我們的數據庫,這篇教程會教你如何利用 Logstash 實現 MySQL 與 Elasticsearch 的實時同步。

Spring Boot 設置

添加依賴

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
	<groupId>io.searchbox</groupId>
	<artifactId>jest</artifactId>
	<version>6.3.1</version>
</dependency>

我們需要通過 jest (Java Rest Client) 向 elasticsearch 9200 端口發送 HTTP Rest 請求來進行查詢。

application.properties 設置

spring.elasticsearch.jest.uris=http://localhost:9200
spring.elasticsearch.jest.read-timeout=5000

EsStudyCard.java

@Document(indexName = "cards", type = "card")
public class EsStudyCard {
    @Id
    private int id;
    private String title;
    private String description;
    
    // setters and getters
}

這裏需要注意的是,我們不能對同一個實體類 (entity class) 同時使用註解 @Entity 與 @Document,因爲 @Entity 表示由 JPA 掌管這個實體類,而 @Document 表示由 Elasticsearch 掌管這個實體類,兩個一起使用的話會產生衝突並導致異常。因此我們需要創建另一個實體類專門給 Elasticsearch 使用。

如果是使用 Docker 容器或外部 Elasticsearch 服務器,請將 localhost 改成相應的主機名或主機地址。

EsStudyCardService.java

@Service
public class EsStudyCardService {
    @Autowired
    private JestClient jestClient;

    public List<EsStudyCard> search(String content) {
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(
            QueryBuilders
            .boolQuery()
            .should(QueryBuilders.matchQuery("title", content).fuzziness("10").operator(Operator.OR))
            .should(QueryBuilders.matchQuery("description", content).fuzziness("10").operator(Operator.OR))
            .minimumShouldMatch(1)
        );
        Search search = new Search
                            .Builder(searchSourceBuilder.toString())
                            .addIndex("cards")
                            .addType("card")
                            .build();
        try {
            JestResult result = jestClient.execute(search);
            return result.getSourceAsObjectList(EsStudyCard.class);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}

這是模糊搜索的一個寫法。首先我們創建一個 SearchSourceBuilder 的實例,接着通過調用 query() 方法並傳遞一個 QueryBuilder 實例 (通過 QueryBuilders 接口創建)。在這裏我們的 QueryBuilder 會查詢任何滿足模糊搜索條件的數據。這裏我們使用 should() 方法並加上 minimumShouldMatch(1) 方法來表示滿足 title or description fields 的邏輯。

在 should() 方法裏傳遞 QueryBuilder,在這裏我們使用 fuziness() 方法表示我們想要使用模糊搜索,傳遞的參數表示模糊搜索詞的 edit distance。接着我們調用 operator() 方法並傳遞 Operator.OR 表示如果我們這裏的 content 包含了多個詞,則會對應每個詞進行模糊搜索並用 OR 的邏輯關係將他們串聯起來。舉例,如果我們的搜索詞是 “red apple”,則搜索條件爲滿足 “red” 模糊搜索 OR 滿足 “apple” 模糊搜索。

接着我們通過 Search 來指定 index 與 type。然後使用 JestClient 發送查詢請求。在這裏我們只需要確保我們加入了 JestClient 的依賴 (如之前所示),然後使用 @Autowired 註解,Spring Boot 就會幫我們注入 JestClient 的實例了。

EsStudyCardController.java

@PostMapping(value = "/esstudycard/search")
public ResponseEntity<Object> search(@RequestBody JSONObject payload) {
    String content = (String) payload.get("content");
    List<EsStudyCard> studyCards = esStudyCardService.search(content);
    JSONObject jsonObject = new JSONObject();
    jsonObject.put("data", studyCards);
    return ResponseEntity.ok().body(jsonObject);
}

接着在 controller 類裏面只需要簡單調用 EsStudyCardService 的 search 方法就可以了。

Logstash 配置文件

以 MySQL 爲例,我們需要下載相應的 jdbc 驅動,並放在指定的目錄下。

接着我們創建一個如下的 Logstash 配置文件。這裏以 Docker 容器爲例,通過 volume 的形式將該 Logstash 配置文件映射到 /usr/share/logstash/pipeline (CentOS 系統). Logstash 會自動檢測 pipeline 文件下的配置文件。

input {
    jdbc {
        jdbc_connection_string => "jdbc:mysql://localhost:3306/vanpanda"
        jdbc_user => "root"
        jdbc_password => "root"
        jdbc_driver_class => "com.mysql.cj.jdbc.Driver"
        statement => "SELECT * FROM study_cards where modified_timestamp > :sql_last_value order by modified_timestamp;"
        use_column_value => true
        tracking_column_type => "timestamp"
        tracking_column => "modified_timestamp"
        schedule => "* * * * *"
        jdbc_default_timezone => "America/Los_Angeles"
        sequel_opts => {
            fractional_seconds => true
        }
    }
}

output {
    elasticsearch {
        index => "cards"
        document_type => "card"
        document_id => "%{id}"
        hosts => ["localhost:9200"]
    }
}

該配置文件主要是告訴 Logstash 數據源的位置。這裏我們的數據源就是我們的 MySQL 數據庫。根據該配置文件,Logstash 每分鐘會發送我們指定的查詢語句(query),接着將返回的數據插入到 Elasticsearch 中,實現近似實時同步。

請將 localhost 改成相應的主機名或主機地址。

查看 Elasticsearch 的狀態或數據

我們可以通過 web api 的方式來查看 Elasticsearch 的狀態,包括它的index。

http://localhost:9200/{index_name}
http://localhost:9200/_cat/indices?v
http://localhost:9200/{index_name}/_search?pretty

將上述 url 直接複製到瀏覽器即可通過 Elasticsearch 9200 端口的 Http Rest api 進行查找。

讓 Logstash 持續運行,並檢查數據更新

當 Logstash 完成第一次任務之後會自動 shut down。我們需要 Logstash 持續在 Docker 的容器中運行並每隔一段時間就執行一次任務來確保 Elasticsearch 與 MySQL 可以達到幾乎實時同步的效果。

在 Logstash 的配置文件裏,在 input jdbc 裏配置 schedule 設定,否則 Logstash 會認爲這只是一次性任務,並在第一次執行之後就會自動關閉。

input {
    jdbc {
		...
        schedule => "* * * * *" #加入這個,表示每分鐘更新
    }
}

例子中的語法代表每分鐘執行一次任務。我們可以查看語法文檔自定義時間。

確保 Logstash 不會重複更新舊數據

如果我們沒有告訴 Logstash 如何判斷數據的新舊,Logstash 每次執行任務時,便會把舊數據刪除,再重新插入一次,導致資源浪費。

對 Logstash 做如下配置:

input {
  jdbc { 
  	...
  	# 需要利用 Modified Date / Timestamp 來判斷數據新舊
    statement => "SELECT" * FROM testtable where Date > :sql_last_value order by Date" 
    # 告訴 Logstash 使用 column value for
    use_column_value => true  :sql_last_value
    # 指定 column Date 作爲 :sql_last_value 的值
    tracking_column => Date 
    # 如果 column type 是 timestamp,則一定要配置這項。默認爲 numeric
    tracking_column_type => timestamp
}

參考文檔:Migrating MySql Data Into Elasticsearch Using Logstash

錯誤信息與解決辦法

Unable to find driver class via URLClassLoader in given driver jars: com.mysql.jdbc.Driver and com.mysql.jdbc.Driver

logstash 版本 6.2.x 以及以上,conf 文件裏不要設定 jdbc_driver_library,將 connector
driver 的 jar 文件直接放在 /usr/share/logstash/logstash-core/lib/jars/ 即可。

參考文檔:Stackoverflow 的一篇提問

Logstash 運行之後,Elasticsearch 裏只有一條數據,其他數據沒有存進來。

主要原因是因爲數據的id重複了,因此 Elasticsearch 會將舊數據刪除,並插入新數據。 通過檢查 index 的狀態
(http://localhost:9200/_cat/indices?v),可以發現有數據被 deleted 了。
當我們在設置 Logstash 的 conf 文件時,在 output document_id 這一項,使用通配符 %{field_name} 來指定一個唯一的 id。比如說,我們從數據庫 select 出來的 fields 裏有一個作爲 primary key 的 field 名爲 id,我們則可以這樣設置:%{id}。Logstash 就會以這個 field 裏的值來作爲 Elasticsearch 裏的 id。

參考文檔:elastic 官網的一篇提問

Logstash sql_last_value is always 0

如果 tracking_column 是 timestamp的話,記得設定
tracking_column_type => timestamp

Logstash sql_last_value timestamp is not consistent with database

一個可能的原因是因爲 logstash :sql_last_value 的 timezone 與數據庫的不一致。因爲
Elasticsearch 跟 Logstash 默認使用 UTC timezone。
解決方法:在 Logstash 的配置文件裏,設置 jdbc_default_timezone 爲你想要的 timezone,則 Elasticsearch 與 Logstash 會據此進行轉換。

參考文檔:Logstash-jdbc-input Timezone issue

MySQL Docker 容器第一次啓動的時候出現 ERROR 1396 (HY000) at line 1: Operation CREATE USER failed for ‘root’@’%’ 然後就關掉了

可能是因爲我們在 docker-compose.yml 裏面傳遞參數給 MySQL 容器的時候,重複創建名爲 root
的用戶所導致的錯誤。嘗試換個用戶名試試看。或者如果要繼續使用 root 用戶來創建表的話,只需要在 docker-compose.yml 設置 MYSQL_ROOT_PASSWORD 就可以了。

參考文檔:ERROR 1396 (HY000): Operation CREATE USER failed for ‘root’@’%’ #129

Logstash :sql_last_value 的 timestamp 把秒之後的毫秒給 truncate 掉了,這時候如果數據庫的 timestamp 保有毫米信息的話,則會發生重複選取數據的問題。
例如: 如果數據庫的時間戳爲 2019-11-20 09:28:03.042000,Logstash 的 sql_last_value 則會變成 2019-11-20 09:28:03。而我們的 query 是想選取 > :sql_last_value 的,由於數據庫裏的這條數據有毫秒信息,那麼他永遠都會比較大而導致重複選取數據。

這個問題已經在 logstash-jdbc-input 4.3.5 版本里修復了。如果我們用的是最新的 Logstash 版本,則我們容器自動安裝的 jdbc-input 插件也應該是有這個修復的版本。
此時,我們還必須在 Logstash 配置文件爲 jdbc 做如下配置纔可以使這個修復生效。

sequel_opts => {
	fractional_seconds => true
}

參考文檔:Force all usage of sql_last_value to be typed according to the settings #260

在 Spring Boot 裏,當我們對同一個Entity同時使用 @Entity 與 @Document 時,出現以下錯誤:
The bean ‘studyCardPagedJpaRepository’, defined in null, could not be registered. A bean with that name has already been defined in null and overriding is disabled.

因爲 @Entity 表示由 JPA 掌管這個實體類,而 @Document 表示由 Elasticsearch
掌管這個實體類,兩個一起使用的話會產生衝突並導致異常。因此我們需要創建另一個實體類專門給 Elasticsearch 使用。

參考文檔:CSDN 的一篇博文
參考文檔:Spring Data 踩坑記錄

作者仍在學習中, 如果有什麼錯誤,請各位指出幷包含,謝謝!
作者:David Chou(溫哥華 Simon Fraser University 計算機學生)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章