对文本数据进行实时过滤的需求在舆情类系统的开发过程中经常碰到。如:对涉黄、涉政、涉恐文本的过滤;对广告数据的过滤;以及对非业务数据的过滤等。这些过滤需求由于比较难于描述其过滤规则,所有出现了很多分类算法用于对各类文本数据的分类过滤,这些算法在网上已经有很多文章进行了深入探讨,本文不再做赘述。本文将主要探讨一种基于规则的实时文本过滤技术。
在舆情系统的开发中,我们也时常会碰到基于规则定义的文本过滤需求。如:SSAS模式下的舆情系统,用户通过基于关键字的规则向舆情系统设定其感兴趣的文本内容,舆情系统根据规则向用户推送相关的文本;用户向舆情系统设置告警规则,当文本匹配告警规则时产生告警等。这类需求字面上随没有蕴含实时的字样,但实际对于数据处理的实时性是有要求的。当舆情采集到文本时,如果能更快的确定需要将该文本推送给哪些客户,客户将更实时的获得到关注的文本,提升客户的使用体验;而对文本进行告警,告警越实时,用户就可以更及时的处理告警。
这类基于规则的实时文本过滤需求,在过去实现时,需要在实时性和功能完整性上做出取舍。因为lucene没有提供文本的实时过滤功能,所以为了能够实现文本处理的实时性,开发者往往会自己动手实现一个仅支持lucene语法子集能力的过滤功能,如对关键词进行匹配过滤等。这种情况,文本在接收后即可在数据流中被实时过滤,不需要等到进入lucene等系统后再进行处理。这种实现方式,数据被实时过滤,但其能支持的过滤条件有限,有些lucene语法支持的功能,其无法满足;在进行文本处理时,lucene无疑已经成为实时的标准,其提供的文本检索匹配能力已基本覆盖了我们对文本处理的需求,但由于它的实现机制,其无法提供实时的文本过滤方案,只提供了准实时的解决方案NRT(Near Realtime Search)。该方案会对lucene整体性能有一定影响,另外,关于NRT,本文不做具体的讨论,有兴趣的朋友可以在网络中找到很多相关资料。但使用lucene技术,可以在文本过滤的功能完整性及性能上找到一个不错的折衷。
Lucene的折衷在过去无疑是一个还不错的选择,但总还是让笔者耿耿于怀。因为其提供的技术总还要引起IO操作(因为其索引是需要保存在磁盘的),而这些IO操作在实时过滤的场景中实际是没有必要的。这与笔者在2006年左右处理结构化数据的过滤时一样,当年需要将数据存储到数据库后,再利用数据库的检索功能将数据检索出来,以达到过滤效果。当时这种方案是因为没有一种基于SQL语法能力的,能够对内存结构化数据进行过滤的有效工具,后来笔者经过数年积累开发了一个针对内存结构化数据实时过滤的开源工具MOQL,该工具是一款基于SQL语法的结构化数据实时处理工具,有兴趣的朋友可以参看我关于MOQL的其它文章。(代码路径: https://github.com/colorknight/moql.git)。
如今碰到的问题与当年碰到的问题如出一辙,则其解决办法也如出一辙。如果有一款兼容lucene语法的,支持实时文本过滤的工具,那么就可以很好的解决文本数据的实时过滤问题了。这个工具可以降低使用者的学习成本,并可以让应用系统实现一个语法同时完成实时计算与对lucene的检索(注:笔者开发的舆情系统中,实际已经完成了这样的应用整合)。
为此,笔者开发了一款兼容lucene语法的实时过滤开源工具Tripod(代码路径: https://github.com/colorknight/tripod.git)。该工具完全兼容lucene的查询语法,相关语法参见lucene查询语法。同时,Tripod也支持lucene的相关度评分机制,评分算法可参见lucene评分机制。在应用系统中,由于实现机制的差异,Tripod给出的分值与lucene不一致,但其所表现的趋势是一致的。其造成这种评分差异的主要原因是,lucene会保留所有文档基于词的反向索引,但Tripod受限于使用内存的大小,无法保留如此巨大的索引,在进行TF/IDF计算时,该值会引起不小的差异,但由于所有的信息都是基于实际环境中的文档信息构建出来的,所以二者所返回的相关度趋势是一致的。
使用Tripod进行文本数据的实时过滤非常简单,示例代码如下:
代码语言:javascript复制// 创建TripodEngine
TripodEngine tripodEngine = createTripodEngine();
// 构造测试数据文档,一个文档由多个部分,如:题目,内容;而每个部分由一堆词组成
Map<String, TermEntity[]> dataMap = TripodTestHelper.createDataMap();
// 匹配文档
tripodEngine.match(dataMap, true);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
protected static TripodEngine createTripodEngine() {
// 描述文档的组成结构,及结构中每一部分的权重
List<FieldMetadata> fieldMetadatas = new LinkedList<FieldMetadata>();
FieldMetadata fieldMetadata = new FieldMetadata("title", 2);
fieldMetadatas.add(fieldMetadata);
fieldMetadata = new FieldMetadata("content", 1);
fieldMetadatas.add(fieldMetadata);
/*
* 初始化TripodEngine,传入待处理的文档对象的字段信息,缺省字段及Idf计算辅助接口
* IdfCounterImpl记录了文档与词的相关关系,可持久化该类的信息,每次使用Tripod时
* 注入这些持久化数据,使Tripod的相关度计算结果尽量保持与lucene一致
* */
TripodEngine tripodEngine = new TripodEngine(fieldMetadatas, fieldMetadata,
new IdfCounterImpl());
// 设置引擎在匹配时计算相关度
tripodEngine.setScoring(true);
// 文档匹配监听器,当规则匹配文档后,通过该接口回调传回匹配结果
TripodListener tripodListener = new TripodPrintListener();
// 向引擎添加基于lucene语法的匹配规则
tripodEngine.addTripodRule("test1", ""第5代 领导" 任命 形式主义", tripodListener);
return tripodEngine;
}
示例代码的执行结果为:
test1 : 0.066580
其原理就是在Tripod引擎中,根据需要,预先设定好所有的过滤规则,并为每个过滤规则设置好监听器。将Tripod引擎设置在文本数据的处理流中,每当有新的文本数据被采集到,流经Tripod引擎时,引擎就会对文本进行过滤处理。引擎会遍历设置在引擎内的所有过滤规则,当文本命中某个过滤规则后,会通过该规则的监听器,将文本返回以完成后续的处理逻辑。一个文本可以同时命中多个过滤规则。Tripod被设定为一个实时过滤工具,因此其内部没有多线程的调度实现。开发者可根据自己应用场景的实际需要,自行完成多线程开发,实现大数据量的文本实时过滤并发处理。