Chef 實戰 part3 — 安裝 IK Analyzer 及更新詞庫

Luyo
verybuy-dev
Published in
19 min readAug 27, 2017

這次要來安裝 Elasticsearch 的 plugin — IK Analyzer,以及建立更新詞庫的機制。

NOTE: 其實一開始想學 Chef 就是為了做這件事,因為 IK Analyzer 雖說有遠端熱更新分詞功能,但我怎麼試就是都不能用,只好用本機端的詞庫。問題是我的 node 不只一台,必須建立一個優雅的機制去同時更新它們的本機端詞庫才行。

1. 安裝 IK plugin

上一篇中我們使用了官方的 Cookbook 來架設 Elasticsearch,並使用了它的 resource elasticsearch_install 以及 elasticsearch_configure ,這次要來嘗試使用它用來管理外掛的 resource elasticsearch_plugin

先看一下 IK 自己的安裝說明:

- optional 1 — download pre-build package from here: https://github.com/medcl/elasticsearch-analysis-ik/releases
unzip plugin to folder your-es-root/plugins/

- optional 2 — use elasticsearch-plugin to install ( version > v5.5.1 ):
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v5.5.1/elasticsearch-analysis-ik-5.5.1.zip

第一個方法是下載 build 好的檔案,unzip 到 es 的 plugins 資料夾下;第二個方法是使用 elasticsaerch-plugin 指令。

顯然我必須用第二個方法才能套用到 elasticsearch_plugin resource 的用法上。打開 recipes/default.rb 在結尾加入以下部分:

elasticsearch_plugin 'analysis-ik' do
url 'https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v5.5.1/elasticsearch-analysis-ik-5.5.1.zip'
action :install
end

上傳至 Chef server 並更新 node:

$ knife cookbook upload elasticsearch_ik; knife ssh 'name:es-1' 'sudo chef-client' --ssh-user centos --identity-file ~/.ssh/test.pem --attribute ipaddress
(...略)
172.31.21.70 * elasticsearch_plugin[analysis-ik] action install
172.31.21.70
172.31.21.70
172.31.21.70 Running handlers:
172.31.21.70 Running handlers complete
172.31.21.70 Chef Client finished, 1/46 resources updated in 42 seconds

看起來有安裝成功,連進 node 中確認一下:

$ sudo su
# cd /etc/elasticsearch/analysis-ik/
# cd /usr/share/elasticsearch/plugins/analysis-ik/

兩個資料夾都在,表示 analysis-ik 這個套件安裝成功囉。

2. 更新詞庫

接下來我要更新 IK 的詞庫設定,並將自訂的詞庫放到指定的路徑之中。

因為詞庫檔不是靜態檔案,而是從資料庫撈出資料並動態產生,是會變動的,所以我不能用之前學過的 filetemplate resource 來做。

那麼要怎麼去更新這個動態檔案呢?如果 Chef 有提供一種 resource 是可以把外部資料以 http request 的方式取得檔案,然後存放到指定的路徑,那麼我就可以在後端製作一支供內部資源使用的 web API 讓 node 去 request 回來存檔,問題就解決了。

翻一下文件中的 resource 列表,發現了一個 remote_file 的 resource,看起來就是為了我這樣的需要所設計的!

這時候我先偷偷回去把後端取得詞庫檔的 API 生了出來,然後再回到工作站開始做相關設定。

NOTE: 以下會以 https://api.xxx.ooo/get-my-dict 表示這支 API 的 endpoint

產生設定檔 template

首先要產生設定檔的 templaste,告訴 IK 要載入哪個詞庫檔:

$ chef generate template IKAnalyzer.cfg.xml
Recipe: code_generator::template
(...略)

然後打開 templates/IKAnalyzer.cfg.xml.erb 加入內容:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict">verybuy.dic</entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords"></entry>
<!--用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

設定 template 及 remote_file resources

再回到 recipes/default.rb 接下去新增相關的 resources:

template '/etc/elasticsearch/analysis-ik/IKAnalyzer.cfg.xml' do
source 'IKAnalyzer.cfg.xml.erb'
end

remote_file '/etc/elasticsearch/analysis-ik/verybuy.dic' do
source 'https://api.xxx.ooo/get-my-dict'
owner 'elasticsearch'
group 'elasticsearch'
mode '0660'
action :create
end

template 的部分是 IK 詞庫設定檔的路徑及內容;而 remote_file 的部分就是剛剛講的要去哪裡抓檔案回來存到哪裡 。

再來就試試看上傳並更新 node:

$ knife cookbook upload elasticsearch_ik; knife ssh 'name:es-1' 'sudo chef-client' --ssh-user centos --identity-file ~/.ssh/test.pem --attribute ipaddress
(...略)
172.31.21.70 Chef Client finished, 2/48 resources updated in 18 seconds

resources 成功更新,進到 node 去確認檔案也有順利更新,再就來可以試試看 analyzer 有沒有順利運作。

測試之前必須先重啟 elasticsearch,新的 ik_smart analyzer 才會生效:

$ knife ssh 'name:es-1' 'sudo service elasticsearch restart' --ssh-user centos --identity-file ~/.ssh/test.pem --attribute ipaddress
172.31.21.70 Restarting elasticsearch (via systemctl): [ OK ]

3. 測試斷詞

我這次要拿「非常勸敗」這個詞來測試。原本的詞庫裡沒這組詞彙, curl 的結果如下:

$ curl -XGET '172.31.21.70:9200/_analyze?pretty' -H 'Content-Type: application/json' -d'
> {
> "analyzer" : "ik_smart",
> "text" : ["非常勸敗"]
> }
> '

{
"tokens" : [
{
"token" : "非常",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "勸",
"start_offset" : 2,
"end_offset" : 3,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "敗",
"start_offset" : 3,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 2
}
]
}

因為原本的詞庫裡有「非常」,所以「非常勸敗」會被斷詞為「非常」、「勸」、「敗」。

現在我到資料庫去新增了「非常勸敗」這組詞彙,然後也確認了 API 回傳的結果裡面有這組詞彙。

接下來,我只要跑更新 node 的動作,應該就會自動更新 node 上的詞庫了:

$ knife ssh 'name:es-1' 'sudo chef-client' --ssh-user centos --identity-file ~/.ssh/test.pem --attribute ipaddress
(...略)
172.31.21.70 * remote_file[/etc/elasticsearch/analysis-ik/verybuy.dic] action create
172.31.21.70 - update content in file /etc/elasticsearch/analysis-ik/verybuy.dic from 6e5d3a to b46e4c
172.31.21.70 (new content is binary, diff output suppressed)
172.31.21.70 - restore selinux security context
172.31.21.70
172.31.21.70 Running handlers:
172.31.21.70 Running handlers complete
172.31.21.70 Chef Client finished, 1/48 resources updated in 13 seconds

連到 node 裡去確認一下,我的詞庫檔 verbuy.dic 已經順利被更新了。

因為詞庫檔若有更新,必須重新啟動 elasticsearch 才會生效,所以先用 knife ssh 來重啟:

$ knife ssh 'name:es-1' 'sudo service elasticsearch restart' --ssh-user centos --identity-file ~/.ssh/test.pem --attribute ipaddress
172.31.21.70 Restarting elasticsearch (via systemctl): [ OK ]

最後再次執行一樣的 curl 指令:

$ curl -XGET '172.31.21.70:9200/_analyze?pretty' -H 'Content-Type: application/json' -d'
{
"analyzer" : "ik_smart",
"text" : ["非常勸敗"]
}
'

{
"tokens" : [
{
"token" : "非常勸敗",
"start_offset" : 0,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 0
}
]
}

斷詞成功!

4. 使用變數

最後,我覺得 recipe 還有一些小地方可以寫得更好。

首先是 remote_file 裡的 usergroup 的部分,應該用變數的方法來寫比較妥當。參考官方 elasticsearch_user 的說明,編輯剛剛在 recipes/default.rb 中新增的 remote_file resource:

remote_file '/etc/elasticsearch/analysis-ik/verybuy.dic' do
source 'https://api.xxx.ooo/get-my-dict'
owner node['elasticsearch']['user']['username']
group node['elasticsearch']['user']['groupname']

mode '0660'
action :create
end

上傳並更新 node:

$ knife cookbook upload elasticsearch_ik; knife ssh 'name:es-1' 'sudo chef-client' --ssh-user centos --identity-file ~/.ssh/test.pem --attribute ipaddress
(...略)
172.31.21.70 Running handlers:
172.31.21.70 Running handlers complete
172.31.21.70 Chef Client finished, 0/48 resources updated in 13 seconds

到 node 去看,檔案權限沒有發生變化,表示變數設定正確。

5. 設定重啟 elasticsearch service 條件

其次,每次更新完詞庫就要重新啟動 elasticsearch 才會生效,所以 recipe 中必須要有重新啟動 service 的動作。但是如果每次跑這個 recipe 都重新啟動 elasticsearch 也蠻蠢的,因為詞庫又不一定有更新。

google 了一下找到這篇文章,介紹了 only_if 的用法,搭配上dict.updated_by_last_action? 就可以指定當某個 resource 有更新的時候才執行這個動作。

馬上來改造一下我的 resources:

dict = remote_file '/etc/elasticsearch/analysis-ik/verybuy.dic' do
source 'https://api.xxx.ooo/get-my-dict'
owner node['elasticsearch']['user']['username']
group node['elasticsearch']['user']['groupname']
mode '0660'
action :create
end
service 'elasticsearch' do
action [:restart]
only_if { dict.updated_by_last_action? }
end

這邊的作法其實有問題,詳見底下的 NOTE2 及 NOTE3

上傳並更新 node:

$ knife cookbook upload elasticsearch_ik; knife ssh 'name:es-1' 'sudo chef-client' --ssh-user centos --identity-file ~/.ssh/test.pem --attribute ipaddress
(...略)
172.31.21.70 Running handlers:
172.31.21.70 Running handlers complete
172.31.21.70 Chef Client finished, 0/48 resources updated in 13 seconds

更新成功,也連進 node 確認了 elasticsearch 沒有被重啟,因為目前詞庫並沒有被更新,所以 only_if 這個條件看起來是有運作了,但我們還要做進一步的確認。

確認方式是在資料庫中隨便新增一筆詞彙,因為資料庫更新了,詞庫也應該要更新;而詞庫更新了,elasticsearch service 就應該要重新啟動才對。

所以我就去資料庫新增了一組詞彙「測試123」,然後更新 node:

$ knife ssh 'name:es-1' 'sudo chef-client' --ssh-user centos --identity-file ~/.ssh/test.pem --attribute ipaddress
(...略)
172.31.21.70 * remote_file[/etc/elasticsearch/analysis-ik/verybuy.dic] action create
172.31.21.70 - update content in file /etc/elasticsearch/analysis-ik/verybuy.dic from 0262aa to 10630d
172.31.21.70 (current file is binary, diff output suppressed)
172.31.21.70 - restore selinux security context
172.31.21.70 * service[elasticsearch] action restart
172.31.21.70 - restart service service[elasticsearch]
172.31.21.70
172.31.21.70 Running handlers:
172.31.21.70 Running handlers complete
172.31.21.70 Chef Client finished, 2/48 resources updated in 12 seconds

確認預期該發生的事情都有發生 — 更新檔案及重啟服務,最後 curl 一下確認斷詞狀況:

$ curl -XGET '172.31.21.70:9200/_analyze?pretty' -H 'Content-Type: application/json' -d'
{
"analyzer" : "ik_smart",
"text" : ["非常勸敗chef實戰測試123"]
}
'

{
"tokens" : [
{
"token" : "非常勸敗",
"start_offset" : 0,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "chef",
"start_offset" : 4,
"end_offset" : 8,
"type" : "ENGLISH",
"position" : 1
},
{
"token" : "實",
"start_offset" : 8,
"end_offset" : 9,
"type" : "CN_CHAR",
"position" : 2
},
{
"token" : "戰",
"start_offset" : 9,
"end_offset" : 10,
"type" : "CN_CHAR",
"position" : 3
},
{
"token" : "測試123",
"start_offset" : 10,
"end_offset" : 15,
"type" : "CN_WORD",
"position" : 4
}
]
}

斷詞也順利變成預期中的樣子囉!

到此,整個 recipes/default.rb 長這樣:

include_recipe 'elasticsearch'elasticsearch_configure 'elasticsearch' do
allocated_memory '256m'
configuration ({
'cluster.name' => 'development',
'network.host' => '172.31.21.70',
})
end
elasticsearch_plugin 'analysis-ik' do
url 'https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v5.5.1/elasticsearch-analysis-ik-5.5.1.zip'
action :install
end
template '/etc/elasticsearch/analysis-ik/IKAnalyzer.cfg.xml' do
source 'IKAnalyzer.cfg.xml.erb'
end
dict = remote_file '/etc/elasticsearch/analysis-ik/verybuy.dic' do
source 'https://api.xxx.ooo/get-my-dict'
owner node['elasticsearch']['user']['username']
group node['elasticsearch']['user']['groupname']
mode '0660'
action :create
end
service 'elasticsearch' do
action [:restart]
only_if { dict.updated_by_last_action? }
end

NOTE 1: 其實開頭少了一行 package 'java-1.8.0-openjdk' ,因為實戰 part1 已經安裝過這個套件,所以這邊就漏掉了。

NOTE 2: 在 Chef 實戰 part4 我才發現最後的這個 service resource 其實會覆蓋掉 elasticsearch_service resource 的預設行為,所以這個作法是有問題的。

NOTE 3: only_if 在這個情境下有更好的解法,就是在 remote_file 中設定 notification 讓指定的 service 去做指定的 action。修改方法也記錄在 Chef 實戰 part4

小結

在這篇文章中,我順利地在 node 上安裝好 IK plugin,也完成了更新詞庫的機制,學到了下列幾項用法:

  1. remote_file resource 的用法
  2. only_if 的用法

但目前 cookbook 雖算是完成了,但整個系統恐怕還不太實用,因為還差了以下兩件事:

  1. 我的 node 不只一台,必須設定讓一個指令可以同時更新多台 nodes
  2. 需要有定時更新的機制

還好這些事情之前有學過:第 1 點可以用 role 解決,第 2 點可以用 chef-client cookbook 解決。

設定過程會不會遇到什麼問題我也不知道,就留待下回分曉囉。

--

--