背景:
Ik分词器支持默认分词器,可以扩展分词词典,但想扩展一个分词器类型就需要个性IK源码了,扩展后的分词器与原生分词器可混用,也可单独使用。
下面简单介绍下修改过程同大家分享共同学习。

过程:

1、克隆IK源码并测试IK插件

1)克隆地址:https://github.com/medcl/elasticsearch-analysis-ik

要根据你目前使用的es版本找到合适IK的版本,版本选择:

es怎么查看所有分词器 es使用ik分词器查询_ide


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中进行处理。