基于IKAnalyzer實現一個Elasticsearch中文分詞插件

碼頭工人 9年前發布 | 34K 次閱讀 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分析器的邏輯。可以想象得到

  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


 本文由用戶 碼頭工人 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!