【Elasticsearch】使用 DSL 撰寫查詢條件


https://unsplash.com/photos/_DyUcJalGAc

在上一篇,筆者介紹了一些 ElasticSearch 的寫入操作,以及少數的查詢操作。本文一開始會引進範例資料,著重於使用 DSL 撰寫「相等」與「範圍」的查詢條件,並將它們用於字串或數值欄位上。接著講解如何組合出更複雜的條件,實現「NOT」與「OR」的邏輯。最後提及全文檢索,並介紹字串在 ES 儲存的方式,讓讀者認識關鍵字查詢的原理。

一、範例資料介紹

由於筆者會在本文示範許多條件的查詢,因此將範例資料展示如下。

這些是描述學生資料的 document,以下為欄位說明。

讀者在練習前可逐一新增 document。

在認識範例資料後,接下來的小節,筆者會示範使用 ES 的 DSL 進行查詢。DSL 的全名是 Domain-Specific Language,中文為「領域特定語言」,意思是專門用於特定應用領域的語言。對 ES 來說,就是專屬於它的語言。


二、相等查詢

(一)term

使用「term」符號,並傳入欄位與值,代表查詢條件是跟某個特定的值相等。以下範例是查詢 1 年級的學生。

{
    "query": {
        "term": {
            "grade": 1
        }
    }
}

此例的查詢結果為:Winnie。

若要對字串欄位做查詢,需在欄位名稱後方加上「.keyword」,這樣條件才會是與給定的字串值完全相等。以下範例是查詢叫做「Dora Pan」的學生。

{
    "query": {
        "term": {
            "name.keyword": "Dora Pan"
        }
    }
}

(二)terms

使用「terms」符號,並傳入欄位與陣列,則代表條件是與陣列中的其中一個值相等。以下範例是查詢擔任衛生股長或康樂股長的學生。

{
    "query": {
        "terms": {
            "job.name.keyword": ["衛生股長", "康樂股長"]
        }
    }
}

此例的查詢結果為:Dora、Mario。


三、範圍查詢

使用「range」符號,並傳入欄位,以及範圍的上限或下限,即可對欄位做範圍查詢。在定義上下限時,可使用的符號包含 gt(大於)、gte(大於等於)、lt(小於)與 lte(小於等於)。讀者可選擇一個上限、一個下限,或兩者都用。

(一)數值欄位

以下範例是查詢操性成績 70 ~ 79 分的學生。

{
    "query": {
        "range": {
            "conductScore": {
                "gte": 70,
                "lte": 79
            }
        }
    }
}

此例的查詢結果為:Dora、Winnie。

關於範圍的上限與下限,其最大、最小值剛好與 Java 的 long 型態相同。也就是最大值為「9,223,372,036,854,775,807」,最小值為「-9,223,372,036,854,775,808」。

(二)日期欄位

以下範例是查詢英文檢定通過日在 2021-07-01 之後的學生。

{
    "query": {
        "range": {
            "englishIssuedDate": {
                "gte": "2021-07-01"
            }
        }
    }
}

此例的查詢結果為:Winnie、Mario。

或是讀者想要使用「UNIX 時間戳」,也是行得通的。要留意的是,範圍上下限的參數是以毫秒為單位。

{
    "query": {
        "range": {
            "englishIssuedDate": {
                "gte": 1625097600000
            }
        }
    }
}

不論是使用字串還是時間戳來表示日期,都可用於查詢。原因是 ES 有在 index 的「mapping」將 englishIssuedDate 欄位設定為「date」類型。

Mapping 是一份資料,用來定義 ES 要如何儲存欄位值。透過「GET /student/_mapping」的 API,我們可看到各欄位的 mapping。

官方文件可知,對於 date 類型,預設是以「時間戳」與「字串(以減號區隔)」來做為解析日期的格式(format)。因此這兩種參數才能正確地查詢。


四、欄位是否存在

使用「exists」符號,並傳入欄位名稱,代表查詢條件是指定的欄位需存在。

(一)字串欄位存在

以下範例是查詢具有血型資料的學生。

{
    "query": {
        "exists": {
            "field": "bloodType"
        }
    }
}

此例的查詢結果為:Vincent、Dora、Mario。

(二)字串陣列欄位存在

以下範例是查詢有提供電話號碼的學生,語法與一般欄位相同。

{
    "query": {
        "exists": {
            "field": "phoneNumbers"
        }
    }
}

要注意的是,若陣列欄位存在,但裡面卻沒有元素,同樣會被 ES 視為欄位不存在。因此 exists 是個用來判斷是否為空陣列的好方法。

此例的查詢結果為:Vincent、Dora。

五、複合查詢

(一)bool

前面的範例都是只有一個條件。若想使用 AND、OR 或 NOT 邏輯,那就得使用「bool」符號了。

該符號內部可再囊括「must」、「filter」「mustNot」與「should」四個符號,語法結構如下:

{
    "query": {
        "bool": {
            "must": [],
            "mustNot": [],
            "should": [],
            "filter": []
        }
    }
}

陣列中可存放前面介紹的查詢條件,或是另一個 bool 物件。而這四個符號各自有不同的目的,接下來將分別說明。

(二)must 與 filter

這兩個符號代表「必須符合」的意思,也就是「AND」的邏輯。以下範例是查詢就讀「財務金融」系,且有修習「程式設計」課程的學生。

{
    "query": {
        "bool": {
            "must": [
                {
                    "term": {
                        "departments.keyword": "財務金融"
                    }
                },
                {
                    "term": {
                        "courses.name.keyword": "程式設計"
                    }
                }
            ]
        }
    }
}

此例的查詢結果為:Vincent。

至於 filter 符號,其用途與 must 相同,唯一的差別在於當 document 符合 must 的條件時,ES 會給予分數,而 filter 不會。

(三)mustNot

此符號代表「必須不符合」的意思,也就是「NOT」的邏輯。以下範例是查詢操行成績不低於 80 分的學生。

{
    "query": {
        "bool": {
            "mustNot": [
                {
                    "range": {
                        "conductScore": {
                            "lt": 80
                        }
                    }
                }
            ]
        }
    }
}

此例的查詢結果為:Vincent、Mario。

(四)should

此符號代表「OR」的邏輯,只要 document 達成其中一個條件,should 的部份就算符合。以下範例是查詢就讀「會計」系,或有修習「會計學」課程的學生。

{
    "query": {
        "bool": {
            "should": [
                {
                    "term": {
                        "departments.keyword": "會計"
                    }
                },
                {
                    "term": {
                        "courses.name.keyword": "會計學"
                    }
                }
            ]
        }
    }
}

本次的查詢結果為:Mario、Vincent、Winnie。其中 Mario 符合 2 個條件,而其他人只符合 1 個,因此 Mario 分數更高。

雖然 should 預設是只要符合 1 個條件就好,但我們可另外使用 minimum_should_match 符號,規定最少要符合的條件數量,藉此讓查詢結果的相符程度有一定水準。

以下筆者對可選擇的參數做個整理。

種類用途參數範例範例說明
正整數至少要符合的數量2-
負整數最多容許多少條件不符合-3-
正百分比至少要符合多少比例的條件(小數點捨去)"75%"假設條件總數為 9,則至少應符合 9×75%=6 個條件。
負百分比最多容許多少比例的條件不符合(小數點捨去)"-25%"假設條件總數為 9,則容許 9×25%=2 個條件不符。
組合當條件總數大於指定數量時,才可部份符合"2<-25%"條件總數小於等於 2 時,需全部符合;
大於 2 時,可容許 25% 的條件不符。
多組合根據條件總數所落在的區間,決定符合的程度"2<-1 6<75%"條件總數小於等於 2 時,需全部符合;
大於 2 但小於等於 6 時,可容許 1 個條件不符;
大於 6 時,需符合 75% 的條件。

以下範例是查詢「就讀會計系」、「就讀資訊管理系」、「修習會計學」或「修習程式設計」,至少符合 2 項的學生。

{
    "query": {
        "bool": {
            "should": [
                {
                    "term": {
                        "departments.keyword": "會計"
                    }
                },
                {
                    "term": {
                        "departments.keyword": "資訊管理"
                    }
                },
                {
                    "term": {
                        "courses.name.keyword": "會計學"
                    }
                },
                {
                    "term": {
                        "courses.name.keyword": "程式設計"
                    }
                }
            ],
            "minimum_should_match": 2
        }
    }
}

此例的查詢結果為:Vincent、Mario。


六、全文檢索

(一)字串的倒排索引(inverted index)

當 document 具有字串欄位,ES 理所當然會在該 document 儲存該字串。但為了做全文檢索,ES 還會另外將該字串拆解成多個單詞(term),儲存在一個叫做「倒排索引」的地方。

假設有以下兩個 document:

{
    "_id": "S123",
    "_source": {
        "name": "Mario Chang",
        "grade": 2
    }
}

{
    "_id": "S456",
    "_source": {
        "name": "Luegi Chang",
        "grade": 1
    }
}

則以下是 name 欄位的倒排索引之示意內容:

term_id
chang"S123", "S456"
luegi"S456"
mario"S123"

若我們想找出某欄位包含指定文字的 document,例如 name 欄位包含「mario」,ES 便可從倒排索引快速地知道哪些 document 符合條件,提升查詢效率。

(二)match

使用此符號,並傳入欄位與關鍵字,即可查詢欄位值包含關鍵字的 document。關鍵字之間用空格隔開,ES 會拆解成不同的單詞,並利用倒排索引進行查詢。

在上一篇文章中,筆者示範過針對一個欄位做關鍵字查詢。以下的範例稍微有變化,是查詢 name 或 introduction 欄位包含「programming」、「accounting」或「vincent」的 document。

{
    "query": {
        "bool": {
            "should": [
                {
                    "match": {
                        "name": "programming accounting vincent"
                    }
                },
                {
                    "match": {
                        "introduction": "programming accounting vincent"
                    }
                }
            ]
        }
    }
}

此例的查詢結果為:Vincent、Mario、Dora。

讀者在實務開發上,可先決定哪些欄位能夠被搜尋,再配合輸入的關鍵字,組合出查詢條件。

上一篇:【ElasticSearch 8】透過 REST API 進行 CRUD 操作

下一篇:【ElasticSearch 8】使用 function score 在查詢時評分

延伸閱讀:【ElasticSearch 8】使用 Java API Client 實作查詢條件與搜尋

留言