天天看點

一文帶你徹底搞懂Elasticsearch中的模糊查詢

寫在前面

Elasticsearch(以下簡稱ES)中的模糊查詢官方是建議慎用的,因為的它的性能不是特别好。不過這個性能不好是相對ES自身的其它查詢(term,match)而言的,如果跟其它的搜尋工具相比ES的模糊查詢性能還是不錯的。

ES都多種方法可以支援模糊查詢,比如wildcard,query_string等,這篇文章可能是全網最全的關于模糊查詢的技術部落格(哈哈)。

可以支援模糊查詢的方案

wildcard

wildcard的用法是這樣的,

GET kibana_sample_data_flights/_search
{
  "query": {
    "wildcard": {
      "OriginCityName": {
        "value": "Frankfurt*"
      }
    }
  }
}
           

複制

*或者?也可以放在前面,但是不建議這麼做,最好是字首開始避免太大的性能消耗。查詢的字段可以是text類型也可以是keyword類型,兩種都支援。

大小寫的話預設情況下,是根據字段本身是否對大小寫敏感決定的。什麼意思呢?比如上面那個查詢,OriginCityName字段是keyword類型,我們知道keyword是要求精确比對,自然就是大小寫敏感的。是以如果用下面的這個查詢,結果是不一樣的:

GET kibana_sample_data_flights/_search
{
  "query": {
    "wildcard": {
      "OriginCityName": {
        "value": "frankfurt*"
      }
    }
  }
}
           

複制

如果查詢的字段是text類型,wildcard模糊查詢的時候就是大小寫不敏感的。

前面說過,模糊查詢的性能都不高,wildcard也不例外。不過在ES7.9中引入了一種新的

wildcard

字段類型,該字段類型經過優化,可在字元串值中快速查找模式。

PUT my-index
{
  "mappings": {
    "properties": {
      "my_wildcard": {
        "type": "wildcard"
      }
    }
  }
}
           

複制

在引入這個字段類型之前,wildcard要麼是在text類型字段查找,要麼是keyword類型。而

wildcard

類型做了特殊的處理,如果某個字段指定了wildcard類型,

  • 與 text 字段不同,它不會将字元串視為由标點符号分隔的單詞的集合。
  • 與 keyword 字段不同,它可以快速地搜尋許多唯一值,并且沒有大小限制。

wildcard字段類型通過兩種優化的資料結構提高模糊查詢的性能,一種使用

n-gram

分詞器,這個分詞器不打算在這裡詳細講,隻需要知道它會把單詞在繼續細分存儲就行,比如,

POST _analyze
{
  "tokenizer": "ngram",
  "text": "Quick Fox"
}
           

複制

輸出的是,

[ Q, Qu, u, ui, i, ic, c, ck, k, "k ", " ", " F", F, Fo, o, ox, x ]
           

複制

相當于把可能用于模糊查詢的詞項都提前拆分好存儲了,這樣就減少了查詢階段需要比較的詞項。

第二種資料結構是

binary doc value

,可以自動查詢驗證由 n-gram 文法比對産生的比對候選,關于它的具體介紹可以參考下面這篇文章:

https://www.amazingkoala.com.cn/Lucene/DocValues/2019/0412/49.html

fuzzy

fuzzy也是一種模糊查詢,我了解它其實屬于比較輕量級别的模糊查詢。fuzzy中有個編輯距離的概念,編輯距離是對兩個字元串差異長度的量化,及一個字元至少需要處理多少次才能變成另一個字元,比如lucene和lucece隻差了一個字元他們的編輯距離是1。

因為可以限制編輯距離,它的性能相對會好一些,畢竟它不是完全的“模糊”。

這樣說可能有點抽象,看個例子,

先寫入一些測試資料,

POST /my_index/_bulk
{ "index": { "_id": 1 }}
{ "text": "Surprise me!"}
{ "index": { "_id": 2 }}
{ "text": "That was surprising."}
{ "index": { "_id": 3 }}
{ "text": "I wasn't surprised."}
           

複制

然後我們可以這樣查詢,

GET /my_index/_search
{
  "query": {
    "fuzzy": {
      "text": "surprize"
    }
  }
}
           

複制

查詢結果是文檔1和文檔3會被查詢出來,surprise 比較 surprise 和 surprised 都在編輯距離 2 以内。為什麼預設值2呢,其實fuzzy有個

fuzziness

參數,可以指派為0,1,2和AUTO,預設其實是AUTO。

AUTO的意思是,根據查詢的字元串長度決定允許的編輯距離,規則是:

  • 0..2 完全比對(就是不允許模糊)
  • 3..5 編輯距離是1
  • 大于5 編輯距離是2

其實我們仔細想一下,即使限制了編輯距離,查詢的字元串比較長的情況下需要查詢的詞項也是非常巨大的。是以fuzzy還有一個選項是prefix_length,表示不能被 “模糊化” 的初始字元數,通過限制字首的字元數量可以顯著降低比對的詞項數量。

query string

query string query是ES的一種進階搜尋,它支援複雜的搜尋方式比如操作符,可以用類似

"query": "this AND that"
           

複制

這樣的組合操作文法。

query string支援wildcard,并且查詢的字段名和查詢字元串都可以使用wildcard,比如:

GET /_search
{
  "query": {
    "query_string" : {
      "fields" : ["city.*"],
      "query" : "this AND that OR thus"
    }
  }
}'
           

複制

GET /_search
{
  "query": {
    "query_string" : {
      "query" : "city.\\*:(this AND that OR thus)"
    }
  }
}
           

複制

是以query string對模糊搜尋的支援本質上還是wildcard。

prefix 字首查詢

這種隻支援字首查詢,屬于模糊查詢的子集。比如要查找所有以 W1 開始的郵編,可以使用簡單的 prefix 查詢。

GET /my_index/_search
{
    "query": {
        "prefix": {
            "postcode": "W1"
        }
    }
}
           

複制

prefix的工作原理這裡也簡單說下。我們知道文檔在寫入ES時會建立反向索引,反向索引都會将包含詞的文檔 ID 列入 倒排表(postings list),下面是一個示例:

Term Doc IDs
"SW5 0BE" 5
"W1F 7HW" 3
"W1V 3DG" 1
"W2F 8HW" 2
"WC1N 1LZ" 4

查詢的步驟是:

  1. 掃描postings list并查找到第一個以 W1 開始的詞。
  2. 搜集關聯的文檔 ID 。
  3. 移動到下一個詞。如果這個詞也是以 W1 開頭,查詢跳回到第二步再重複執行,直到下一個詞不以 W1 為止。

可以看到,如果倒排表比較大,滿足字首的詞項比較多的情況下,查詢的代價也是非常大的。不過對于字首查詢ES提供了一種名叫

index_prefixes

的機制來提高查詢性能。

原理也比較簡單,就是字段在mapping中指定

index_prefixes

,然後ES在索引的時候就會把指定範圍的字首都先存起來,這樣查詢的時候需要比較的次數就會大大降低。

PUT my-index-000001
{
  "mappings": {
    "properties": {
      "body_text": {
        "type": "text",
        "index_prefixes": { }    
      }
    }
  }
}
           

複制

regexp正規表達式模糊查詢

regexp對模糊查詢的支援更智能,它能支援更為複雜的比對模式。比如下面這個示例

GET /my_index/_search
{
    "query": {
        "regexp": {
            "postcode": "W[0-9].+" 
        }
    }
}
           

複制

這個正規表達式要求詞必須以 W 開頭,緊跟 0 至 9 之間的任何一個數字,然後接一或多個其他字元。

regexp 查詢的工作方式與 prefix 查詢基本是一樣的,需要掃描反向索引中的詞清單才能找到所有比對的詞,然後依次擷取每個詞相關的文檔 ID。

參考:

  • https://www.elastic.co/guide/en/elasticsearch/reference/7.11/index.html
  • https://www.elastic.co/cn/blog/find-strings-within-strings-faster-with-the-new-elasticsearch-wildcard-field