IK VS ansj
| 分词器 | elasticsearch-analysis-ik | elasticsearch-analysis-ansj |
|---|---|---|
| github | github.com/medcl/elast… | github.com/NLPchina/el… |
| stars | 10516 | 528 |
| issues | 260 | 15 |
| 最后更新 | 2019.3 | 2020.7 |
| 分词器 | ik_max_word/ik_smart | index_ansj/query_ansj |
Ik分词原理
词典加载
IK定义了三类词典:
-
_MainDict;
-
_QuantifierDict;
-
_StopWords;
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();
}
_MainDict和_StopWords词典加载时除了加载IK自带的词典,同时还会加载用户在IKAnalyzer.cfg.xml中自定义的本地词典和远程词典。同时IK创建了监听线程,每隔60s重新加载一次_MainDict和_StopWords。
词典数据结构
IK字典采用字典树结构,DicSegment代表字典树的一个分枝。
//数组大小上限
private static final int ARRAY_LENGTH_LIMIT = 3;
//Map存储结构
private Map<Character , DictSegment> childrenMap;
//数组方式存储结构
private DictSegment[] childrenArray;
根据ARRAY_LENGTH_LIMIT作为阈值来存储方式,如果当子节点数,不太于阈值时,采用数组的方式childrenArray来存储,当子节点数大于阈值时,采用Map的方式childrenMap来存储。查找的时候hashmao可以获得O(1)的算法复杂度。
切分词元
Lexeme(词元),就可以理解为是一个词语或个单词。Lexeme类实现Comparable的,起始位置靠前的优先,长度较长的优先,这可以用来决定一个词在一条分词结果的词元链中的位置
public int compareTo(Lexeme other) {
//起始位置优先
if(this.begin < other.getBegin()){
return -1;
}else if(this.begin == other.getBegin()){
//词元长度优先
if(this.length > other.getLength()){
return -1;
}else if(this.length == other.getLength()){
return 0;
}else {//this.length < other.getLength()
return 1;
}
}else{//this.begin > other.getBegin()
return 1;
}
}
IK中默认用到三个子分词器,分别是LetterSegmenter(字母分词器),CN_QuantifierSegment(量词分词器),CJKSegmenter(中日韩分词器)。
CN_QuantifierSegmenter的词典来源两个地方:1.quantifier.dic文件,包含量词 2.数词直接写到ChnNumberChars类中了,内容如下:"一二两三四五六七八九十零壹贰叁肆伍陆柒捌玖拾百千万亿拾佰仟萬億兆卅廿"
LetterSegmenter分别有三个类似的处理器:字母、数字、字母和数字的组合。
输入词循环单字经过分词器,匹配的结果一共三种UNMATCH(未匹配),MATCH(匹配), PREFIX(前缀匹配),Match指完全匹配已经到达叶子节点,而PREFIX是指当前对上所经过的匹配路径存在,但未到达到叶子节点。此外一个词也可以既是MATCH也可以是PREFIX。前缀匹配的都被存入了tempHit中去。而完整匹配的都存入context中保存。
歧义判断
IKArbitrator(歧义分析裁决器)是处理歧义的主要类。ik_smart模式会进行歧义判断,ik_max_word会返回所有的分词词元。
ik解决歧义流程:
1.贪心选择其中不相交的分词结果,存放到分词候选结果集option中
2.把存在歧义的词,也就是和option中的词相交的词放入conflickStack中
3.从conflictStack中选出一个歧义词c,从option结尾回滚option词元链,直到能放下词c
4.从词c的位置执行forwardPath,生成一个可选分词结果集
5.直到conflictStack中的所有歧义词处理完毕
通过上面方式生成分词结果集,对结果集排序,取最优:
1)比较有效文本长度
2)比较词元个数,越少越好
3)路径跨度越大越好
4)根据统计学结论,逆向切分概率高于正向切分,因此位置越靠后的优先
5)词长越平均越好
6)词元位置权重比较
public int compareTo(LexemePath o) {
//比较有效文本长度
if(this.payloadLength > o.payloadLength){
return -1;
}else if(this.payloadLength < o.payloadLength){
return 1;
}else{
//比较词元个数,越少越好
if(this.size() < o.size()){
return -1;
}else if (this.size() > o.size()){
return 1;
}else{
//路径跨度越大越好
if(this.getPathLength() > o.getPathLength()){
return -1;
}else if(this.getPathLength() < o.getPathLength()){
return 1;
}else {
//根据统计学结论,逆向切分概率高于正向切分,因此位置越靠后的优先
if(this.pathEnd > o.pathEnd){
return -1;
}else if(pathEnd < o.pathEnd){
return 1;
}else{
//词长越平均越好
if(this.getXWeight() > o.getXWeight()){
return -1;
}else if(this.getXWeight() < o.getXWeight()){
return 1;
}else {
//词元位置权重比较
if(this.getPWeight() > o.getPWeight()){
return -1;
}else if(this.getPWeight() < o.getPWeight()){
return 1;
}
}
}
}
}
}
return 0;
}
输出阶段
Lexeme getNextLexeme(){
//从结果集取出,并移除第一个Lexme
Lexeme result = this.results.pollFirst();
while(result != null){
//数量词合并
this.compound(result);
if(Dictionary.getSingleton().isStopWord(this.segmentBuff , result.getBegin() , result.getLength())){
//是停止词继续取列表的下一个
result = this.results.pollFirst();
}else{
//不是停止词, 生成lexeme的词元文本,输出
result.setLexemeText(String.valueOf(segmentBuff , result.getBegin() , result.getLength()));
break;
}
}
return result;
}
最后输出阶段做了以下结果处理:
1.smart模式进行英文数字+中文数字,英文数词+中文量词合并
2.移除停用词
Ansj原理
字典数据结构
DAT参考文档:zhuanlan.zhihu.com/p/35193582