敏感(舆情)词过滤功能开发总结
公司最近有这样的一个需求:针对用户发布的动态、评论,如果触发了后台设置的舆情词功能,则触发下沉操作(状态标识)。查询时候如果是下沉的动态或者评论,那么自己能看到但是其他人看不到。
这个需求里面有几个功能点:后台需要一个舆情词功能管理,当用户发布动态和评论后 ,需要判断出来是否包含有舆情词。这就涉及到一个算法以及速度的问题。
一般这种功能是有三方的,比如腾讯百度之类的,都有这种三方接口。但是考虑到接入三方rpc调用的速度问题,决定看看有没有类似的组件。 于是,github上搜了下,确实还有。
github地址:https://github.com/toolgood/ToolGood.Words
我主要是用java语言的,把java相关的代码重新建一个maven项目拷贝到自己的项目里面。作为过滤使用
后台的crud功能,写完后有个问题是这样的:每次我新增或者修改了舆情词,在过滤时候每次都要把这些内容重新加载到内存中,但是如果每次都加载,随着内存越来越多,会出现oom的问题。
于是我在项目启动时候加了预热功能,代码如下:
package com.gwm.lafeng.consts;
import com.gwm.lafeng.WordsSearchEx2;
import com.gwm.lafeng.dao.community.BbsSentimentDao;
import org.apache.commons.collections.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
/**
* @Author fanht
* @Description
* @Date 2022/1/21 2:44 下午
* @Version 1.0
*/
@Service
public class WarmUpConstants {
private Logger logger = LoggerFactory.getLogger(this.getClass());
public static WordsSearchEx2 sensitiveWords;
@Resource
private BbsSentimentDao bbsSentimentDao;
@PostConstruct
public void WarmUpConstants() {
initSentiment();
}
private void initSentiment() {
logger.info("=====================初始化舆情词=========================");
try {
List<String> sentimentList = bbsSentimentDao.queryAllSentimentWords();
if (CollectionUtils.isNotEmpty(sentimentList)) {
sensitiveWords = new WordsSearchEx2();
sensitiveWords.SetKeywords(sentimentList);
}
} catch (Exception e) {
logger.error("初始化舆情词异常",e);
}
logger.info("===============初始化舆情词end============");
}
}
然后每次新增修改删除时候,把数据库的数据重新查询出来放到内存中。考虑到这个如果同步比较慢,因此加入了异步操作。
application 添加允许异步
@EnableAsync
/**
* 每次新增、修改、导入成功后刷新内存中的舆情词库
*/
private void resetWarmUp(){
try {
asyncWarmUpUtils.resetWarmUp();
} catch (Exception e) {
logger.error("重置舆情词库异常",e);
}
}
/**
* @Author fanht
* @Description
* @Date 2022/1/24 5:58 下午
* @Version 1.0
*/
@Component
public class AsyncWarmUpUtils {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private BbsSentimentDao bbsSentimentDao;
/**
* 每次新增、修改、导入成功后刷新内存中的舆情词库 考虑比较慢 加上异步
*/
@Async
public void resetWarmUp(){
try {
List<String> sentimentList = bbsSentimentDao.queryAllSentimentWords();
if (org.apache.commons.collections.CollectionUtils.isNotEmpty(sentimentList)) {
WarmUpConstants.sensitiveWords.SetKeywords(sentimentList);
}
} catch (Exception e) {
logger.error("重置舆情词库异常",e);
}
}
}
它这个慢主要是用toolGoods 自带的过滤词时候,如果每次都调用,是特别慢的。
这样放进去之后,每次检测时候直接判断就行了:
/**
* 判断发布评论是否含有舆情词
* @param commentDTO
* @return true or false
*/
private PublishCommentDTO hasSentimentWords(PublishCommentDTO commentDTO){
try {
//如果已经触发了下沉操作 则不再判断 此处主要影响mq计算评论的个数
if(BbsConstant.IS_SINK_1.equals(commentDTO.getIsSink()) || StringUtils.isEmpty(commentDTO.getCommentContent())){
return commentDTO;
}
if(WarmUpConstants.sensitiveWords == null){
return commentDTO;
}
//todo 这一步很耗时 此处为优化后的
WordsSearchEx2 sensitiveWords = WarmUpConstants.sensitiveWords;
if(sensitiveWords.ContainsAny(commentDTO.getCommentContent())){
LOG.info("评论含有舆情词");
List<WordsSearchResult> senWords = sensitiveWords.FindAll(commentDTO.getCommentContent());
StringBuffer stringBuffer = new StringBuffer();
senWords.forEach(f ->{
stringBuffer.append(f.Keyword + ",");
});
commentDTO.setIsSink(BbsConstant.IS_SINK_1);
commentDTO.setHasSentimentWords(BbsConstant.HAS_SENTIMENT);
commentDTO.setSentimentWords(stringBuffer.toString().length() > 200 ? stringBuffer.toString().substring(0,200):stringBuffer.toString());
}else {
commentDTO.setIsSink(BbsConstant.IS_SINK_0);
commentDTO.setHasSentimentWords(BbsConstant.NO_SENTIMENT);
commentDTO.setSentimentWords(null);
}
return commentDTO;
} catch (Exception e) {
LOG.error("判断评论是否含有舆情词异常",e);
}
return commentDTO;
}
如果是定时检测的,代码如下
package com.gwm.lafeng.timingtask;
import com.alibaba.excel.util.CollectionUtils;
import com.alibaba.fastjson.JSONObject;
import com.gwm.lafeng.WordsSearchEx2;
import com.gwm.lafeng.WordsSearchResult;
import com.gwm.lafeng.common.config.BbsConstant;
import com.gwm.lafeng.common.util.DateUtils;
import com.gwm.lafeng.consts.WarmUpConstants;
import com.gwm.lafeng.dao.community.BbsPostsDao;
import com.gwm.lafeng.dao.community.BbsSentimentDao;
import com.gwm.lafeng.document.CommunityStreamDocument;
import com.gwm.lafeng.entity.community.BbsPosts;
import com.gwm.lafeng.service.community.EsCommunityStreamService;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
/**
* @Author fanht
* @Description 触发舆情词下沉功能
* @Date 2022/1/15 9:53 上午
* @Version 1.0
*/
@Component
@EnableScheduling
public class TimingBbsSentimentManager {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Resource
private EsCommunityStreamService esCommunityStreamService;
@Resource
private BbsPostsDao bbsPostsDao;
/**
*每10S执行一次
*/
@Scheduled(cron = "10 * * * * ?")
public void job() {
logger.info("=======触发舆情词则自动下沉start============");
//查询10s内 审核通过的动态
List<BbsPosts> postsList = bbsPostsDao.queryCheckPass(DateUtils.jumpMinute(new Date(), -24*60));
if (!CollectionUtils.isEmpty(postsList)) {
WordsSearchEx2 sensitiveWords = WarmUpConstants.sensitiveWords;
postsList.forEach(t -> {
BbsPosts bbsPosts = new BbsPosts();
bbsPosts.setId(t.getId());
StringBuffer stringBuffer = new StringBuffer();
boolean tas = StringUtils.isEmpty(t.getTitle())? false : sensitiveWords.ContainsAny(t.getTitle());
boolean cas =StringUtils.isEmpty(t.getContent())? false : sensitiveWords.ContainsAny(t.getContent());
if(tas ){
List<WordsSearchResult> senWords = sensitiveWords.FindAll(t.getTitle());
senWords.forEach(s -> {
stringBuffer.append(s.Keyword + ",");
});
}
if(cas){
List<WordsSearchResult> senWords = sensitiveWords.FindAll(t.getContent());
senWords.forEach(s -> {
stringBuffer.append(s.Keyword + ",");
});
}
if(tas || cas){
bbsPosts.setHasSentimentWords(BbsConstant.HAS_SENTIMENT);
bbsPosts.setSentimentWords(stringBuffer.toString().length() > 200 ? stringBuffer.toString().substring(0,200):stringBuffer.toString());
logger.info("动态标题or内容还有舆情词,触发下沉操作"+ JSONObject.toJSON(bbsPosts));
bbsPostsDao.updateSentimentSink(bbsPosts);
CommunityStreamDocument communityStreamDocument = new CommunityStreamDocument();
communityStreamDocument.setId(bbsPosts.getId().toString());
communityStreamDocument.setIsSink(BbsConstant.IS_SINK_1);
try {
esCommunityStreamService.saveOrUpdateDocument(communityStreamDocument);
} catch (Exception e) {
logger.error("定时更新话题舆情词下沉异常",e);
}
}
});
}
}
}
遇到的问题记录:
1.首先就是oom的问题
刚开始开发完的时候,写完定时后发现服务器在dev环境老是报oom 的问题。是因为自己写的job ,每隔一分钟执行一次。每次都要把信息加载到toolGoodls的内存中。但是不清楚为何没有回收,导致内存越来越大,最终oom。解决方案就是 自己写了一个static的 静态类,在预热中加载到内存中。我比较了下之前的的写法:
//todo 这一步很耗时
WordsSearchEx2 sensitiveWords = new WordsSearchEx2();
sensitiveWords.SetKeywords(sentimentList);
这样写是特别耗时和耗费内存的。
2.crud后及时生效
为了实时生效,我是在项目初始化后先查下库之后放入到了内存中,之后每次修改删除添加,就同步做修改。这样做到了实时生效,速度也基本在100ms以内。也没有出现oom的问题
3.异步处理
同步的话,在crud之后把最新的敏感词放入内存中比较慢,这里是用了注解@Async的异步注解,用异步线程池也是可以的。如果是异步线程池 可以这样处理
ThreadFactory factory = new ThreadFactoryBuilder().setNameFormat("async-sentiment-words-%d").build(); public final ExecutorService executorService = new ThreadPoolExecutor(5,20,10L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(20),factory,new ThreadPoolExecutor.AbortPolicy()); executorService.submit(new Callable<Object>() { @Override public Object call() throws Exception { //异步方法 return null; } });

浙公网安备 33010602011771号