使用IK中文分词器做敏感词过滤

需求

部分字段(可能为富文本)需要做敏感词过滤,敏感词词库由产品给出。

前置条件

项目使用的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<String> getAnalyzes(String index, String analyzer, String text) {
        //调用ES客户端分词器进行分词
        AnalyzeRequestBuilder ikRequest = new AnalyzeRequestBuilder(elasticsearchTemplate.getClient(),
                AnalyzeAction.INSTANCE, index, text).setAnalyzer(analyzer);
        List<AnalyzeResponse.AnalyzeToken> ikTokenList = ikRequest.execute().actionGet().getTokens();
        // 赋值
        List<String> 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文件,去掉扩展字典的注释,并赋值为上面上传的字典名称,如下示例:

<properties>
        <comment>IK Analyzer 扩展配置</comment>
        <!--用户可以在这里配置自己的扩展字典 -->
        <entry key="ext_dict">sensitive_word.dic</entry>
         <!--用户可以在这里配置自己的扩展停止词字典-->
        <entry key="ext_stopwords"></entry>
        <!--用户可以在这里配置远程扩展字典 -->
        <!--<entry key="remote_ext_dict"></entry>-->
        <!--用户可以在这里配置远程扩展停止词字典-->
        <!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

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<String> 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<XXX> 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("敏感词同步结束");
    }

总结

1、基本实现需求,过滤效果达到预期,并且速度也比较理想
2、加深了对es的使用

发布了1 篇原创文章 · 获赞 0 · 访问量 12

猜你喜欢

转载自blog.csdn.net/m0_37453314/article/details/105407205