Press "Enter" to skip to content

基于 HanLP 的 ES 中文分词插件

在 ES 中,分词器的作用是从文本中提取出若干词元(token)来支持索引的存储和搜索,分词器(Analyzer)由 一个分解器 (Tokenizer)、 零个或多个词元过滤器 (TokenFilter)组成。

 

分解器用于将字符串分解成一系列词元,词元过滤器的作用是对分词器提取出来的词元做进一步处理,比如转成小写,增加同义词等。处理后的结果称为索引词(Term),引擎会建立 Term 和原文档的倒排索引(Inverted Index),这样就能根据 Term 很快到找到源文档了。

2、选择分词器

 

目前 ES 分词插件的选择性还是很多的,分词插件的核心就是提供各种分词器(Analyzer)、分解器(Tokenizer)、词元过滤器(TokenFilter);根据依赖的核心分词包(分词算法)的不同显现出不同的差异性,除了分词算法之外,是否支持用户自定义词典,是否支持词典热更新等其他附加功能也是选择分词插件时需要参考的。

 

下面列出选择分词插件需要考虑的因素(仅供参考):

分词准确性:大家都希望分词结果能够尽可能准确,与分词准确性直接相关的就是用户词典了,此外才是分词算法;
分词算法:个人认为无需纠结于分词算法,大多数分词包提供的分词算法都比较类似,选择时不需要过于纠结;
分词速度:这个与分词算法直接相关,基于词典的分词算法一般比基于模型的分词算法要快;基于词典如果考虑词频、命名实体识别、词性标注则会慢一些;
启动速度:当词典较大时,初始化词典会比较慢,某些分词器会对词典进行缓存,第二次启动会非常速度;
内存占用:与分词算法、词典大小、模型大小均有关系,设计精巧的算法对内存占用较小;
易用性:分词器是否开箱即用,是否可以直接使用在线链接或者压缩包进行安装,是否需要复杂的配置;
扩展性:是否支持用户自定义词典、是否支持自定义分词算法、是否支持热更新等;
是否开源:开源的分词器在遇到问题的时候可以自己进行深度调试,甚至可以进行二次开发;
社区活跃度:这个看一下 github 的 star 数或者依赖的分词包的 star 数和 issue 数目即可判定;
更新频率:是否能够与最新版的 ES 同步更新。

二、HanLP 简介

 

HanLP 是一系列模型与算法组成的 NLP 工具包,具备功能完善、性能高效、架构清晰、语料时新、可自定义的特点,详情可参考 github 介绍: github.com/hankcs/HanL…

 

选择 HanLP 作为核心的分词包开发 ES 分词插件,主要考虑以下因素:

HanLP 是 Java 分词包中最为流行的;
HanLP 提供了多种分词器,既可以基于词典也可以基于模型(在一亿字的大型综合语料库上训练的分词模型);
HanLP 坚持使用明文词典,这样可以借助社区的力量对词典不断进行完善;
完善的开发文档和代码样例,较为活跃的用户群体;
个人参与了部分功能的开发,对代码结构较为熟悉。

三、开发分词插件

 

1、代码结构

 

conf
scr.main.java.assemby
org.elasticsearch.plugin.hanlp.analysis
org.elasticsearch.plugin.hanlp.conf
org.elasticsearch.plugin.hanlp.lucene
scr.main.resources

2、TokenStream

 

Analyzer 类是一个抽象类,是所有分词器的基类,它通过 TokenStream 类将文本转换为词汇单元流;TokenStream 有两种实现 Tokenizer(输入为 Reader) 和 TokenFilter(输入为另一个 TokenStream)。

TokenStream 基本使用流程:

 

reset()
incrementToken()
end()
close()

 

// 实例化 TokenStream
TokenStream tokenStream = new IKAnalyzer().tokenStream("keywords",new StringReader("思想者"));
// 向 AttributeSource 添加/获取属性
CharTermAttribute attribute = tokenStream.addAttribute(CharTermAttribute.class);
// 将流(stream)重置到原始(clean)状态
tokenStream.reset();
// 判断是否还有下一个 Token
while(tokenStream.incrementToken()) {
  System.out.println(attribute);
}
tokenStream.end();
tokenStream.close();

 

综上,开发 Tokenizer 或者 TokenFilter 时,需要重点关注 reset、incrementToken、end、close 四个方法的实现。

 

3、开发中的小技巧

 

获取插件目录或文件目录

 

//获取插件根目录
private static Path getPluginPath() {
    return env.pluginsFile().resolve("analysis-hanlp");
}
//获取插件目录下的文件
private static Path getDefDicConfigPath() {
    return env.pluginsFile().resolve("analysis-hanlp/hanlp.properties").toAbsolutePath();
}

 

插件属性文件

 

如果希望插件属性文件( plugin-descriptor.properties )能够自动根据 pom.xml 中的属性进行赋值,则需要将文件防止到 resources 文件夹下。

 

插件版本兼容性

 

从实际测试来看:

 

elasticsearch

 

也就是说,如果你升级了新版本 ES,对于插件升级,大多数情况只需要修改下 plugin-descriptor.properties 文件中 ES 的版本号即可。

 

4、安全策略文件

 

在插件开发中经常会使用到文件读取、属性读取、网络链接等功能,如果不提前注册安全策略,在调用这些功能的时候会报以下错误 java.security.AccessControlException: access denied

 

官方给出的解决方案就是新建一个 plugin-security.policy 文件,然后在文件中声明需要的权限信息,最后在 打包的时候将文件放置到插件的根目录 ,这样在使用 zip 包进行安装的时候,ES 会提示用户插件所需的权限信息,需要用户确认后插件才能正常安装。

 

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@     WARNING: plugin requires additional permissions     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
* java.io.FilePermission <<ALL FILES>> read,write,delete
* java.lang.RuntimePermission createClassLoader
* java.lang.RuntimePermission getClassLoader
* java.lang.RuntimePermission setContextClassLoader
* java.net.SocketPermission * connect,resolve
* java.util.PropertyPermission * read,write
See http://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html
for descriptions of what these permissions allow and the associated risks.
Continue with installation? [y/N]y
-> Installed analysis-hanlp

 

5、安全策略的坑

 

最开始认为只需要添加了 policy 文件,且打包到正确的位置即可解决插件的权限问题,因为在插件安装的时候 ES 已经提示了所需权限,但是代码在实际执行的时候依旧报 AccessControlException 的错误。

 

参考了多个 HanLP 的 ES 分词插件,都没有获得较好的方法,后来考虑到 IK 分词器远程加载词典时,需要网络连接权限,就去看了下其远程词典加载的代码,最终找到了正确的使用方法。

 

// 需要特殊权限的代码
AccessController.doPrivileged((PrivilegedAction<Segment>) () -> {
    Segment segment;
    if (config.getAlgorithm().equals("extend")) {
        segment = new ViterbiSegment();
    } else {
        segment = HanLP.newSegment(config.getAlgorithm());
    }
    // 在此处显示调用一下分词,使得加载词典、缓存词典的操作可以正确执行
    System.out.println( segment.seg("HanLP中文分词工具包!"));
    return segment;
});

Be First to Comment

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注