【Elasticsearch】使用 Java API Client 實作查詢條件與搜尋


https://unsplash.com/photos/_DyUcJalGAc

上一篇文章中,我們在 Spring Boot 的專案裡使用 ElasticSearch 的 Java API Client 示範簡單的 CRUD。而本文將專注於搜尋的功能,除了展示查詢條件要如何透過該 library 實作出來,也會提及排序與分頁的操作。

一、程式專案介紹

本文完成後的範例專案將放置於 GitHub 上,repo 連結在此

(一)Repository 層簡介

專案中實作了一個 Repository 層,名為 StudentEsRepository。其中包含了存取 ES 的邏輯,如下:

(二)資料類別介紹

本文所示範的是將學生資料存放到 ES,因此筆者設計了一個學生類別(Student),其內部還包含課程(Course)與職務(Job)的資料。

(三)範例資料

由於會在 ES 上新增 document,因此準備了以下範例資料。

筆者將這份 JSON 檔放置於程式專案中,並撰寫一個類別與方法,來讀取成我們自訂的 Student 物件。

(四)準備測試類別

在本文,筆者將透過撰寫測試的方式,來使用 Repository 元件發出搜尋請求。因此請在 pom 檔加入測試相關的 library。

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
</dependency>

接著再建立一個 Spring Boot 的測試類別,將 Repository 元件注入在其中。

另外還寫了一個名為 setup 的 Before 方法做初始化,要做的事情是讓 Repository 重新建立 ES 的 index,再將範例資料新增進去。

這邊故意讓程式暫停 2000 毫秒再繼續,理由是新增 document 可能需要一段時間。若在這期間,測試程式很快地試圖去存取它,容易發生找不到 document 的情形。

筆者在測試類別再撰寫一個名為 assertDocumentIds 的方法,用來檢查搜尋到的資料是否與預期相同。

其中第一個參數叫做 ignoreOrder,用以控制檢查時是否要忽略順序。本文第六節在排序的測試情境中會傳入 false。

二、Search API 的使用方式

讓我們複習一下如何使用 ES 提供的 REST API 進行搜尋。假設 index 名稱為「student」,則以下是一個範例請求,包含:

  • 查詢條件:無條件
  • 排序方式:將 courses 陣列欄位的 point 欄位加總後遞減
  • 分頁方式:取前 3 個
POST /student/_search
{
    "query": {
        "bool": {
            "filter": [
                "match_all": {}
            ]
        }
    },
    "sort": [
        {
            "courses.point": {
                "mode": "sum",
                "order": "desc"
            }
        }
    ],
    "from": 0,
    "size": 3
}

那麼在程式中,要如何透過 library 建立出搜尋的 request 呢?為了方便,筆者定義一個叫做 SearchInfo 的類別,它包裝了搜尋時會用到的參數:

接著在 Repository 撰寫一方法,它會接收剛剛的 SearchInfo 物件,建立出搜尋請求後發出,並取得結果。

建立 SearchRequest 物件的過程中,給予了四個重要的參數。

  • query:查詢條件。與第三、四、五節相關。
  • sort:排序方式。與第六節相關。
  • from、size:分頁方式。與第六節第二段相關。

準備好 SearchRequest 後,便交由 client 的 search 方法進行搜尋。隨後可得到 SearchResponse 物件,從中呼叫 source 方法,最終得到 document 的物件。


三、相等查詢

從這節開始,將示範如何用 library 實作出查詢條件。筆者在專案中建立一個 SearchUtils 類別,用來提供建立查詢條件的方法。

public class SearchUtils {
    // TODO
}

要注意的是,library 使用「Query」這個類別當作條件的傳遞介面。而查詢條件有許多種,也都有各自的物件類別,因此它們需要被轉換為 Query 物件,才能實際使用。

(一)與單一值相等

需建立出 TermQuery 物件,過程中會透過 field 方法給予欄位名稱,並透過 value 方法給予指定的值。

而 value 方法尚接受 boolean、double 型態的值,由於本文不會用到,因此以拋出例外處理。

附帶一提,上例中判斷為 Integer 型態後所呼叫的 value 方法,其所接受的型態其實是 long,而非 int。

以下範例是查詢年級為 3 的學生,寫成測試如下:

(二)與任一值相等

需利用多個「候選值」建立出一個 TermsQuery 物件。但過程比前面的 TermQuery 繁瑣。以下是範例程式:

首先利用這些候選值,建立出多個 FieldValue 物件。每個 FieldValue 物件都帶有一個值,是透過 longValue、stringValue 方法分別給予整數與字串值的。另外也有適用於 boolean 與 double 值的方法。

隨後再將這些 FieldValue 轉換成一個 TermsQueryField 物件,它相當於 DSL 中 terms 參數的候選值陣列。最後用它就能產生 TermsQuery。

以下範例是查詢就讀「資訊管理」或「企業管理」系的學生,寫成測試如下:

四、範圍查詢

(一)數值範圍

需建立 RangeQuery 物件,過程中可用 field 方法給予欄位名稱,並透過 gte 與 lte 方法給予範圍的下限與上限。其他方法尚有 gt(大於)與 lt(小於)可選擇。

此處宣告的 createRangeQuery 方法接受 Number 型態的參數,因此 int、long、double 等數值型態均能適用。

定義上下限時,需傳入一個 JsonData 物件作為參數。使用它的 of 方法建立時,可傳入任何型態的值。

以下範例是查詢年級為 2 到 4 的學生,寫成測試如下:

(二)日期範圍

建立 JsonData 物件時,亦可傳入 Date 物件。此處再撰寫一個建立日期範圍條件的方法。

以下範例是查詢英文檢定通過日介於 2021-07-01 與 2022-06-30 之間的學生,寫成測試如下:


五、關鍵字查詢(全文檢索)

需建立出 MatchQuery 物件,過程中會透過 field 方法給予欄位名稱,並透過 query 方法給予關鍵字。關鍵字同樣可以給予多個,並以空格區隔。

我們假設 document 可被搜尋的欄位有多個。因此實作上是將多個 MatchQuery 放入 Bool 複合查詢的 should 參數中(OR 邏輯),以達到任一欄位有包含關鍵字,即符合條件。

以下範例是查詢 name 或 introduction 欄位中包含「vincent」或「career」字眼的資料,寫成測試如下:

此處是將全文檢索的條件以「must」的方式添加到 Bool 查詢,原因是為了讓 ES 以相符程度計分。


六、欄位是否存在

需建立出 ExistsQuery 物件,過程中會透過 field 方法給予欄位名稱。

(一)字串欄位存在

以下範例是查詢具有血型資料的學生,寫成測試如下:

(二)字串陣列欄位存在

以下範例是查詢有提供電話號碼的學生,寫成測試如下:

(三)物件陣列欄位存在

對於這個情況,筆者就不特別示範程式了,依此類推。不過其實有另一種做法,那就是直接對物件元素的任一欄位做 exists 的檢查。

七、排序與分頁

(一)排序

在建立 SearchRequest 物件的過程中,使用 sort 方法,並傳入一至多個 SortOptions 物件,即可設定好排序規則。

以下同樣在 SearchUtils 類別撰寫方法,藉此建立代表排序規則的 SortOptions 物件。

過程中需建立出 FieldSort 物件,並將欄位名稱、排序方向(SortOrder)與陣列的取值模式(SortMode)添加上去。然而我們未必是用陣列欄位做排序,所以上面的範例程式中會有另一個略過該參數的方法。

以下範例是將學生修習課程中的最高學分數遞減排序,學分數相同者,再以姓名遞增排序。寫成測試如下:

(二)分頁

ES 的分頁是採取「skip」與「limit」的概念。也就是先跳過一定數量的資料,再取接下來的幾筆作為結果。

對 SearchRequest 添加分頁相關參數的方式相當單純,只要透過 from 與 size 方法即可。

以下範例是將學生以年級遞減排序,並取得前 2 筆資料。寫成測試如下:

本文的完成專案:

上一篇:【ElasticSearch 8】導入到 Spring Boot 並實作 CRUD

下一篇:【ElasticSearch 8】使用 Java API Client 實作 function score

留言