Sphinx添加了相当多的匹配和rank模式,并且将添加更多。一些不同的问题经常被提出,从“我怎样让指定的文档排在第一位”到 “我怎么根据匹配度来评定星级”,实际处理要归结于内在的匹配和排序。
匹配方式有基础匹配模式和拓展的匹配模式。
Sphinx 1.10版本中使用的两个最重要的权重因子是:
1)经典统计学BM25因子,从80年代开始被大部分的搜索引擎使用,
2)Sphinx特有的短语相似因子。
根据Sphinx排序使用的准确算法,可以通过调整字段权重微调排序。
虽然排序因子可能是整型,布尔型,浮点数或者其他任何可能的值,但权重值一定是个单标量值。在Sphinx里,权重值是一个整数,浮点数权重值可以通过各种各样方法映射到一个整数值。
SphinxAPI 提供两个不同的方法,分别是 MatchMode 和RankingMode。
MatchMode 匹配模式都是为了遗留和兼容。而RankingMode模式是关于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)只跟选择的rank模式有关。
例如,下面两个查询将得到完全一样的权重(同样一样的处理时间):
// 1st route
$cl->SetMatchMode ( SPH_MATCH_ALL );
$cl->Query ( "helloworld" );
// 2nd route
$cl->SetMatchMode ( SPH_MATCH_EXTENDED2 );
$cl->SetRankingMode ( SPH_RANK_PROXIMITY );
$cl->Query ("helloworld" );
注意第二种方法允许使用(@titlehello world)语法,因为这种匹配模式允许这样做。第一种是不允许的,因为在那种匹配模式中所有的特殊操作符会被忽略,@title会被当成一个关键字。
MatchMode 功能是过滤关键字和选择一个合适的排序。它相当于一个历史调用,因为将不会再有新的匹配模式。
RankingMode只是让你明确的指定一个排序方式。
rank模式,或者简称排序,可以正式的定义成函数:对一个给定的查询和文档参数计算相关值(权重)。
相关度基本上是主观的,因此没有一个适合所有的排序模式,将来也不会有。因此有多个不同的因子用于计算最终的权重,有无数的方法合并这些因子为一个权重,讨论这些是另外单独帖子的主题。
以官方C的API为例,sphinx自带的一些匹配模式如下:
匹配查询词中的任意一个。
通常想搜索到尽可能多的一句话中的内容,使用的是SPH_MATCH_ANY,但使用它之后,任何关键词中的字都可能做为一个单独的词进行搜索。这种匹配模式对词频也很有权重,这种搜索结果一般不是很准确
将整个查询参数看作一个词组,要求按顺序完整匹配才会搜索出结果,搜索时会过滤掉特殊符号,比较符合需要搜索出相关度最高的结果。
在创建连接对象时,设置:
sphinx_client *client;
client->mode=SPH_MATCH_PHRASE;
匹配所有查询词(默认模式)
在创建连接对象时,设置:
sphinx_client * client;
client->mode= SPH_MATCH_ALL;
将查询看作一个布尔表达式
将查询看作一个Sphinx内部查询语言的表达式
要搜索的关键词同时存在才会被搜索出来。因为SPHINX默认不是通过空格分词的。而是通过""来分。比如两个关键词:我们他是。
如果单这样写sphinx_query (client, “我们他是“, index, NULL );使用any模式会折成我 们 他 是 。似乎是一元分词法。而使用extended2则要搜索的字段同时存在这2个词才可以被搜索到。如果写成 sphinx_query ( client,("\"我们\"|\"他是\"",index);那么他就会分成我们和他是2个词。而且同时存在的权重会较高。
在创建连接对象时,设置:
sphinx_client *client;
client->mode=SPH_MATCH_EXTENDED2;
使用完全扫描,忽略查询词汇
Rank模式为拓展的匹配模式。
以官方C的API为例,设置为拓展匹配模式SPH_MATCH_EXTENDED2后,请求的匹配方式将由其rank模式来决定。
可使用如下模式对匹配搜索结果:
禁用排序的模式,这是最快的模式。实际上这种模式与布尔搜索相同。所有的匹配项都被赋予权重1
排序模式只是简单的给每个文档赋权重为1.
weight = 1
为什么这样并且实际跳过所有的排序呢?答案就是性能。
如果你需要搜索结果按价格排序,那为什么要浪费CPU周期来处理耗时而你并不需要的排序呢。
排序模式计算所有的关键字出现的次数并乘以用户设置的字段权重。
weight = 0
foreach ( field inmatching_fields )
weight += num_keyword_occurrences ( field )
注意它计算所有关键字出现的次数,而不只是唯一的关键字。因此1个匹配的关键字出现3次和3个不同关键字出现1次是一样的。
排序模式返回一个匹配字段的位标识。
weight = 0
foreach ( field inmatching_fields )
set_bit ( weight, index_of ( field ) )
// or in otherwords, weight |= ( 1 << index_of ( field ) )
通过简单的短语相似算法得到一个权重值:
weight = doc_phrase_weight
由短语权重的定义可知,当文档匹配了查询但是没有保持匹配关键字的顺序,所有这样的文档的权重都为1.很显然,它跟建议使用的PROXIMITY_BM25排序模式得到的结果并没有区别。相关的搜索性能影响可以忽略不计。
短语相似,与上面BM25截然相反,根本不关心关键词出现的频率和查询关键词在文档中的位置。代替BM25使用的关键词频率,Sphinx分析关键词在每个字段的位置,并且用最长公共子串算法(LCS)计算关键词和文档的短语相似度。
基本上,每个字段的短语相似度就是一些关键词在文档中出现并且顺序和查询一致。这里是一些例子:
1) query = one twothree, field = one and two three
field_phrase_weight = 2 (because 2-keywordlong "two three" subphrase matched)
2) query = one twothree, field = one and two and three
field_phrase_weight = 1 (becausesingle keywords matched but no subphrase did)
3) query = one twothree, field = nothing matches at all
field_phrase_weight = 0
每个字段的短语权重将乘以每个字段的权重值,字段权重值通过调用SetFieldWeights() API或者在SphinxQL中的field_weights选项设置的,然后再全部相加起来生成每个文档的短语权重。
字段的默认权重值为1,不能设成小于1的值。整个短语相似算法的伪代码如下所示:
doc_phrase_weight = 0
foreach ( field inmatching_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 awonderful 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
正是由于短语相似因子保证了越相似的短语将排在前面,而精确匹配的短语将排在非常前面。可以使用字段权重值来调整排序,例如,上面例子中,匹配单个关键字的标题的权重值和匹配两个关键字短语的内容一样。
用来模拟匹配模式的MATCH_ANY模式,结合了短语相似算法和匹配关键字次数,因此每个字段默认权重,a)较长子短语匹配(即更大短语相似)在任何字段将获得更高的排序,b)与短语相似一致,文档匹配不同关键字越多则排名越高。换句话说,先看最大匹配子短语的长度,再看匹配不同关键字的数量。
伪代码如下:
k = 0
foreach ( field inall_fields )
k += user_weight ( field ) * num_keywords ( query )
weight = 0
foreach ( field inmatching_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,因为遗留的模式没有使用,要保持兼容。
排序模式计算匹配字段用户设置的权重和BM25的总和.
field_weights = 0
foreach ( field inmatching_fields )
field_weights += user_weight ( field )
weight = field_weights*1000 + integer(doc_bm25*999)
和PROXIMITY_BM25模式基本相似,除了用户权重没有乘以每个字段的短语相似值。不使用短语相似允许引擎只使用文档列表来评估搜索,跳过处理关键字出现。除非你的文档非常短(think tweets, titles, etc),关键字出现列表比文档列表大,并且需要更多的时间去处理。因此BM25比其他任何相似算法快。
同样,很多其他搜索系统默认使用BM25排序模式,或者有的只提供它做为唯一选择。因此当做性能测试展示的时候使用BM25排序可能有意义。
BM25是一个只依赖于匹配关键字出现频率的浮点数值。Frequencies in question are in-document and in-collectionfrequencies.基本上,关键字和/或在文档字段中出现多次,那个文档的权重越大,这是很罕见的。
统计相关度计算模式,仅使用 BM25 评分计算(与大多数全文检索引擎相同)。这个模式比较快,但是可能使包含多个词的查询的结果质量下降。
标准的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 inmatching_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%的文档中而很少的文档没有包含关键词时,或许大概会更有趣,应该得到更大的权重。
怎样计算最大可能的权重
怎样计算最大可能的权重,然后根据返回的权重评定A-F等级,或者百分比,或者其他任何东西?
从前面的章节可以看到没有简单的办法可以实现。最大权重依靠于选择的排序模式和特定的查询。
例如,PROXIMITY_BM25模式权重的上界应该是
max_weight = num_keywords * sum ( user_field_weights ) * 1000 + 999
但这个上界可以达到吗?实际上几乎不可能,因为那需要a)精确短语匹配b)在所有的字段c)附加的BM25峰值达到999。
此外,如果查询使用字段权限符将会怎样?例如:@title hello world? 在那种情况我们的上界将永远不会被达到,因为我们除了标题字段外的其他字段都不会匹配。
强调需要字段
即可以使用一个适合你需求的排序模式,或者使用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)
使用词组评分和 BM25 评分。
计算权重如下:
weight = doc_phrase_weight*1000 + integer(doc_bm25*999)
因此文档短语相似度是主要因子,BM25是辅助部分,当相同的短语相似时进行附加的文档排序。BM25在0到1之间,因此最终权重包含的最后3个数字是由BM25决定的,所有其他的数字用于短语权重。
如果需要要把结果精确匹配排在前面,更进一步改善PROXIMITY_BM25模式,版本1.10-beta中加入SPH04模式。
短语相似仍然是主导因素,但是当给定一个短语相似的时候,在字段最开始匹配将排序更高,如果是整个字段完全匹配的话将排到最高处。
例如,当查询“Market Street”,SPH04模式基本上将某个字段完全匹配“Market Street”的文档排序在最前面,接着排像“Market Street Grocery”这样在字段最开始匹配的文档,然后排像“WestMarket Street”这样在字段某处有与短语相匹配的文档,最后排那些有包含短语所有关键字但不是一个短语的文档(例如,“Flea Market on 26th Street”)。
伪代码如下,
field_weights = 0
foreach ( field inmatching_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)
在1.10-beta版本,Sphinx有8种不同的排序模式,并且在将来还会添加更多的。每个排序模式计算得到不同的权重值,因此可能不会适合一个特殊的方案。
不同的rank模式使用一个或多个rank因子。
(1)短语相似度和BM25
短语相似度设计成比BM25需要更多的计算,因为它需要计算所有在文档中匹配的关键词,而不仅仅只计算文档本身。Sphinx默认使用短语相似算法,因为我们相信这个产生更好的搜索质量。当然你也可以选择使用一个更轻量级的排序器来省掉这些昂贵的相似计算。
短语相似和BM25是两个最重要的因子,最终的权重值是由排序模式决定的,一个或者多个因子经过特殊函数的处理得到一个值。
有两种遗留的排序模式(PROXIMITY,MATCHANY)是只依靠短语相似算法,并分别用于模拟MATCH_ALL 和MATCH_ANY两种遗留模式。
有三种排序模式(BM25,PROXIMITY_BM25, SPH04)是可以合并短语相似、BM25还有其他。SphinxQL现在默认是用PROXIMITY_BM25。BM25被推荐做为一个合适的快速排序模式,不亚于其他系统。SPH04是建立在PROXIMITY_BM25之上,但另外排序精确字段匹配,字段开头匹配比仅仅只是匹配等级高。
PROXIMITY_BM25 和 SPH04被期望产生最佳的质量,但是有可能需要特殊的结果。
(2)关键字次数
有三种简单的排序模式(NONE,WORDCOUNT, FIELDMASK)不做任何事,只统计关键字出现的次数,然后分别的返回匹配字段的位标识。它们在根本不需要排序或者由于应用端以某种方式计算时很有用。
(3)性能比较
选择的排序模式会严重影响搜索的性能。
NONE模式显明是最快的排序模式。
处理关键字位置(出现次数)是典型的最耗时的部分,因此不需要处理这部分的排序模式(FIELDMASK, BM25)总是比其他的快,也需要较少的磁盘IO(不需要读取位置)。
处理关键字位置的排序模式(WORDCOUNT,PROXIMITY, MATCHANY, PROXIMITY_BM25, SPH04)只在CPU影响上有所区别。
使用排序模式和文档属性对搜索结果排序。
已知的文档的内置属性:
@id (匹配文档的 ID)
@weight (匹配权值)
@rank (等同 weight)
@relevance (等同 weight)
@random (随机顺序返回结果)
其他的属性为用户设置属性。
以官方C的API为例,模式如下:
按相关度降序排列(最好的匹配排在最前面)
SPH_SORT_RELEVANCE忽略任何附加的参数,永远按相关度评分排序。所有其余的模式都要求额外的排序子句,子句的语法跟具体的模式有关。
SPH_SORT_RELEVANCE模式等价于在扩展模式中按"weightDESC, id ASC"排序。
按属性降序排列 (属性值越大的越是排在前面)
SPH_SORT_ATTR_DESC等价于"attribute DESC, weight DESC, id ASC"。
按属性升序排列(属性值越小的越是排在前面)
SPH_SORT_ATTR_ASC模式等价于"attribute ASC, weight DESC, id ASC"
先按时间段(最近一小时/天/周/月)降序,再按相关度降序
在SPH_SORT_TIME_SEGMENTS模式中,属性值被分割成“时间段”,然后先按时间段排序,再按相关度排序。
时间段是根据搜索发生时的当前时间戳计算的,因此结果随时间而变化。
时间段的分法固化在搜索程序中了,但如果需要,也可以比较容易地改变(需要修改源码)。
这种模式是为了方便对Blog日志和新闻提要等的搜索而增加的。使用这个模式时,处于更近时间段的记录会排在前面,但是在同一时间段中的记录又根据相关度排序-这不同于单纯按时间戳排序而不考虑相关度。
SPH_SORT_ATTR_ASC,SPH_SORT_ATTR_DESC以及SPH_SORT_TIME_SEGMENTS这三个模式仅要求一个属性名。
按一种类似SQL的方式将列组合起来,升序或降序排列。
在 SPH_SORT_EXTENDED 模式中,您可以指定一个类似SQL的排序表达式,但涉及的属性(包括内部属性)不能超过5个。
例如:
sphinx_set_sort_mode(client,SPH_SORT_EXTENDED,"@relevanceDESC, price ASC, @id DESC")
relevance和id是内部属性,而price是用户定义属性
只要做了相关设置,不管是内部属性(引擎动态计算出来的那些属性)还是用户定义的属性就都可以使用。内部属性的名字必须用特殊符号@开头,用户属性按原样使用就行了。
表达式排序模式使您可以对匹配项按任何算术表达式排序,表达式中的项可以是属性值,内部属性(id和weight),算术运算符和一些内建的函数。
例如:
sphinx_set_sort_mode(client,SPH_SORT_EXPR,"weight+ ( user_karma + ln(pageviews) )*0.1")
支持的运算符和函数为:运算符: +, -,*, /, <, >
算术表达式是模仿MySQL设计的。函数接受参数,参数的数目根据具体函数的不同而不同。
如果需要按照权重排序,可以使用以下方式:
按照字段命中计算权值
sphinx_set_match_mode( client,SPH_MATCH_EXTENDED2 );//设置匹配模式
sphinx_set_ranking_mode ( client, SPH_RANK_PROXIMITY);//设置评分模式
const char * field_names[3];
int field_weights[3];
field_names[0] = "industry";
field_weights[0] = 1;
field_names[1] = "area";
field_weights[1] = 2;
field_names[2] = "expr";
field_weights[2] = 2;
//设置字段的权重,如果area命中,那么权重算2
sphinx_set_field_weights ( client, 3, field_names, field_weights );
//按照权重排序
sphinx_set_sort_mode ( client, SPH_SORT_EXPR, "@weight");//内部属性的名字必须用特殊符号@开头