基於 HanLP 的 ES 中文分詞外掛

rochy_he發表於2018-12-23

一、分詞外掛

1、分詞器概念

在 ES 中,分詞器的作用是從文字中提取出若干詞元(token)來支援索引的儲存和搜尋,分詞器(Analyzer)由一個分解器(Tokenizer)、零個或多個詞元過濾器(TokenFilter)組成。

分解器用於將字串分解成一系列詞元,詞元過濾器的作用是對分詞器提取出來的詞元做進一步處理,比如轉成小寫,增加同義詞等。處理後的結果稱為索引詞(Term),引擎會建立 Term 和原文件的倒排索引(Inverted Index),這樣就能根據 Term 很快到找到源文件了。

文字分詞並索引的過程

2、選擇分詞器

目前 ES 分詞外掛的選擇性還是很多的,分詞外掛的核心就是提供各種分詞器(Analyzer)、分解器(Tokenizer)、詞元過濾器(TokenFilter);根據依賴的核心分詞包(分詞演算法)的不同顯現出不同的差異性,除了分詞演算法之外,是否支援使用者自定義詞典,是否支援詞典熱更新等其他附加功能也是選擇分詞外掛時需要參考的。

下面列出選擇分詞外掛需要考慮的因素(僅供參考):

  • 分詞準確性:大家都希望分詞結果能夠儘可能準確,與分詞準確性直接相關的就是使用者詞典了,此外才是分詞演算法;
  • 分詞演算法:個人認為無需糾結於分詞演算法,大多數分詞包提供的分詞演算法都比較類似,選擇時不需要過於糾結;
  • 分詞速度:這個與分詞演算法直接相關,基於詞典的分詞演算法一般比基於模型的分詞演算法要快;基於詞典如果考慮詞頻、命名實體識別、詞性標註則會慢一些;
  • 啟動速度:當詞典較大時,初始化詞典會比較慢,某些分詞器會對詞典進行快取,第二次啟動會非常速度;
  • 記憶體佔用:與分詞演算法、詞典大小、模型大小均有關係,設計精巧的演算法對記憶體佔用較小;
  • 易用性:分詞器是否開箱即用,是否可以直接使用線上連結或者壓縮包進行安裝,是否需要複雜的配置;
  • 擴充套件性:是否支援使用者自定義詞典、是否支援自定義分詞演算法、是否支援熱更新等;
  • 是否開源:開源的分詞器在遇到問題的時候可以自己進行深度除錯,甚至可以進行二次開發;
  • 社群活躍度:這個看一下 github 的 star 數或者依賴的分詞包的 star 數和 issue 數目即可判定;
  • 更新頻率:是否能夠與最新版的 ES 同步更新。

二、HanLP 簡介

HanLP 是一系列模型與演算法組成的 NLP 工具包,具備功能完善、效能高效、架構清晰、語料時新、可自定義的特點,詳情可參考 github 介紹:github.com/hankcs/HanL…

選擇 HanLP 作為核心的分詞包開發 ES 分詞外掛,主要考慮以下因素:

  • HanLP 是 Java 分詞包中最為流行的;
  • HanLP 提供了多種分詞器,既可以基於詞典也可以基於模型(在一億字的大型綜合語料庫上訓練的分詞模型);
  • HanLP 堅持使用明文詞典,這樣可以藉助社群的力量對詞典不斷進行完善;
  • 完善的開發文件和程式碼樣例,較為活躍的使用者群體;
  • 個人參與了部分功能的開發,對程式碼結構較為熟悉。

三、開發分詞外掛

1、程式碼結構

  • conf:外掛的配置檔案、HanLP 的配置檔案、Java 安全策略檔案;
  • scr.main.java.assemby:外掛打包(maven-assembly-plugin)配置檔案;
  • org.elasticsearch.plugin.hanlp.analysis:分詞外掛核心構建器;
  • org.elasticsearch.plugin.hanlp.conf:管理外掛配置、分詞器配置以及 HanLP 配置;
  • org.elasticsearch.plugin.hanlp.lucene:HanLP 中文分詞 Lucene 外掛,對 Lucune 分詞進行實現;
  • scr.main.resources:外掛屬性檔案所在目錄

外掛程式碼結構

2、TokenStream

Analyzer 類是一個抽象類,是所有分詞器的基類,它通過 TokenStream 類將文字轉換為詞彙單元流;TokenStream 有兩種實現 Tokenizer(輸入為 Reader) 和 TokenFilter(輸入為另一個 TokenStream)。

文字分詞流程

TokenStream 基本使用流程:

  1. 例項化 TokenStream,向 AttributeSource 新增/獲取屬性(詞彙單元文字、位置增量、偏移量、詞彙型別等);
  2. 呼叫 reset() 方法,將流(stream)重置到原始(clean)狀態;
  3. 迴圈呼叫 incrementToken() 方法,並處理 Attribute 屬性資訊,直到它返回 false 表示流處理結束;
  4. 呼叫 end() 方法,確保流結束(end-of-stream)的操作可以被執行;
  5. 呼叫 close() 方法釋放資源。
// 例項化 TokenStream
TokenStream tokenStream = new IKAnalyzer().tokenStream("keywords",new StringReader("思想者"));
// 向 AttributeSource 新增/獲取屬性
CharTermAttribute attribute = tokenStream.addAttribute(CharTermAttribute.class);
// 將流(stream)重置到原始(clean)狀態
tokenStream.reset();
// 判斷是否還有下一個 Token
while(tokenStream.incrementToken()) {
  System.out.println(attribute);
}
tokenStream.end();
tokenStream.close();
複製程式碼

綜上,開發 Tokenizer 或者 TokenFilter 時,需要重點關注 reset、incrementToken、end、close 四個方法的實現。

3、開發中的小技巧

獲取外掛目錄或檔案目錄

//獲取外掛根目錄
private static Path getPluginPath() {
    return env.pluginsFile().resolve("analysis-hanlp");
}
//獲取外掛目錄下的檔案
private static Path getDefDicConfigPath() {
    return env.pluginsFile().resolve("analysis-hanlp/hanlp.properties").toAbsolutePath();
}
複製程式碼

外掛屬性檔案

如果希望外掛屬性檔案(plugin-descriptor.properties)能夠自動根據 pom.xml 中的屬性進行賦值,則需要將檔案防止到 resources 資料夾下。

外掛版本相容性

從實際測試來看:

  • ES5.X 及其以上的程式碼是完全複用的,也就是說程式碼邏輯不需要調整;
  • ES5.X 到 ES6.2.X 的外掛是可以通用的,其特徵是打包的時候需要將外掛的檔案全部打包到 elasticsearch 資料夾下;
  • ES6.3.X 以上的外掛是可以通用的,打包的時候外掛的檔案全部打包到根目錄即可。

也就是說,如果你升級了新版本 ES,對於外掛升級,大多數情況只需要修改下 plugin-descriptor.properties 檔案中 ES 的版本號即可。

4、安全策略檔案

在外掛開發中經常會使用到檔案讀取、屬性讀取、網路連結等功能,如果不提前註冊安全策略,在呼叫這些功能的時候會報以下錯誤java.security.AccessControlException: access denied

官方給出的解決方案就是新建一個 plugin-security.policy 檔案,然後在檔案中宣告需要的許可權資訊,最後在打包的時候將檔案放置到外掛的根目錄,這樣在使用 zip 包進行安裝的時候,ES 會提示使用者外掛所需的許可權資訊,需要使用者確認後外掛才能正常安裝。

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@     WARNING: plugin requires additional permissions     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
* java.io.FilePermission <<ALL FILES>> read,write,delete
* java.lang.RuntimePermission createClassLoader
* java.lang.RuntimePermission getClassLoader
* java.lang.RuntimePermission setContextClassLoader
* java.net.SocketPermission * connect,resolve
* java.util.PropertyPermission * read,write
See http://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html
for descriptions of what these permissions allow and the associated risks.

Continue with installation? [y/N]y
-> Installed analysis-hanlp
複製程式碼

5、安全策略的坑

最開始認為只需要新增了 policy 檔案,且打包到正確的位置即可解決外掛的許可權問題,因為在外掛安裝的時候 ES 已經提示了所需許可權,但是程式碼在實際執行的時候依舊報 AccessControlException 的錯誤。

參考了多個 HanLP 的 ES 分詞外掛,都沒有獲得較好的方法,後來考慮到 IK 分詞器遠端載入詞典時,需要網路連線許可權,就去看了下其遠端詞典載入的程式碼,最終找到了正確的使用方法。

// 需要特殊許可權的程式碼
AccessController.doPrivileged((PrivilegedAction<Segment>) () -> {
    Segment segment;
    if (config.getAlgorithm().equals("extend")) {
        segment = new ViterbiSegment();
    } else {
        segment = HanLP.newSegment(config.getAlgorithm());
    }
    // 在此處顯示呼叫一下分詞,使得載入詞典、快取詞典的操作可以正確執行
    System.out.println( segment.seg("HanLP中文分詞工具包!"));
    return segment;
});
複製程式碼

四、外掛特色

簡單介紹一下外掛的特點:

  • 內建多種分詞模式,適合不同場景;
  • 內建詞典,無需額外配置即可使用;
  • 支援外接詞典,使用者可自定義分詞演算法,基於詞典或是模型;
  • 支援分詞器級別的自定義詞典,便於用於多租戶場景;
  • 支援遠端詞典熱更新(待開發);
  • 拼音過濾器、繁簡體過濾器(待開發);
  • 基於詞語或單字的 ngram 切分分詞(待開發)。

Github 地址:github.com/AnyListen/e…


Any Code,Code Any!

掃碼關注『AnyCode』,程式設計路上,一起前行。

基於 HanLP 的 ES 中文分詞外掛

相關文章