一起學Elasticsearch系列-Query DSL
DSL是Domain Specific Language的縮寫,指的是為特定問題領域設計的計算機語言。這種語言專注于某特定領域的問題解決,因而比通用編程語言更有效率。
在Elasticsearch中,DSL指的是Elasticsearch Query DSL,是一種以JSON形式表示的查詢語言。通過這種語言,用戶可以構建復雜的查詢、排序和過濾數據等操作。這些查詢可以是全文搜索、聚合搜索,也可以是結構化的搜索。
一、查詢上下文
搜索是Elasticsearch中最關鍵和重要的部分,使用query關鍵字進行檢索,更傾向于相關度搜索,故需要計算評分。
在查詢上下文中,一個查詢語句表示一個文檔和查詢語句的匹配程度。無論文檔匹配與否,查詢語句總能計算出一個相關性分數在_score字段上。
1.相關度評分:score
相關度評分用于對搜索結果排序,評分越高則認為其結果和搜索的預期值相關度越高,即越符合搜索預期值,默認情況下評分越高,則結果越靠前。在7.x之前相關度評分默認使用TF/IDF算法計算而來,7.x之后默認為BM25。
score是根據各種因素計算出來的,包括:
- Term Frequency(詞頻):一個詞在文檔中出現的次數越多,score就越高。
- Inverse Document Frequency(逆文檔頻率):一個詞在所有文檔中出現的次數越少,score就越高。
- Field Length Norm(字段長度規范):字段的長度越短,score就越高。
這三個因素共同決定了score的值。然而,你也可以通過設置自定義評分或者禁用評分來影響score的計算。
2.TF/IDF & BM25
TF/IDF是一種在信息檢索和文本挖掘中廣泛使用的統計方法,用于評估一個詞語對于一個文件集或一個語料庫中的一個文件的重要程度。名稱中的TF表示“術語頻率”,IDF表示“逆向文件頻率”。
- TF (Term Frequency) :這是衡量詞在文檔中出現的頻率。通常來說,一個詞在文檔中出現的次數越多,其重要性就可能越大。但這并不總是正確的,比如在很多英文文檔中,“the”、“and”等詞出現的頻率非常高,但我們并不能因此認為它們就非常重要。因此,需要結合 IDF 來使用。
- IDF (Inverse Document Frequency) :這是衡量詞是否常見的度量。如果某個詞在許多文檔中都出現,那么它可能并不具有區分性,對于搜索和分類的幫助就不大。例如,每篇英文文章中都會出現的“the”對于區分文章內容就沒有什么幫助。所以,如果一個詞在所有文檔中出現得越多,那么其 IDF 值就會越小,相反,如果一個詞很少在文檔中出現,那么其 IDF 值就會較大。
TF-IDF 會將這兩個因子結合起來,為每個詞產生一個權重。具有較高 TF-IDF 分數的詞被認為在文檔中更重要。通過這種方式,ES 能夠提供相關性排序,使得包含用戶查詢詞匯的最相關文檔排在搜索結果的前面。
BM25是一種更先進的排名函數,也是基于TF/IDF的一種改進型方法。它引入了兩個新概念:
- 文檔長度歸一化:長文檔可能會有更多的關鍵詞,但這并不意味著它與查詢更相關。BM25通過調整文檔長度來解決這個問題。
- 飽和度:在TF/IDF中,詞項的出現頻率越高,其重要性就越大。然而在實踐中,一旦一個詞在文檔中出現過,再次出現時增加的相關性可能會降低。BM25通過設置一個飽和點來解決這個問題,超過這個點,詞的權重增加就會變得不那么敏感。
總結而言,BM25是TF/IDF的改進版,通過文檔長度歸一化和頻率飽和度控制來優化搜索結果。
3.源數據:source
_source字段包含索引時原始的JSON文檔內容,字段本身不建立索引(因此無法進行搜索),但是會被存儲,所以當執行獲取請求是可以返回_source字段。
雖然很方便,但是_source字段的確會對索引產生存儲開銷,你可以通過關閉_source字段來節省空間,但這通常不建議,因為有了原始數據,我們可以對數據進行重新索引,并且在獲取數據時也更加靈活。
如果你禁用了_source字段,那么會有以下幾個影響:
- 無法獲取原始數據:當你查詢某個文檔時,你將無法獲取到原始的_source字段內容,因為它沒有被存儲在Elasticsearch中。
- 更新和重新索引的問題:如果你想更新文檔或者執行重新索引操作,可能會遇到問題,因為這兩種操作都需要原始的_source字段。
- 腳本字段和某些Aggregations可能受到影響:如果你正在使用腳本字段或者依賴_source字段的Aggregations,那么禁用_source可能導致這些特性出問題。
下面是一些使用_source字段的例子:
(1) 在索引文檔時啟用/禁用_source:
PUT my_index
{
"mappings": {
"_source": {
"enabled": false
},
"properties": {
"field1": { "type": "text" }
}
}
}
在這個例子中,新創建的my_index索引將不會存儲_source字段。
(2) 獲取文檔的_source字段:
GET /my_index/_doc/1
返回的結果中會包含_source字段。
(3) 在獲取文檔時只獲取_source字段中特定的字段:
GET /my_index/_doc/1?_source=field1,field2
在這個例子中,返回的_source字段只包含field1和field2。
注意:_source字段并不用于搜索,禁用_source字段不會影響你的搜索結果。
二、源數據過濾
假設你的應用只需要獲取部分字段(如"name"和"price"),而其他字段(如"desc"和"tags")不經常使用或者數據量較大,導致傳輸和處理這些額外的數據會增加網絡開銷和處理時間。在這種情況下,通過設置includes和excludes可以有效地減少每次請求返回的數據量,提高效率。
例如:
PUT product
{
"mappings": {
"_source": {
"includes": ["name", "price"],
"excludes": ["desc", "tags"]
}
}
}
- Including:結果中返回哪些field。
- Excluding:結果中不要返回哪些field,Excluding優先級比Including更高。
需要注意的是,盡管這些設置會影響搜索結果中_source字段的內容,但并不會改變實際存儲在Elasticsearch中的數據。也就是說,"desc"和"tags"字段仍然會被索引和存儲,只是在獲取源數據時不會被返回。
上述這種在mapping中定義的方式不推薦,因為mapping不可變。我們可以在查詢過程中指定返回的字段,如下:
GET product/_search
{
"_source": {
"includes": ["owner.*", "name"],
"excludes": ["name", "desc", "price"]
},
"query": {
"match_all": {}
}
}
Elasticsearch的_source字段在查詢時支持使用通配符(wildcards)來包含或排除特定字段。使得能夠更靈活地操縱返回的數據。
關于規則,可以參考以下幾點:
- *:匹配任意字符序列,包括空序列。
- ?:匹配任意單個字符。
- [abc]: 匹配方括號內列出的任意單個字符。例如,[abc]將匹配"a", "b", 或 "c"。
請注意,通配符表達式可能會導致查詢性能下降,特別是在大型索引中,因此應謹慎使用。
三、全文檢索
全文檢索是Elasticsearch的核心功能之一,它可以高效地在大量文本數據中尋找特定關鍵詞。
在Elasticsearch中,全文檢索主要依靠兩個步驟:"分析"(Analysis)和"查詢"(Search)。
(1) 分析: 當你向Elasticsearch插入一個文檔時,會進行"分析"處理,將原始文本數據轉換成稱為"tokens"或"terms"的小片段。這個過程可能包括如下操作:
- 切分文本(Tokenization)
- 將所有字符轉換為小寫(Lowercasing)
- 刪除常見但無重要含義的單詞(Stopwords)
- 提取詞根(Stemming)
(2) 查詢:當執行全文搜索時,查詢字符串也會經過類似的分析過程,然后再與已經分析過的數據進行比對,找出匹配的結果并返回。
Elasticsearch提供了許多種全文搜索的查詢類型,例如:
- Match Query:最基本的全文搜索查詢。
- Match Phrase Query:用于查找包含特定短語的文檔。
- Multi-Match Query:類似Match Query,但可以在多個字段上進行搜索。
- Query String Query:提供了豐富的搜索語法,可以執行復雜的、靈活的全文搜索。
1.match:匹配包含某個term的子句
match 查詢是 Elasticsearch 中的一種全文查詢方式,它包括標準分析和詞項搜索。盡管它可以應用于精確字段,但其主要用途是進行全文搜索。當與全文字段一起使用時,match 查詢可以解析查詢字符串,并執行短語查詢或者構建一個布爾查詢,這意味著它會考慮字段中的每個單詞。
下面有一個簡單的 match 查詢示例:
GET /_search
{
"query": {
"match": {
"message": "this is a test"
}
}
}
在這個示例中,Elasticsearch 會在 "message" 字段中搜索包含 "this"、"is"、"a" 和 "test" 的文檔。
請注意,match 查詢不僅僅會匹配完全相同的短語,它還可以處理更復雜的情況,如多個單詞(它會匹配任何一個)、誤拼、同義詞等,這主要取決于你所使用的分析器和搜索設置。
match 查詢還有一些其他參數,例如:
- operator:定義多個搜索詞之間的關系,默認為 or。如果設為 and,則返回的文檔必須包含所有搜索詞。
- minimum_should_match:控制返回的文檔應至少匹配的搜索詞的數量或比例。
- fuzziness:允許模糊匹配,可以找到那些拼寫錯誤或接近的詞匯。
2.match_all:匹配所有結果的子句
match_all是Elasticsearch中的一個查詢類型,用于獲取索引中的所有文檔。
這是一個match_all查詢的基本示例:
{
"query": {
"match_all": {}
}
}
在上述示例中,我們可以看到查詢對象中存在一個"match_all"字段,其值是一個空對象。這表示我們希望匹配所有文檔。
需要注意,由于 match_all 查詢可能返回大量的數據,所以一般在使用時都會與分頁(pagination)功能結合起來,這樣可以控制返回結果的數量,避免一次性加載過多數據導致的性能問題。例如,你可以使用 from 和 size 參數來限制返回結果:
GET /_search
{
"query": {
"match_all": {}
},
"from": 10,
"size": 10
}
Elasticsearch的 match_all 查詢是最簡單的查詢,它不需要任何參數,但如果你想為它添加權重,可以使用 boost 參數。例如:
GET /_search
{
"query": {
"match_all": { "boost" : 1.2 }
}
}
在上面的查詢中,boost 參數被設置為1.2,給匹配到的所有文檔增加了額外的相關性得分提升。
3.multi_match:多字段條件
multi_match 可以用來在多個字段上進行全文搜索。它接受一個查詢字符串和一組需要在其中執行查詢的字段列表。
例如:
{
"query": {
"multi_match" : {
"query": "這是測試",
"fields": [ "field1", "field2" ]
}
}
}
在此示例中,查詢字符串"這是測試"將在字段"field1"和"field2"中搜索。
multi_match查詢也支持使用通配符(*)來匹配多個字段:
{
"query": {
"multi_match" : {
"query": "這是測試",
"fields": [ "*_name" ]
}
}
}
在這個例子中,會在所有以"_name"結尾的字段中進行搜索。
此外,multi_match 查詢還支持許多參數,包括:
- type:設置查詢類型,可選值包括:best_fields, most_fields, cross_fields, phrase, phrase_prefix 等。
例如,“best_fields” 類型會從指定的字段中挑選分數最高的匹配結果計算最終得分,而“most_fields” 類型則會在每個字段中都尋找匹配項并將其分數累加起來。
- tie_breaker:當一個詞在多個字段中找到時,用于決定最終得分的參數。
- minimum_should_match:用于控制應匹配的最小子句數。
- operator:主要有兩個操作符 OR 和 AND,默認為 OR。
需要注意的是,當使用 multi_match 查詢時,如果字段不同,其權重可能也會不同。你可以通過在字段名后面添加尖括號(^)和權重值來調整特定字段的權重。例如,"fields": [ "name^3", "description" ]表示在"name"字段中的匹配結果權重是"description"字段的三倍。
4.match_phrase:短語查詢
match_phrase 用于精確匹配包含指定短語的文檔。match_phrase 查詢需要字段值中的單詞順序與查詢字符串中的單詞順序完全一致。
例如:
GET /_search
{
"query": {
"match_phrase": {
"message": "this is a test"
}
}
}
這個查詢將會找到"message"字段中包含完整短語"this is a test"的所有文檔。
此外,match_phrase 查詢還有一個 slop 參數,可以定義詞組中的詞語可能存在的位置偏移量。例如,如果將 slop 設置為 1,則查詢 "this is a test" 也可匹配 "this is test a",因為 "a" 和 "test" 只需移動一個位置即可匹配。
GET /_search
{
"query": {
"match_phrase": {
"query": "this is a test",
"slop": 2
}
}
}
請注意,match_phrase 查詢需要整個短語完全匹配,而不僅僅是查詢中的所有單詞都存在。如果你只是希望所有單詞都存在,而不關心它們的順序或精確出現方式,那么你應該使用 match 查詢。
四、Term Query
精確查詢用于查找包含指定精確值的文檔,而不是執行全文搜索。
1.term:匹配和搜索詞項完全相等的結果
term 查詢主要用于查詢某個字段完全匹配給定值的文檔。這對精確匹配非常有效,例如數字、布爾值或者字符串。
用法示例:
GET /_search
{
"query": {
"term" : { "user" : "Kimchy" }
}
}
在這個例子中,我們正在搜索"user"字段中完全匹配"Kimchy"的文檔。
需要注意的是,term 查詢對于分析過的字段(例如,文本字段)可能不會像你預期的那樣工作,因為它會搜索精確的詞匯項,而不是單詞。如果你想要對文本字段進行全文搜素,應該使用 match 查詢。
另外一個需要注意的點就是 term 查詢對大小寫敏感,所以 "Kimchy" 和 "kimchy" 是兩個不同的詞條。
2.term和match_phrase的區別
term 查詢和 match_phrase 查詢是 Elasticsearch 提供的兩種查詢方式,它們都用于查找文檔,但主要的區別在于如何解析查詢字符串以及匹配的精確度。
- term:這個查詢做的是精確匹配。當你使用term查詢時,Elasticsearch會查找完全等于你指定的詞匯的文檔。例如,如果你搜索term "apple",那么只有包含完全為"apple"的文檔會被匹配到,而包含"apples"或"APPLE"的文檔則不會被匹配到。因此,term查詢對大小寫敏感,且不會進行任何形式的分析(如停用詞移除、詞干提取等)。
- match_phrase:這個查詢是用來匹配一系列詞匯或者短語的。match_phrase查詢會保證你查詢的詞匯必須以你提供的順序完全匹配。比如,如果你使用match_phrase查詢 "quick brown fox",那么只有包含這個完整短語的文檔才會被匹配到,單獨包含"quick"、"brown"或者"fox"的文檔則不會被匹配到。此外,與term查詢不同,match_phrase查詢會進行文本分析,這意味著它會考慮詞匯的大小寫、復數形式等。
總結來說,term查詢更適合精確匹配,而match_phrase查詢更適合短語匹配。但是,match_phrase并不能100%保證精確匹配,因為它會處理和考慮文本的各種變體(比如,大小寫、單復數形式等)。
3.terms:匹配和搜索詞項列表中任意項匹配的結果
terms 查詢用于匹配指定字段中包含一個或多個值的文檔。這是一個精確匹配查詢,不會像全文查詢那樣對查詢字符串進行分析。
假設你有一個 "user" 的字段,并且你想找到該字段值為 "John" 或者 "Jane" 的所有文檔,你可以使用 terms 查詢:
GET /_search
{
"query": {
"terms" : {
"user" : ["John", "Jane"],
"boost" : 1.0
}
}
}
上面的查詢將返回所有"user" 字段等于 "John" 或者 "Jane" 的文檔。
其中boost 參數用于增加或減少特定查詢的相對權重。它將改變查詢結果的相關性分數(_score),以影響最終結果的排名。
例如,在上述 terms 查詢中,boost 參數被設置為 1.0。這意味著如果字段 "user" 的值包含 "John" 或 "Jane",那么其相關性分數(_score)就會乘以 1.0。因此,這個設置實際上并沒有改變任何東西,因為乘以 1 不會改變原始分數。但是,如果你將 boost 參數設置為大于 1 的數,那么匹配的文檔的 _score 將會提高,反之則會降低。
五、Range:范圍查找
Range查詢允許我們查找某個范圍內的值。假設我們有一個商品表,其中有商品價格字段,我們可以用range查詢來查找價格在一定范圍內的商品。
以下是一個基礎的范圍查詢的例子:
GET /products/_search
{
"query": {
"range" : {
"price" : {
"gte" : 10,
"lte" : 20,
"boost" : 2.0
}
}
}
}
在這個例子中,我們正在查詢價格大于或等于(gte)10且小于或等于(lte)20的所有商品。"boost"參數表示增加該查詢的重要性。
Range查詢支持以下參數:
- gte:大于或等于。
- lte:小于或等于。
- gt:大于。
- lt:小于。
- boost:增加查詢的重要性。
此外,對于日期類型的字段,你還可以使用如下方式進行范圍查詢:
{
"query": {
"range" : {
"timestamp" : {
"gte" : "now-1d/d",
"lt" : "now/d"
}
}
}
}
在上述查詢中,我們正在查找過去24小時內的數據。"now-1d/d"表示從現在算起的一天前,而"now/d"表示當前時間。
六、Filter
過濾器(Filter)是用于篩選數據的一種工具。過濾器和查詢(query)相似,但有幾個重要的區別:
- 過濾不關心文檔的相關度得分(relevance score):查詢會為每個匹配的文檔計算一個相關度得分,以決定返回結果的排序。相比之下,過濾器只關心文檔是否匹配 - 沒有“部分匹配”,只有“匹配”或“不匹配”。
- 過濾器可以被緩存:由于過濾器不需要計算得分,因此它們的結果可以被緩存起來用于之后的搜索請求,這可以大大提高性能。
常見的過濾器類型包括:term、terms、range、bool、match_all 等。例如,范圍過濾器 range 可以用于查找數字或日期字段在指定范圍內的文檔;布爾過濾器 bool 則允許你組合多個過濾器,并定義它們如何互相交互。
使用過濾器時,通常會把它們放在 bool 查詢的 filter 子句中。例如:
{
"query": {
"bool": {
"filter": [
{ "term": { "status": "active" }},
{ "range": { "age": { "gte": 30, "lte": 40 }}}
]
}
}
}
這個查詢會返回所有“狀態為 active 并且年齡在 30 到 40 之間”的文檔,而不會考慮它們的相關度得分。
Filter緩存機制
在 Elasticsearch 中,過濾查詢結果的緩存機制是非常重要的一個性能優化手段。由于過濾器(filter)只關心是否匹配,而不關心評分 (_score),因此它們的結果可以被緩存以提高性能。
每次 filter 查詢執行時,Elasticsearch 都會生成一個名為 "bitset" 的數據結構,其中每個文檔都對應一個位(0 或 1),表示這個文檔是否與 filter 匹配。這個 bitset 就是被存儲在緩存中的部分。
如果相同的 filter 查詢再次執行,Elasticsearch 可以直接從緩存中獲取這個 bitset,而不需要再次遍歷所有的文檔來找出哪些文檔符合這個 filter。這大大提高了查詢速度,并減少了 CPU 使用。
這種緩存策略特別適合那些重復查詢的場景,例如用戶界面的過濾器和類似的功能,因為他們通常會產生很多相同的 filter 查詢。
然而,值得注意的是,雖然這種緩存可以顯著改善查詢性能,但也會占用內存空間。如果你有很多唯一的過濾條件,那么過濾器緩存可能會變得很大,從而導致內存問題。這就需要你對使用的過濾器進行適當的管理和限制。
Filter緩存功能會遵循以下原則:
- 同一Filter的多次應用:如果在后續查詢中有多次使用相同的Filter,則ES會把第一次查詢的結果儲存在緩存中,后續的查詢將直接從緩存中獲取結果,而不再做任何磁盤I/O或者其他計算。
- 根據需求清理緩存:ES會根據內存使用情況自動清理緩存,當然你也可以手動清空緩存。但這并不意味著我們無限制地依賴Filter緩存,大量的緩存可能導致更重的GC壓力。
- 不緩存復雜查詢:一些查詢條件較復雜的過濾器可能不會被緩存,比如script filter、geo filter等。這是因為這些過濾器本身的構建和維護成本可能就超過了查詢的計算成本。
ES的Filter緩存機制可以大大提高查詢效率,但如果不慎用,比如緩存過多或者不適合緩存的查詢,可能會對性能產生負面影響。因此,在設計和優化ES查詢時,應當充分考慮Filter的使用和緩存策略。
七、Bool Query
Bool Query(組合查詢)可以組合多個查詢條件,bool查詢也是采用more_matches_is_better的機制,因此滿足must和should子句的文檔將會合并起來計算分值。
boost和minumum_should_match是參數,其他四個都是查詢子句。
- must:必須滿足子句(查詢)必須出現在匹配的文檔中,并將有助于得分。
- filter:過濾器不計算相關度分數。
- should:滿足 or子句(查詢)應出現在匹配的文檔中。
- must_not:必須不滿足,不計算相關度分數 ,not子句(查詢)不得出現在匹配的文檔中。子句在過濾器上下文中執行,這意味著計分被忽略,并且子句被視為用于緩存。
例子1:下面的語句表示:包含"xiaomi"或"phone" 并且包含"shouji"的文檔例子:
GET product/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "xiaomi phone"
}
},
{
"match_phrase": {
"desc": "shouji"
}
}
]
}
}
}
1.should與must或filter一起使用
當 should 子句與 must 或 filter 子句一起使用時,這時候需要注意了。
只要滿足了 must 或 filter 的條件,should 子句就不再是必須的。換句話說,如果存在一個或者多個 must 或 filter 子句,那么 should 子句的條件會被視為可選。
然而,如果 should 子句與 must_not 子句單獨使用(也就是沒有 must 或 filter),則至少需要滿足一個 should 子句的條件。
這里有一個例子來說明:
GET /_search
{
"query": {
"bool": {
"must": [
{ "term": { "user": "kimchy" }}
],
"filter": [
{ "term": { "tag": "tech" }}
],
"should": [
{ "term": { "tag": "wow" }},
{ "term": { "tag": "elasticsearch" }}
]
}
}
}
在這個查詢中,must 和 filter 子句的條件是必須滿足的,而 should 子句的條件則是可選的。如果匹配的文檔同時滿足 should 子句的條件,那么它們的得分將會更高。
那如果我們一起使用的時候想讓should滿足該怎么辦?這時候minimum_should_match 參數就派上用場了。
2.minimum_should_match
minimum_should_match參數定義了在 should 子句中至少需要滿足多少條件。
例如,如果你有5個 should 子句并且設置了 "minimum_should_match": 3,那么任何匹配至少三個 should 子句的文檔都會被返回。
這個參數可以接收絕對數值(如 2)、百分比(如 30%)、和組合(如 3<90% 表示至少匹配3個或者90%,取其中較大的那個)等不同類型的值。
注意:如果 bool 查詢中只有 should 子句(沒有 must 或 filter),那么默認情況下至少需要匹配一個 should 條件,也就是minimum_should_match默認值是1,除非 minimum_should_match 明確設定為其他值。如果包含 must 或 filter的情況下minimum_should_match默認值 0。
所以我們可以在包含must 或 filter的情況下,設置minimum_should_match值來滿足should子句中的條件。