Lucene的IK分词器学习,增加支持单个特殊符号搜索
前言
感谢CSDN这篇文章,原始代码基于这里。
正常对于“abc@google.com”这段文字,搜索'@'这个符号是搜不出来的。本文主要修改是扩展IK分词器,增加了对诸如"@ -"这种特殊文字的检索。
当然这个其实并没有多少实际意义,所以基本也是出于学习的目的。
正文
IK分词器分析
这里不深入原理,只涉及到修改时需要考虑的地方。
- 分词逻辑:
IKAnalyzer
->IKTokenizer
->IKSegmenter
,最终实际是IKSegmenter
中的next()
方法对输入流进行分词,获取下一个分词(也叫词元Lexeme)。 IKSegementer
分词:包含三个子分词器,实现ISegmenter
接口,依次调用,彼此独立分词(既一段文字可以被不通分词器重复分词)LetterSegmenter
: 识别字母、数字、符号的组合CN_QuantifierSegmenter
: 识别中文量词分词,如一条,三艘CJKSegmenter
: IK的核心,中文分词,通过从字典数据中查找匹配分词。不过不是本文的核心了。
- 子分词器逻辑:
AnalyzeContext
为输入参数,存储输入流信息,子分词器解析每个字符,通过字符类型判断词元开始和结尾。当一个分词结束,通过AnalyzeContext
提供的addLexeme
方法增加一个分词(词元) IKArbitrator
歧义裁决:其实这里涉及到一个IK分词器的smart
参数,表面上,配置smart
为true
时,分词比较粗,为false
时分词粒度更细,这个参数影响的就是这个裁决器。当拥有smart
为true
时,多个分词如果彼此有覆盖关系,会自动进行合并,如热反应堆
,原本会被拆分成反应``反应堆``堆``热反应堆
等多个词,但是如果配置了smart
,由于热反应堆
本身包含了其他词,则最后分词结果只会有一个热反应堆
词元。所以一般来说,建立索引的时候,不配置smart
,而搜索前分词的时候,可以配置smart
。有点扯远了。
修改说明
核心思路:在原有的分词器中,创建一个信息的分词器SpecialCharSegmenter
,当碰到特殊字符时,插入一个新的Lexeme
。
由于原始的几个类IKAnalyzer
IKTokenizer
IKSegmenter
等都是final的,所以我直接单独创建了类MyIKAnalyzer
,MyIKTokenizer
,MyIKSegmenter
替代这几个类。
比较重要的修改点说明如下:
MyCharacterUtil
扩展原有CharacterUtil
工具类,增加特殊字符类型值,在解析字符流时,识别该类型
//需要进行索引的特殊字符
public static final int CHAR_SPECIAL = 0X00000010;
//需要进行索引的特殊字符,可以扩充修改
private static final char[] Letter_Special = new char[]{'#', '-', '@', '.'};
static int identifyCharType(char input) {
.......省略非关键部分
} else if (isSpecialChar(input)) {
return CHAR_SPECIAL;
}
}
//特殊字符的判断
static boolean isSpecialChar(char input) {
int index = Arrays.binarySearch(Letter_Special, input);
return index >= 0;
}
MyAnalyzeContext
读取字符流的工具类,原本调用CharacterUtil
,修改为调用我们自定义的MyCharacterUtil
。
/**
* 初始化buff指针,处理第一个字符
*/
void initCursor() {
this.cursor = 0;
this.segmentBuff[this.cursor] = CharacterUtil.regularize(this.segmentBuff[this.cursor]);
//修改点
this.charTypes[this.cursor] = MyCharacterUtil.identifyCharType(this.segmentBuff[this.cursor]);
}
/**
* 指针+1 成功返回 true; 指针已经到了buff尾部,不能前进,返回false 并处理当前字符
*/
boolean moveCursor() {
if (this.cursor < this.available - 1) {
this.cursor++;
this.segmentBuff[this.cursor] = CharacterUtil.regularize(this.segmentBuff[this.cursor]);
//修改点
this.charTypes[this.cursor] = MyCharacterUtil.identifyCharType(this.segmentBuff[this.cursor]);
return true;
} else {
return false;
}
}
MyIKSegmenter
loadSegmenters
中,加载自定义的分词器
// 处理特殊字符的子分词器
segmenters.add(new SpecialCharSegmenter());
SpecialCharSegmenter
核心是碰到特殊字符,增加一个词元,逻辑非常简单。
public void analyze(AnalyzeContext context) {
if (MyCharacterUtil.CHAR_SPECIAL == context.getCurrentCharType()) {
// 遇到特殊字符,输出词元
Lexeme newLexeme = new Lexeme(context.getBufferOffset(), context.getCursor(), 1,
Lexeme.TYPE_LETTER);
context.addLexeme(newLexeme);
}
}
CustomLetterSegmenter
扩展原有的LetterSegmenter
,之所以要修改,是发现该类会将部分特殊符号,如abc@google.com
识别为一个整体类,而识别的方式是通过字符类型为CHAR_USELESS
。
当我修改了字符解析时,为特殊符号定义了新的类型CHAR_SPECIAL
,所以需要修改processMixLetter
:
//原始
} else if ((CharacterUtil.CHAR_USELESS == context.getCurrentCharType())
&& this.isLetterConnector(context.getCurrentChar())) {
// 记录下可能的结束位置
this.end = context.getCursor();
} else {
//修改为
} else if ((CharacterUtil.CHAR_USELESS == context.getCurrentCharType()
//可能被修改为了特殊字符类型
|| MyCharacterUtil.CHAR_SPECIAL == context.getCurrentCharType())
&& this.isLetterConnector(context.getCurrentChar())) {
// 记录下可能的结束位置
this.end = context.getCursor();
} else {
验证
代码上传在 github https://github.com/mosakashaka/wbdemos/tree/master/lucene-ik-demo
直接运行后通过地址http://localhost:8080/lucene/createIndex
,创建普通索引(不支持符号),索引中自动添加了几条数据,其中包含了@
符号。
此时通过搜索接口http://localhost:8080/test/searchText?text=@
检索,搜索不到数据。
如果通过http://localhost:8080/lucene/createIndex2
(地址后多了2),则会创建支持符号的索引,此时搜索@
-
等符号则可以搜索到。
结语
本身可能意义不大,纯粹是学习的目的。
IK原始的代码看起来很久了,我看了下ES用的那个IK分词器的提交记录,16年以后几乎也没有修改。