本文对HanLP的es插件踩坑过程做个简单的记录。
HanLP的ES插件基本都收录在HanLP的这个wiki页面里了:
https://github.com/hankcs/HanLP/wiki/%E8%A1%8D%E7%94%9F%E9%A1%B9%E7%9B%AE
这次踩坑选的是 https://github.com/shikeio/elasticsearch-analysis-hanlp
gradle mvn
假设es解压后的目录为 {es_home}
,然后有另一个不相关的任意的目录 {hanlp_home}
,将data-for-1.6.4.zip解压到此目录下,得到如下目录结构:
. ├── data │ ├── README.url │ ├── dictionary │ └── model └── data-for-1.6.4.zip
GET _search
{
"query": {
"match_all": {}
}
}
如果以上步骤都没有报错,说明初始状态的所有东西都是正常的。如果不正常,请自行解决。
将 analysis-hanlp-6.2.4.zip
解压得到 elasticsearch
目录,将其重命名为 analysis-hanlp
,移动到 {es_home}/plugins
目录下,然后按照插件的文档去修改 hanlp.properties
和 {es_home}/config/jvm.options
文件,然后重启es(不用重启kibana)。
如无意外,你将会得到如下错误:
Jun 30, 2018 10:38:11 PM com.hankcs.hanlp.HanLP$Config <clinit>
SEVERE: 没有找到hanlp.properties,可能会导致找不到data
========Tips========
请将hanlp.properties放在下列目录:
Web项目则请放到下列目录:
Webapp/WEB-INF/lib
Webapp/WEB-INF/classes
Appserver/lib
JRE/lib
并且编辑root=PARENT/path/to/your/data
现在HanLP将尝试从/home/gordon/Dev/es/6.2.4/elasticsearch-6.2.4读取data……
[2018-06-30T22:38:11,995][INFO ][o.e.p.PluginsService ] [3dzf4Ix] loaded module [aggs-matrix-stats]
[2018-06-30T22:38:11,995][INFO ][o.e.p.PluginsService ] [3dzf4Ix] loaded module [analysis-common]
[2018-06-30T22:38:11,996][INFO ][o.e.p.PluginsService ] [3dzf4Ix] loaded module [ingest-common]
[2018-06-30T22:38:11,996][INFO ][o.e.p.PluginsService ] [3dzf4Ix] loaded module [lang-expression]
[2018-06-30T22:38:11,996][INFO ][o.e.p.PluginsService ] [3dzf4Ix] loaded module [lang-mustache]
[2018-06-30T22:38:11,996][INFO ][o.e.p.PluginsService ] [3dzf4Ix] loaded module [lang-painless]
[2018-06-30T22:38:11,996][INFO ][o.e.p.PluginsService ] [3dzf4Ix] loaded module [mapper-extras]
[2018-06-30T22:38:11,996][INFO ][o.e.p.PluginsService ] [3dzf4Ix] loaded module [parent-join]
[2018-06-30T22:38:11,997][INFO ][o.e.p.PluginsService ] [3dzf4Ix] loaded module [percolator]
[2018-06-30T22:38:11,997][INFO ][o.e.p.PluginsService ] [3dzf4Ix] loaded module [rank-eval]
[2018-06-30T22:38:11,997][INFO ][o.e.p.PluginsService ] [3dzf4Ix] loaded module [reindex]
[2018-06-30T22:38:11,997][INFO ][o.e.p.PluginsService ] [3dzf4Ix] loaded module [repository-url]
[2018-06-30T22:38:11,997][INFO ][o.e.p.PluginsService ] [3dzf4Ix] loaded module [transport-netty4]
[2018-06-30T22:38:11,997][INFO ][o.e.p.PluginsService ] [3dzf4Ix] loaded module [tribe]
[2018-06-30T22:38:11,998][INFO ][o.e.p.PluginsService ] [3dzf4Ix] loaded plugin [analysis-hanlp]
[2018-06-30T22:38:14,236][INFO ][o.e.d.DiscoveryModule ] [3dzf4Ix] using discovery type [zen]
[2018-06-30T22:38:14,920][INFO ][o.e.n.Node ] initialized
[2018-06-30T22:38:14,921][INFO ][o.e.n.Node ] [3dzf4Ix] starting ...
[2018-06-30T22:38:15,093][INFO ][o.e.t.TransportService ] [3dzf4Ix] publish_address {127.0.0.1:9300}, bound_addresses {[::1]:9300}, {127.0.0.1:9300}
[2018-06-30T22:38:18,153][INFO ][o.e.c.s.MasterService ] [3dzf4Ix] zen-disco-elected-as-master ([0] nodes joined), reason: new_master {3dzf4Ix}{3dzf4Ix_RgWyUEeodaAkDA}{oT5oXRhcTNuDvM8NbIAqNg}{127.0.0.1}{127.0.0.1:9300}
看上去好像找不到配置文件之后缺省配置生效了,并且也没出错,但是当你请求一下用hanlp,es就会挂了:
[2018-06-30T22:38:18,158][INFO ][o.e.c.s.ClusterApplierService] [3dzf4Ix] new_master {3dzf4Ix}{3dzf4Ix_RgWyUEeodaAkDA}{oT5oXRhcTNuDvM8NbIAqNg}{127.0.0.1}{127.0.0.1:9300}, reason: apply cluster state (from master [master {3dzf4Ix}{3dzf4Ix_RgWyUEeodaAkDA}{oT5oXRhcTNuDvM8NbIAqNg}{127.0.0.1}{127.0.0.1:9300} committed version [1] source [zen-disco-elected-as-master ([0] nodes joined)]])
[2018-06-30T22:38:18,183][INFO ][o.e.h.n.Netty4HttpServerTransport] [3dzf4Ix] publish_address {127.0.0.1:9200}, bound_addresses {[::1]:9200}, {127.0.0.1:9200}
[2018-06-30T22:38:18,183][INFO ][o.e.n.Node ] [3dzf4Ix] started
[2018-06-30T22:38:18,197][INFO ][o.e.g.GatewayService ] [3dzf4Ix] recovered [0] indices into cluster_state
[2018-06-30T22:43:31,191][ERROR][o.e.b.ElasticsearchUncaughtExceptionHandler] [] fatal error in thread [elasticsearch[3dzf4Ix][index][T#1]], exiting
java.lang.ExceptionInInitializerError: null
at com.hankcs.hanlp.seg.common.Vertex.newB(Vertex.java:462) ~[?:?]
at com.hankcs.hanlp.seg.common.WordNet.<init>(WordNet.java:73) ~[?:?]
at com.hankcs.hanlp.seg.Viterbi.ViterbiSegment.segSentence(ViterbiSegment.java:40) ~[?:?]
at com.hankcs.hanlp.seg.Segment.seg(Segment.java:557) ~[?:?]
at com.hankcs.lucene.SegmentWrapper.next(SegmentWrapper.java:98) ~[?:?]
at com.hankcs.lucene.HanLPTokenizer.incrementToken(HanLPTokenizer.java:67) ~[?:?]
at org.elasticsearch.action.admin.indices.analyze.TransportAnalyzeAction.simpleAnalyze(TransportAnalyzeAction.java:266) ~[elasticsearch-6.2.4.jar:6.2.4]
at org.elasticsearch.action.admin.indices.analyze.TransportAnalyzeAction.analyze(TransportAnalyzeAction.java:243) ~[elasticsearch-6.2.4.jar:6.2.4]
at org.elasticsearch.action.admin.indices.analyze.TransportAnalyzeAction.shardOperation(TransportAnalyzeAction.java:164) ~[elasticsearch-6.2.4.jar:6.2.4]
at org.elasticsearch.action.admin.indices.analyze.TransportAnalyzeAction.shardOperation(TransportAnalyzeAction.java:80) ~[elasticsearch-6.2.4.jar:6.2.4]
at org.elasticsearch.action.support.single.shard.TransportSingleShardAction$ShardTransportHandler.messageReceived(TransportSingleShardAction.java:293) ~[elasticsearch-6.2.4.jar:6.2.4]
at org.elasticsearch.action.support.single.shard.TransportSingleShardAction$ShardTransportHandler.messageReceived(TransportSingleShardAction.java:286) ~[elasticsearch-6.2.4.jar:6.2.4]
at org.elasticsearch.transport.TransportRequestHandler.messageReceived(TransportRequestHandler.java:30) ~[elasticsearch-6.2.4.jar:6.2.4]
at org.elasticsearch.transport.RequestHandlerRegistry.processMessageReceived(RequestHandlerRegistry.java:66) ~[elasticsearch-6.2.4.jar:6.2.4]
at org.elasticsearch.transport.TransportService$7.doRun(TransportService.java:656) ~[elasticsearch-6.2.4.jar:6.2.4]
at org.elasticsearch.common.util.concurrent.ThreadContext$ContextPreservingAbstractRunnable.doRun(ThreadContext.java:672) ~[elasticsearch-6.2.4.jar:6.2.4]
at org.elasticsearch.common.util.concurrent.AbstractRunnable.run(AbstractRunnable.java:37) ~[elasticsearch-6.2.4.jar:6.2.4]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1135) ~[?:?]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) ~[?:?]
at java.lang.Thread.run(Thread.java:844) [?:?]
Caused by: java.lang.IllegalArgumentException: 核心词典data/dictionary/CoreNatureDictionary.txt加载失败
at com.hankcs.hanlp.dictionary.CoreDictionary.<clinit>(CoreDictionary.java:44) ~[?:?]
... 20 more
➜ bin
那么问题来了,为什么会找不到 hanlp.properties
呢?
在经过多次google之后也不知道为什么,文档也没有说,只有找源码。提示是在 com.hankcs.hanlp.HanLP
这个类里出现的,所以去找HanLP的源码: https://github.com/hankcs/HanLP
将它下下来之后全文搜索『没有找到hanlp.properties』,只有一个地方,就是 src/main/java/com/hanks/hanlp/HanLP.java
,关键的地方是:
p.load(new InputStreamReader(Predefine.HANLP_PROPERTIES_PATH == null ?
loader.getResourceAsStream("hanlp.properties") :
new FileInputStream(Predefine.HANLP_PROPERTIES_PATH)
, "UTF-8"));
在源码里, Predefine.HANLP_PROPERTIES_PATH
只有声明,并没赋值,因此当时以为这里造成问题的是 loader.getResourceAsStream
,所以尝试将 hanlp.properties
放到各种地方,然而都无效。后来想着尝试在加载失败之后执行多一个加载逻辑,于是改写了源码,重新打包替代插件里的hanlp-1.6.4.jar,结果发现其实在插件运行的时候, Predefine.HANLP_PROPERTIES_PATH
其实是有值的,值为 analysis-hanlp/hanlp.properties
,因为我修改的逻辑是没有吃掉异常的,所以在初始化FileInputStream的时候,找不到文件时打印异常会将文件的绝对路径打印出来,才发现这个问题。看看HanLP.java的源码,也是挺无语的。。。
Properties p = new Properties();
try
{
ClassLoader loader = Thread.currentThread().getContextClassLoader();
if (loader == null)
{ // IKVM (v.0.44.0.5) doesn't set context classloader
loader = HanLP.Config.class.getClassLoader();
}
try
{
logger.info("System.getProperties().get(/"java.class.path/"): {}", System.getProperties().get("java.class.path"));
p.load(new InputStreamReader(Predefine.HANLP_PROPERTIES_PATH == null ?
loader.getResourceAsStream("hanlp.properties") :
new FileInputStream(Predefine.HANLP_PROPERTIES_PATH)
, "UTF-8"));
}
catch (Exception e)
{
e.printStackTrace();
String HANLP_ROOT = System.getenv("HANLP_ROOT");
if (HANLP_ROOT != null)
{
HANLP_ROOT = HANLP_ROOT.trim();
p = new Properties();
p.setProperty("root", HANLP_ROOT);
logger.info("使用环境变量 HANLP_ROOT=" + HANLP_ROOT);
}
else throw e;
}
String root = p.getProperty("root", "").replaceAll("////", "/");
if (root.length() > 0 && !root.endsWith("/")) root += "/";
CoreDictionaryPath = root + p.getProperty("CoreDictionaryPath", CoreDictionaryPath);
// balabala
}
catch (Exception e)
{
if (new File("data/dictionary/CoreNatureDictionary.tr.txt").isFile())
{
logger.info("使用当前目录下的data");
}
else
{
StringBuilder sbInfo = new StringBuilder("========Tips========/n请将hanlp.properties放在下列目录:/n"); // 打印一些友好的tips
if (new File("src/main/java").isDirectory())
{
sbInfo.append("src/main/resources");
}
else
{
String classPath = (String) System.getProperties().get("java.class.path");
if (classPath != null)
{
for (String path : classPath.split(File.pathSeparator))
{
if (new File(path).isDirectory())
{
sbInfo.append(path).append('/n');
}
}
}
sbInfo.append("Web项目则请放到下列目录:/n" +
"Webapp/WEB-INF/lib/n" +
"Webapp/WEB-INF/classes/n" +
"Appserver/lib/n" +
"JRE/lib/n");
sbInfo.append("并且编辑root=PARENT/path/to/your/data/n");
sbInfo.append("现在HanLP将尝试从").append(System.getProperties().get("user.dir")).append("读取data……");
}
logger.error("没有找到hanlp.properties,可能会导致找不到data/n" + sbInfo);
}
吃掉异常,然后提示的信息又没什么用。。。。而且报异常的时候不打印,反而去判断另一个文件是否存在,根据另一个文件是否存在,反推 hanlp.properties
文件是否存在……简单点,说话的方式简单点……
所以,令配置文件的路径为 {es_home}/config/analysis-hanlp/hanlp.properties
即可,你可以通过链接来实现,也可以将文件拷贝过去,随你。
然后重启es,再在kibana里发送:
GET /_analyze?pretty=true
{
"analyzer": "hanlp-index",
"text": "张柏芝士蛋糕店"
}
(插件的github README里那段是不行的)
正常的话会得到:
{
"tokens": [
{
"token": "张柏",
"start_offset": 0,
"end_offset": 2,
"type": "nr",
"position": 0
},
{
"token": "芝士蛋糕",
"start_offset": 2,
"end_offset": 6,
"type": "nf",
"position": 1
},
{
"token": "芝士",
"start_offset": 2,
"end_offset": 4,
"type": "nf",
"position": 2
},
{
"token": "蛋糕",
"start_offset": 4,
"end_offset": 6,
"type": "nf",
"position": 3
},
{
"token": "店",
"start_offset": 6,
"end_offset": 7,
"type": "n",
"position": 4
}
]
}
至此完毕。