Elasticsearch搜尋中的部分匹配
本文絕大部分內容來自http://www.cnblogs.com/richaaaard/p/5254285.html,其中部分內容做了新增和修改。
摘要
到目前為止,我們介紹的所有查詢都是基於完整術語的,為了匹配,最小的單元為單個術語,我們只能查詢反向索引中存在的術語。但是,如果我們想匹配部分術語而不是全部改怎麼辦?部分匹配(Partial matching)允許使用者指定查詢術語的一部分,然後找出所有包含這部分片段的詞。與我們想象的不一樣,需要對術語進行部分匹配的需求在全文搜尋引擎的世界並不常見,但是如果讀者有SQL方面的背景,可能會在某個時候使用下面的SQL語句對全文進行搜尋:WHERE text LIKE "%quick%"
AND text LIKE "%brown%"
AND text LIKE "%fox%"
注:
當然,在ElasticSearch中,我們有分析過程,反向索引讓我們不需要使用這種蠻力。為了解決同時匹配“fox”和“foxes”的應用場景,我們只需要簡單的將它們的詞幹作為索引形式,不需要做部分匹配。有人說,在某些情況下部分匹配會比較有用,這些應用場景如下:
- 匹配郵編,產品序列號,或其他未分析(not_analyzed)值,它們可以是以某個特定字首開始,可以是與某種模式匹配,也可以是與某個正則式相匹配。
- 輸入即查詢——在使用者輸入搜尋術語的時候就向用戶呈現最可能的結果。
- 匹配如德語或荷蘭語這樣的語言,他們有很長的組合詞,如:Weltgesundheitsorganisation (World Health Organization) 世界衛生組織。
我們的介紹會始於一個not_analyzed準確值欄位的字首匹配。
elasticsearch版本: elasticsearch-2.x
內容
郵編與結構化資料(Postcodes and Structured Data)
我們會使用美國的郵編(UK postcodes)來說明如何用部分匹配查詢結構化資料。這種格式的郵編有著良好的結構定義。例如,郵編W1V 3DG可以分解為:
- W1V:這是郵編的外部,它定義了郵件的區域和行政區:
W 代表區域(1或2個字母)
1V 代表行政區(1或2個數字,可能跟著一個字元)
- 3DG:內部定義了街道或建築:
3 代表街區區塊(1個數字)
DG 代表單元(2個字母)
我們假設將郵編作為not_analyzed的準確值欄位索引,我們可以為其建立索引:
curl -XPUT 192.168.205.153:9200/my_index -d '
{
"mappings": {
"address": {
"properties": {
"postcode": {
"type": "string",
"index": "not_analyzed"
}
}
}
}
}'
然後新增一些郵編:
curl -XPUT 192.168.205.153:9200/my_index/address/1 -d '{ "postcode": "W1V 3DG" }'
curl -XPUT 192.168.205.153:9200/my_index/address/2 -d '{ "postcode": "W2F 8HW" }'
curl -XPUT 192.168.205.153:9200/my_index/address/3 -d '{ "postcode": "W1F 7HW" }'
curl -XPUT 192.168.205.153:9200/my_index/address/4 -d '{ "postcode": "WC1N 1LZ" }'
curl -XPUT 192.168.205.153:9200/my_index/address/5 -d '{ "postcode": "SW5 0BE" }'
現在這些資料可供查詢了。
字首查詢(prefix Query)
為了找到所有以 W1 開始的郵編,我們可以使用簡單的字首查詢:
curl -XGET 192.168.205.153:9200/my_index/address/_search?pretty -d '
{
"query": {
"prefix": {
"postcode": "W1"
}
}
}'
字首查詢是一個術語級別的低層次的查詢,在搜尋之前它不會分析查詢字串,它認為傳入的字首就是想要查詢的字首。
預設狀態下,字首查詢不做相關度分數計算,它只是將所有匹配的文件返回,然後賦予所有相關分數值為1。它的行為更像是一個過濾器而不是查詢。兩者實際的區別就是過濾器是可以被快取的,而字首查詢不行。
之前我們提到過:“我們只能找到反向索引中存在的術語”,但是我們並沒有對這些郵編的索引做特殊處理,每個郵編還是以它們準確值的方式存在於每個文件的索引中,那麼字首查詢是如何工作的呢?
回想反向索引包含了一個有序的唯一的術語列表(這個例子中是郵編),對於每個術語,反向索引都會將包含術語的文件ID列入 關聯列表(postings list)。與我們例子對於的反向索引是:
Term: Doc IDs:
-------------------------
"SW5 0BE" | 5
"W1F 7HW" | 3
"W1V 3DG" | 1
"W2F 8HW" | 2
"WC1N 1LZ" | 4
-------------------------
為了支援字首匹配,查詢會做以下事情:- 掃描術語列表並查詢到第一個以W1開始的術語。
- 蒐集關聯的ID
- 移動到下一個術語
- 如果這個術語也是以 W1 開頭,查詢跳回到第二步再重複執行,直到下一個術語不以W1為止。
這對於小的例子當然可以正常工作,但是如果我們的反向索引中有數以百萬的郵編都是以W1開頭時,字首查詢則需要訪問每個術語然後計算結果。
字首越短需要訪問的術語越多,如果我們要以 W 作為字首而不是 W1,那麼就可能需要做千萬次的匹配。
字首查詢或過濾對於一些特定的匹配非常有效,但是使用時還是需要當心,當欄位的術語集合很小時,我們可以放心使用,但是它的伸縮性並不好,會對我們的叢集帶來很多壓力。可以使用較長的字首來減小這種壓力,這樣可以大大減少需要訪問的術語數量。
後面我們會介紹一個索引時的解決方案讓字首匹配更高效,不過在此之前,我們需要先看看兩個相關的查詢:模糊(wildcard)和正則(regexp)查詢。
模糊查詢與正則式查詢(wildcard and regexp Queries)
與字首查詢的特性類似,模糊(wildcard)查詢也是一種低層次基於術語的查詢,與字首查詢不同的是它可以讓我們給出匹配的正則式。它使用標準的shell模糊查詢:?匹配任意字元,*匹配0個或多個字元。
這個查詢會匹配包含W1F 7HW和W2F 8HW的文件:
curl -XGET 192.168.205.153:9200/my_index/address/_search?pretty -d '
{
"query": {
"wildcard": {
"postcode": "W?F*HW"
}
}
}'
注:?匹配1和2,*與空格以及7和8匹配。
如果現在我們只想匹配W區域的所有郵編,字首匹配也會匹配以WC為開始的所有郵編,與模糊匹配碰到的問題類似,如果我們只想匹配以 W 開始並跟著一個數字的所有郵編,正則式(regexp)查詢讓我們能寫出這樣更復雜的模式:
curl -XGET 192.168.205.153:9200/my_index/address/_search?pretty -d '
{
"query": {
"regexp": {
"postcode": "W[0-9].+"
}
}
}'
注:這個正則表示式要求術語必須以W開頭,緊跟0至9之間的任何一個數字,然後跟著1或多個其他字元。
模糊和正則查詢與字首查詢的工作方式一樣,他們也需要掃描反向索引中的術語列表才能找到所有的匹配術語,然後依次獲得每個術語相關的文件ID,它與字首查詢唯一的不同是:它能支援更為複雜的匹配模式。
這也意味著我們需要注意與字首查詢中相同的效能問題,執行這些查詢可能會消耗非常多的資源,所以我們需要避免使用左模糊這樣的模式匹配(如,*foo或.foo*這樣的正則式)
依靠資料在索引時的準備可以幫助我們提高字首匹配的效率,但是隻能在查詢時處理模糊匹配和正則表示式查詢,雖然這些查詢有他們的應用場景,但仍需謹慎使用。
注意
prefix、wildcard和regrep查詢是基於術語操作的,如果我們用它們來查詢分析過的欄位(analyzed field),他們會檢查欄位裡面的每個術語,而不是將欄位作為整體進行處理。
例如,我們說title包含“Quick brown fox”會生成術語:quick、brown和fox。
下面這個查詢會匹配:{ "regexp": { "title": "br.*" }}
但是下面這兩個查詢都不會匹配:
{ "regexp": { "title": "Qu.*" }}#在術語表中存在的是quick而不是Quick
{ "regexp": { "title": "quick br*" }}#quick和brown在術語表中是分開的。
查詢時輸入即搜尋(Query-Time Search-as-You-Type)
先把郵編的事情放一邊,讓我們先看看字首查詢是如何在全文查詢中起作用的。使用者已經漸漸習慣在他們輸入查詢內容的時候,搜尋的結果就能展現在他們面前,這就是所謂的即時搜尋(instant search)或輸入即搜尋(search-as-you-type),不僅使用者能在更短的時間內得到搜素結果,我們也能引導使用者搜尋我們索引中存在的結果。
比如,如果使用者輸入johnnie walker pl,我們希望在使用者結束搜尋輸入前就能得到結果:Johnnie Walker Black Label和Johnnie Walker Blue Label。
就像貓的花色遠不只一種,我們希望找到一個最簡單的實現方式。我們並不需要對資料做任何準備,我們在查詢時,就能對任意的全文欄位實現search-as-you-type的查詢。
在短語匹配(Phrase Matching)中,我們引入了match_phrase查詢,它匹配相對順序一致的所有文件,查詢時的輸入即搜尋(search-as-you-type),我們可以使用match_phrase的一種特殊形式,即match_phrase_prefix查詢:
{
"match_phrase_prefix" : {
"brand" : "johnnie walker bl"
}
}
這種查詢的行為與match_phrase查詢一致,不同的是它將查詢字串的最後一個詞作為字首使用,換句話說,可以把前面的例子看成:
- johnnie
- 跟著walker
- 跟著一個以bl開始的詞
如果我們通過 validate-query 查詢,explaination的結果為:"johnnie walker bl*"
[[email protected] ~]$ curl -XGET '192.168.205.153:9200/hui/my_type/_validate/query?explain&pretty' -d '
> {
> "query": {
> "match_phrase_prefix": {
> "brand": "johnnie walker bl*"
> }
> }
> }'
{
"valid" : true,
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"explanations" : [ {
"index" : "hui",
"valid" : true,
"explanation" : "+brand:\"johnnie walker bl*\" #ConstantScore(+_type:my_type)"
} ]
}
與 match_phrase 一樣,它也可以接受 slop 引數讓相對詞序位置不那麼嚴格:
{
"match_phrase_prefix" : {
"brand" : {
"query": "walker johnnie bl",
"slop": 10
}
}
}
注:儘管詞語的順序不正確,查詢仍然能夠匹配,因為我們為它設定了較高的slop值使匹配時的詞序有更大的靈活性。但是隻有最後一個詞才能作為字首。
在之前的字首查詢(prefix Query)中,我們提示過字首使用的風險,即字首查詢對於資源的消耗嚴重,這種方式(match_phrase_prefix)同樣如此。一個以a開頭的字首可能會匹配成千上萬的術語,這不僅會消耗很多系統資源,而且結果對使用者也用處不大。
我們可以通過設定 max_expansions 引數來限制字首擴充套件的影響,一個合理的值是可能是50:
{
"match_phrase_prefix" : {
"brand" : {
"query": "johnnie walker bl",
"max_expansions": 50
}
}
}
注:引數max_expansions控制著可以與字首匹配的術語的數量,它會先查詢第一個與字首bl匹配的術語,然後依次查詢蒐集與之匹配的術語,直到沒有更多可以匹配的術語或者當數量超過max_expansions時結束。不要忘記,當用戶每多輸入一個字元的時候,這個查詢又會執行一遍,所以這個查詢需要非常快速,如果第一個結果集不是使用者想要的,他們會繼續輸入直到能搜尋出他們滿意的結果為止。
索引時優化(Index-Time Optimizations)
到目前為止,所有談到的解決方案都是在查詢時(query time)實現的。這樣做並不需要特殊的對映抑或特殊的索引模式,只是簡單的使用已經索引好的資料。
查詢時的靈活性通常會以犧牲搜尋效能為代價,有些時候將這些消耗從查詢過程中轉移出去是有其意義的。在實時的網站應用中,100毫秒可能是一個難以忍受的巨大延遲。
我們可以通過在索引時處理資料來提高搜尋的靈活性以及系統性能。為此我們仍然需要付出應有的代價:增加的索引空間與變慢的索引能力,但這與每次查詢都需要付出代價不同,在索引時付出的代價是一次性的。
使用者會感謝我們。
Ngram部分匹配(Ngrams for Partial Matching)
我們之前提到:“只能在反向索引中找到存在的術語”,儘管prefix、wildcard、以及regexp查詢告訴我們這種說法並不完全正確,但單術語查詢的確要比在術語列表中盲目挨個查詢的效率要高得多。在搜尋之前提前準備好供部分匹配的資料會提高搜尋的效能。
在索引時準備資料意味著要選擇合適的分析鏈,這裡我們使用的工具叫n-gram。可以把n-gram看成一個在詞語上移動的窗子,n代表這個“窗子”的長度,如果我們說要n-gram一個詞quick,它的結果依賴於我們對於這個長度n的選擇:
- 長度 1 (unigram): [ q, u, i, c, k ]
- 長度 2 (bigram): [ qu, ui, ic, ck ]
- 長度 3 (trigram): [ qui, uic, ick ]
- 長度 4 (four-gram): [ quic, uick ]
- 長度 5 (five-gram): [ quick ]
樸素的n-gram對於詞語內部的匹配非常有用,即我們在稍後Ngram匹配複合詞(Ngrams for Compound Words)介紹的那樣。但是,對於輸入即搜尋(search-as-you-type)這種行為,我們會使用一種特殊的n-gram叫做邊界n-grams(edge n-grams)。所謂的邊界n-gram是說它會固定詞語開始的一邊,以單詞 quick 為例,它的邊界n-gram的結果是:
- q
- qu
- qui
- quic
- quick
我們會注意到這與使用者在搜尋時輸入“quick”的次序是一致的,換句話說,這種方式正好滿足即時搜尋(instant search)。
索引時輸入即搜尋(Index-Time Search-as-You-Type)
設定索引時輸入即搜尋的第一步是需要定義好我們的分析鏈,我們在配置分析器(Configuring Analyzers)中提到過,但是這裡我們會對此再次說明。
準備索引(Preparing the Index)
第一步需要配置一個自定義的token過濾器edge_ngram,我們將其稱為autocompleted_filter :
{
"filter": {
"autocomplete_filter": {
"type": "edge_ngram",
"min_gram": 1,
"max_gram": 20
}
}
}
這個配置的意思是:對於所有這個token過濾器收到的術語,它都會為之生成一個n-gram,這個n-gram固定的最小開始的位置為1,最大長度為20。
然後我們會在一個自定義分析器autocomplete中使用到上面的這個過濾器:
{
"analyzer": {
"autocomplete": {
"type": "custom",
"tokenizer": "standard",
"filter": [
"lowercase",
"autocomplete_filter" #1
]
}
}
}
注:這裡使用的是我們自定義的邊界Ngram token過濾器。
這個分析器使用標準的標記器將字串標記為獨立的術語,並且將他們都變成小寫形式,然後為每個術語生成一個邊界Ngram。
建立索引、例項化token過濾器和分析器的完整例子如下:
curl -XPUT 192.168.205.153:9200/my_index -d '
{
"settings": {
"number_of_shards": 1,
"analysis": {
"filter": {
"autocomplete_filter": {
"type": "edge_ngram",
"min_gram": 1,
"max_gram": 20
}
},
"analyzer": {
"autocomplete": {
"type": "custom",
"tokenizer": "standard",
"filter": [
"lowercase",
"autocomplete_filter"
]
}
}
}
}
}'
我們可以拿analyze API測試這個新的分析器:
curl -XGET '192.168.205.153:9200/my_index/_analyze?analyzer=autocomplete&pretty' -d 'quick brown'
返回正確的術語如下:
- q
- qu
- qui
- quic
- quick
- b
- br
- bro
- brow
- brown
我試了下中文,卻只能將一個漢字為術語(建立索引時的引數min_gram必須是1,如果大於1則無法分析,我也沒明白是個什麼原理)
min_gram為1時:
[[email protected] ~]$ curl -XGET '192.168.205.153:9200/my_index/_analyze?analyzer=autocomplete&pretty' -d '我愛你'
{
"tokens" : [ {
"token" : "我",
"start_offset" : 0,
"end_offset" : 1,
"type" : "word",
"position" : 0
}, {
"token" : "愛",
"start_offset" : 1,
"end_offset" : 2,
"type" : "word",
"position" : 1
}, {
"token" : "你",
"start_offset" : 2,
"end_offset" : 3,
"type" : "word",
"position" : 2
} ]
}
[[email protected] ~]$ curl -XGET '192.168.205.153:9200/my_index/_analyze?analyzer=autocomplete&pretty' -d '愛你ha'
{
"tokens" : [ {
"token" : "愛",
"start_offset" : 0,
"end_offset" : 1,
"type" : "word",
"position" : 0
}, {
"token" : "你",
"start_offset" : 1,
"end_offset" : 2,
"type" : "word",
"position" : 1
}, {
"token" : "h",
"start_offset" : 2,
"end_offset" : 4,
"type" : "word",
"position" : 2
}, {
"token" : "ha",
"start_offset" : 2,
"end_offset" : 4,
"type" : "word",
"position" : 2
} ]
}
min_gram為2時:
[[email protected] ~]$ curl -XGET '192.168.205.153:9200/my_index/_analyze?analyzer=autocomplete&pretty' -d '我愛你'
{
"tokens" : [ ]
}
我們可以用update-mapping API將這個分析器應用到具體欄位:
curl -XPUT 192.168.205.153:9200/my_index/_mapping/my_type -d '
{
"my_type": {
"properties": {
"name": {
"type": "string",
"analyzer": "autocomplete"
}
}
}
}'
現在我們建立一些測試文件:
注意:是這樣寫
[[email protected] ~]$ [[email protected] ~]$ curl -XPOST '192.168.205.153:9200/my_index/my_type/_bulk?pretty' -d'
> { "index": { "_id": 1 }}
> { "name": "Brown foxes" }
> { "index": { "_id": 2 }}
> { "name": "Yellow furballs" }
> '
{
"took" : 182,
"errors" : false,
"items" : [ {
"index" : {
"_index" : "my_index",
"_type" : "my_type",
"_id" : "1",
"_version" : 1,
"_shards" : {
"total" : 2,
"successful" : 2,
"failed" : 0
},
"status" : 201
}
}, {
"index" : {
"_index" : "my_index",
"_type" : "my_type",
"_id" : "2",
"_version" : 1,
"_shards" : {
"total" : 2,
"successful" : 2,
"failed" : 0
},
"status" : 201
}
} ]
}
而不是這樣寫:
[[email protected] ~]$ curl -XPOST '192.168.205.153:9200/my_index/my_type/_bulk?pretty' -d'
> { "index": { "_id": 1 }}
> { "name": "Brown foxes" }
> { "index": { "_id": 2 }}
> { "name": "Yellow furballs" }'
{
"took" : 251,
"errors" : false,
"items" : [ {
"index" : {
"_index" : "my_index",
"_type" : "my_type",
"_id" : "1",
"_version" : 1,
"_shards" : {
"total" : 2,
"successful" : 2,
"failed" : 0
},
"status" : 201
}
} ]
}
只因'在下一行而不在}的緊後面,我也是醉了。。。
查詢欄位(Querying the Field)
如果我們使用簡單match查詢“brown fo”:
curl -XGET 192.168.205.153:9200/my_index/my_type/_search?pretty -d '
{
"query": {
"match": {
"name": "brown fo"
}
}
}'
我們可以看到兩個文件同時都能匹配,儘管Yellow furballs這個文件並不包含brown和fo:
{
"took" : 5,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"hits" : {
"total" : 2,
"max_score" : 2.2406044,
"hits" : [ {
"_index" : "my_index",
"_type" : "my_type",
"_id" : "1",
"_score" : 2.2406044,
"_source" : {
"name" : "Brown foxes"
}
}, {
"_index" : "my_index",
"_type" : "my_type",
"_id" : "2",
"_score" : 0.08716954,
"_source" : {
"name" : "Yellow furballs"
}
} ]
}
}
validate-query API可以為我們提供一些線索:
[[email protected] ~]$ curl -XGET '192.168.205.153:9200/haha/my_type/_validate/query?explain&pretty' -d '
> {
> "query": {
> "match": {
> "name": "brown fo"
> }
> }
> }'
{
"valid" : true,
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"explanations" : [ {
"index" : "haha",
"valid" : true,
"explanation" : "+((name:b name:br name:bro name:brow name:brown) (name:f name:fo)) #ConstantScore(+ConstantScore(_type:my_type))"
} ]
}
explaination告訴我們這個查詢會查詢邊界Ngram裡的每個詞:name:b name:br name:bro name:brow name:brown name:f name:fo
name:f條件可以滿足第二個文件,因為 furballs是以f、fu、fur形式索引的。回過頭看這並不令人吃驚,相同的autocomplete分析器同時被應用於索引時和搜尋時,這在大多數情況下是正確的,只有在少數場景下才需要改變這種行為。
我們需要保證索引表中包含邊界Ngram的每個詞,但是我們只想匹配使用者輸入的完整片語(brown和fo),可以通過在索引時使用autocomplete分析器,並在搜尋時使用standard標準分析器來實現這種想法,只需要改變查詢中搜索分析器的analyzer引數即可:
curl -XGET 192.168.205.153:9200/my_index/my_type/_search?pretty -d '
{
"query": {
"match": {
"name": {
"query": "brown fo",
"analyzer": "standard"#覆蓋了name欄位analyzer的設定
}
}
}
}'
換種方式,我們可以在對映中,為name欄位分別指定index_analyzer和search_analyzer。因為只想改變search_analyzer,這裡只需更新現有的對映而不用對資料重新建立索引:
curl -XPUT 192.168.205.153:9200/my_index/my_type/_mapping -d '
{
"my_type": {
"properties": {
"name": {
"type": "string",
"index_analyzer": "autocomplete",
#在索引時,使用autocompleted分析器生成邊界Ngram的每個術語。
"search_analyzer": "standard"
#在搜尋時,使用standard分析器搜尋使用者輸入的術語。
}
}
}
}
如果我們再次用validate-query API檢視請求,現在的 explaination是:
name:brown name:fo
這樣結果中就只返回“Brown foxes”這個文件。
因為大多數工作是在索引時完成的,所有的查詢只需要查詢brown和fo這兩個術語,這比使用match_phrase_prefix查詢所有以fo開始的術語的方式要高效許多。
完成建議者(Completion Suggester)
使用邊界Ngram進行輸入即搜尋的查詢設定簡單、靈活且快速,但是,有些時候它並不足夠快,特別是當我們試圖立刻獲得反饋時,延遲的問題就會凸顯,很多時候不搜尋才是最快的搜尋方式。
ElasticSearch裡的完成建議者(Completion Suggester) 採用了與上面完全不同的方式,我們需要為搜尋條件生成一個所有可能完成的單詞列表,然後將它們置入一個有限狀態機(finite state transducer)內,這是一個經過優化的圖結構。為了搜尋建議,ElasticSearch從圖的開始處順著匹配路徑一個字元一個字元進行匹配,一旦它處於使用者輸入的末尾,ElasticSearch就會查詢所有可能的結束當前路徑,然後生成一個建議列表。
這個資料結構存在於記憶體裡使得對字首的查詢非常快速,比任何一種基於術語的查詢都要快很多,這對名字或品牌的自動完成非常適用,因為這些詞通常是以普通順序組織的:用“Johnny Rotten”而不是“Rotten Johnny”。
當詞序不是那麼容易被預見時,邊界Ngram比完成建議者(Completion Suggester)更合適。如果說不可能所有的貓都是一個花色,那這隻貓的花色也是相當特殊的。
邊界n-grams與郵編(Edge n-grams and Postcodes)
邊界n-gram的方式可以被用來查詢結構化的資料,比如之前例子中的:郵編(postcode)。當然postcode欄位需要是分析過的(analyzed)而不是未分析過的(not_analyzed),即使未分析過(not_analyzed),我們還是可以用關鍵詞(keyword)標記器來處理它。
keyword標記器是一個非操作型標記器,這個標記器不做任何事情,它接收的任何字串都會被原樣發出,因此它可以用來處理not_analyzed的欄位值,但這也需要其他的一些分析轉換,如:小寫化。
下面這個例子使用keyword標記器將郵編轉換成標記流(token stream),這樣我們就能使用邊界n-gram標記過濾器:
{
"analysis": {
"filter": {
"postcode_filter": {
"type": "edge_ngram",
"min_gram": 1,
"max_gram": 8
}
},
"analyzer": {
"postcode_index": {#postcode_index 分析器使用 postcode_filter 將郵編轉換成邊界n-gram形式
"tokenizer": "keyword",
"filter": [ "postcode_filter" ]
},
"postcode_search": {#postcode_search 分析器可以將搜尋術語看成未索引的(not_indexed)
"tokenizer": "keyword"
}
}
}
}
Ngram匹配複合詞(Ngrams for Compound Words)
最後,我們來看看n-gram是如何應用於搜尋複合詞的語言中的。德語聞名於它可以將許多小詞組合成一個非常巨大的複合詞以獲得它準確而又複雜的意義。例如:
- Aussprachewörterbuch
發音字典(Pronunciation dictionary)
- Militärgeschichte
戰爭史(Military history)
- Weißkopfseeadler
禿鷹(White-headed sea eagle, or bald eagle)
- Weltgesundheitsorganisation
世界衛生組織(World Health Organization)
- Rindfleischetikettierungsüberwachungsaufgabenübertragungsgesetz
法案考慮代理監管牛和牛肉的標記的職責(The law concerning the delegation of duties for the supervision of cattle marking and the labeling of beef)
有些人希望在搜尋“Wörterbuch”(字典)的時候,能在結果中看到“Aussprachewörtebuch”(發音字典)。同樣,搜尋“Adler”(鷹)的時候,能將“Weißkopfseeadler”(禿鷹)包含在結果集中。
處理這種語言的一種方式可以是將複合詞拆分成各自部分,然後用組合詞標記過濾器(compound word token filter),但這種方式的結果質量依賴於我們組合詞字典的好壞。
另一種就是將所有的詞用n-gram的方式進行處理,然後搜尋任何匹配的片段——能匹配的片段越多,文件的相關度越大。
因為n-gram是一個詞上的移動窗,一個具有所有長度的n-gram能涵蓋所有的詞。我們希望選擇有足夠長度讓詞有意義,但是也不能太長而生成過多的唯一術語,一個長度為3的trigram可能是一個不錯的開始:
curl -XPUT 192.168.205.153:9200/my_index -d '
{
"settings": {
"analysis": {
"filter": {
"trigrams_filter": {
"type": "ngram",
"min_gram": 3,
"max_gram": 3
}
},
"analyzer": {
"trigrams": {
"type": "custom",
"tokenizer": "standard",
"filter": [
"lowercase",
"trigrams_filter"
]
}
}
}
},
"mappings": {
"my_type": {
"properties": {
"text": {
"type": "string",
"analyzer": "trigrams" #text欄位用trigrams分析器索引它的內容,這裡n-gram的長度是3。
}
}
}
}
}'
測試trigram分析器:
curl -XGET '192.168.205.153:9200/my_index/_analyze?analyzer=trigrams&pretty' -d 'Weißkopfseeadler'
返回的術語:
wei, eiß, ißk, ßko, kop,opf,pfs, fse, see, eea,ead,adl, dle, ler
補充:我試了下中文卻並不好使
建立複合詞的測試文件:
curl -XPOST '192.168.205.153:9200/my_index/my_type/_bulk?pretty' -d '
{ "index": { "_id": 1 }}
{ "text": "Aussprachew?rterbuch" }
{ "index": { "_id": 2 }}
{ "text": "Milit?rgeschichte" }
{ "index": { "_id": 3 }}
{ "text": "Wei?kopfseeadler" }
{ "index": { "_id": 4 }}
{ "text": "Weltgesundheitsorganisation" }
{ "index": { "_id": 5 }}
{ "text": "Rindfleischetikettierungsüberwachungsaufgabenübertragungsgesetz" }
'
“Adler”(鷹)的搜尋轉化為查詢三個術語adl、dle和ler:
curl -XGET 192.168.205.153:9200/my_index/my_type/_search?pretty -d '
{
"query": {
"match": {
"text": "Adler"
}
}
}'
正好可以與“Wei?kopfsee-adler”相匹配:
{
"hits": [
{
"_id": "3",
"_score": 3.3191128,
"_source": {
"text": "Weißkopfseeadler"
}
}
]
}
類似查詢“Gesundheit”(健康)可以與“Welt-gesundheit-sorganisation”匹配,同時也能與“Militär-ges-chichte”和“Rindfleischetikettierungsüberwachungsaufgabenübertragungs-ges-etz”匹配,因為它們同時具有trigram生成的術語ges:
使用合適的minimum_should_match可以將這些奇怪的結果排除,只有當trigram最少匹配數滿足要求時,一個文件才能被認為是匹配的:
curl -XGET 192.168.205.153:9200/my_index/my_type/_search?pretty -d '
{
"query": {
"match": {
"text": {
"query": "Gesundheit",
"minimum_should_match": "80%"
}
}
}
}'
這有點像全文搜尋中的獵槍法,可能會導致反向索引很大,儘管如此,在索引具有很多複合詞的語言,或詞之間沒有空格的語言(如:泰語)時,它仍不失為一種通用的有效方法。
這種技術可以用來提升召回(Recall)——搜尋結果中相關文件的數目。它通常會與其他技術(如:瓦片詞shingles)一起使用以提高精度和每個文件的相關度分數。