elasticsearch - ik动态分词管理
场景
对于抓取的资讯,有些敏感词需要屏蔽,但有些词又是新的词,放在 es 中可能搜索不到,或者需要先中入分词字典,重新启动服务
解决方案
通过 es 的远程获取字典方式, 这里使用 ik 中文分词
ik 分词
地址: 安装ik 中文分词, 安装略
动态地址配置
xx@a:/opt/soft/lib/dc/es$ cat plugins/ik/config/IKAnalyzer.cfg.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict"></entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords"></entry>
<!--用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
## 填写自定义 地址
<entry key="remote_ext_dict">http://192.168.xx.xx:8080/v1/1/sensitive/dict</entry>
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
原理
查询敏感词字典
其中 location 是指一个 url,比如 http://yoursite.com/getCustomDict,该请求只需满足以下两点即可完成分词热更新。
- 该 http 请求需要返回两个头部(header),一个是 Last-Modified,一个是 ETag,这两者都是字符串类型,
只要有一个发生变化,该插件就会去抓取新的分词进而更新词库。 - 该 http 请求返回的内容格式是一行一个分词,换行符用 \n 即可
- ik 分词会请求两次
- 一次校验Last-Modified和ETag,如果两者有变化,则会去抓取新的分词
- 真正拉取字典值
- es 每隔 1 分钟定时请求 字典这个接口
实现
架构
- 使用 mongodb 作为敏感词 管理, 保存敏感词字典
- 向 redis 保存 数据更新状态,提示已经有数据变更
- 因为填了 远程字典地址, ES 会起定时任务 每隔 1 分钟拉取敏感词字典, 如果发现 redis 中有数据状态是有更新的,ES 则会再次请求真正的敏感词字典列表
java 实现
- 拉取字典
@GetMapping("/v1/{app}/sensitive/dict")
public void getSensitiveWordDict(@PathVariable(value = "app")Integer app, HttpServletResponse response) {
log.info(">>>>>>>>> getSensitiveWordDict app:{}", app);
//从 redis 获取 数据更新状态
SensitiveWordTag wordTag = sensitiveWordService.getSensitiveWordTag(app);
try {
String content = "";
if (wordTag.getRequestTimes() == Constants.WORD_TAG_REQUEST_IS_UPDATED) {
//第一次表示有数据有更新
log.info(">>>>>>>>> getSensitiveWordDict app:{} 数据有更新啦", app);
} else if (wordTag.getRequestTimes() == Constants.WORD_TAG_REQUEST_DATA_UPDATED) {
//第二次表示更新数据输出
content = sensitiveWordService.getSensitiveWordDict(app);
log.info(">>>>>>>>>>> getSensitiveWordDict app:{} 数据更新 content:{}",app, content);
} else {
//其他代表没有更新
log.info(">>>>>>>>> getSensitiveWordDict app:{} is 没有数据更新", app);
}
ServletOutputStream out = response.getOutputStream();
response.setHeader("Last-Modified", wordTag.getLastModified());
response.setHeader("ETag", wordTag.getETag());
response.setContentType("text/plain;charset=utf-8");
out.write(content.getBytes("utf-8"));
out.flush();
} catch (IOException e) {
log.error("########### getSensitiveWordDict", e);
} finally {
wordTag.setRequestTimes(wordTag.getRequestTimes() - 1);
if (wordTag.getRequestTimes() >= 0) {
sensitiveWordService.sensitiveWordDataChanged(app, wordTag);
}
}
}
- 保存敏感词, 并通知数据更新
@Override
public boolean addOrUpdSensitiveWord(SensitiveWordDto sensitiveWordDto) {
SensitiveWordDoc doc = new SensitiveWordDoc();
if (sensitiveWordDto.getWordId() == null) {
sensitiveWordDto.setCreateTime(LocalDateTime.now());
sensitiveWordDto.setUpdateTime(LocalDateTime.now());
BeanUtils.copyProperties(sensitiveWordDto, doc);
} else {
SensitiveWordDoc oldDoc =
mongoTemplate.findOne(new Query(Criteria.where("wordId").is(sensitiveWordDto.getWordId())),
SensitiveWordDoc.class);
if (Objects.isNull(oldDoc)) {
throw new ServiceException("500", "该敏感词不存在");
}
sensitiveWordDto.setUpdateTime(LocalDateTime.now());
BeanUtils.copyProperties(oldDoc, doc);
}
SensitiveWordDoc result = mongoTemplate.save(doc);
if (!Objects.isNull(result)) {
SensitiveWordTag tag = new SensitiveWordTag();
String val = String.valueOf(System.currentTimeMillis());
tag.setLastModified(val);
tag.setETag(val);
//数据变化 写入 redis
tag.setRequestTimes(Constants.WORD_TAG_REQUEST_IS_UPDATED);
String key = MessageFormat.format(RedisKeyConsts.KEY_SENSITIVE_WORDS, sensitiveWordDto.getApp());
log.info(">>>>>> addOrUpdSensitiveWord key:{}", key);
redisTemplate.opsForValue().set(key, tag);
return true;
}
return false;
}
结果:
# 第一次
2022-04-16 15:50:44.159 INFO 391071 --- : >>>>>>>>> getSensitiveWordDict app:1
2022-04-16 15:50:44.165 INFO 391071 --- : >>>>>>>>> getSensitiveWordDict app:1 数据有更新啦
# 第二次
2022-04-16 15:50:44.253 INFO 391071 --- : >>>>>>>>> getSensitiveWordDict app:1
2022-04-16 15:50:44.293 INFO 391071 --- : >>>>>>>>>>> getSensitiveWordDict app:1 数据更新 content:
2022-04-16 15:51:44.158 INFO 391071 --- : >>>>>>>>> getSensitiveWordDict app:1
2022-04-16 15:51:44.166 INFO 391071 --- : >>>>>>>>> getSensitiveWordDict app:1 is 没有数据更新
可以发现
- 当数据有变化时, ES 确实拉取了两次
- 一次用于鉴定数据有变化
- 第二次才是真正拉取数据
- 后面就是一直检测数据没有更新
验证分词
未动态添加敏感词
GET _analyze
{
"analyzer": "ik_smart",
"text":"美好的中国,大好河山"
}
{
"tokens" : [
{
"token" : "美好",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "的",
"start_offset" : 2,
"end_offset" : 3,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "中国",
"start_offset" : 3,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "大好河山",
"start_offset" : 6,
"end_offset" : 10,
"type" : "CN_WORD",
"position" : 3
}
]
}
加入分词
http://localhost:8080/v1/sensitive/word
{
"word": "美好的中国",
"type": 1,
"app": 1,
"level": 1,
"description": "测试"
}
# 查询 mongo
> db.sensitive_word.find({"app":1}).pretty()
{
"_id" : "1",
"wordId" : NumberLong(1),
"word" : "美好的中国",
"type" : 1,
"app" : 1,
"level" : 1,
"description" : "测试",
"createTime" : ISODate("2022-04-16T08:41:52.267Z"),
"updateTime" : ISODate("2022-04-16T08:41:52.267Z")
}
再次分词:
GET _analyze
{
"analyzer": "ik_smart",
"text":"美好的中国,大好河山"
}
{
"tokens" : [
{
"token" : "美好的中国",
"start_offset" : 0,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "大好河山",
"start_offset" : 6,
"end_offset" : 10,
"type" : "CN_WORD",
"position" : 1
}
]
}
可以发现,成功了
good luck!