BM25和Lucene Default Similarity比较 (原文标题:BM25 vs Lucene Default Similarity)
原文链接: https://www.elastic.co/blog/found-bm-vs-lucene-default-similarity
原文 By Konrad Beiske
翻译 By 高家宝
这篇文章是之前讨论相似度模型(vsm和bm25)的文章的后续,在这篇文章中我们将使用维基百科的文章数据比较这两个模型的准确率和召回率。
概述
在前一篇文章中我从定义上比较了BM25和tf-idf的不同。然而Lucene/Elasticsearch中的默认相似度并非是纯粹的tf-idf实现,事实上里面加入文档长度的归一化参数。因此学术论文中关于tf-idf与BM25比较的结论(BM25比tf-idf更好)并不能完全适用于Lucene中,这使得比较Lucene中的默认相似度和BM25变得更加困难。为了解决这个疑惑我将使用维基百科的文章作为数据来测试比较两者的准确率和召回率。
索引数据
使用Wikipedia river来索引文章是一个很直接的方法,但是索引整个数据集的话欠缺一些灵活性。基于数据集的大小,仅仅是下载数据就会花费数个小时,你可能要为此等上一晚。我起初尝试使用默认的镜像,但是在我下载的中途它更新成了一个新的镜像导致下载失败了。解决办法很简单,选择一个指定的镜像。这也带来了附加的好处:使得使用不同的相似度模型重新索引完全相同的文档集变得更容易。你可能想要自己尝试一下,我推荐在river使得你的集群过载前马上开始查询,而且最好修改mapping和index设置以防因为配置不当将所有的时间都浪费在等待上。
river 配置:
{
"type" : "wikipedia",
"index" : {
"index": "wikipedia",
"type": "page",
"url" : "http://dumps.wikimedia.org/enwiki/20131202/enwiki-20131202-pages-articles.xml.bz2",
"bulk_size" : 1000,
"flush_interval" : "1s",
"max_concurrent_bulk" : 2
}
}
default similarity的index配置:
{
"settings":{
"number_of_shards":2,
"number_of_replicas":0
},
"mappings":{
"page":{
"properties":{
"category":{
"type":"string"
},
"disambiguation":{
"type":"boolean"
},
"link":{
"type":"string"
},
"redirect":{
"type":"boolean"
},
"redirect_page":{
"type":"string"
},
"special":{
"type":"boolean"
},
"stub":{
"type":"boolean"
},
"text":{
"type":"string"
},
"title":{
"type":"string"
}
}
}
}
}
BM25 similarity的index配置:
{
"settings":{
"number_of_shards":2,
"number_of_replicas":0
},
"mappings":{
"page":{
"properties":{
"category":{
"type":"string",
"similarity":"BM25"
},
"disambiguation":{
"type":"boolean",
"similarity":"BM25"
},
"link":{
"type":"string",
"similarity":"BM25"
},
"redirect":{
"type":"boolean"
},
"redirect_page":{
"type":"string",
"similarity":"BM25"
},
"special":{
"type":"boolean"
},
"stub":{
"type":"boolean"
},
"text":{
"type":"string",
"similarity":"BM25"
},
"title":{
"type":"string",
"similarity":"BM25"
}
}
}
}
}
可以看到,改变一个特定字段的相似度算法只要简单的修改mapping就可以了。
版本
在本次测试中我使用的是Elasticsearch 0.90.7 和 elasticsearch-river-wikipedia 1.3.0。
使用的查询
我用一个基本的匹配查询开始了我的实验,并把我得到的搜索结果和维基搜索的结果相比较。有两件事情很清楚:维基百科搜索的时候主要使用你输入的关键词匹配标题;返回的文档集合里也有很多文档只是一个标题别称,重定向到另一个文档。基于维基百科的词典属性,使用标题进行搜索匹配是文档的标题有强相关性的正常推论。重定向是一个指向一篇文档的替代标题或拼写,但是事实上在索引到Elasticsearch的时候重定向是作为只有重定向信息的文档存在于文档集中,更好的实现应该是包含文档具体内容的。
Wikipedia river事实上检测到了重定向文档并且用布尔字段redirect标记了他们,因此可以很容易地过滤掉。我决定将这些重定向文档过滤掉,因为对于其他文档来说,重定向文档往往在包含搜索词的同时更短从而获取更高的分数。为了更好的获得匹配文档我还尝试了一些只匹配标题的查询,这种查询更容易找到我想要的文档。如果我想要用Elasticsearch重新实现一个真实的维基百科的搜索,我当然会给标题/重定向标题加上boost。
现在我们回到最初的问题。我需要一种方法来创建一个query集合并且对于每个query应该有一个已知的正确返回结果。基于文章标题能够代表文章的特点,我只是随机找出了1000篇文档,使用它们的标题作为查询词,这些标题对应的文档就是查询对应的正确返回结果。
然后我写了一个脚本来循环查询所有测试集中的查询,下面代码中的<title>
替换为对应的查询。
{
"query": {
"filtered": {
"query": {
"match": {
"text": "<title>"
}
},
"filter": {
"bool": {
"must_not": [
{
"term": { "redirect": true }
},
{
"regexp": { "text": "redirect[^\\s]+" }
}
]
}
}
},
"explain": true
}
对这个测试来说理想情况下不应该索引重定向文档,但是事实上是索引后使用上面的查询来将这些文档过滤掉。过滤器通过river创建的redirect字段进行过滤,对于一些river没有检测到的重定向文档,使用正则表达式来过滤掉。
这个查询只返回前10条结果(Elasticsearch的默认设定)。通常情况下,比较相似度模型使用准确率(在查询结果中相关文档的比率)和召回率(所有相关文档中被返回的比率),但是只返回前10条结果的限制以及每个查询只有一个相关文档让这两个度量值基本失去了意义,所有的查询的召回率只会是0或100%,准确率只会是0或10%。对于我这次的非学术测试,我使用自己的度量来替代准确率和召回率:使用没有找到目标文档的查询的比率反映召回率,使用找到了目标文档的所有查询的目标文档排名平均值反映准确率。
这个脚本的执行结果是对于两种相似度模型分别计算准确率和召回率,匹配方法是用文档的标题去匹配文档的内容(除去标题)。
结果和结论
相似度模型 | 相关文档平均排名 | 未返回相关文档的查询比率 |
BM25 | 2.07 | 16.0% |
Default | 2.44 | 57.7% |
很显然在这次测试中BM25要比默认的相似度模型表现得更好,但是在解读这个结果的时候要注意到查询的时候有一个只返回前10条结果的限制。如果返回更多的结果,两个相似度模型的未返回相关文档的查询比率都会下降(召回率提升),但是相关文档平均排行也会上升(准确率下降)。
除非在每次查询都调整返回数量保证相关结果返回,否则没有办法计算确切的准确率和召回率,但是这个证明并不重要,因为这个结果只是关于用英文版本的维基百科的标题匹配内容而已。这次实验更倾向于搜索实践而不是标准证明,我选择使用前10个返回结果的另一个原因是因为当用户在前10个结果中找不到相关结果时,往往更倾向于重新定义更精炼的查询词重新进行搜索。
换句话说本次实验的结论并不是一个BM25总是比Default similarity要好的标准证明,但是实验结论揭示了比起Default similarity,BM25确实有很大的潜力,至少在一部分场景下。我强烈建议你使用自己真实场景下的数据花些时间来测试一下这两种相关性模型的差异。