虽然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中安装。希望大家能多给建议。
文章转载自 开源中国社区 [http://www.oschina.net]