Elasticsearch analysis & 自定義 analyzers

當一個 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

curl -XPUT 'http://localhost:9200/blog/' -d '
{
  "mappings": {
     "post": {
        "properties": {
           "title": {
              "type": "string",
              "analyzer": "standard",
              "term_vector": "yes"
           }
        }
     }
  }
}'

存入數據如下,

curl -XPUT 'http://localhost:9200/blog/post/1?pretty=true' -d '
{
    "title": "the Good Cats & the Good Dogs!"
}
'

使用 termvector 來查看 title 這個 field 的數據是如何存儲的(倒排索引)

curl -XGET 'http://localhost:9200/blog/post/1/_termvector?fields=title&pretty=true'{
  "_index" : "blog",
  "_type" : "post",
  "_id" : "1",
  "_version" : 1,
  "found" : true,
  "took" : 1,
  "term_vectors" : {
    "title" : {
      "field_statistics" : {
        "sum_doc_freq" : 4,
        "doc_count" : 1,
        "sum_ttf" : 6
      },
      "terms" : {
        "cats" : {
          "term_freq" : 1
        },
        "dogs" : {
          "term_freq" : 1
        },
        "good" : {
          "term_freq" : 2
        },
        "the" : {
          "term_freq" : 2
        }
      }
    }
  }}

因爲在定義 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 是基於語法(英語)做分詞的, 並且把標點符號比如上面的 & 和 ! 去掉, Lower Case Token Filter 則是把所有的單詞都統一成小寫, 除此以外, standard analyzer 還用到了 Stop Token Filter, 沒說設置了哪些停詞, 所以, 這裏看不到其作用, 不過可以肯定的是, 單詞 the 不是停詞, 其實, 我們可以設置單詞 the 爲 stopwords, 因爲其實對於索引而言, 這個詞的並沒有什麼意義, 可以去掉. 最後, term_freq 表示每個詞出現的次數.

基於如上的結果來做一些搜索, 先試試這個:

curl -XGET 'http://localhost:9200/blog/post/_search?pretty=true' -d '
{
  "query" : {
    "match" : {
      "title" : "dog"
    }
  }
}'

輸出的結果, 居然是…….

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 0,
    "max_score" : null,
    "hits" : [ ]
  }}

什麼鬼, 居然搜不到??? 其實, 仔細看就會發現其實也不奇怪, 我們搜索的關鍵詞是 dog, 但是 title 的倒排索引索引中的詞只有 [cats, dogs, good, the] 這幾個詞, 注意是 dogs 不是我們要搜索的 dog

好吧, 瞬間覺得好不智能是伐 = . =

下面換一個 snowball analyzer 再試試看, 不過要先刪掉當前索引, 再重新跑一次(mappings 每次更改都要修改 reindex 才能生效)

curl -XDELETE 'http://localhost:9200/blog'
curl -XPUT 'http://localhost:9200/blog/' -d '
{
  "mappings": {
     "post": {
        "properties": {
           "title": {
              "type": "string",
              "analyzer": "snowball",
              "term_vector": "yes"
           }
        }
     }
  }
}'

還是存入同樣的數據,

curl -XPUT 'http://localhost:9200/blog/post/1?pretty=true' -d '
{
    "title": "the Good Cats & the Good Dogs!"
}
'

再次, 使用 termvector 來查看 title 這個 field 的數據是如何存儲的

curl -XGET 'http://localhost:9200/blog/post/1/_termvector?fields=title&pretty=true'{
  "_index" : "blog",
  "_type" : "post",
  "_id" : "1",
  "_version" : 1,
  "found" : true,
  "took" : 1,
  "term_vectors" : {
    "title" : {
      "field_statistics" : {
        "sum_doc_freq" : 3,
        "doc_count" : 1,
        "sum_ttf" : 4
      },
      "terms" : {
        "cat" : {
          "term_freq" : 1
        },
        "dog" : {
          "term_freq" : 1
        },
        "good" : {
          "term_freq" : 2
        }
      }
    }
  }}

可以看到, 這一次, 存入的詞語只有 [cat, dog, good]官方描述 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 把 the 設置爲停詞了, 然後還多了個 snowball filter. 沒有詳細深挖這個 filter, 不過大意應該是提取詞幹, 比如 computing 和 computed 的詞幹 comput, 或者上面的 dogs, cats 變爲 dog, cat.

現在再去搜索 dog, 肯定是可以命中的.

那麼問題來了, 如果我搜索的是 dogs 呢? 會不會不中? 答案是, 能夠命中的, 因爲這裏搜索 dogs, 會先把搜索的詞同樣的處理爲 dog 再去匹配.

curl -XGET 'http://localhost:9200/blog/post/_search?pretty=true' -d '
{
  "query" : {
    "match" : {
      "title" : "dogs"
    }
  }
}'
{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 0.2169777,
    "hits" : [ {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "1",
      "_score" : 0.2169777,
      "_source" : {
        "title" : "the Good Cats & the Good Dogs!"
      }
    } ]
  }}




以上, 大概說了一些默認的 Analyzers, 接下來, 看下怎麼自定義 Analyzers 並運用到 mappings 中.

官方給出了一個自定義 analyzer 語法例子如下:

index :
    analysis :
        analyzer :
            myAnalyzer2 :
                type : custom
                tokenizer : myTokenizer1
                filter : [myTokenFilter1, myTokenFilter2]
                char_filter : [my_html]
                position_increment_gap: 256
        tokenizer :
            myTokenizer1 :
                type : standard
                max_token_length : 900
        filter :
            myTokenFilter1 :
                type : stop
                stopwords : [stop1, stop2, stop3, stop4]
            myTokenFilter2 :
                type : length
                min : 0
                max : 2000
        char_filter :
              my_html :
                type : html_strip
                escaped_tags : [xxx, yyy]
                read_ahead : 1024

接下來, 試下寫一個自己的.

上面的第一個栗子中, 可以看到, 當存入的是 dogs, 搜索 dog 都不能命中, 更不用說搜索 do go 什麼的.

很多時候我們一些電商網站, 輸入一兩個字符, 就會給出一些提示選項給我們, 如圖:

這種 Autocomplete 的功能需要把字符串分的粒度很細, 這時候我們就可以用到 Ngrams for Partial Matching

寫一個用於 Autocomplete 的 Analyzer.

curl -XDELETE 'http://localhost:9200/blog'
curl -XPUT 'http://localhost:9200/blog/' -d '
{
  "settings" : {
    "analysis" : {
      "analyzer" : {
        "autocomplete_analyzer": {
          "type" : "custom",
          "char_filter" : ["replace_ampersands"],
          "tokenizer" : "my_edgeNGram_tokenizer",
          "filter" : ["lowercase", "my_stop"]
        }
      },
      "char_filter" : {
                     "replace_ampersands" : {
                         "type" : "mapping",
                         "mappings" : ["&=>and"]
                     }
      },
      "tokenizer" : {
        "my_edgeNGram_tokenizer" : {
          "type" : "edgeNGram",
          "min_gram" : "2",
          "max_gram" : "8",
          "token_chars": [ "letter", "digit" ]
        }
      },
      "filter" : {
          "my_stop" : {
              "type" :      "stop",
              "stopwords" : ["the"]
          }
      }
    }
  },
  "mappings": {
     "post": {
        "properties": {
           "title": {
              "type": "string",
              "analyzer": "autocomplete_analyzer",
              "term_vector": "yes"
           }
        }
     }
  }
}
'

其中, my_stop 這個 filter 和 replace_ampersands 這個 char_filter 其實可以不用, 只是拿來示範一下, my_stop 是把單詞 the 去掉, 這裏細分以後, 就會很奇怪的有 th, 但是沒有 the, 因爲 the 被停詞去掉了, 好吧, 在實際中不會這麼去用 nGram,

replace_ampersands 這個 char_filter 是在分詞前預處理字符串, 把 & 變成 and.

值得注意的是, 這裏用的是 edgeNGram, 而不是 nGram, 從名字可以猜出來, 結果看後面

存入數據,

curl -XPUT 'http://localhost:9200/blog/post/1?pretty=true' -d '
{
    "title": "the Cats & the Dogs!"
}
'

Again, 使用 termvector 來查看此時 title 是如何存儲的

curl -XGET 'http://localhost:9200/blog/post/1/_termvector?fields=title&pretty=true'{
  "_index" : "blog",
  "_type" : "post",
  "_id" : "1",
  "_version" : 1,
  "found" : true,
  "took" : 1,
  "term_vectors" : {
    "title" : {
      "field_statistics" : {
        "sum_doc_freq" : 9,
        "doc_count" : 1,
        "sum_ttf" : 10
      },
      "terms" : {
        "an" : {
          "term_freq" : 1
        },
        "and" : {
          "term_freq" : 1
        },
        "ca" : {
          "term_freq" : 1
        },
        "cat" : {
          "term_freq" : 1
        },
        "cats" : {
          "term_freq" : 1
        },
        "do" : {
          "term_freq" : 1
        },
        "dog" : {
          "term_freq" : 1
        },
        "dogs" : {
          "term_freq" : 1
        },
        "th" : {
          "term_freq" : 2
        }
      }
    }
  }}

倒排索引的粒度變細了, 這樣, 在輸入前面兩個字符 th 就可以命中匹配. edgeNGram 是僅從詞頭開始分詞, 這裏如果用的是 nGram 的話, 那麼粒度會更細, 會多出一些在 edgeNGram 中沒的, 比如, atsogs 等等. min_gram 和 max_gram 都還比較好理解, token_chars 是指分割點, 比如這裏加了 letter 和 digit, 因爲空格不屬於這兩樣, 那麼空格就會被當成分割點, 倒排索引裏也就看不到有空格. 如果要保留空格, 也就是留下 the c 和 the ca …這些分詞的話, 就得再加上 whitespace 了.

curl -XGET 'http://localhost:9200/blog/post/_search?pretty=true' -d '
{
  "query" : {
    "match" : {
      "title" : "th"
    }
  }
}'
{
  "took" : 5,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 0.13561106,
    "hits" : [ {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "1",
      "_score" : 0.13561106,
      "_source" : {
        "title" : "the Cats & the Dogs!"
      }
    } ]
  }}




最後, 如果只是想精確的存儲值而不被分析的話, 可以用 “index”: “not_analyzed”

比如 United Kingdom 就是 United Kingdom, 不想被拆成 UnitedKingdom什麼的, 這種時候就可以用上了, not_analyzed 的效率會高一些.

long, double, date 這些類型的數據是永遠不會被分析的. 要麼是 "index" : "no", 或者 "index" : "not_analyzed"

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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章