ElasticSearch 如何使用 ik 进行中文分词?

image.png

你们好,我是历小冰。在《为何 ElasticSearch 比 MySQL 更适合复杂条件搜索》 一文中,咱们讲解了 ElasticSearch 如何在数据存储方面支持全文搜索和复杂条件查询,本篇文章则着重分析 ElasticSearch 在全文搜索前如何使用 ik 进行分词,让你们对 ElasticSearch 的全文搜索和 ik 中文分词原理有一个全面且深刻的了解。html

全文搜索和精确匹配

ElasticSearch 支持对文本类型数据进行全文搜索和精确搜索,可是必须提早为其设置对应的类型:java

  • keyword 类型,存储时不会作分词处理,支持精确查询和分词匹配查询;
  • text 类型,存储时会进行分词处理,也支持精确查询和分词匹配查询。

好比,建立名为 article 的索引(Index),并为其两个字段(Filed)配置映射(Mapping),文章内容设置为 text 类型,而文章标题设置为 keyword 类型。node

curl -XPUT http://localhost:9200/article
curl -XPOST http://localhost:9200/article/_mapping -H 'Content-Type:application/json' -d'
{
        "properties": {
            "content": {
                "type": "text"
            },
            "title": {
                "type": "keyword"
            }
        }

}'

Elasticsearch 在进行存储时,会对文章内容字段进行分词,获取并保存分词后的词元(tokens);对文章标题则是不进行分词处理,直接保存原值。git

image.png

上图的右半边展现了 keyword 和 text 两种类型的不一样存储处理过程。而左半边则展现了 ElasticSearch 相对应的两种查询方式:程序员

  • term 查询,也就是精确查询,不进行分词,而是直接根据输入词进行查询;
  • match 查询,也就是分词匹配查询,先对输入词进行分词,而后逐个对分词后的词元进行查询。

举个例子,有两篇文章,一篇的标题和内容都是“程序员”,另一篇的标题和内容都是“程序”,那么两者在 ElasticSearch 中的倒排索引存储以下所示(假设使用特殊分词器)。github

image.png

这时,分别使用 term 和 match 查询对两个字段进行查询,就会得出如图右侧的结果。算法

Analyzer 处理过程

可见,keyword 与 text 类型, term 与 match 查询方式之间不一样就在因而否进行了分词。在 ElasticSearch 中将这个分词的过程统称了 Text analysis,也就是将字段从非结构化字符串(text)转化为结构化字符串(keyword)的过程。编程

Text analysis 不只仅只进行分词操做,而是包含以下流程:json

  • 使用字符过滤器(Character filters),对原始的文本进行一些处理,例如去掉空白字符等;
  • 使用分词器(Tokenizer),对原始的文本进行分词处理,获得一些词元(tokens);
  • 使用词元过滤器(Token filters),对上一步获得的词元继续进行处理,例如改变词元(小写化),删除词元(删除量词)或增长词元(增长同义词),合并同义词等。

image.png

ElasticSearch 中处理 Text analysis 的组件被称为 Analyzer。相应地,Analyzer 也由三部分组成,character filters、tokenizers 和 token filters。网络

Elasticsearch 内置了 3 种字符过滤器、10 种分词器和 31 种词元过滤器。此外,还能够经过插件机制获取第三方实现的相应组件。开发者能够按照自身需求定制 Analyzer 的组成部分。

"analyzer": {
    "my_analyzer": {
        "type":           "custom",
        "char_filter":  [ "html_strip"],
        "tokenizer":      "standard",
        "filter":       [ "lowercase",]
    }
}

按照上述配置,my_analyzer 分析器的功能大体以下:

  • 字符过滤器是 html_strip,会去掉 HTML 标记相关的字符;
  • 分词器是 ElasticSearch 默认的标准分词器 standard
  • 词元过滤器是小写化 lowercase 处理器,将英语单词小写化。

通常来讲,Analyzer 中最为重要的就是分词器,分词结果的好坏会直接影响到搜索的准确度和满意度。ElasticSearch 默认的分词器并非处理中文分词的最优选择,目前业界主要使用 ik 进行中文分词。

ik 分词原理

ik 是目前较为主流的 ElasticSearch 开源中文分词组件,它内置了基础的中文词库和分词算法帮忙开发者快速构建中文分词和搜索功能,它还提供了扩展词库字典和远程字典等功能,方便开发者扩充网络新词或流行语。

ik 提供了三种内置词典,分别是:

  • main.dic:主词典,包括平常的通用词语,好比程序员和编程等;
  • quantifier.dic:量词词典,包括平常的量词,好比米、公顷和小时等;
  • stopword.dic:停用词,主要指英语的停用词,好比 a、such、that 等。

此外,开发者能够经过配置扩展词库字典和远程字典对上述词典进行扩展。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
  <comment>IK Analyzer 扩展配置</comment>
  <!--用户能够在这里配置本身的扩展字典 -->
  <entry key="ext_dict">custom/mydict.dic</entry>
   <!--用户能够在这里配置本身的扩展中止词字典-->
  <entry key="ext_stopwords">custom/ext_stopword.dic</entry>
   <!--用户能够在这里配置远程扩展字典 -->
  <entry key="remote_ext_dict">location</entry>
   <!--用户能够在这里配置远程扩展中止词字典-->
  <entry key="remote_ext_stopwords">http://xxx.com/xxx.dic</entry>
</properties>

ik 跟随 ElasticSearch 启动时,会将默认词典和扩展词典读取并加载到内存,并使用字典树 tire tree (也叫前缀树)数据结构进行存储,方便后续分词时使用。

image.png

字典树的典型结构如上图所示,每一个节点是一个字,从根节点到叶节点,路径上通过的字符链接起来,为该节点对应的词。因此上图中的词包括:程序员、程门立雪、编织、编码和工做。

1、加载字典

ik 的 Dictionary 单例对象会在初始化时,调用对应的 load 函数读取字典文件,构造三个由 DictSegment 组成的字典树,分别是 MainDictQuantifierDictStopWords。咱们下面就来看一下其主词典的加载和构造过程。loadMainDict 函数较为简单,它会首先建立一个 DictSegment 对象做为字典树的根节点,而后分别去加载默认主字典,扩展主字典和远程主字典来填充字典树。

private void loadMainDict() {
    // 创建一个主词典实例
    _MainDict = new DictSegment((char) 0);

    // 读取主词典文件
    Path file = PathUtils.get(getDictRoot(), Dictionary.PATH_DIC_MAIN);
    loadDictFile(_MainDict, file, false, "Main Dict");
    // 加载扩展词典
    this.loadExtDict();
    // 加载远程自定义词库
    this.loadRemoteExtDict();
}

loadDictFile 函数执行过程当中,会从词典文件读取一行一行的词,交给 DictSegmentfillSegment 函数处理。

fillSegment 是构建字典树的核心函数,具体实现以下所示,处理逻辑大体有以下几个步骤:

  • 1、按照索引,获取词中的一个字;
  • 2、检查当前节点的子节点中是否有该字,若是没有,则将其加入到 charMap中;
  • 3、调用 lookforSegment 函数在字典树中寻找表明该字的节点,若是没有则插入一个新的;
  • 4、递归调用 fillSegment 函数处理下一个字。
private synchronized void fillSegment(char[] charArray , int begin , int length , int enabled){
    //获取字典表中的汉字对象
    Character beginChar = Character.valueOf(charArray[begin]);
    Character keyChar = charMap.get(beginChar);
    //字典中没有该字,则将其添加入字典
    if(keyChar == null){
        charMap.put(beginChar, beginChar);
        keyChar = beginChar;
    }
    
    //搜索当前节点的存储,查询对应keyChar的keyChar,若是没有则建立
    DictSegment ds = lookforSegment(keyChar , enabled);
    if(ds != null){
        //处理keyChar对应的segment
        if(length > 1){
            //词元尚未彻底加入词典树
            ds.fillSegment(charArray, begin + 1, length - 1 , enabled);
        }else if (length == 1){
            //已是词元的最后一个char,设置当前节点状态为enabled,
            //enabled=1代表一个完整的词,enabled=0表示从词典中屏蔽当前词
            ds.nodeState = enabled;
        }
    }
}

ik 初始化过程大体如此,再进一步详细的逻辑你们能够直接去看源码,中间都是中文注释,相对来讲较为容易阅读。

2、分词逻辑

ik 中实现了 ElasticSearch 相关的抽象类,来提供自身的分词逻辑实现:

  • IKAnalyzer 继承了 Analyzer ,用来提供中文分词的分析器;
  • IKTokenizer 继承了 Tokenizer,用来提供中文分词的分词器,其 incrementToken 是 ElasticSearch 调用 ik 进行分词的入口函数。

incrementToken 函数会调用 IKSegmenternext方法,来获取分词结果,它是 ik 分词的核心方法。

image.png

如上图所示,IKSegmenter 中有三个分词器,在进行分词时会遍历词中的全部字,而后将单字按照顺序,让三个分词器进行处理:

  • LetterSegmenter,英文分词器比较简单,就是把连续的英文字符进行分词;
  • CN_QuantifierSegmenter,中文量词分词器,判断当前的字符是不是数词和量词,会把连起来的数词和量词分红一个词;
  • CJKSegmenter,核心分词器,基于前文的字典树进行分词。

咱们只讲解一下 CJKSegmenter 的实现,其 analyze 函数大体分为两个逻辑:

  • 根据单字去字典树中进行查询,若是单字是词,则生成词元;若是是词前缀,则放入到临时命中列表中;
  • 而后根据单字和以前处理时保存的临时命中列表数据一块儿去字典树中查询,若是命中,则生成词元。
public void analyze(AnalyzeContext context) {
            
    //优先处理tmpHits中的hit,根据单字和 hit 一块儿去查询
    if(!this.tmpHits.isEmpty()){
        ....
        for(Hit hit : tmpArray){
            hit = Dictionary.getSingleton().matchWithHit(context.getSegmentBuff(), context.getCursor() , hit);
            if(hit.isMatch()){
                //输出当前的词
                Lexeme newLexeme = new Lexeme(context.getBufferOffset() , hit.getBegin() , context.getCursor() - hit.getBegin() + 1 , Lexeme.TYPE_CNWORD);
                context.addLexeme(newLexeme);
                ....                 
            }else if(hit.isUnmatch()){
                //hit不是词,移除
                this.tmpHits.remove(hit);
            }                    
        }
    }            
    
    //*********************************
    //再对当前指针位置的字符进行单字匹配
    Hit singleCharHit = Dictionary.getSingleton().matchInMainDict(context.getSegmentBuff(), context.getCursor(), 1);
    if(singleCharHit.isMatch()){//首字成词
        //输出当前的词
        Lexeme newLexeme = new Lexeme(context.getBufferOffset() , context.getCursor() , 1 , Lexeme.TYPE_CNWORD);
        context.addLexeme(newLexeme);

        //同时也是词前缀
        if(singleCharHit.isPrefix()){
            //前缀匹配则放入hit列表
            this.tmpHits.add(singleCharHit);
        }
    }else if(singleCharHit.isPrefix()){//首字为词前缀
        //前缀匹配则放入hit列表
        this.tmpHits.add(singleCharHit);
    }
    .... // 判断是否结束,清理工做
}

具体的代码逻辑,如上所示。为了方便你们理解,举个例子,好比输入的词是 编码工做

  • 首先处理
  • 由于当前 tmpHits 为空,直接进行单字判断;
  • 直接拿 字去前文示意图的字典树查询(详见 matchInMainDict 函数),发现可以命中,而且该字不是一个词的结尾,因此将 和其在输入词中的位置生成 Hit 对象,存储到 tmpHits 中。
  • 接着处理
  • 由于 tmpHits 不为空,因此拿着 对应的 Hit 对象和 字去字典树中查询(详见 matchWithHit 函数), 发现命中了 编码 一词,因此将这个词做为输出词元之一,存入 AnalyzeContext;可是由于 已是叶节点,并无子节点,表示不是其余词的前缀,因此将对应的 Hit 对象删除掉;
  • 接着拿单字 去字典树中查询,看单字是否成词,或者构成词的前缀。
  • 依次类推,将全部字处理完。

3、消除歧义和结果输出

经过上述步骤,有时候会生成不少分词结果集合,好比说,程序员爱编程 会被分红 程序员程序编程 五个结果。这也是 ik 的 ik_max_word 模式的输出结果。可是有些场景,开发者但愿只有 程序员编程 三个分词结果,这时就须要使用 ik 的 ik_smart 模式,也就是进行消除歧义处理。

ik 使用 IKArbitrator 进行消除歧义处理,主要使用组合遍历的方式进行处理。从上一阶段的分词结果中取出不相交的分词集合,所谓相交,就是其在文本中出现的位置是否重合。好比 程序员程序 三个分词结果是相交的,可是 编程 是不相交的。因此分歧处理时会将 程序员程序 做为一个集合, 做为一个集合,编码 做为一个集合,分别进行处理,将集合中按照规则优先级最高的分词结果集选出来,具体规则以下所示:

  • 有效文本长度长优先;
  • 词元个数少优先;
  • 路径跨度大优先;
  • 位置越靠后的优先,由于根据统计学结论,逆向切分几率高于正向切分;
  • 词长越平均优先;
  • 词元位置权重大优先。

根据上述规则,在第一个集合中,程序员 明显要比 程序 要更符合规则,因此消除歧义的结果就是输出 程序员,而不是 程序

最后,对于输入字来讲,有些位置可能并不在输出结果中,因此会以单字的方式做为词元直接输出(详见AnalyzeContextoutputToResult 函数)。好比 程序员是职业 字是不会被分词出来的,可是在最终输出结果时,要将其做为单字输出。

后记

ElasticSearch 和 ik 组合是目前较为主流的中文搜索技术方案,理解其搜索和分词的基础流程和原理,有利于开发者更快地构建中文搜索功能,或基于自身需求,特殊定制搜索分词策略。

image.png

相关文章
相关标签/搜索