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


https://unsplash.com/photos/_DyUcJalGAc

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



一、範例資料介紹

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

[
{
"id": "103",
"name": "Vincent Zheng",
"departments": ["資訊管理", "財務金融"],
"courses": [
{ "name": "計算機概論", "point": 3 },
{ "name": "程式設計", "point": 4 },
{ "name": "投資學", "point": 3 },
{ "name": "會計學", "point": 0 }
],
"grade": 2,
"conductScore": 86,
"job": {
"name": "班長",
"primary": true
},
"introduction": "I have a blog used to record what I learn in my career. All of them are about information technology and programming.",
"englishIssuedDate": "2021-01-01",
"bloodType": "A",
"phoneNumbers": [
"0988123456",
"0224123456"
]
},
{
"id": "101",
"name": "Dora Pan",
"departments": ["財務金融"],
"courses": [
{ "name": "財金概論", "point": 3 },
{ "name": "保險學", "point": 3 },
{ "name": "投資學", "point": 3 }
],
"grade": 4,
"conductScore": 74,
"job": {
"name": "衛生股長",
"primary": null
},
"introduction": "Wealth ignores those who ignore it. So I apply knowledge about accounting in my life.",
"englishIssuedDate": "2021-04-01",
"bloodType": "B",
"phoneNumbers": [
"0912345678"
]
},
{
"id": "104",
"name": "Winnie Kuo",
"departments": ["企業管理"],
"courses": [
{ "name": "會計學", "point": 3 },
{ "name": "商業概論", "point": 1 }
],
"grade": 1,
"conductScore": 71,
"job": {
"name": "班長",
"primary": false
},
"introduction": "To lead a team in company in career, learn to lead students in university first.",
"englishIssuedDate": "2021-12-01",
"phoneNumbers": []
},
{
"id": "102",
"name": "Mario Lu",
"departments": ["會計"],
"courses": [
{ "name": "會計學", "point": 5 },
{ "name": "審計學", "point": 3 },
{ "name": "企業資源規劃", "point": 3 }
],
"grade": 3,
"conductScore": 83,
"job": {
"name": "康樂股長"
},
"introduction": "Accounting work can be done by technology. So I start to learn programming on internet.",
"englishIssuedDate": "2022-05-01",
"bloodType": "O"
}
]
view raw students.json hosted with ❤ by GitHub

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

欄位 說明 欄位 說明
id 編號 conductScore 操行成績
name 姓名 job 職務
departments 修習科系 job.name 職務名稱
courses 修習課程 job.isPrimary 是否主要(正副),若空值則代表主要
courses.name 課程名稱 introduction 自我介紹
courses.point 學分數 englishIssuedDate 英文檢定通過日
grade 年級 bloodType 血型
phoneNumbers 電話號碼 - -

讀者在練習前可逐一新增 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 實作查詢條件與搜尋

留言