背景:
Ik分词器支持默认分词器,可以扩展分词词典,但想扩展一个分词器类型就需要个性IK源码了,扩展后的分词器与原生分词器可混用,也可单独使用。
下面简单介绍下修改过程同大家分享共同学习。
过程:
1、克隆IK源码并测试IK插件
1)克隆地址:https://github.com/medcl/elasticsearch-analysis-ik
要根据你目前使用的es版本找到合适IK的版本,版本选择:
2)这里面有个坑如你的ES是6.7.1,IK克隆的也是6.7.1当编译好后,发现报IK插件与ES版本不一至问题,
(报错代码:Caused by: java.lang.IllegalArgumentException: Plugin [analysis-ik] was built for Elasticsearch version 6.5.0 but version 6.7.1 is running)
这是因为IK6.7.1发布的ES版本为6.5.0,所以需要修改pom文件中的版本如下:
<elasticsearch.version>6.5.0</elasticsearch.version>
修改为
<elasticsearch.version>6.7.1</elasticsearch.version>
3)测试IK编译插件
加载项目后进行maven package 编译打包,解压target/releases/elasticsearch-analysis-ik-6.7.1.zip,修改文件名为IK,复制到es的home下的plugins文件夹中后启动ES,如无报错信息 使用_analyzer进行测试是否成功安装IK分词器。
2、修改源码扩展小区分词器
1)IK与ES是通过plugin.analysis.ik.AnaysisIkPlugin类的Map进行配合使用,那么请记住这块后面开发完分词要添加在这个地方给ES使用。
public class AnalysisIkPlugin extends Plugin implements AnalysisPlugin {
public static String PLUGIN_NAME = "analysis-ik";
@Override
public Map<String, AnalysisModule.AnalysisProvider<TokenizerFactory>> getTokenizers() {
Map<String, AnalysisModule.AnalysisProvider<TokenizerFactory>> extra = new HashMap<>();
extra.put("ik_smart", IkTokenizerFactory::getIkSmartTokenizerFactory);
extra.put("ik_max_word", IkTokenizerFactory::getIkTokenizerFactory);
2)扩展 IkAnalyzerProvider和IkTokenizerFactory
通过
extra.put(“ik_smart”, IkTokenizerFactory::getIkSmartTokenizerFactory);
和
extra.put(“ik_smart”, IkAnalyzerProvider::getIkSmartAnalyzerProvider);
为线索找出创建IKAnalyzer类IkAnalyzerProvider实现ES抽象类AbstractIndexAnalyzerProvider来创建IKAnalyzer分词器,这是一个lambda写法,把方法存储到map中使用时传入方法所需的参数,我们仿照这个方法创建自己的分词器就可以了。
public class IkAnalyzerProvider extends AbstractIndexAnalyzerProvider<IKAnalyzer> {
private final IKAnalyzer analyzer;
public IkAnalyzerProvider(IndexSettings indexSettings, Environment env, String name, Settings settings,boolean useSmart) {
super(indexSettings, name, settings);
Configuration configuration=new Configuration(env,settings).setUseSmart(useSmart);
analyzer=new IKAnalyzer(configuration);
}
public static IkAnalyzerProvider getIkSmartAnalyzerProvider(IndexSettings indexSettings, Environment env, String name, Settings settings) {
return new IkAnalyzerProvider(indexSettings,env,name,settings,true);
}
public static IkAnalyzerProvider getIkAnalyzerProvider(IndexSettings indexSettings, Environment env, String name, Settings settings) {
return new IkAnalyzerProvider(indexSettings,env,name,settings,false);
}
@Override public IKAnalyzer get() {
return this.analyzer;
}
}
3、扩展Ik子分词器
首先我们看一下IKAnalyzer构造方法都做了些什么
/**
- IK分词器Lucene Analyzer接口实现类
- @param configuration IK配置
*/
public IKAnalyzer(Configuration configuration){
super();
this.configuration = configuration;
}
/**
- 重载Analyzer接口,构造分词组件
*/
@Override
protected TokenStreamComponents createComponents(String fieldName) {
Tokenizer _IKTokenizer = new IKTokenizer(configuration);
return new TokenStreamComponents(_IKTokenizer);
}
构造要求传入Configuration 看一下,是一些配置解析如下
public class Configuration<boole> {
// ES环境如 eshome等
private Environment environment;
// ES索引配置信息如 分词器 索引等
private Settings settings;
//是否启用智能分词
private boolean useSmart;
//是否启用远程词典加载
private boolean enableRemoteDict = false;
//是否启用小写处理
private boolean enableLowercase = true;
//是否加载基础字典 扩展加入,用于判断加载那些子分词器
private int useDict = 1;
// 是否过滤相同位置词源 重复不同类型,扩展加入
private boolean lexemeBitBoot = true;
createComponents方法生成Lucene Tokenizer实现类IKTokenizer,构造函数中确认获取分词信息的类用于反向获取和生成IKSegmenter,在IKSegmenter中启动词典、子分词器的加载。
/**
* Lucene 4.0 Tokenizer适配器类构造函数
*/
public IKTokenizer(Configuration configuration){
super();
offsetAtt = addAttribute(OffsetAttribute.class);
termAtt = addAttribute(CharTermAttribute.class);
typeAtt = addAttribute(TypeAttribute.class);
posIncrAtt = addAttribute(PositionIncrementAttribute.class);
_IKImplement = new IKSegmenter(input,configuration);
}
/**
* IK分词器构造函数
* @param input
*/
public IKSegmenter(Reader input, Configuration configuration) {
this.input = input;
this.configuration = configuration;
this.init();
}
/**
* 初始化
*/
private void init() {
//初始化分词上下文
this.context = new AnalyzeContext(configuration);
//加载子分词器
this.segmenters = this.loadSegmenters();
//加载歧义裁决器
this.arbitrator = new IKArbitrator();
}
在加载子分词器中加入自己的子分词器 this.segmenters = this.loadSegmenters();
这里面加入了扩展的小区子分词器XiaoquSegmenter,configuration.getUseDict()是在Configuration中定义的分词器加载控制属性,加载子分词器是有顺序的把权重高的子分词器优先加载,处理歧义词会考虑这一规则。
/**
* 初始化词典,加载子分词器实现
* @return List<ISegmenter>
*/
private List<ISegmenter> loadSegmenters() {
List<ISegmenter> segmenters = new ArrayList<ISegmenter>(20);
//处理字母的子分词器
segmenters.add(new LetterSegmenter());
// //处理中文数量词的子分词器
segmenters.add(new CN_QuantifierSegmenter());
// 优先级 1
if ((configuration.getUseDict() & 32) == 32) {
segmenters.add(new XiaoquSegmenter()); // 小区
} if ((configuration.getUseDict() & 64) == 64) {
segmenters.add(new OfficeParkSegmenter()); // 办公园区
}
if ((configuration.getUseDict() & 128) == 128) {
segmenters.add(new OfficeBuildingSegmenter()); // 办公楼
}
if ((configuration.getUseDict() & 4096) == 4096) {
segmenters.add(new SubwayLineSegmenter()); // 地铁线
}
if ((configuration.getUseDict() & 8192) == 8192) {
segmenters.add(new SubwayNameSegmenter()); // 地铁站
}
XiaoquSegmenter小区分词器
/**
* 中文-小区子分词器
*/
class XiaoquSegmenter implements ISegmenter {
//子分词器标签
static final String SEGMENTER_NAME = "XIAOQU_SEGMENTER";
//待处理的分词hit队列
private List<Hit> tmpHits;
XiaoquSegmenter(){
this.tmpHits = new LinkedList<Hit>();
}
/* (non-Javadoc)
* @see org.wltea.analyzer.core.ISegmenter#analyze(org.wltea.analyzer.core.AnalyzeContext)
*/
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_XIAOQU);
context.addLexeme(newLexeme);
if(!hit.isPrefix()){//不是词前缀,hit不需要继续匹配,移除
this.tmpHits.remove(hit);
}
}else if(hit.isUnmatch()){
//hit不是词,移除
this.tmpHits.remove(hit);
}
}
}
//*********************************
//再对当前指针位置的字符进行单字匹配
Hit singleCharHit = Dictionary.getSingleton().matchInExtraDict(context.getSegmentBuff(), context.getCursor(), 1,"_XiaoquDict");
if(singleCharHit.isMatch()){//首字成词
//输出当前的词
Lexeme newLexeme = new Lexeme(context.getBufferOffset() , context.getCursor() , 1 , Lexeme.TYPE_XIAOQU);
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);
}
}
首先看第46行,matchInExtraDict方法传入预分词字符串转char数组,子串位置,词典key,返回的Hit中存储有命中分词前缀的匹配词信息用于第25行进行后续处理。
singleton._ExtraDics.get(keyDict)方法获取相关分词器的词典并执行match操作。
/**
* 检索匹配词典
*
* @return Hit 匹配结果描述
*/
public Hit matchInExtraDict(char[] charArray, int begin, int length,String keyDict) {
return singleton._ExtraDics.get(keyDict).match(charArray, begin, length);
}
_ExtraDics字典加过程
/**
* 加载城市词典及扩展词典
*/
private void loadExtraDics() {
// 建立一个主词典实例
_ExtraDics = new HashMap<String, DictSegment>();
Iterator iter = Dictionary.PATH_DIC_EXTRA.keySet().iterator();
while(iter.hasNext()){
String key = iter.next().toString();
String val = Dictionary.PATH_DIC_EXTRA.get(key);
DictSegment ds = new DictSegment((char) 0);
// 读取主词典文件
Path file = PathUtils.get(getDictRoot(), val);
loadDictFile(ds, file, false, key);
_ExtraDics.put(key,ds);
}
}
第13行的词典文件路径由Dictionary提供。
private static final Map<String,String> PATH_DIC_EXTRA = Dictionary.getExtraDicPath();
private static Map<String,String> getExtraDicPath(){
Map<String,String> map = new HashMap<String,String>();
map.put("_XiaoquDict","extra_xiaoqu.dic");
词典文件存储路径获取 默认从Environment的analysis-ik属性进行配置第4行。
如果没有配置则以本地项目下的config文件夹为词典目录第12行
private Dictionary(Configuration cfg) {
this.configuration = cfg;
this.props = new Properties();
this.conf_dir = cfg.getEnvironment().configFile().resolve(AnalysisIkPlugin.PLUGIN_NAME);
Path configFile = conf_dir.resolve(FILE_NAME);
InputStream input = null;
try {
logger.info("try load config from {}", configFile);
input = new FileInputStream(configFile.toFile());
} catch (FileNotFoundException e) {
conf_dir = cfg.getConfigInPluginDir();
configFile = conf_dir.resolve(FILE_NAME);
try {
logger.info("try load config from {}", configFile);
input = new FileInputStream(configFile.toFile());
} catch (FileNotFoundException ex) {
// We should report origin exception
logger.error("ik-analyzer", e);
}
}
回到小区子分词器XiaoquSegmenter第47行,if(singleCharHit.isMatch()){// 判断是否首字成词 如果成词则创建Lexeme增加到AnalyzeContext等待后续歧义处理,其中需要在Lexeme类中创建小区分词类型
public static final int TYPE_XIAOQU = 32;
修改Lexeme.getLexemeTypeString增加扩展分词器的类型。
/**
* 获取词元类型标示字符串
*
* @return String
*/
public String getLexemeTypeString() {
switch (lexemeType) {
case TYPE_ENGLISH:
return "ENGLISH";
case TYPE_XIAOQU:
return String.valueOf(TYPE_XIAOQU);
XiaoquSegmenter类 第47行 成词后 如果还是前缀词 则加入到tmpHits队列中拱第25行再次处理后续字符是否与词典进行匹配,如果匹配并成词则生成Lexeme,继续加入到tmpHits中进行处理。