基于IKAnalyzer實現一個Elasticsearch中文分詞插件
雖然Elasticsearch有原生的中文插件elasticsearch-analysis-smartcn(實際上是lucence的org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer),但它似乎沒能滿足我的要求。比如我希望對文檔中的“林夕”不分詞(就是不要把它當成“林”,“夕”兩個字索引),smartcn沒法做到。
然后我找到了IK,以及elasticsearch-analysis-ik。elasticsearch-analysis-ik已經有些時候沒人維護了。而且它使用的httpclient來獲取分詞詞典。總之各種糾結。
最后,我決定還是自己寫一個吧。
原來IKAnalyzer的目錄結構 ├── IKAnalyzer.cfg.xml
├── ext.dic
├── org
│ └── wltea
│ └── analyzer
│ ├── cfg
│ │ ├── Configuration.java
│ │ └── DefaultConfig.java
│ ├── core
│ │ ├── AnalyzeContext.java
│ │ ├── CJKSegmenter.java
│ │ ├── CN_QuantifierSegmenter.java
│ │ ├── CharacterUtil.java
│ │ ├── IKArbitrator.java
│ │ ├── IKSegmenter.java
│ │ ├── ISegmenter.java
│ │ ├── LetterSegmenter.java
│ │ ├── Lexeme.java
│ │ ├── LexemePath.java
│ │ └── QuickSortSet.java
│ ├── dic
│ │ ├── DictSegment.java
│ │ ├── Dictionary.java
│ │ ├── Hit.java
│ │ ├── main2012.dic
│ │ └── quantifier.dic
│ ├── lucene
│ │ ├── IKAnalyzer.java
│ │ └── IKTokenizer.java
│ ├── query
│ │ ├── IKQueryExpressionParser.java
│ │ └── SWMCQueryBuilder.java
│ ├── sample
│ │ └── IKAnalyzerDemo.java
│ └── solr
│ └── IKTokenizerFactory.java
└── stopword.dic
加入構建腳本
我發現沒有使用任何的構建工具。我不是說不使用構建工具就是不好,而是我已經習慣了使用構建工具,不用就沒有安全感。所以,我第一步是給它加構建腳本。
同時,我把原來的IKAnalyzerDemo.java改成兩個測試類。最后運行測試,確保我的修改沒有破壞原有邏輯
└── src ├── main │ ├── java │ │ └── ...... │ └── resources │ ├── IKAnalyzer.cfg.xml │ ├── main2012.dic │ ├── quantifier.dic │ └── stopword.dic └── test ├── java │ └── org │ └── wltea │ └── analyzer │ ├── IKAnalzyerTest.java │ └── LuceneIndexAndSearchTest.java └── resources ├── IKAnalyzer.cfg.xml ├── main2012.dic ├── quantifier.dic └── stopword.dic
build.gradle
apply plugin: 'java' //apply plugin: 'checkstyle' apply plugin: 'idea' sourceCompatibility = 1.7 version = '1.0' repositories { mavenCentral() } dependencies { compile( 'org.apache.lucene:lucene-core:4.10.4', 'org.apache.lucene:lucene-queryparser:4.10.4', 'org.apache.lucene:lucene-analyzers-common:4.10.4' ) testCompile group: 'junit', name: 'junit', version: '4.11' }
將項目拆成core和lucence兩個子項目
我發現IK實際上由兩部分組成:真正的分詞邏輯和擴展Lucence分析器的邏輯。可以想象得到
- 我們需要支持不同版本的Lucence
- 我們可以把IK的分詞邏輯應用到其它的搜索引擎上
基于這兩點,我決定把原有的項目分成兩個子項目。并加上測試:
├── build.gradle ├── ik-analyzer-core │ ├── build.gradle │ └── src │ ├── main │ │ ├── java │ │ │ └── ..... │ │ └── resources │ └── test ├── ik-analyzer-lucence │ ├── build.gradle │ └── src │ ├── main │ │ └── java │ │ └── org │ │ └── wltea │ │ └── analyzer │ │ ├── lucene │ │ │ ├── IKAnalyzer.java │ │ │ └── IKTokenizer.java │ │ └── query │ │ ├── IKQueryExpressionParser.java │ │ └── SWMCQueryBuilder.java │ └── test │ ├── java │ │ └── ..... └── settings.gradle
創建Elasticsearch插件
一開始,我還想讓Elasticsearch插件只依賴core子項目就好了。誰知道要實現Elasticsearch的插件還需要依賴Lucence。所以Elasticsearch插件需要依賴lucence子項目。
實現的過程發現Elasticsearch的版本之間有些不同,你可以對比下:AnalysisIkPlugin.java和IKAnalyzerPlugin.java
目前,Elasticsearch文檔中,關于它的插件的概念和原理說的都非常少!
├── build.gradle ├── ik-analyzer-core │ ├── ...... ├── ik-analyzer-elasticseaarch-plugin │ ├── build.gradle │ └── src │ └── main │ ├── java │ │ └── org │ │ └── elasticsearch │ │ └── plugin │ │ └── ikanalyzer │ │ ├── IKAnalyzerComponent.java │ │ ├── IKAnalyzerModule.java │ │ └── IKAnalyzerPlugin.java │ └── resources │ └── es-plugin.properties ├── ik-analyzer-lucence │ ├── ..... └── settings.gradle
## es-plugin.properties
plugin=org.elasticsearch.plugin.ikanalyzer.IKAnalyzerPlugin
重構Core子項目
目前IK還有一個問題沒有解決:靈活擴展現有的詞典。比如我希望將“林夕”加入詞典,從而使其不分被索引成“林”,“夕”。這樣的應用場景非常多的。以至于elasticsearch-analysis-ik自己實現從遠程讀取詞典的功能:Dictionary.java:338
但是我覺得這樣還是夠好。比如,我期望從本地的sqlite中讀取詞典呢?所以,我將IK原有的關于配置的讀取的邏輯抽取出來:
/** * 加載主詞典及擴展詞典 */ private void loadMainDict(){ //建立一個主詞典實例 _MainDict = new DictSegment((char)0); //讀取主詞典文件 InputStream is = this.getClass().getClassLoader().getResourceAsStream(cfg.getMainDictionary()); if(is == null){ throw new RuntimeException("Main Dictionary not found!!!"); } try { BufferedReader br = new BufferedReader(new InputStreamReader(is , "UTF-8"), 512); String theWord = null; do { theWord = br.readLine(); if (theWord != null && !"".equals(theWord.trim())) { _MainDict.fillSegment(theWord.trim().toLowerCase().toCharArray()); } } while (theWord != null); } catch (IOException ioe) { System.err.println("Main Dictionary loading exception."); ioe.printStackTrace(); }finally{ try { if(is != null){ is.close(); is = null; } } catch (IOException e) { e.printStackTrace(); } } //加載擴展詞典 this.loadExtDict(); }
其中cfg.getMainDictionary(),cfg是一個接口Configuration的實例,但是Dictionary假設getMainDictionary返回的一個文件的路徑。所以,我認為這個接口的設計是沒有意義的。
我們為什么不讓cfg.getMainDictionary()直接Dictionary要求的詞典內容呢,像這樣:
/** * 加載主詞典及擴展詞典 */ private void loadMainDict() { //建立一個主詞典實例 _MainDict = new DictSegment((char) 0); for (char[] segment : cfg.loadMainDictionary()) { _MainDict.fillSegment(segment); } }
這樣,我們就可以實現像FileConfiguration,HttpConfiguraion,SqliteConfiguration,RedisConfiguration等任何你期望的擴展詞典方式了。
但是,目前,我還沒有實現任何的一種 :P
小結
實際上的重構和本文寫的相差無幾。還算比較順利,要感謝原作者。這里,我還有一個問題沒想通,就是如何打包才能讓大家都方便用,比如方便在Elasticsearch中安裝。希望大家能多給建議。
重構后的項目地址是:https://github.com/zacker330/ik-analyzer
來自:http://my.oschina.net/zjzhai/blog/425484