Sphinx关联排序是怎样工作的
一直以来,我们给Sphinx添加了相当多的匹配和排序模式,并且将添加更多。一些不同的问题经常被提出,从“我怎样让指定的文档排在第一位”到 “我怎么根据匹配度来评定星级”,实际处理要归结于内在的匹配和排序。因些,让我们看看内部的匹配和排序模式到底是怎样工作的,影响最终权重值的权重因素 有哪些,是怎样影响的, how does one tweak stuff,等等,当然还有我们的目标,评定星级。
什么是匹配模式?
首先,我们归类那些令人困惑的模式。SphinxAPI 提供两个不同的方法,分别是 SetMatchMode() 和 SetRankingMode() 。SphinxQL没有提供方法,它只提供了排序选项,对应排序模式,但是没有匹配模式。它们都是干嘛的呢?
匹配模式都是为了遗留和兼容。而排序模式是关于Sphinx计算相关度的。
在版本0.9.8之前,Sphinx只有匹配模式,并且每个匹配模式是用不同的代码路径实现。每个代码路径实现一组不同类型的匹配和排序。例如,SPH_MATCH_ALL要求所有的关键字都出现,并且只用phrase proximity计算文档的权重。SPH_MATCH_ANY要求任何关键字中的一个,并且用不同的方式计算文档权重。等等。
在版本0.9.8中,我们开始启用一个全新的,统一的匹配引擎。为了避免在使用它工作时破坏兼容性,在版本0.9.8中,只提供一个独立的匹配模式,叫做SPH_MATCH_EXTENDED2。到版本0.9.9时,显然新的引擎已经变的稳定并且表现的足够好,因而我们赞成从新引擎中移除所有遗留的代码路径。因此从版本0.9.9开始,所有的查询都用统一的引擎处理,这跟之前的情况不同,而且维护困难(指之前维护困难)。因此实际上现在所有的匹配模式都只是历史遗留。
当然Sphinx仍然继续兼容那些遗留的模式,并且当你使用其中的一种时,它会自动转换成一个简单的查询短语代码(完全忽略查询语法)然后自动选择一种适当的排序模式。但其实本来就是这样。 一切都是由我们的统一引擎处理,因此,文档权重(即@weight)只跟选择的排序模式(即ranker)有关。例如,下面两个查询将得到完全一样的权重(同样一样的处理时间):
// 1st route $cl->SetMatchMode ( SPH_MATCH_ALL ); $cl->Query ( "hello world" ); // 2nd route $cl->SetMatchMode ( SPH_MATCH_EXTENDED2 ); $cl->SetRankingMode ( SPH_RANK_PROXIMITY ); $cl->Query ( "hello world" );
注意第二种方法允许使用(@title hello world)语法,因为这种匹配模式允许这样做。第一种是不允许的,因为在那种匹配模式中所有的特殊操作符会被忽略,@title会被当成一个关键字。
因此SetMatchMode()除了过滤关键字和选择一个合适的排序外没有做任何事情。它相当于一个历史调用,因为将不会再有新的匹配模式(起初在我们有一个完全成熟的查询语法之前,有一种临时的解决办法,但是这个临时解决办法臭名昭著for their tendency to last)而且支持查询语法的匹配模式允许你使用任何以前旧模式提供的东西,这更不用说了。SetRankingMode()做的更少,它只是让你明确的指定一个排序。它带领我们到达问题……
什么是排序?
排序模式,或者简称排序,可以正式的定义成函数:对一个给定的查询和文档参数计算相关值(权重)。
相关度基本上是主观的,因此没有一个适合所有的排序模式,将来也不会有。因此有多个不同的因子用于计算最终的权重,有无数的方法合并这些因子为一个权重,讨论这些是另外单独帖子的主题。
Sphinx 1.10版本中使用的两个最重要的权重因子是:1)经典统计学BM25因子,从80年代开始被大部分的搜索引擎使用,2)Sphinx特有的短语相似因子。
BM25 因子
BM25是一个只依赖于匹配关键字出现频率的浮点数值。Frequencies in question are in-document and in-collection frequencies.基本上,关键字和/或在文档字段中出现多次,那个文档的权重越大,这是很罕见的。
标准的BM25实现在Wikipedia article on BM25解释的非常明白,但是Sphinx使用的是稍微修改过的变体。首先,考虑到性能原因,我们计算所有的关键字在文档中出现的次数,而不只是计算匹配的关键字。例如(@title “hello world”)查询只在标题中匹配“hello world”的单一实例,它的BM25的计算结果和(hello world)查询一样,(hello world)查询文档中匹配所有同时出现关键字的实例。第二,我们不强制任何文档属性,因此不需要文档的长度,这样我们也忽略了文档长度(等于在原始的BM25中设置 b=0)。全部的变化都是内部的,在我们的测试中,使用原始BM25得到的计算结果不足够说明排序关联性能作用的改善。在Sphinx中使用的BM25计算算法的伪代码如下:
BM25 = 0
foreach ( keyword in matching_keywords )
{
n = total_matching_documents ( keyword )
N = total_documents_in_collection
k1 = 1.2
TF = current_document_occurrence_count ( keyword )
IDF = log((N-n+1)/n) / log(1+N)
BM25 = BM25 + TF*IDF/(TF+k1)
}
// normalize to 0..1 range
BM25 = 0.5 + BM25 / ( 2*num_keywords ( query ) )
TF是指在一个文档中被排序的检索词频。它是基于在一个文档内关键字出现的次数,但是因为用对数函数平滑处理,因此出现1000次并不会得到1000倍的影响,而是1。
TF一般在0到1之间变化,但是在条件k=1.2的情况下,它实现的变化范围是0.4545…到1之间。
IDF是指在整个文档集中的反向文档频率。常见词(如“the” or “to”等)的IDF值小,罕见词的IDF值大,当一个关键词只在一个文档中出现时,达到峰值IDF=1,而当关键词在每个索引文档都出现时,IDF=-1。
因此,就像你上面看到的代码,BM25值当关键字出现频率小时会增大,相反在文档中频繁出现的话,BM25值会减小。要注意的是当关键词过度频繁匹配索引文档超过一半以上时会降低BM25的值!事实上,当一个关键词出现在90%的文档中而很少的文档没有包含关键词时,或许大概会更有趣,应该得到更大的权重。
短语相似因子
短语相似因子,与上面BM25截然相反,根本不关心关键词出现的频率和查询关键词在文档中的位置。代替BM25使用的关键词频率,Sphinx分析关键词在每个字段的位置,并且用最长公共子串算法(LCS)计算关键词和文档的短语相似度。基本上,每个字段的短语相似度就是一些关键词在文档中出现并且顺序和查询一致。这里是一些例子:
1) query = one two three, field = one and two three
field_phrase_weight = 2 (because 2-keyword long "two three" subphrase matched)
2) query = one two three, field = one and two and three
field_phrase_weight = 1 (because single keywords matched but no subphrase did)
3) query = one two three, field = nothing matches at all
field_phrase_weight = 0
每个字段的短语权重将乘以每个字段的权重值,字段权重值通过调用SetFieldWeights() API或者在SphinxQL中的field_weights选项设置的,然后再全部相加起来生成每个文档的短语权重。字段的默认权重值为1,不能设成小于1的值。整个短语相似算法的伪代码如下所示:
doc_phrase_weight = 0
foreach ( field in matching_fields )
{
field_phrase_weight = max_common_subsequence_length ( query, field )
doc_phrase_weight += user_weight ( field ) * field_phrase_weight
}
Example:
doc_title = hello world
doc_body = the world is a wonderful place
query = hello world
query_title_weight = 5
query_body_weight = 3
title_phrase_weight = 2
body_phrase_weight = 1
doc_phrase_weight = 2*5+3*1 = 13
正是由于短语相似因子保证了越相似的短语将排在前面,而精确匹配的短语将排在非常前面。可以使用字段权重值来调整排序,例如,上面例子中,匹配单个关键字的标题的权重值和匹配两个关键字短语的内容一样。
短语相似设计成比BM25需要更多的计算,因为它需要计算所有在文档中匹配的关键词,而不仅仅只计算文档本身。Sphinx默认使用短语相似算法,因为我们相信这个产生更好的搜索质量。当然你也可以选择使用一个更轻量级的排序器来省掉这些昂贵的相似计算。
Orbital view of the rankers
短语相似和BM25是两个最重要的因子,就是说,决定最终的文档权重。虽然,最终的权重值是由排序模式决定的,也就是,一个或者多个因子经过特殊函数的处理得到一个值(同样,除了短语权重和BM25外,Sphinx还可以使用其他的排序因子。)
在1.10-beta版本,Sphinx有8种不同的排序模式,并且在将来还会添加更多的。每个排序模式计算得到不同的权重值,因此可能或者可能不会适合一个特殊的方案。
有三种简单的排序模式(NONE, WORDCOUNT, FIELDMASK)不做任何事,只统计关键字出现的次数,然后分别的返回匹配字段的位标识。它们在根本不需要排序或者由于应用端以某种方式计算时很有用。
有两种遗留的排序模式(PROXIMITY, MATCHANY)是只依靠短语相似算法,并分别用于模拟MATCH_ALL 和 MATCH_ANY两种遗留模式。
有三种排序模式(BM25, PROXIMITY_BM25, SPH04)是可以合并短语相似、BM25还有其他。允许查询语法模式并且SphinxQL现在默认是用PROXIMITY_BM25,同时强烈建议PROXIMITY_BM25内部替换PROXIMITY。BM25被推荐做为一个合适的快速排序模式,不亚于其他系统。SPH04是建立在PROXIMITY_BM25之上,但另外排序精确字段匹配,字段开头匹配比仅仅只是匹配等级高。
PROXIMITY_BM25 和 SPH04被期望产生最佳的质量,但是你特殊的结果可能不同。
选择的排序模式会严重影响搜索的性能。NONE模式显明是最快的排序模式,但是其他几个呢?处理关键字位置(出现次数)是典型的最耗时的部分,因此不需要处理这部分的排序模式(FIELDMASK, BM25)总是比其他的快。同样也需要较少的磁盘IO(不需要读取位置)。处理关键字位置的排序模式(WORDCOUNT, PROXIMITY, MATCHANY, PROXIMITY_BM25, SPH04)只在CPU影响上有所区别。
排序本质的详细信息
这章节描述Sphinx排序使用的准确算法并且提供伪代码。你可以直接跳过去,除非你想微调排序,调整字段权重等。
虽然因子可能是整型,布尔型,浮点数或者其他任何可能的值,但权重值一定是个单标量值 。在Sphinx里,权重值不只是标量而是一个整数。这不是一个强制限制,浮点数权重值可以通过各种各样方法映射到一个整数值。
让我们从三种最简单的排序模式开始吧。
1) SPH_RANK_NONE 排序模式只是简单的给每个文档赋权重为1.
weight = 1
为什么这样并且实际跳过所有的排序呢?答案就是性能。
如果你需要搜索结果按价格排序,那为什么要浪费CPU周期来处理耗时而你并不需要的排序呢?
2) SPH_RANK_WORDCOUNT 排序模式计算所有的关键字出现的次数并乘以用户设置的字段权重。
weight = 0
foreach ( field in matching_fields )
weight += num_keyword_occurrences ( field )
注意它计算所有关键字出现的次数,而不只是唯一的关键字。因此1个匹配的关键字出现3次和3个不同关键字出现1次是一样的。
3) SPH_RANK_FIELDMASK 排序模式返回一个匹配字段的位标识。
weight = 0
foreach ( field in matching_fields )
set_bit ( weight, index_of ( field ) )
// or in other words, weight |= ( 1 << index_of ( field ) )
其他五种排序模式稍微有点复杂并且大部分都依赖于短语相似。
4) SPH_RANK_PROXIMITY, 是遗留模式SPH_MATCH_ALL的默认排序模式,通过简单的短语相似算法得到一个权重值:
weight = doc_phrase_weight
由短语权重的定义可知,当文档匹配了查询但是没有保持匹配关键字的顺序,所有这样的文档的权重都为1.很显然,它跟建议使用的PROXIMITY_BM25排序模式得到的结果并没有区别。相关的搜索性能影响可以忽略不计。
5) SPH_RANK_MATCHANY 排序模式,用来模拟遗留的MATCH_ANY模式,结合了短语相似算法和匹配关键字次数,因此每个字段默认权重,a)较长子短语匹配(即更大短语相似)在任何字段将获得更高的排序,b)与短语相似一致,文档匹配不同关键字越多则排名越高。换句话说,我们先看最大匹配子短语的长度,再看匹配不同关键字的数量。伪代码如下,
k = 0
foreach ( field in all_fields )
k += user_weight ( field ) * num_keywords ( query )
weight = 0
foreach ( field in matching_fields )
{
field_phrase_weight = max_common_subsequence_length ( query, field )
field_rank = ( field_phrase_weight * k + num_matching_keywords ( field ) )
weight += user_weight ( field ) * field_rank
}
它不使用BM25,因为遗留的模式没有使用,我们要保持兼容。
6) SPH_RANK_PROXIMITY_BM25, SphinxQL的默认排序模式,同样也是SphinxAPI中“extended”匹配模式使用的默认排序,计算权重如下,
weight = doc_phrase_weight*1000 + integer(doc_bm25*999)
因此文档短语相似是主要因子,BM25是辅助部分,当相同的短语相似时进行附加的文档排序。BM25在0到1之间,因此最终权重包含的最后3个数字是由BM25决定的,所有其他的数字用于短语权重。
7) SPH_RANK_BM25 排序模式计算匹配字段用户设置的权重和BM25的总和.
field_weights = 0
foreach ( field in matching_fields )
field_weights += user_weight ( field )
weight = field_weights*1000 + integer(doc_bm25*999)
和PROXIMITY_BM25模式基本相似,除了用户权重没有乘以每个字段的短语相似值。不使用短语相似允许引擎只使用文档列表来评估搜索,跳过处理关键字出现。除非你的文档非常短(think tweets, titles, etc),关键字出现列表比文档列表大,并且需要更多的时间去处理。因此BM25比其他任何相似算法快。
同样,很多其他搜索系统默认使用BM25排序模式,或者有的只提供它做为唯一选择。因此当做性能测试展示的时候使用BM25排序可能有意义。
8) SPH_RANK_SPH04 排序模式更进一步改善PROXIMITY_BM25模式(引入数字代替有意义的名字,因为名字太复杂)。短语相似仍然是主导因素,但是当给定一个短语相似的时候,在字段最开始匹配将排序更高,如果是整个字段完全匹配的话将排到最高处。伪代码如下,
field_weights = 0
foreach ( field in matching_fields )
{
f = 4*max_common_subsequence_length ( query, field )
if ( exact_field_match ( query, field ) )
f += 3
else if ( first_keyword_matches ( query, field ) )
f += 2
field_weights += f * user_weight ( field )
}
weight = field_weights*1000 + integer(doc_bm25*999)
因此,当查询“Market Street”,SPH04模式基本上将某个字段完全匹配“Market Street”的文档排序在最前面,接着排像“Market Street Grocery”这样在字段最开始匹配的文档,然后排像“West Market Street”这样在字段某处有与短语相匹配的文档,最后排那些有包含短语所有关键字但不是一个短语的文档(例如,“Flea Market on 26th Street”)。
那我怎么画出那些星星?
或者,更正式点,我怎样计算最大可能的权重,然后根据返回的权重评定A-F等级,或者百分比,或者其他任何东西?
从前面的章节可以看到没有简单的办法可以实现。最大权重依靠于选择的排序模式和特定的查询。例如,PROXIMITY_BM25模式权重的上界应该是
max_weight = num_keywords * sum ( user_field_weights ) * 1000 + 999
但这个上界可以达到吗?实际上几乎不可能,因为那需要a)精确短语匹配b)在所有的字段c)附加的BM25峰值达到999,which roughly translates to only using one-in-a-million keywords.此外,如果查询使用字段权限符将会怎样?例如:@title hello world? 在那种情况我们的上界将永远不会被达到,因为我们除了标题字段外的其他字段都不会匹配。For this particular query the practical upper bound, which could possibly be reached by an “ideal” document, is different.
Therefore, computing the “ideal” maximum weight (one that can actually be reached) is really, really complicated. We could possibly do that on Sphinx side but that’s a lengthy R&D project with questionable outcome. So if you can’t live without percentiles (or stars!), you can either use the “absolute” upper bound estimate like the one given above (that might never be practically reached and result in “100% match”), or just use the maximum weight from your particular query, and rescale everything to that weight. Using multi-queries, the latter can be made pretty cheap.
那么我怎样把精确匹配排在前面?
你使用一个排序模式实现。
不论SphinxAPI-默认使用PROXIMITY还是SphinxQL-默认使用PROXIMITY_BM25都不行。它们只把更长子短语匹配排在前面,但是不关心在哪字段的哪里出现,还有是否匹配整个字段。
版本1.10-beta中添加的SPH04模式可以实现。
那么我怎样强制把文档D排在第一位?
根据文档D需要被排在前面的原因,你即可以使用一个适合你需求的排序模式,或者使用Sphinx运行时表达式来计算你所需要的并把结果集排序成不同。
例子如下,把精确匹配排在前面可以用表达式模拟排序:
SELECT *, @weight+IF(fieldcrc==$querycrc,1000,0) AS myweight ...
ORDER BY myweight DESC
fieldcrc是CRC(field)属性在索引时计算并存在索引文件里,querycrc是在搜索时计算CRC(query)。
例子如下,代替严格检查CRC值匹配,你可以索引并保存字段长度,然后通过表达式把越短的字段排越前面
SELECT *, @weight+ln(len+1)*1000 AS myweight ...
例子,当搜索一个关键字时为了强制一个文档排的更靠前,你可以创建一个单独的字段,放超级重要的关键字,然后给这个字段赋一个很高的权重。(不要把权重设置超过1000000)
那么和系统XYZ相比Sphinx是怎样排序的?
主要的WEB搜索引擎(像Google)都有完全不同的主题。WEB范围排序(还有垃圾信息过滤)迫使他们在排序中考虑成百上千种因素。虽然它们其中很多因素(PageRank,页面和域名年龄,反向链接数量,代码文本比等等)都是与文本无关,但也可以用于Sphinx,在某个特殊应用中使用表达式实现。Sphinx本身很普通而且它使用的排序模式只和文本相关,在上文都说的很清楚了。
虽然大部分其他全文搜索系统仍然使用BM25做为默认的文本相关因子,或者甚至限制只能使用它。不要误会我意思,BM25是重要的,它是一个重要的权重因子。但是使用它做为唯一的排序因子确实是上世纪的事情。Sphinx基于相似的排序模式向前改进了一大步,并且我们计划继续改进。敬请期待,有趣的事将要发生。