1、词典加载
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();
}
2、词典数据结构
public class DictSegment {
//数组大小上限
private static final int ARRAY_LENGTH_LIMIT = 3;
//当前节点上存储的字符
private Character nodeChar;
//Map存储结构
private Map<Character , DictSegment> childrenMap;
//数组方式存储结构
private DictSegment[] childrenArray;
}
根据ARRAY_LENGTH_LIMIT作为阈值来存储方式,如果当子节点数,不太于阈值时,采用数组的方式childrenArray来存储,当子节点数大于阈值时,采用Map的方式childrenMap来存储。查找的时候hashmao可以获得O(1)的算法复杂度。
3、填充字典树
/**
* 加载填充词典片段
* @param charArray
* @param begin
* @param length
* @param enabled
*/
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;
}
}
}
4、查询字典树
/**
* 匹配词段
* @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 = (DictSegment) 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;
}
5、切分词元
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中保存。
6、歧义判断(选择最优路径)
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;
}
7、输出阶段
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.移除停用词
8、根据业务需求进行增加优化
字典树生成时候可以对每个dictSegment,给出词性分或者热度分。再歧义裁决时候考虑进去。