首先摘抄一段关于IK的特性介绍:

采用了特有的“正向迭代最细粒度切分算法”,具有60万字/秒的高速处理能力。

采用了多子处理器分析模式,支持:英文字母(IP地址、Email、URL)、数字(日期,常用中文数量词,罗马数字,科学计数法),中文词汇(姓名、地名处理)等分词处理。

优化的词典存储,更小的内存占用。支持用户词典扩展定义。

针对Lucene全文检索优化的查询分析器IKQueryParser,采用歧义分析算法优化查询关键字的搜索排列组合,能极大的提高Lucene检索的命中率。


Part1:词典

从上述内容可知,IK是一个基于词典的分词器,首先我们需要了解IK包含哪些词典?如果加载词典?

IK包含哪些词典?

主词典

停用词词典

量词词典

如何加载词典?

IK的词典管理类为Dictionary,单例模式。主要将以文件形式(一行一词)的词典加载到内存。

以上每一类型的词典都是一个DictSegment对象,DictSegment可以理解成树形结构,每一个节点又是一个DictSegment对象。

节点的子节点采用数组(DictSegment[])或map(Map(Character, DictSegment))存储,选用标准根据子节点的数量而定。

如果子节点的数量小于等于ARRAY_LENGTH_LIMIT,采用数组存储;

如果子节点的数量大于ARRAY_LENGTH_LIMIT,采用Map存储。

ARRAY_LENGTH_LIMIT默认为3。

这么做的好处是:

子节点多的节点在向下匹配时(find过程),用Map可以保证匹配效率。

子节点不多的节点在向下匹配时,在保证效率的前提下,用数组节约存储空间。

数组匹配实现如下(二分查找):

int position = Arrays.binarySearch(segmentArray, 0, this.storeSize, keySegment);

其中加载词典的过程如下:

1)加载词典文件

2)遍历词典文件每一行内容(一行一词),将内容进行初处理交给DictSegment进行填充。

初处理:theWord.trim().toLowerCase().toCharArray()

3)DictSegment填充过程

private synchronized void fillSegment(char[] charArray, int begin, int length, int enabled) {
//获取字典表中的汉字对象
Character beginChar = new Character(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;
}
} }

  

/**
* 查找本节点下对应的keyChar的segment *
* @param keyChar
* @param create =1如果没有找到,则创建新的segment ; =0如果没有找到,不创建,返回null
* @return
*/
private DictSegment lookforSegment(Character keyChar, int create) { DictSegment ds = null; if (this.storeSize <= ARRAY_LENGTH_LIMIT) {
//获取数组容器,如果数组未创建则创建数组
DictSegment[] segmentArray = getChildrenArray();
//搜寻数组
DictSegment keySegment = new DictSegment(keyChar);
int position = Arrays.binarySearch(segmentArray, 0, this.storeSize, keySegment);
if (position >= 0) {
ds = segmentArray[position];
} //遍历数组后没有找到对应的segment
if (ds == null && create == 1) {
ds = keySegment;
if (this.storeSize < ARRAY_LENGTH_LIMIT) {
//数组容量未满,使用数组存储
segmentArray[this.storeSize] = ds;
//segment数目+1
this.storeSize++;
Arrays.sort(segmentArray, 0, this.storeSize); } else {
//数组容量已满,切换Map存储
//获取Map容器,如果Map未创建,则创建Map
Map<Character, DictSegment> segmentMap = getChildrenMap();
//将数组中的segment迁移到Map中
migrate(segmentArray, segmentMap);
//存储新的segment
segmentMap.put(keyChar, ds);
//segment数目+1 , 必须在释放数组前执行storeSize++ , 确保极端情况下,不会取到空的数组
this.storeSize++;
//释放当前的数组引用
this.childrenArray = null;
} } } else {
//获取Map容器,如果Map未创建,则创建Map
Map<Character, DictSegment> segmentMap = getChildrenMap();
//搜索Map
ds = segmentMap.get(keyChar);
if (ds == null && create == 1) {
//构造新的segment
ds = new DictSegment(keyChar);
segmentMap.put(keyChar, ds);
//当前节点存储segment数目+1
this.storeSize++;
}
} return ds;
}

(IK作者注释太全面了,不再做赘述!)  

举个例子,例如“人民共和国”的存储结构如下图:


Part2:分词

IK的分词主类是IKSegmenter,他包括如下重要属性:

Read:待分词内容

Configuration:分词器配置,主要控制是否智能分词,非智能分词能细粒度输出所有可能的分词结果,智能分词能起到一定的消歧作用。

AnalyzerContext:分词器上下文,这是个难点。其中包含了字符串缓冲区、字符串类型数组、缓冲区位置指针、子分词器锁、原始分词结果集合等。

List<ISegment>:分词处理器列表,目前IK有三种类型的分词处理器,如下:

  •   CJKSegmenter:中文-日韩文子分词器
  •   CN_QuantifierSegmenter:中文数量词子分词器
  •   LetterSegmenter:英文字符及阿拉伯数字子分词器

IKArbitrator:分词歧义裁决器

在IKSegment中主要的方法是next(),如下:

/**
* 分词,获取下一个词元
* @return Lexeme 词元对象
* @throws IOException
*/
public synchronized Lexeme next() throws IOException {
if (this.context.hasNextResult()) {
//存在尚未输出的分词结果
return this.context.getNextLexeme();
} else {
/*
* 从reader中读取数据,填充buffer
* 如果reader是分次读入buffer的,那么buffer要进行移位处理
* 移位处理上次读入的但未处理的数据
*/
int available = context.fillBuffer(this.input);
if (available <= 0) {
//reader已经读完
context.reset();
return null; } else {
//初始化指针
context.initCursor();
do {
//遍历子分词器
for (ISegmenter segmenter : segmenters) {
segmenter.analyze(context);
}
//字符缓冲区接近读完,需要读入新的字符
if (context.needRefillBuffer()) {
break;
}
//向前移动指针
} while (context.moveCursor());
//重置子分词器,为下轮循环进行初始化
for (ISegmenter segmenter : segmenters) {
segmenter.reset();
}
}
//对分词进行歧义处理
this.arbitrator.process(context, this.cfg.useSmart());
//处理未切分CJK字符
context.processUnkownCJKChar();
//记录本次分词的缓冲区位移
context.markBufferOffset();
//输出词元
if (this.context.hasNextResult()) {
return this.context.getNextLexeme();
}
return null;
}
}

这个过程主要做3件事:

1)将输入读入缓冲区(AnalyzerContext.fillBuffer());

2)移动缓冲区指针,同时对指针所指字符进行处理(进行字符规格化-全角转半角、大写转小写处理)以及类型判断(识别字符类型),将所指字符交由子分词器进行处理;

3)字符缓冲区接近读完时停止移动缓冲区指针,对当前分词器上下文(AnalyzerContext)中的原始分词结果进行歧义消除、处理一些残余字符,为下一次读入缓冲区做准备。最后输出词条。

在这个过程中,一些中间状态都记录在分词器上下文当中,可以理解IK作者当时的设计思路。

在上面next()方法当中,最主要的步骤是调用各个子分词器的analyze()方法,这里重点介绍CJKSegmenter,如下:

public void analyze(AnalyzeContext context) {
if (CharacterUtil.CHAR_USELESS != context.getCurrentCharType()) { //优先处理tmpHits中的hit
if (!this.tmpHits.isEmpty()) {
//处理词段队列
Hit[] tmpArray = this.tmpHits.toArray(new Hit[this.tmpHits.size()]);
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); if (!hit.isPrefix()) {//不是词前缀,hit不需要继续匹配,移除
this.tmpHits.remove(hit);
} } 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);
} } else {
//遇到CHAR_USELESS字符
//清空队列
this.tmpHits.clear();
} //判断缓冲区是否已经读完
if (context.isBufferConsumed()) {
//清空队列
this.tmpHits.clear();
} //判断是否锁定缓冲区
if (this.tmpHits.size() == 0) {
context.unlockBuffer(SEGMENTER_NAME); } else {
context.lockBuffer(SEGMENTER_NAME);
}
}

这里需要注意tmpHits,在匹配的过程中属于前缀匹配的临时放入tmpHits,hit中记录词典匹配过程中当前匹配到的词典分支节点,可以继续匹配。

在遍历tmpHits的过程中,如果不是前缀词(全匹配)、或者不匹配则从tmpHits中移除。遇到遇到CHAR_USELESS字符、或者缓冲队列已经读完,则清空tmpHits。

是否匹配由DictSegment的match()方法决定。

(时时刻刻想想那棵字典树!)

什么时候上下文会收集临时词条呢?

1)首字成词的情况(如果首字还是前缀词,同时加入tmpHits,待后继处理)

2)在遍历tmpHits的过程中如果“全匹配”,也会加入临时词条。

下面再了解下match()方法,如下:

/**
* 匹配词段
* @param charArray
* @param begin
* @param length
* @param searchHit
* @return Hit
*/
Hit match(char[] charArray, int begin, int length, Hit searchHit) { if (searchHit == null) {
//如果hit为空,新建
searchHit = new Hit();
//设置hit的其实文本位置
searchHit.setBegin(begin);
} else {
//否则要将HIT状态重置
searchHit.setUnmatch();
}
//设置hit的当前处理位置
searchHit.setEnd(begin); Character keyChar = new Character(charArray[begin]);
DictSegment ds = null; //引用实例变量为本地变量,避免查询时遇到更新的同步问题
DictSegment[] segmentArray = this.childrenArray;
Map<Character, DictSegment> segmentMap = this.childrenMap; //STEP1 在节点中查找keyChar对应的DictSegment
if (segmentArray != null) {
//在数组中查找
DictSegment keySegment = new DictSegment(keyChar);
int position = Arrays.binarySearch(segmentArray, 0, this.storeSize, keySegment);
if (position >= 0) {
ds = segmentArray[position];
} } else if (segmentMap != null) {
//在map中查找
ds = segmentMap.get(keyChar);
} //STEP2 找到DictSegment,判断词的匹配状态,是否继续递归,还是返回结果
if (ds != null) {
if (length > 1) {
//词未匹配完,继续往下搜索
return ds.match(charArray, begin + 1, length - 1, searchHit);
} else if (length == 1) { //搜索最后一个char
if (ds.nodeState == 1) {
//添加HIT状态为完全匹配
searchHit.setMatch();
}
if (ds.hasNextNode()) {
//添加HIT状态为前缀匹配
searchHit.setPrefix();
//记录当前位置的DictSegment
searchHit.setMatchedDictSegment(ds);
}
return searchHit;
} }
//STEP3 没有找到DictSegment, 将HIT设置为不匹配
return searchHit;
}

注意hit几个状态的判断:

//Hit不匹配
private static final int UNMATCH = 0x00000000;
//Hit完全匹配
private static final int MATCH = 0x00000001;
//Hit前缀匹配
private static final int PREFIX = 0x00000010;

在进入match方法时,hit都会被重置为unMatch,然后根据Character获取子节点集合的节点。

如果节点为NULL,hit状态就是unMatch。

如果节点存在,且nodeState为1,hit状态就是match,

同时还要判断节点的子节点数量是否大于0,如果大于0,hit状态还是prefix。

(时时刻刻想想那棵字典树!)

对一次buffer处理完后,需要对上下文中的临时分词结果进行消歧处理(具体下文再分析)、词条输出。

在词条输出的过程中,需要判断每一个词条是否match停用词表,如果match则抛弃该词条。


Part3:消歧

稍等!

IKAnalyzer 源码走读的更多相关文章

  1. Apache Spark源码走读之23 -- Spark MLLib中拟牛顿法L-BFGS的源码实现

    欢迎转载,转载请注明出处,徽沪一郎. 概要 本文就拟牛顿法L-BFGS的由来做一个简要的回顾,然后就其在spark mllib中的实现进行源码走读. 拟牛顿法 数学原理 代码实现 L-BFGS算法中使 ...

  2. Apache Spark源码走读之16 -- spark repl实现详解

    欢迎转载,转载请注明出处,徽沪一郎. 概要 之所以对spark shell的内部实现产生兴趣全部缘于好奇代码的编译加载过程,scala是需要编译才能执行的语言,但提供的scala repl可以实现代码 ...

  3. Apache Spark源码走读之13 -- hiveql on spark实现详解

    欢迎转载,转载请注明出处,徽沪一郎 概要 在新近发布的spark 1.0中新加了sql的模块,更为引人注意的是对hive中的hiveql也提供了良好的支持,作为一个源码分析控,了解一下spark是如何 ...

  4. Apache Spark源码走读之7 -- Standalone部署方式分析

    欢迎转载,转载请注明出处,徽沪一郎. 楔子 在Spark源码走读系列之2中曾经提到Spark能以Standalone的方式来运行cluster,但没有对Application的提交与具体运行流程做详细 ...

  5. twitter storm 源码走读之5 -- worker进程内部消息传递处理和数据结构分析

    欢迎转载,转载请注明出处,徽沪一郎. 本文从外部消息在worker进程内部的转化,传递及处理过程入手,一步步分析在worker-data中的数据项存在的原因和意义.试图从代码实现的角度来回答,如果是从 ...

  6. storm-kafka源码走读之KafkaSpout

    from: http://blog.csdn.net/wzhg0508/article/details/40903919 (五)storm-kafka源码走读之KafkaSpout 原创 2014年1 ...

  7. spring-data-redis-cache 使用及源码走读

    预期读者 准备使用 spring 的 data-redis-cache 的同学 了解 @CacheConfig,@Cacheable,@CachePut,@CacheEvict,@Caching 的使 ...

  8. ConcurrentHashMap源码走读

    目录 ConcurrentHashMap源码走读 简介 放入数据 容器元素总数更新 容器扩容 协助扩容 遍历 ConcurrentHashMap源码走读 简介 在从JDK8开始,为了提高并发度,Con ...

  9. underscorejs 源码走读笔记

    Underscore 简介 Underscore 是一个JavaScript实用库,提供了类似Prototype.js的一些功能,但是没有继承任何JavaScript内置对象.它弥补了部分jQuery ...

随机推荐

  1. Python算法——递归思想

    编程语言在构建程序时的基本操作有:内置数据类型操作.选择.循环.函数调用等,递归实际属于函数调用的一种特殊情况(函数调用自身),其数学基础是数学归纳法.递归在计算机程序设计中非常重要,是许多高级算法实 ...

  2. python------模块定义、导入、优化 ------->random模块

    2.random模块 #随机浮点数 random.random()   #生成0到1之间的随机浮点数,不能自己指定 random.uniform(1,10)   #可以指定 #随机整数 random. ...

  3. Go Example--递归

    package main import "fmt" func main() { fmt.Println(fact(7)) } //函数的递归 func fact(n int) in ...

  4. struts2(二)值栈 threadlocal ogal ui

    值栈(重要)和ognl表达式 1.  只要是一个mvc框架,必须解决数据的存和取的问题 2.  Struts2利用值栈来存数据,所以值栈是一个存储数据的内存结构 3.  把数据存在值栈中,在页面上利用 ...

  5. golang-test-tool-gotests

    gotests介绍 gotests是一个Golang命令行工具 ,让Go测试变得容易.它根据目标源文件的函数和方法签名生成表驱动的测试(TDD).任何测试文件中新的依赖都会被自动倒入 Demo 下面是 ...

  6. gtk_init()

    #include<stdio.h> #if 0int main(int argc, char *argv[]){ char ***p = &argv; //传参退化成二级指针,对二 ...

  7. Unity 5.x Shader and Effects Cookbook(2nd) (Alan Zucconi Kenneth Lammers 著)

    1. Creating Your First Shader 2. Surface Shaders and Texture Mapping 3. Understanding Lighting Model ...

  8. APP前端易用性和UI测试

    移动APP使用场景的特点 1.屏幕小: 与Web系统相比,APP安装在手机端,展示屏幕只有几英寸,能够展示的信息就显得非常有限和珍贵,我们需要将有价值的信息放大,放在显眼的位置. 2.场景复杂化: 由 ...

  9. 设计一个 硬件 实现的 Dictionary(字典)

    Dictionary 就是 字典, 是一种可以根据 Key 来 快速 查找 Value 的 数据结构 . 比如 我们在 C# 里用到的 Dictionary<T>, 在 程序设计 里, 字 ...

  10. node api 之:stream - 流

    stream 模块可以通过以下方式使用: const stream = require('stream'); 流可以是可读的.可写的.或者可读可写的. 所有的流都是 EventEmitter 的实例. ...