即使是不成熟的尝试,也胜于胎死腹中的策略。

Day 23 - 基于 HanLP 的 ES 中文分词插件

Advent | 作者 rochy | 发布于2018年12月23日 | | 阅读数:7616

一、分词插件

1、分词器概念

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

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

文本分词并索引的过程

2、选择分词器

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

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

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

二、HanLP 简介

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

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

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

三、开发分词插件

1、代码结构

  • conf:插件的配置文件、HanLP 的配置文件、Java 安全策略文件;
  • scr.main.java.assemby:插件打包(maven-assembly-plugin)配置文件;
  • org.elasticsearch.plugin.hanlp.analysis:分词插件核心构建器;
  • org.elasticsearch.plugin.hanlp.conf:管理插件配置、分词器配置以及 HanLP 配置;
  • org.elasticsearch.plugin.hanlp.lucene:HanLP 中文分词 Lucene 插件,对 Lucune 分词进行实现;
  • scr.main.resources:插件属性文件所在目录

插件代码结构

2、TokenStream

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

文本分词流程

TokenStream 基本使用流程:

  1. 实例化 TokenStream,向 AttributeSource 添加/获取属性(词汇单元文本、位置增量、偏移量、词汇类型等);
  2. 调用 reset() 方法,将流(stream)重置到原始(clean)状态;
  3. 循环调用 incrementToken() 方法,并处理 Attribute 属性信息,直到它返回 false 表示流处理结束;
  4. 调用 end() 方法,确保流结束(end-of-stream)的操作可以被执行;
  5. 调用 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 文件夹下。

插件版本兼容性

从实际测试来看:

  • ES5.X 及其以上的代码是完全复用的,也就是说代码逻辑不需要调整;
  • ES5.X 到 ES6.2.X 的插件是可以通用的,其特征是打包的时候需要将插件的文件全部打包到 elasticsearch 文件夹下;
  • ES6.3.X 以上的插件是可以通用的,打包的时候插件的文件全部打包到根目录即可。

也就是说,如果你升级了新版本 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;
});

四、插件特色

简单介绍一下插件的特点:

  • 内置多种分词模式,适合不同场景;
  • 内置词典,无需额外配置即可使用;
  • 支持外置词典,用户可自定义分词算法,基于词典或是模型;
  • 支持分词器级别的自定义词典,便于用于多租户场景;
  • 支持远程词典热更新(待开发);
  • 拼音过滤器、繁简体过滤器(待开发);
  • 基于词语或单字的 ngram 切分分词(待开发)。

Github 地址:https://github.com/AnyListen/elasticsearch-analysis-hanlp


[尊重社区原创,转载请保留或注明出处]
本文地址:http://elasticsearch.cn/article/6219


0 个评论

要回复文章请先登录注册