閱讀本文需要先了解 Elastic Search 的 index 和 analyzer 的相關知識
在此之前,ES 所有的查詢都是針對整個詞進行操作,也就是說倒排索引存了 hello
這個詞,一定得輸入 hello 才能找到這個詞,輸入 h 或是 he 都找不到倒排索引中的 hello
edge n-gram
建立索引,如此可以避免使用前綴查詢,在建立索引時就進行優化,使用空間換取時間,讓查詢的速率增快使用 edge n-gram
建立索引
假設有一個詞 hello
,普通建索引時,就是把這個詞 hello
放入倒排索引
而對於輸入即搜索這種應用場景,可以使用一種特殊的 n-gram,稱爲 edge n-grams
所謂的 edge n-gram,就是指它會固定詞語開始的一邊滑動窗口,他的結果取決於 n 的選擇長度
以單詞 hello
爲例,它的 edge n-gram 的結果如下
h
he
hel
hell
hello
因此可以發現到,在使用 edge n-gram 建索引時,一個單詞會生成好幾個索引,而這些索引一定是重頭開始
這符合了輸入即搜索的特性,即是用戶打 h、he 能找到倒排中的索引 h
、he
,而這些索引對應著的數據就是 hello
具體實例
建立索引時使用 edge n-gram 的 token 過濾器,為每個經過這個 token 過濾器的詞條們,都生成從頭開始的字符組合
QUICK! RUN!
,分詞器會先將它分詞成兩個詞 quick
和 run
,此時這些詞再一一的通過 edge n-gram token 過濾器,產生了 8 個索引 q、qu、qui、quic、quick、r、ru、run,接著存入倒排索引中另外要注意,要額外定義一個 search_analyzer
分析器,供查詢使用
原因是因為我們為了要保證倒排索引中包含各種組合的詞,所以在建索引時才加入了 edge n-gram 過濾器,然而在查詢時,我們只想匹配用戶輸入的完整詞組,像是用戶的輸入 run
或 qu
因此需要定義兩套分析器,一套是建索引的分析器(包含edge n-gram 過濾器),另一套是查詢使用的正常的分析器
PUT my_index
{
"settings": {
"number_of_shards": 1,
"analysis": {
"filter": {
//定義一個edge n-gram的token過濾器
//並設置任何通過這個過濾器的詞條,都會生成一個最小固定值為1,最大固定值為20的n-gram
"my_autocomplete_filter": {
"type": "edge_ngram",
"min_gram": 1,
"max_gram": 20
}
},
"analyzer": {
//自定義一個分析器,並使用自定義的edge n-gram過濾器
"my_autocomplete_analyzer": {
"type": "custom",
"tokenizer": "standard",
"filter": [
"lowercase",
"my_autocomplete_filter"
]
}
}
}
},
"mapping": {
"my_type": {
"properties": {
"name": {
"type": "text",
"analyzer": "my_autocomplete_analyzer", //在索引時用,生成edge n-gram的每個詞
"search_analyzer": "standard" //查詢用,只搜索用戶輸入的詞
}
}
}
}
}
讓非 text 字段也能使用 edge n-gram
由於 edge n-gram 是一個 token 過濾器,他包含在 analyzer 分析器裡面,因此只有 text
類型的字段才能使用(其他類型的字段不會被分詞,所以不會使用到 analyzer,因此不能用 edge n-gram)
但是可能會有一種情況是,有些精確值也希望能通過 edge n-gram 生成組合,這時就要搭配使用一個叫做 keyword
的分詞器
keyword
分詞器和 keyword
字段類型是不同的東西具體實例
將 postcode 這個本來是 keyword 類型的精確值,改成使用 text 類型並搭配 keyword 分詞器
因此假設有一個輸入 ABC EF
,先經過 keyword 分詞器分詞成 ABC EF
(和輸入一模一樣),接著再經過 edge n-gram 生成 A、AB、ABC、ABC (有一個空格) 、ABC E、ABC EF
如果是使用正常的分詞器,生成的 edge n-gram 會是 A、AB、ABC、E、EF,他們是有差別的
PUT my_index
{
"settings": {
"analysis": {
"filter": {
"postcode_filter": {
"type": "edge_ngram",
"min_gram": 1,
"max_gram": 8
}
},
"analyzer": {
"postcode_index": {
"tokenizer": "keyword",
"filter": [
"postcode_filter"
]
},
"postcode_search": {
"tokenizer": "keyword"
}
}
}
},
"mapping": {
"my_type": {
"properties": {
"postcode": {
"type": "text",
"analyzer": "postcode_index",
"search_analyzer": "postcode_search"
}
}
}
}
}