掌握 analyze API,搞定分詞難題

qianfeng_dashuju發表於2018-08-27

  初次接觸Elasticsearch的同學經常會遇到分詞相關的難題,比如如下這些場景:

  1.為什麼命名有包含搜尋關鍵詞的文件,但結果裡面就沒有相關文件呢?

  2.我存進去的文件到底被分成哪些詞(term)了?

  3.我得自定義分詞規則,但感覺好麻煩呢,無從下手

  如果你遇到過類似的問題,希望本文可以解決你的疑惑。

  

1.上手

 

  讓我們從一個例項出發,如下建立一個文件:

  PUT test/doc/1

  {

  msg:Eating an apple a day keeps doctor away

  }

  然後我們做一個查詢,我們試圖通過搜尋eat這個關鍵詞來搜尋這個文件

  POST test/_search

  {

  query:{

  match:{

  msg:eat

  }

  }

  }

  ES的返回結果為0。這不太對啊,我們用最基本的字串查詢也應該能匹配到上面新建的文件才對啊!

  各位不要急,我們先來看看什麼是分詞。

  

2.分詞

 

  搜尋引擎的核心是倒排索引(這裡不展開講),而倒排索引的基礎就是分詞。所謂分詞可以簡單理解為將一個完整的句子切割為一個個單詞的過程。在 es 中單詞對應英文為term。我們簡單看個例子:

  

http://p8z8qq24s.bkt.clouddn.com/img20180825091854.png

 

  ES 的倒排索引即是根據分詞後的單詞建立,即我、愛、北京、天安門這4個單詞。這也意味著你在搜尋的時候也只能搜尋這4個單詞才能命中該文件。

  實際上 ES 的分詞不僅僅發生在文件建立的時候,也發生在搜尋的時候,如下圖所示:

  

http://p8z8qq24s.bkt.clouddn.com/img20180825103328.png

 

  讀時分詞發生在使用者查詢時,ES 會即時地對使用者輸入的關鍵詞進行分詞,分詞結果只存在記憶體中,當查詢結束時,分詞結果也會隨即消失。而寫時分詞發生在文件寫入時,ES 會對文件進行分詞後,將結果存入倒排索引,該部分最終會以檔案的形式儲存於磁碟上,不會因查詢結束或者 ES 重啟而丟失。

  ES 中處理分詞的部分被稱作分詞器,英文是Analyzer,它決定了分詞的規則。ES 自帶了很多預設的分詞器,比如Standard、Keyword、Whitespace等等,預設是Standard。當我們在讀時或者寫時分詞時可以指定要使用的分詞器。

  

3.寫時分詞結果

 

  回到上手階段,我們來看下寫入的文件最終分詞結果是什麼。通過如下 api 可以檢視:

  POST test/_analyze

  {

  field: msg,

  text: Eating an apple a day keeps doctor away

  }

  其中test為索引名,_analyze為檢視分詞結果的endpoint,請求體中field為要檢視的欄位名,text為具體值。該 api 的作用就是請告訴我在 test 索引使用 msg 欄位儲存一段文字時,es 會如何分詞。

  返回結果如下:

  {

  tokens: [

  {

  token: eating,

  start_offset: 0,

  end_offset: 6,

  type: ALPHANUM,

  position: 0

  },

  {

  token: an,

  start_offset: 7,

  end_offset: 9,

  type: ALPHANUM,

  position: 1

  },

  {

  token: apple,

  start_offset: 10,

  end_offset: 15,

  type: ALPHANUM,

  position: 2

  },

  {

  token: a,

  start_offset: 16,

  end_offset: 17,

  type: ALPHANUM,

  position: 3

  },

  {

  token: day,

  start_offset: 18,

  end_offset: 21,

  type: ALPHANUM,

  position: 4

  },

  {

  token: keeps,

  start_offset: 22,

  end_offset: 27,

  type: ALPHANUM,

  position: 5

  },

  {

  token: doctor,

  start_offset: 28,

  end_offset: 34,

  type: ALPHANUM,

  position: 6

  },

  {

  token: away,

  start_offset: 35,

  end_offset: 39,

  type: ALPHANUM,

  position: 7

  }

  ]

  }

  返回結果中的每一個token即為分詞後的每一個單詞,我們可以看到這裡是沒有eat這個單詞的,這也解釋了在上手中我們搜尋eat沒有結果的情況。如果你去搜尋eating,會有結果返回。

  寫時分詞器需要在 mapping 中指定,而且一經指定就不能再修改,若要修改必須新建索引。如下所示我們新建一個名為ms_english的欄位,指定其分詞器為english:

  PUT test/_mapping/doc

  {

  properties: {

  msg_english:{

  type:text,

  analyzer: english

  }

  }

  }

  

4.讀時分詞結果

 

  由於讀時分詞器預設與寫時分詞器預設保持一致,拿 上手 中的例子,你搜尋msg欄位,那麼讀時分詞器為Standard,搜尋msg_english時分詞器則為english。這種預設設定也是非常容易理解的,讀寫採用一致的分詞器,才能盡最大可能保證分詞的結果是可以匹配的。

  然後ES 允許讀時分詞器單獨設定,如下所示:

  POST test/_search

  {

  query:{

  match:{

  msg:{

  query: eating,

  analyzer: english

  }

  }

  }

  }

  如上analyzer欄位即可以自定義讀時分詞器,一般來講不需要特別指定讀時分詞器。

  如果不單獨設定分詞器,那麼讀時分詞器的驗證方法與寫時一致;如果是自定義分詞器,那麼可以使用如下的 api 來自行驗證結果。

  POST _analyze

  {

  text:eating,

  analyzer:english

  }

  返回結果如下:

  {

  tokens: [

  {

  token: eat,

  start_offset: 0,

  end_offset: 6,

  type: ALPHANUM,

  position: 0

  }

  ]

  }

  由上可知english分詞器會將eating處理為eat,大家可以再測試下預設的standard分詞器,它沒有做任何處理。

  

5.解釋問題

 

  現在我們再來看下 上手 中所遇問題的解決思路。

  1.檢視文件寫時分詞結果

  2.檢視查詢關鍵詞的讀時分詞結果

  3.匹對兩者是否有命中

  我們簡單分析如下:

  

http://p8z8qq24s.bkt.clouddn.com/img20180825212343.png

 

  由上圖可以定位問題的原因了。

  

6.解決需求

 

  由於eating只是eat的一個變形,我們依然希望輸入eat時可以匹配包含eating的文件,那麼該如何解決呢?

  答案很簡單,既然原因是在分詞結果不匹配,那麼我們就換一個分詞器唄~ 我們可以先試下 ES 自帶的english分詞器,如下:

  # 增加欄位 msg_english,與 msg 做對比

  PUT test/_mapping/doc

  {

  properties: {

  msg_english:{

  type:text,

  analyzer: english

  }

  }

  }

  # 寫入相同文件

  PUT test/doc/1

  {

  msg:Eating an apple a day keeps doctor away,

  msg_english:Eating an apple a day keeps doctor away

  }

  # 搜尋 msg_english 欄位

  POST test/_search

  {

  query: {

  match: {

  msg_english: eat

  }

  }

  }

  執行上面的內容,我們會發現結果有內容了,原因也很簡單,如下圖所示:

  

http://p8z8qq24s.bkt.clouddn.com/img20180825212639.png

 

  由上圖可見english分詞器會將eating分詞為eat,此時我們搜尋eat或者eating肯定都可以匹配對應的文件了。至此,需求解決。

  

7.深入分析

 

  最後我們來看下為什麼english分詞器可以解決我們遇到的問題。一個分詞器由三部分組成:char filter、tokenizer 和 token filter。各部分的作用我們這裡就不展開了,我們來看下standard和english分詞器的區別。

  

http://p8z8qq24s.bkt.clouddn.com/img20180825215109.png

 

  從上圖可以看出,english分詞器在 Token Filter 中和Standard不同,而發揮主要作用的就是stemmer,感興趣的同學可以自行去看起它的作用。

  

8.自定義分詞

 

  如果我們不使用english分詞器,自定義一個分詞器來實現上述需求也是完全可行的,這裡不詳細講解了,只給大家講一個快速驗證自定義分詞器效果的方法,如下:

  POST _analyze

  {

  char_filter: [],

  tokenizer: standard,

  filter: [

  stop,

  lowercase,

  stemmer

  ],

  text: Eating an apple a day keeps doctor away

  }

  通過上面的 api 你可以快速驗證自己要定製的分詞器,當達到自己需求後,再將這一部分配置加入索引的配置。

  至此,我們再看開篇的三個問題,相信你已經心裡有答案了,趕緊上手去自行測試下吧!

  

相關文章