Tripod是一款基于lucene语法实现的,可对文本数据进行实时匹配的开源工具,其工程路径为https://github.com/colorknight/tripod.git。在一定场景中,使用它,可以节省对磁盘IO的消耗,提升应用的实时效果,且对于有lucene和elastic search使用经验的人而言几乎没有学习曲线。在很多文本处理的应用中,会有类似数据订阅的需求,即将客户关注的数据推送给客户。一种典型的实现是,我们会根据用户设定的关键词或规则,在lucene或elasticsearch中匹配数据,并最终将数据推送给客户。这种实现方式,要求文本数据必须已经建立好索引,访问时,会带来大量的IO消耗。各种基于定时策略的访问优化,都无法根本解决随着客户数量的增多而带来的这种资源消耗。Tripod能很好的改善这种应用需求,它支持对文本进行实时匹配,不会引起IO消耗。另外,它兼容lucene语法,可以与即有的基于lucene与elasticsearch的应用无缝整合,即如果采用lucene语法描述客户关注的信息,那么即可以使用Tripod解决实时推送问题,也可以实现从lucene或elasticsearch中对历史数据进行检索。下面将对Tripod进行详细介绍。
Tripod的接口设计借鉴了lucene中的概念,将每个待处理数据都视为一个文档。多数情况下,Tripod提供的接口都要求将文档以map的形式表示,如下接口,是Tripod中最为重要的一个接口:
/**
* 对map格式描述的文档数据进行匹配。
* @param dataMap 待匹配数据,Map中的key为lucene规则中要处理的field;value为文本
* 进行分词处理后的有序词组。每组数据会被视为一个文档,内部处理时会
* 转换为DocumentEntity对象。
* @param termDistance 是否以词距离进行匹配,缺省为true;若该值为false,表示以字
* 符距离进行距离计算。
* @param hitMap 该对象是一个返出结果对象,它会被填充所有由规则命中的词、短语或一段
* 文本的起止位置。由于文档数据已被做过分词,文字空间上不再连续,当规
* 则中带有距离计算时,无法复原文本串,故只给出其起止位置。key值与
* dataMap的key值含义相同。
* @return 匹配的分值,若该值为负表示没有匹配
*/
public double match(Map
dataMap, boolean termDistance, Map
代码片段(1)> hitMap);
如代码中所示,dataMap参数即表示了一个文档。Tripod将文档看作是由多个部分组成的,如:文档包括题目、摘要、正文等。这些不同的部分的名字作为Map的key存在,而其对应的文字部分将被分词处理为有序词组后存放。由于开源的分词工具很多,所以Tripod并没有提供分词工具,而更多的希望Tripod作为整个NLP处理流程中的一环,能够复用分词后的序列,有效提升数据处理的效率。但是,对于分词后的对象要求其能够支持TermEntity接口,该接口如下:
/**
* 词实体接口
*/
public interface TermEntity extends Serializable {
// 词在文档中的序号,以词作为单位进行临近距离匹配时,依赖此属性
int getIndex();
// 词文本
String getTerm();
// 词在文档中的起始字符偏移,以字符为单位进行临近距离匹配时,依赖此属性
int getOffset();
// 词性(可选,计算时并不需要)
String getPartOfSpeech();
}
代码片段(2)
TermEntity接口中给出了要实现的方法以及相关的解释,其中getIndex()和getOffset()两个方法的注释都提到了临近距离匹配,这个匹配即对应lucene语法的Proximity Searches查询。代码片段(1)match方法的第二个参数termDistance就是用于标示做临近距离匹配时以何为单位做距离计算的。如临近距离表达式为
”中国 发展”~2。当该值为true时,表示以词为单位进行距离计算,那么“中国进入飞速发展时期。”这句话就可以被命中,因为在“中国”和“发展”之间有两个词,满足表达式距离为2的要求;而当该值为false时,表示以字符为单位进行距离计算,那么“中国进入飞速发展时期。”这句话不符合表达式的匹配要求。因为在“中国”和“发展”两个词之间共有4个字符,不满足距离为2的要求。接口之所以设计这个参数,主要是考虑到分词技术的局限问题。如果不能很好的处理未登录词,或者进行过了停止词处理,那么以字符为单位计算距离很难给出合适的距离范围。而当以词为单位进行距离计算时,就会相对容易一些。
代码片段(1)match方法的第三个参数为一个[in/out]参数,该参数用于输出所有匹配命中的文本标记。如注释所说,该标记可以是词、短语或一段文本,当有临近距离匹配时,由于文本进行过了分词处理,有些文本内容可能已被舍弃,故无法还原原始的文本标记,故返回时只能返回命中文本标记的起止字符偏移了。该参数的返回值可用于高亮显示以及匹配规则调试。返回这些信息有一定的计算代价,应用时可根据需要填充是否要返回。
match方法的返回值为一个双精度浮点值。该值为lucene规则与文档的匹配相关度。关于相关度计算,可参见lucene的评分机制。在进行评分时,由于要用到IDF值,该值的计算需要依赖词的通用程度,即词在多少个文档中出现过。这个值需要对所有处理过的数据进行累积,不能随着应用的终止而释放,并在下次计算时重新累积。每次都重新累积时,无法获得准确的评估值。为此,Tripod设计了IdfCounter接口,接口如下:
public interface IdfCounter {
/**
* 对集合中的词进行记数
* @param terms 词集合,一般认为一个set集合为一篇文档中出现的所有去重后的词
*/
void count(Set
terms); /**
* 获得指定词的idf值
* @param term 词
* @return idf分值
*/
double idf(String term);
int getTotalDocs();
Map
getTermDocs(); }
代码片段(3)
并给出了一个缺省实现,IdfCounterImpl,该实现拥有save()和load()的方法,但何时save,何时load交由使用者根据需要调用。
需要注意的是,Tripod采用的相关度打分原理与lucene一致,但很难保证打分值一致。但分值所表现的相关度强弱关系是一致的。另外,Tripod提供了一个是否进行分值评估的开关选项,setScoring()方法。在对实时文本匹配处理时,若只需要知道是否命中,而不需知道相关度时,可将打分机制关闭,即设置该值为false,则可以优化匹配效率。
Tripod对外提供服务的类主要有以下三个:
Tripod类
一个lucene语法描述的匹配规则会被构建为一个Tripod对象,该对象为一个lucene匹配规则的解释执行器。通过该对象的match方法,传入待处理的文档数据可返回文档与规则是否匹配以及匹配程度。
TripodEngine类
该对象可以被视为一个Tripod的集合。在实际应用场景中,一般不会只有一个匹配规则。往往会有多个匹配规则需要对文档进行同时匹配。如向不同客户推送数据的应用场景,或者依据特征规则进行文档分类的应用场景等。
TripodEvaluator类
该对象给出了一种计算lucene规则复杂度的实现。该复杂度的计算值无法精准体现计算资源的耗用情况,只能反映占用趋势。使用者可通过调节相应的计算系数,获得适用自己环境的复杂度评估值。在用户可以自定义数据匹配规则的应用场景里,可以通过设定复杂度阀值来约束用户,防止其设置的规则过于复杂,导致计算资源占用过大。
Tripod使用非常简单,其主要功能都集中在tripod-engine模块中,可通过如下maven依赖引入tripod-engine模块。
org.datayoo.tripod
tripod-engine
1.0.0
Tripod工程对每个主要服务类都有相关的测试示例代码。以下是org.datayoo.tripod.engine.TripodEngineTest的一段示例代码:
public void test1() {
// 创建TripodEngine
TripodEngine tripodEngine = createTripodEngine();
// 构造测试数据文档
Map
dataMap = TripodTestHelper.createDataMap(); // 匹配文档
tripodEngine.match(dataMap, true);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
protected static TripodEngine createTripodEngine() {
List
fieldMetadatas = new LinkedList (); FieldMetadata fieldMetadata = new FieldMetadata("title", 2);
fieldMetadatas.add(fieldMetadata);
fieldMetadata = new FieldMetadata("content", 1);
fieldMetadatas.add(fieldMetadata);
/*
* 初始化TripodEngine,传入待处理的文档对象的字段信息,缺省字段及Idf计算辅助接口
* */
TripodEngine tripodEngine = new TripodEngine(fieldMetadatas, fieldMetadata,
new IdfCounterImpl());
// 设置引擎在匹配时计算相关度
tripodEngine.setScoring(true);
// 文档匹配监听器,当规则匹配文档后,通过该接口回调传回匹配结果
TripodListener tripodListener = new TripodPrintListener();
// yoolerEngine
// .addYoolerRule("test", "(中办&title:中办)^2 任命 形式主义", yoolerListener);
// 向引擎添加匹配规则
tripodEngine.addTripodRule("test1", "\"第5代领导\" 任命 形式主义", tripodListener);
return tripodEngine;
}
代码片段(4)
示例代码的执行结果为:
test1 : 0.066580
Tripod的语法与lucene的查询语法基本一致,只是稍做了调整。关于lucene查询语法这里不做赘述,可参见lucene查询语法了解详情。本部分只简单描述二者之间的差异。
Tripod语法中舍去了所有由字母组成的运算符,一律采用符号做为运算符,主要为了防止这些字母运算符作为词出现在文本中时,为使用带来不必要的混乱。下表给出二者间的差异:
Lucene查询语法 |
Tripod语法 |
说明 |
mod_date:[20020101 TO 20030101] |
mod_date:[20020101,20030101] |
区域运算符中的“TO”运算符在Tripod中用”,”替代 |
"jakarta apache" OR "Apache Lucene" |
"jakarta apache" | "Apache Lucene" |
逻辑或运算符“OR”在Tripod中用”|”或者”||”替代。 |
"jakarta apache" AND "Apache Lucene" |
"jakarta apache" & "Apache Lucene" |
逻辑与运算符“AND”在Tripod中用”&”或者”&&”替代。 |
NOT "jakarta apache" |
! "jakarta apache" |
逻辑非运算符“NOT”在Tripod中用”!”替代。 |
另外,Tripod中还有一个增强语法,“in语法”,其与 sql语言中的in运算符类似。其表达式如下:
title:[愤怒 被喷 垃圾 碾压]
其运算符与区域运算符类似,只是“[]”内的值使用“空格”分隔,且可以排列任意多个。“[]”内的值可以是词和短语。表达式在匹配时,若有任何一个值匹配中对应文档字段中的内容,即表示匹配成功。In语法可以用于有类似笛卡尔积计算的场景,如下应用场景:
[北京 上海 广州] & [腾讯 阿里 百度]
该表达式表示文档的内容中至少要包括“北京”、“上海”、“广州”中的一个词以及“腾讯”、“阿里”、“百度”中的一个词,规则才能被命中。该逻辑lucene语法也能表达,只是表达起来较为繁琐。