當一個 document 被索引時, 通常是對應每個 field 都生成一個反向索引(Inverted Index)用于作為存儲的資料結構, 關于反向索引, 推薦炮哥之前寫的一篇文章可以結合參考了解. 每個 field 的反向索引是由「對應」于這個 field 的那些詞語(term)所組成. 進而, 搜尋的時候, 就可查到某個 document 是否含有(或者說命中)某些 terms, 進而傳回所有命中查找詞的 documents.
這裡強調的「對應」其實就是 Analyzers 在支援着.
-
Elasticsearch 的官方文檔:
Analyzers are composed of a single Tokenizer and zero or more TokenFilters. The tokenizer may be preceded by one or more CharFilters.
Analyzers 是由一個 Tokenizer 和任意個數的 TokenFilters 組成. 而在把需要處理的字元串傳給 Tokenizer 之前需要經過一個或多個的 CharFilters(Character filters) 做預處理.
Elasticsearch(2.3) 預設給了我們八種 Analyzers(standard, simple, whitespace, stop, keyword, pattern, language, snowball) 開箱即用. 此外, 還提供給了我們 3 種 CharFilters, 12 種 Tokenizer, 以及一大堆 TokenFilters 用于自定義 Analyzers.
說半天, 都是很抽象的東西. 下面用一些栗子說明.
建立索引, 并設定好 mappings
存入資料如下,
使用 termvector 來檢視 title 這個 field 的資料是如何存儲的(反向索引)
因為在定義 mappings 的時候, title 用的 analyzer 是 standard, 官方給的 standard 是由以下幾個部分組成的
An analyzer of type standard is built using the Standard Tokenizer with the Standard Token Filter, Lower Case Token Filter, and Stop Token Filter.
Standard Tokenizer 是基于文法(英語)做分詞的, 并且把标點符号比如上面的 <code>&</code> 和 <code>!</code> 去掉, Lower Case Token Filter 則是把所有的單詞都統一成小寫, 除此以外, standard analyzer 還用到了 Stop Token Filter, 沒說設定了哪些停詞, 是以, 這裡看不到其作用, 不過可以肯定的是, 單詞 <code>the</code> 不是停詞, 其實, 我們可以設定單詞 <code>the</code> 為 stopwords, 因為其實對于索引而言, 這個詞的并沒有什麼意義, 可以去掉. 最後, term_freq 表示每個詞出現的次數.
基于如上的結果來做一些搜尋, 先試試這個:
輸出的結果, 居然是…….
什麼鬼, 居然搜不到??? 其實, 仔細看就會發現其實也不奇怪, 我們搜尋的關鍵詞是 dog, 但是 title 的反向索引索引中的詞隻有 <code>[cats, dogs, good, the]</code> 這幾個詞, 注意是 dogs 不是我們要搜尋的 dog
好吧, 瞬間覺得好不智能是伐 = . =
下面換一個 snowball analyzer 再試試看, 不過要先删掉目前索引, 再重新跑一次(mappings 每次更改都要修改 reindex 才能生效)
還是存入同樣的資料,
再次, 使用 termvector 來檢視 title 這個 field 的資料是如何存儲的
可以看到, 這一次, 存入的詞語隻有 <code>[cat, dog, good]</code>, 官方描述 Snowball Analyzer
An analyzer of type snowball that uses the standard tokenizer, with standard filter, lowercase filter, stop filter, and snowball filter.
大部分跟 standard analyzer 差不多, 從結果看出差別, snowball 把 <code>the</code> 設定為停詞了, 然後還多了個 snowball filter. 沒有詳細深挖這個 filter, 不過大意應該是提取詞幹, 比如 computing 和 computed 的詞幹 comput, 或者上面的 dogs, cats 變為 dog, cat.
現在再去搜尋 <code>dog</code>, 肯定是可以命中的.
那麼問題來了, 如果我搜尋的是 dogs 呢? 會不會不中? 答案是, 能夠命中的, 因為這裡搜尋 dogs, 會先把搜尋的詞同樣的處理為 dog 再去比對.
–
以上, 大概說了一些預設的 Analyzers, 接下來, 看下怎麼自定義 Analyzers 并運用到 mappings 中.
官方給出了一個自定義 analyzer 文法例子如下:
接下來, 試下寫一個自己的.
上面的第一個栗子中, 可以看到, 當存入的是 dogs, 搜尋 dog 都不能命中, 更不用說搜尋 <code>do</code> <code>go</code> 什麼的.
很多時候我們一些電商網站, 輸入一兩個字元, 就會給出一些提示選項給我們, 如圖:
這種 Autocomplete 的功能需要把字元串分的粒度很細, 這時候我們就可以用到 Ngrams for Partial Matching
寫一個用于 Autocomplete 的 Analyzer.
其中, my_stop 這個 filter 和 replace_ampersands 這個 char_filter 其實可以不用, 隻是拿來示範一下, my_stop 是把單詞 <code>the</code> 去掉, 這裡細分以後, 就會很奇怪的有 th, 但是沒有 the, 因為 the 被停詞去掉了, 好吧, 在實際中不會這麼去用 nGram,
<code>replace_ampersands</code> 這個 char_filter 是在分詞前預處理字元串, 把 & 變成 and.
值得注意的是, 這裡用的是 edgeNGram, 而不是 nGram, 從名字可以猜出來, 結果看後面
存入資料,
Again, 使用 termvector 來檢視此時 title 是如何存儲的
反向索引的粒度變細了, 這樣, 在輸入前面兩個字元 <code>th</code> 就可以命中比對. edgeNGram 是僅從詞頭開始分詞, 這裡如果用的是 nGram 的話, 那麼粒度會更細, 會多出一些在 edgeNGram 中沒的, 比如, <code>ats</code>, <code>ogs</code> 等等. min_gram 和 max_gram 都還比較好了解, token_chars 是指分割點, 比如這裡加了 letter 和 digit, 因為空格不屬于這兩樣, 那麼空格就會被當成分割點, 反向索引裡也就看不到有空格. 如果要保留白格, 也就是留下 <code>the c</code> 和 <code>the ca</code> …這些分詞的話, 就得再加上 whitespace 了.
最後, 如果隻是想精确的存儲值而不被分析的話, 可以用 “index”: “not_analyzed”
比如 <code>United Kingdom</code> 就是 <code>United Kingdom</code>, 不想被拆成 <code>United</code>, <code>Kingdom</code>什麼的, 這種時候就可以用上了, not_analyzed 的效率會高一些.
long, double, date 這些類型的資料是永遠不會被分析的. 要麼是 <code>"index" : "no"</code>, 或者 <code>"index" : "not_analyzed"</code>
Last but not least, 如果想知道具體怎麼計算出來的比對得分, 還可以看看 Explain API
相關閱讀:
https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis.html
How search and index works (Ruby 語言描述)
An Introduction to Ngrams in Elasticsearch