基于IKAnalyzer实现一个Elasticsearch中文分词插件

admin 9年前

虽然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分析器的逻辑。可以想象得到

  1. 我们需要支持不同版本的Lucence
  2. 我们可以把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.javaIKAnalyzerPlugin.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