lucene搜索系统搭建和算法反思

信息检索大作业组队做了一个检索系统(咸鱼如我大学真的是平时随随便便期末战战兢兢,感谢一起爆肝的同学哈哈哈哈),虽然做的比较简单不够完善但也学到了一些东西,写一篇博客记录一下自己在搭建整个系统时候学到的一点东西和对lucene的一些思考吧。

我们做的是电影领域的论文搜索系统,数据来源于知网,人工挑选了部分电影领域的术语并进行爬虫,获取到论文的题目、作者、来源、摘要和关键词这几个属性。我们将基于摘要建立索引,视关键词为正确标签,挑选部分搜索词作为验证词,评价指标包括无序检索结果指标:精确率precision、召回率recall、F1,有序检索结果指标:平均正确率AP和MAP值以及正确率-召回率曲线。我主要就是负责lucene的应用和优化。最后我对lucene的搜索过程做了一些反思,感觉有些地方可以进行优化并做了部分实验。

建立索引

这个图是lucene建立索引的过程:
lucene搜索系统搭建和算法反思

Directory:描述了索引的存储位置,底层封装了I/O操作,负责对索引进行存储。
Analyzer:分析器,负责文本分析,从被索引的文本文件中提取出语汇单元。
IndexWriter:创建、更新、删除索引的操作。
Documenet:每一个文档都被处理成一个Document,包含多个Field。以我们的数据为例,每一篇论文都是一个Document,包括域:title/content/author/source/keyWords。
而在lucene的底层,它将会把这些数据都处理成倒排索引表的形式,一个Term(词项)对应出现过它的文档的标志。

而下面这个是lucene的搜索过程,Query将用户的搜索转换成lucene可理解的对象,IndexReader读取索引而IndexSearcher进行搜索,返回搜索结果对象TopDocs。
lucene搜索系统搭建和算法反思

建立索引这一块其实代码很少也非常简单(这里感慨一句封装大法好啊,嘻嘻嘻),很多博客都可以搜到,当然我也会放上自己的github了,这里就不贴了。

评价指标

无序检索结果集合的评价可以用下面的关联表来展示:

表格 相关 不相关
检索到 TP(真正例) FP(伪正例)
未检索到 FN(伪反例) TN(真反例)

精确率为:precision=TPTP+FP

召回率为:precision=TPTP+FN

F1为:F1=1p+1r

我们计算出93个验证词的以上三个指标并进行可视化,发现绝大多数词语召回率很高但是精确率很低。于是我们返回到系统中查找原因。
由上面公式我们可以了解到,低精确率的原因是存在大量伪正例,查看被标为伪正例文档我们发现两个原因。

一是该搜索词高频而非关键。搜索词语的概念范围很大,在许多文档中都多次出现但并不是描述的重点。
二是关键词与索引词部分相似,但描述的细节不同,而我们的判断标准是搜索词与关键词完全匹配才认为是正确的。举个例子说,搜索词为“周星驰”,返回的文档中被标为伪正例的出现大量关键词为“周星驰台词”“周星驰风格”这种有前后缀的词语。

以上两个原因导致系统精确率不高。第二个原因的影响很大,我觉得吧,如果我们要往细了做要往优了做,那么判断方法,即搜索词与关键词的匹配粒度可能需要一个标准:如果我的搜索词为“周星驰”,那么返回了关键词为“周星驰台词”的文档,到底算不算正确搜索?

而召回率的影响因素主要是伪反例,即,关键词有我们的搜索词但我们却没有将这篇文档搜出来。查看伪反例文档我们发现,这些文档的摘要部分并没有出现过我们的搜索词,但在语义上相关,而我们的索引只建立在摘要上,同时,如果你去看lucene的打分公式,你会发现lucene只考虑到了词频而没有考虑到语义,所以它无法在语义上检索,亦即无法把这些实际相关的文档查找出来。

lucene的打分公式:
lucene搜索系统搭建和算法反思
coord(q,d):评分因子,基于文档中出现的查询项个数。计算为文档命中的搜索词个数除以搜索条件的词数目。
queryNorm(q):计算每个查询条目的方差和,此项并不影响文档排序,而对于不同的查询q是不一样的(有一个比较复杂的计算公式,这里不详写了)。
tf(t in d):对于每一个查询q中出现的词项t ,在文档d中的词频。
idf(t):词项t的文档频率。
t.getBoost():查询时查询项t的权重。
norm(t,d):与长度相关的加权因子,有一个比较复杂的计算公式,这里也不写了,想详细了解的可以去查查。总之这个因子可以用来压制长文档的分数的(长文档的词频可能比较高)。

其实写到这里已经出现了一些我们可以进行优化的东西。

有序检索评价指标:
平均正确率(average precision, AP)对于单个查询,返回结果在每篇相关文档位置上的正确率的平均值。
嗯,没错,这个定义可以说相当不直观了,所以我们举个例子,某个查询q有6个相关文档,我们的系统排序返回了5篇,位置分别为第1、2、4、7、 10,则AP = ( 1/1 + 2/2 + 3/4 + 4/7 + 5/10) / 6

因此我们可以看到,相关文档位置越靠前,AP值就越大。而MAP就是对所有查询的AP求均值。

而正确率-召回率曲线,如图所示,适用于评价单个查询的优劣,简单直观,既考虑了检索结果的覆盖度,又考虑了检索结果的排序情况,但是很难明确表示多个查询的整体效果。
lucene搜索系统搭建和算法反思

搜索优化

对系统我们可以有的优化方向:

1、对搜索词进行处理,比如模糊查询或者扩展,这里涉及我们要定义的搜索词与关键词的匹配原则。
2、对文档的题目也建立索引,这时我们的搜索要考虑两个权重:xtitle+ycontentx 的取值更大的时候意味着我们认为论文的题目更重要。
3、重新定义lucene的打分公式,除了考虑词频以外还加入语义因子的衡量。
4、使用自定义词典的分词器。导入我们的关键词词汇表作为分词器的扩展词典,可以提高索引的质量。

这几点是我当时能想到全部内容,但是基于时间和精力有限我当然不可能全部去实现……但是谈谈自己的理解是ok的啦!(^-^

对于第三个点,有一篇正合我意的论文,这是作者提出的打分公式:
lucene搜索系统搭建和算法反思
来源:论文《一种改进的Lucene语义相似度检索算法》

sim(t, Q)表示查询q中和词项t 相似的词的频率,sim_df(t)表示文档集合中包含与词项t相似的文档数目,sim(t, D)表示文档D中与词项t相似的词项频率。

这里,就能考虑到被索引的字段中与查询词语义相似的词语,至于语义相似的计算,又是另一个需要选择的东西了。

最后我选择了对第二和第四点进行调节。在对题目和摘要都建立了索引后,搜索时这两个域的权重占比需要一个调参的过程,我们选择了title: content 占比分别为0.5:1、1:1、1.5:1这三个。

加入自定义词典可以提高lucene的分词效果,导入领域词典后lucene就不会把关键词切开,比如”美国电影“就不会被切成“美国”和“电影”,这样更有利于我们的匹配。加不加入自定义词典就能有两种类型的索引。
(ps:在实现上,原来我用的分词器是lucene的标准分词器,后来了解到它是单元分词,额……这里关于lucene的分词器又有好几个,有兴趣也可以研究,后来使用IKAnalyzer,自定义词典放在/IK-Analyzer-2012FF/dist/ext.dic里面,修改IKAnalyzer.cfg.xml里面注释使其加载扩展词典)

因此我们有(3*2+1)7个对比实验。
每一个实验都计算93个验证词的precision、recall、F1和MAP,再算最终均值。最后发现对于无序检索指标表现最好的加了自定义词典并且title:content=1.5:1的索引,而有序检索反馈最好的是加了自定义词典并且title:content=1:1的索引。

逼逼了一堆反正就是非常开心又解决了一个大作业。很高兴队友们都很nice很配合,即使是不会打代码的人也能参与到系统开发的整个过程,很多时候把自己的思路和计算过程讲给别人听常常能有写新的发现,比如在计算一个索引模型的总体精确率的时候应该是累加所有验证词的精确率再除总数目,而不是累加验证词的TP和FP去计算。我相信讲解一个系统,只有能让外人也明白其中道理,才是真的ok了。而不少细节,也许就需要不“身处此山中”的人去提意见。