部分字段(可能为富文本)需要做敏感词过滤,敏感词词库由产品给出。
项目使用的spring cloud全家桶,springboot版本2.1.0 ,项目中集成了spring Data elasticsearch 单独为一个子模块被各个服务引用,elasticsearch 插件版本为6.6.2,并且es插件中已经安装ik中文分词器
1、敏感词能精确过滤,速度尽量快
2、如果敏感词有更新,要求能实时更新,并且更新过程尽量简单
1、把敏感词导入数据库,再把数据库中的敏感词同步至ES
2、如果敏感词有更新,则通过任务调度器把数据同步至ES
3、使用IK分词器对输入文本进行分词,分词结果在ES中进行匹配查找,其中同步至ES的敏感词类型设置为 keyword,查找匹配使用termsQuery等精确匹配
1、ik分词器分词不理想,输入一段文本,由于IK分词器没有按照预期的分词结果进行分词,导致没有过滤出敏感词
2、teamsQuery默认一次只能输入1024个词,超过则会报错,尝试修改ES设置,增大参数,但是按照网上查找的资料设置失败。修复方案为写一个IK分词器工具类,对输入的文本进行分词检查,如果分词结果大于1024个,则去重后分组匹配,最后把结果放入一个list返回
设置字段类型为Keyword,设置为keyword类型,则在索引过程中不分再词
@Field(type = FieldType.Keyword)
private String word;
IK分词工具类:
public List getAnalyzes(String index, String analyzer, String text) {
//调用ES客户端分词器进行分词
AnalyzeRequestBuilder ikRequest = new AnalyzeRequestBuilder(elasticsearchTemplate.getClient(),
AnalyzeAction.INSTANCE, index, text).setAnalyzer(analyzer);
List ikTokenList = ikRequest.execute().actionGet().getTokens();
// 赋值
List searchTermList = new ArrayList<>();
if (ToolUtil.isNotEmpty(ikTokenList)) {
ikTokenList.forEach(ikToken -> {
searchTermList.add(ikToken.getTerm());
});
}
return searchTermList;
}
匹配方式:
//sensitives = getAnalyzes(...)
QueryBuilder queryBuilder = QueryBuilders.termsQuery("word", sensitives);
QueryBuilder queryBuilder = QueryBuilders.multiMatchQuery(word, "word").analyzer(analyzer).operator(Operator.OR);
term为精确匹配,即不分词,而带match的查询则是先分词,再进行匹配
同步数据至es
@Override
public void fetchFromMysql() {
log.info("从数据库同步敏感词至ES");
list = ;//查询数据库
if (CollectionUtil.isNotEmpty(list)) {
log.info("从数据库中查询到的敏感词总大小list.size={}", list.size());
repository.deleteAll();
repository.saveAll(list);
}
log.info("敏感词同步结束");
}
1、主要是对分词进行优化,考虑到IK分词器支持自定义字典,于是把敏感词在导入数据库的同时,也生成一份字典,并把它配置到ik分词器插件中,使用的是本地字典
1、已经达到初步预期,输入文本能按照预期的敏感词进行分词,并且返回正确的结果
2、缺点也很明显,如果有敏感词更新,一是需要更新数据库,然后通过调度器同步至ES,二是要更新ik分词器的字典,并且要重启es才能生效,第二点是麻烦,生产环境ES不能随便重启
1、创建字典 sensitive_word.dic,其中一个词一行
2、上传sensitive_word.dic至服务器es的插件安装目录,如
[root@izwz970jhtovyr0spqrk88z ~]# cd /data/env/es66/plugins/ik/config/
[root@izwz970jhtovyr0spqrk88z config]# ll
总用量 8304
-rw-r--r-- 1 root root 5225922 2月 19 00:07 extra_main.dic
-rw-r--r-- 1 root root 63188 2月 19 00:07 extra_single_word.dic
-rw-r--r-- 1 root root 63188 2月 19 00:07 extra_single_word_full.dic
-rw-r--r-- 1 root root 10855 2月 19 00:07 extra_single_word_low_freq.dic
-rw-r--r-- 1 root root 156 2月 19 00:07 extra_stopword.dic
-rw-r--r-- 1 root root 700 4月 9 10:24 IKAnalyzer.cfg.xml
-rw-r--r-- 1 root root 3058510 2月 19 00:07 main.dic
-rw-r--r-- 1 root root 123 2月 19 00:07 preposition.dic
-rw-r--r-- 1 root root 1824 2月 19 00:07 quantifier.dic
-rw-r--r-- 1 root root 41628 4月 8 11:51 sensitive_word.dic
-rw-r--r-- 1 root root 164 2月 19 00:07 stopword.dic
-rw-r--r-- 1 root root 192 2月 19 00:07 suffix.dic
-rw-r--r-- 1 root root 752 2月 19 00:07 surname.dic
3、修改IKAnalyzer.cfg.xml文件,去掉扩展字典的注释,并赋值为上面上传的字典名称,如下示例:
IK Analyzer 扩展配置
sensitive_word.dic
4、重启es
1、主要对第二次优化方案的缺点进行改进,通过查找文档发现使用IK分词器的远程字典,可以达到热更新的效果
1、分词结果已经达到预期
2、敏感词如果有更新,直接修改数据库的数据,然后通过调度器同步至ES即可
3、如果在生产过程中,ES崩溃重启,需要手动执行一次调度器同步数据至ES
1、新增一个数据接口,数据接口返回为查询es上的敏感词,这里的坑是es的热更新请求是两次,第一次请求,检验响应的Header中的Last-Modified 和eTag是否有变化,有变化,再次请求,并且重新加载敏感词(ES的热更新监听是每分钟请求一次)
@GetMapping("/getSensitiveWords")
public void loadDic(HttpServletRequest request, HttpServletResponse response) {
log.debug("================检查敏感词是否更新=================");
Object object = RedisUtil.get(CacheConst.SENSITIVE_WORD_IS_UPDATED);
String content = "";
//ES进行两次请求,第一次校验Last-Modified 和eTag,如果这两个有变化,就会进行第二次请求,第二次请求时,才会把内容写入
SensitiveWordResponseVO vo;
if (ToolUtil.isNotEmpty(object)) {
vo = (SensitiveWordResponseVO) object;
log.info("【检查敏感词是否更新】,当前vo={}", JsonUtil.toJson(vo));
if (vo.getUpdateTime() == 2) {
log.debug("【检查敏感词是否更新】vo.getUpdateTime=1");
}
if (vo.getUpdateTime() == 1) {
log.debug("【检查敏感词是否更新】vo.getUpdateTime=1");
content = updateWords(content);
} else {
log.debug("【检查敏感词是否更新】vo.getUpdateTime={}", vo.getUpdateTime());
}
} else {
vo = new SensitiveWordResponseVO();
log.debug("【检查敏感词是否更新】通过redis未查找到对应vo");
content = updateWords(content);
vo.setUpdateTime(2);
long now = System.currentTimeMillis();
vo.setETag(String.valueOf(now));
vo.setLastModified(String.valueOf(now));
log.debug("【检查敏感词是否更新】更新redis完成,vo={}", vo);
}
OutputStream out = null;
try {
log.debug("【检查敏感词是否更新】执行response");
out = response.getOutputStream();
response.setHeader("Last-Modified", vo.getLastModified());
response.setHeader("ETag", vo.getETag());
response.setContentType("text/plain; charset=utf-8");
out.write(content.getBytes("utf-8"));
out.flush();
} catch (IOException e) {
log.error("【检查敏感词是否更新】catch IOException={}", e);
} finally {
vo.setUpdateTime(vo.getUpdateTime() - 1);
if (vo.getUpdateTime() >= 0) {
RedisUtil.set(CacheConst.SENSITIVE_WORD_IS_UPDATED, vo, CacheConst.TTL_YEAR);
}
log.debug("【检查敏感词是否更新】执行response finally vo={}", vo);
if (null != out) {
try {
out.close();
} catch (IOException e) {
log.error("【检查敏感词是否更新】catch IOException={}", e);
}
}
}
}
private String updateWords(String content) {
list =;//查询es数据
if (CollectionUtil.isNotEmpty(list)) {
List words = list.stream().map(XXX::getWord).distinct().collect(Collectors.toList());
content = StrUtil.join("\n", words);
}
return content;
}
2、调度器更新同步数据库数据至ES方法:
@Override
public void fetchFromMysql() {
log.info("从数据库同步敏感词至ES");
List list = this.baseMapper.listDocument();
if (CollectionUtil.isNotEmpty(list)) {
log.info("从数据库中查询到的敏感词总大小list.size={}", list.size());
repository.deleteAll();
repository.saveAll(list);
SensitiveWordResponseVO vo = new SensitiveWordResponseVO();
vo.setUpdateTime(2);
long now = System.currentTimeMillis();
vo.setETag(String.valueOf(now));
vo.setLastModified(String.valueOf(now));
log.info("es同步完成,更新redis");
RedisUtil.set(CacheConst.SENSITIVE_WORD_IS_UPDATED, vo, CacheConst.TTL_YEAR);
log.info("更新redis完成,vo={}", vo);
}
log.info("敏感词同步结束");
}
3、修改IKAnalyzer.cfg.xml文件,去掉远程扩展字典的注释,并赋值上面的接口请求路径,其中的ip:port可以为域名,如下示例:
IK Analyzer 扩展配置
http://ip:port/xxx/xxx/xxx/getSensitiveWords
1、基本实现需求,过滤效果达到预期,并且速度也比较理想,稳定在100ms以内,最快12ms,并且主要时间消耗是在es的连接上(开发环境的ES)
2、加深了对es的使用
附上测试结果
04-09 17:08:48.148 INFO [xxxx.aop.ControllerLog] - id=750252485 =========开始请求:xxxxController.testSensitiveWord(),参数:word=东风汽车集团fuck傻逼有限公司(英文名为Dongfeng Motor Corporation)是由国家单独出资、依法设立的有限责任公司,由中华人民共和国国务院国有资产监督管理委员会作为履行出资人义务的机构,根据法律、行政法规以及国务院的授权,代表国务院对公司依法履行出资人职责。 [1] 东风汽车公司是中国四大汽车集团之一,中国品牌500强,总部位于华中地区最大城市武汉,其前身是1969年始建于湖北十堰的“第二汽车制造厂”,经过五十年的建设,已陆续建成了十堰(主要以中、重型商用车 [2] 、零部件、汽车装备事业为主)、襄阳(以轻型商用车、乘用车为主)、武汉(以乘用车为主)、广州(以乘用车为主)四大基地。除此之外,还在上海、广西柳州、江苏盐城、四川南充、河南郑州、新疆、辽宁朝阳、浙江杭州、云南昆明等地设有分支企业。 [3] 2017年11月4日,国家工商行政管理总局公告,原东又粗又硬的肉棒风汽车公司名称变更为东风汽车集团有限公司。2017年11月14日,原东风汽车公司完成工商变更登记。 [4] 2018年《财富》世界500强排行榜第65名。 [5] 2019中国制造业企业500强排名第4位。 [6] 2019年10月16日,中国机械500强企业名单发布,东风位居第二。 [7] “一带一路”中国企业100强榜单排名第33位毛公安局,type=1,
04-09 17:08:48.149 DEBUG [c.x.s.e.s.impl.XySensitiveWordDocumentServiceImpl] - 开始敏感词过滤,原文=东风汽车集团fuck傻逼有限公司(英文名为Dongfeng Motor Corporation)是由国家单独出资、依法设立的有限责任公司,由中华人民共和国国务院国有资产监督管理委员会作为履行出资人义务的机构,根据法律、行政法规以及国务院的授权,代表国务院对公司依法履行出资人职责。 [1] 东风汽车公司是中国四大汽车集团之一,中国品牌500强,总部位于华中地区最大城市武汉,其前身是1969年始建于湖北十堰的“第二汽车制造厂”,经过五十年的建设,已陆续建成了十堰(主要以中、重型商用车 [2] 、零部件、汽车装备事业为主)、襄阳(以轻型商用车、乘用车为主)、武汉(以乘用车为主)、广州(以乘用车为主)四大基地。除此之外,还在上海、广西柳州、江苏盐城、四川南充、河南郑州、新疆、辽宁朝阳、浙江杭州、云南昆明等地设有分支企业。 [3] 2017年11月4日,国家工商行政管理总局公告,原东又粗又硬的肉棒风汽车公司名称变更为东风汽车集团有限公司。2017年11月14日,原东风汽车公司完成工商变更登记。 [4] 2018年《财富》世界500强排行榜第65名。 [5] 2019中国制造业企业500强排名第4位。 [6] 2019年10月16日,中国机械500强企业名单发布,东风位居第二。 [7] “一带一路”中国企业100强榜单排名第33位毛公安局
04-09 17:08:48.190 DEBUG [c.x.s.e.s.impl.XySensitiveWordDocumentServiceImpl] - 结束敏感词过滤,共发现敏感词3个,消耗时长=40 ms
04-09 17:08:48.190 INFO [xxxx.aop.ControllerLog] - id=750252485 =========结束请求:CommonController.testSensitiveWord(),耗时:42 ms