該文章同步發佈於:我的部落格
也歡迎關注我的 Facebook 以及 Instagram 接收軟體相關的資訊!
上一篇文章 中,對於搜尋已經有了基礎的概念,這篇文章將會介紹 Term Level 的搜尋以及 Range 搜尋方式。
Term 在中文翻譯應該可以翻作單字,或是特定的術語,下面是劍橋字典的解釋:
a word or expression used in relation to a particular subject, often to describe something official or technical
所以我們可以理解在 Elasticsearch ( 以下簡稱 ES ) 之中使用 term level 的搜尋意味著精準地針對欄位進行搜尋,常見的使用場景像是搜尋品牌的名稱,或是產品的分類等等。
直接進入示範,使用以下的搜尋條件來篩選 genres
的 keyword
類型是喜劇的電影:
// GET /movies/_search
{
"query": {
"term": {
"genres.keyword": "Comedy"
}
}
}
結果如下:
不太意外地命中了 979 筆資料,這邊我們做一個實驗,把 Comedy
改成 comedy
試試看:
馬上變成 0 筆資料,這是為什麼呢?
就要講到 term level 的一個特點,搜尋的單字本身不會被 tokenize,也意味搜尋的單字本身是不會受到大小寫轉換的,還記得 tokenize 的作用嗎?不記得的可以回去看 這篇文章 中 ES 的 Analyzer 做了什麼。
所以這邊搜尋中的 comedy
確實沒辦法在 inverted indices 中找到和 comedy
相符的文件,因為當初這個欄位是以 keyword
的資料類型儲存,而且儲存的內容本身是 Comedy
大寫的格式,所以一定找不到。
那你可能心裡就想說,這樣真的好麻煩,好難用,有沒有什麼辦法可以改變這件事?
有,我們可以用更具體的搜尋方式 ( Explicit Query ) 搭配 case_insensitive
來查詢:
// GET /movies/_search
{
"query": {
"term": {
"genres.keyword": {
"value": "comedy",
"case_insensitive": true
}
}
}
}
就可以得到原本我們期待的 979 筆資料。
所以使用 term level 搜尋最需要注意的就是搜尋的單字本身可能會因為不被 Analyzer 處理過而受限於大小寫的影響,當然這也和我們查詢的欄位資料類型是 keyword
有關係。
接著你會聯想到,那我去查詢 text
資料類型的欄位呢?試試看:
我們嘗試使用 28 Days 這個標題去搜尋電影,我很確定有一部電影的標題就叫做 28 Days 沒錯。
// GET /movies/_search
{
"query": {
"term": {
"title": "28 Days"
}
}
}
答案是一筆都不會對到:
為什麼會這樣呢?回想一下 text
的資料類型是怎麼被 Analyzer 處理和儲存的,下面是 28 Days 轉換成 inverted index 的樣子:
而搭配上我們說的 term level 並不會對搜尋的單字進行分析,所以是直接用 28 Days 這樣的 key 下去找,當然找不到囉!
換個想法,只要我們使用 28 / days / 28 days 都可以找到這個 document 才對:
// GET /movies/_search
{
"query": {
"term": {
"title": "28"
}
}
}
結果出爐,確實有一部電影叫做 28 Days 沒錯!
所以在使用 term level 搜尋時,切記不要針對 text
資料類型的欄位進行搜尋,這很容易導致一些你很難 debug 的狀況,像是剛剛的情境,一開始使用 days 去搜尋會找到想要的資料,覺得沒問題了,部署上去後,想說使用一個看起來更精準的 28 Days 搜尋卻找不到,這時候如果不理解 tokenizer 是如何作用的話,真的會一個頭兩個大。
這是一個很直覺的用法,就像在使用關聯式資料庫一樣,直接使用範例就能夠明白了:
// GET /movies/_search
{
"query": {
"ids": {
"values": ["1", "10", "100"]
}
}
}
分別得到 ID 是 1, 10 以及 100 的資料:
這個用法的好處在哪呢?可以在 index 資料時搭配關聯式資料庫的 ID 來建立,這樣在應用程式層面要互動也會比較簡單,甚至是 debug 時也很好找資料。
Range 搜尋的使用場景百百種,常見的有數字大小的區間,還有一個很重要的就是時間的區間,在 ES 之中當然也支援時間的區間搜尋,因為我當初提供的 movies 的資料中,忘記加入時間的欄位,所以下面就用假設的方式來示範 ( 你拿去查詢會壞掉 ):
// GET /movies/_search
{
"query": {
"range": {
"release_date": {
// ES 也支援 2001/01/01 不含時間的寫法
"gt": "2001/01/01 00:00:00",
"lt": "2005/12/31 23:59:59"
}
}
}
}
至於要用哪一種寫法呢?要加上時分秒嗎?端看你的 mapping 當初設定的格式是什麼。
而 date
資料欄位的格式也是在建立 mapping 時需要知道,常見的三種格式:
其中需要稍微介紹的就是 epoch_millis
,其實就是 1970-01-01T00:00:00Z
到某個時間點的毫秒數,舉例來說,1588302800000 表示 2020-05-01T00:00:00Z
至於要用什麼就取決於你們應用程式需要的精準程度,只需要特別注意如果是選擇 yyyy/MM/dd 的格式,ES 會自動帶入 00:00:00 作為搜尋的起始點,所以如果你的搜尋要更準確的話,一開始設計的時候就要考慮到這件事情。
而關於搜尋時間這件事,ES 還支援了 format
以及 time_zone
的兩種參數,關於 format
就是特殊的時間格式,因為 mapping 的 date
格式是可以客製的,所以有些人的格式可能是 dd/MM/yyyy,這樣在搜尋時就要特別加入,像是這樣:
// GET /movies/_search
{
"query": {
"range": {
"release_date": {
"format": "dd/MM/yyyy",
"gt": "01/01/2000",
"lt": "31/12/2005"
}
}
}
}
至於 time_zone
的參數呢?則是因應 ES 儲存的時間都是 UTC,所以我們可以利用給搜尋時間加上 time_zone
,讓 ES 在執行查詢之前先將你的查詢轉換為指定的 UTC 時間。
怎麼用呢?很簡單:
// GET /movies/_search
{
"query": {
"range": {
"release_date": {
"time_zone": "+08:00",
"gt": "2001/01/01 00:00:00",
"lt": "2005/12/31 23:59:59"
}
}
}
}
這樣實際上在搜尋時會被轉換成下面的格式,才執行真正的查詢!
{
"gt": "2000/12/31 16:00:00",
"lt": "2005/12/31 15:59:59"
}
下一篇文章會繼續來討論一些搜尋的用法和該注意的細節!