Sphinx搜索

5.1. 匹配模式

有如下可选的匹配模式:

  • SPH_MATCH_ALL, 匹配所有查询词(默认模式);

  • SPH_MATCH_ANY, 匹配查询词中的任意一个;

  • SPH_MATCH_PHRASE, 将整个查询看作一个词组,要求按顺序完整匹配;

  • SPH_MATCH_BOOLEAN, 将查询看作一个布尔表达式 (参见 第 5.2 节 “布尔查询语法”);

  • SPH_MATCH_EXTENDED, 将查询看作一个CoreSeek/Sphinx内部查询语言的表达式 (参见 第 5.3 节 “扩展查询语法”). 从版本Coreseek 3/Sphinx 0.9.9开始, 这个选项被选项SPH_MATCH_EXTENDED2代替,它提供了更多功能和更佳的性能。保留这个选项是为了与遗留的旧代码兼容——这样即使 Sphinx及其组件包括API升级的时候,旧的应用程序代码还能够继续工作。

  • SPH_MATCH_EXTENDED2, 使用第二版的“扩展匹配模式”对查询进行匹配.

  • SPH_MATCH_FULLSCAN, 强制使用下文所述的“完整扫描”模式来对查询进行匹配。注意,在此模式下,所有的查询词都被忽略,尽管过滤器、过滤器范围以及分组仍然起作用,但任何文本匹配都不会发生.

当如下条件满足时,SPH_MATCH_FULLSCAN模式自动代替其他指定的模式被激活:

  1. 查询串是空的(即长度字符串为零)

  2. docinfo 存储方式为 extern.

在完整扫描模式中,全部已索引的文档都被看作是匹配的。这类匹配仍然会被过滤、排序或分组,但是并不会做任何真正的全文检索。这种模式可以用来统一全文检索和非全文检索的代码,或者减轻SQL服务器的负担(有些时候Sphinx扫描的速度要优于类似的MySQL查询)。 “在论坛中搜索帖子”这件事可用作完整搜索模式的例子:用SetFilter()指定用户ID但不提供任何查询词,Sphinx会匹配SetFilter()所能匹配的全部文档,也就是这个用户ID对应的全部帖子。默认情况下,其结果的第一排序标准是相关度,其次是Sphinx文档ID,正序(较老的文档在前)。

注意,在完整扫描模式中,文档必须有至少一个属性。否则,即便设置docinfo的存储方式为extern,也无法启用完整扫描模式。

5.2. 布尔查询语法

布尔查询SPH_MATCH_BOOLEAN允许使用下列特殊运算符:

  • 显式的与(AND)运算符:

    hello & world
  • 或(OR)运算符:

    hello | world
  • 非(NOT)运算符:

    hello -world
    hello !world
  • 分组(grouping):

    ( hello world )

以下是一个使用了如上全部运算符的例子:

例 5.1. 布尔查询例子

( cat -dog ) | ( cat -mouse)


与(AND)运算符为默认操作,所以“hello world”其实就是“hello & world”

或(OR)运算符的优先级高于与运算符,因此“lookingfor cat | dog | mouse”意思是"looking for ( cat | dog | mouse )" 而不是 "(looking for cat) | dog | mouse"

像“-dog”这种查询不能被执行,因为它差不多包括索引所有文档。这既有技术上的原因,也有性能上的原因。从技术上说,Sphinx并不总是保持一个全部文档ID的列表。性能方面,当文档集非常大的时候(即10-100M个文档),对这种执行查询可能需要很长的时间。

5.3. 扩展查询语法

在扩展查询模式SPH_MATCH_EXTENDED2中可以使用如下特殊运算符:

  • 或(OR)运算符:

    hello | world
  • 非(NOT)运算符:

    hello -world
    hello !world
  • 字段(field)搜索符:

    @title hello @body world
  • 字段限位修饰符(版本Coreseek 3/Sphinx 0.9.9-rc1中引入):

    @body[50] hello
  • 多字段搜索符:

    @(title,body) hello world
  • 全字段搜索符:

    @* hello
  • 词组搜索符:

    "hello world"
  • 近似距离搜索符:

    "hello world"~10
  • 阀值匹配符:

    "the world is a wonderful place"/3
  • 严格有序搜索符(即“在前”搜索符):

    aaa << bbb << ccc
  • 严格形式修饰符(版本Coreseek 3/Sphinx 0.9.9-rc1中引入):

    raining =cats and =dogs
  • 字段开始和字段结束修饰符 (版本Coreseek 3.1/Sphinx 0.9.9-rc2中引入):

    ^hello world$
  • NEAR, 广义临近运算符 (版本2.0.1-beta中引入):

    hello NEAR/3 world NEAR/4 "my test"
  • SENTENCE 句子运算符 (版本2.0.1-beta中引入):

    all SENTENCE words SENTENCE "in one sentence"
  • PARAGRAPH 段落运算符 (版本2.0.1-beta中引入):

    "Bill Gates" PARAGRAPH "Steve Jobs"
  • ZONE 区域限制运算符 (版本2.0.1-beta中引入):

    ZONE:(h3,h4) only in these titles

以下是上述某些运算符的示例:

例 5.2. 扩展匹配模式:查询例子

"hello world" @title "example program"~10 @body python -(php|perl) @* code


例子中查询的完整解释如下:

  • 在文档的任意字段中找相邻的“hello”和“world”

  • 不仅如此,符合上述条件的文档的title字段中还必须包含 “example”和“program”这两个词,并且他们之间至多有10个(不包括10个)其他的词(例如“example PHP program”可以匹配,但“example script to introduce outside data into the correct context for your program”就不行,因为中间有10个或以上的词。

  • 同时,body字段必须含有词“python”,但既没有“php”也没有“perl”

  • 最后, 任一字段中包含”code“.

与(AND)操作为默认操作,因此“hello world”意思是“hello”和“world”必须同时存在文档才能匹配。

或(OR)运算符的优先级要高于与运算符,因此"looking for cat | dog | mouse" 意思是"looking for ( cat | dog | mouse )" 而不是"(looking for cat) | dog | mouse";

字段限制(field limit)符(field limit)将其后指定的搜索限制在某个特定的字段中。通常,如果给出的字段名实际并不存在,你会得到一条错误信息。但可以通过在查询的最开始处加上@@relaxed选项来放宽限制。

@@relaxed @nosuchfield my query

当搜索多个具有不同schema的索引时这可能有用。

版本Coreseek 3/Sphinx 0.9.9-rc1又引入了字段限位(field position limit)符。它把搜索限制在指定字段(一个或多个)的前N个位置。例如“@body[50] hello”不会匹配那些body字段包含“hello”,但它出现在第51个位置或者更靠后的文档。

近似距离以词为单位,随词数变化而变化,并应用于引号中的全部词。举个例子,"cat dog mouse"~5 这个查询的意思是必须有一个少于8个词的词串,它要包含全部的三个词,也就是说"CAT aaa bbb ccc DOG eee fff MOUSE" 这个文档不会匹配这个查询,因为这个词串正好是8个词。

阀值匹配符引入了一种模糊匹配。它允许至少含有某个阈值数量个匹配词的文档通过。上述例子("the world is a wonderful place"/3)会匹配含有指定的六个词中的至少三个的那些文档。上面例子中的一个查询”the world is a wonderful place”/3匹配的文档至少含有指定的6个词中的3个。

严格有序搜索符(即“在前”搜索符)是在版本0.9.9-rc2中引入的,它的几个参数在被匹配的文档中必须严格按查询中出现的顺序出现。例如,“black << cat”这个查询(不包括引号)可以匹配“black and white cat”,但不能匹配“the cat was black”。顺序运算符的优先级最低,它既可以应用在最简单的关键词上,也可以用在更复杂的表达式上,比如下面也是个正确的查询:

(bag of words) << "exact phrase" << red|green|blue

版本0.9.9-rc1引入了“严格形式”关键字修饰符,它保证关键词在匹配文档中严格以指定的形式出现,而默认行为只要求词根相同。例如,查询“runs”既可以匹配含有“runs”的文档,可以匹配含有“running”的文档,因为这二者的词根都是“run”——而如果查询是“=runs”,那就只有前者能匹配。严格形式修饰符要求index_exact_words选项处于启用状态。这是个影响关键字的修饰符,可以与其他一些运算符混合使用,例如词组搜索符、近似搜索符和阈值搜索符等。

关键字修饰符“字段开始”和“字段结束”是在版本Coreseek 3.1/Sphinx 0.9.9-rc2中引入的,它们确保只在一个全文字段的最开始或最结束位置匹配关键字。例如,查询“^hello world$”(包括引号,也就是说这个查询是词组搜索符和字段起止修饰符的组合)匹配的文档必然包括某个严格只有“hello world”这个词组的字段。

自版本Coreseek 3/Sphinx 0.9.9-rc1始,可以嵌套任意层数的括号和“非”操作,但这类查询要想能够计算出结果,就必须保证不能隐含地涉及所有文档。

// 正确查询
aaa -(bbb -(ccc ddd))

// 不能处理的查询
-aaa
aaa | -bbb

NEAR 广义临近运算符, 版本2.0.1-beta引入, 是近似距离运算符的广义版本。其语法是 NEAR/N, 区分大小写, 并且不允许有空格在 NEAR 关键词与斜线以及距离值之间。

原始的近似距离运算符仅对给如的关键词集起作用。而NEAR则更加通用,可以接受任意的子表达式作为其参数,当两个表达式在N个词汇的距离中(不论先后顺序)发现对方时文档被选中。NEAR是左结合的,与“在前”搜索符具有相同(最低)的优先级。

你需要明白为什么一个使用NEAR的查询(one NEAR/7 two NEAR/7 three) 并不完全等同于一个使用关键词近似距离的查询 ("one two three"~7)。 这里的区别在于,近似距离运算符允许最多6个没有匹配的词汇出现在3个匹配到的词汇之间,但是NEAR的版本限制较少: 它允许最多6个词汇出现在 'one' 与 'two' 之间,然后允许匹配到的two关键词与'three'之间有最多6个词汇。

SENTENCE 句子 和 PARAGRAPH 段落运算符, 在版本2.0.1-beta中引入, 当它的参数都处于同一个句子或者段落中时文档被选中。参数可以是关键词或者短语,活着相同的运算符实例。下面是几个例子:

one SENTENCE two
one SENTENCE "two three"
one SENTENCE "two three" SENTENCE four

参数在句子或者段落中的顺序不产生影响。该功能仅在index_sp (句子和段落索引功能) 启用时生效,否则作为单纯的AND操作对待。参考index_sp章节的文档了解系统是如何来判断一个句子或者段落的。

ZONE 区域限制运算符, 版本2.0.1-beta引入, 非常类似于字段限制运算符,但是限制在一个给定的字段内或者区域的列表。请注意,其随后的字表达式并要求限定在一个给定的连续区域之内,可以跨越多个相同的区域进行匹配。例如, (ZONE:th hello world) 查询  匹配下面演示的文档:

<th>Table 1. Local awareness of Hello Kitty brand.</th>
.. some table data goes here ..
<th>Table 2. World-wide brand awareness.</th>

ZONE运算符对查询的影响直到下一个查询字段或者区域限制符号,或者到右括号结束。它仅适用于启用了区域支持(参看 第 11.2.9 节 “index_zones:索引标签区域信息”) 的索引,否则将被忽略掉。

5.4. 权值计算

采用何种权值计算函数(目前)取决于查询的模式。

权值计算函数有如下两个主要部分:

  1. phrase rank,

  2. statistical rank.

词组评分根据文档和查询的最长公共子串(LCS,longest common subsequence)的长度进行。因此如果文档对查询词组有一个精确匹配(即文档直接包含该词组),那么它的词组评分就取得了可能的最大值,也就是查询中词的个数。

统计学评分基于经典的BM25函数,该函数仅考虑词频。如果某词在整个数据库中很少见(即文档集上的低频词)或者在某个特定文档中被经常提及(即特定文档上的高频词),那么它就得到一个较高的权重。最终的BM25权值是一个0到1之间的浮点数。

在所有模式中,数据字段的词组评分是LCS乘以用户指定的数据字段权值。数据字段权值是整数,默认为1,且字段的权值必须不小于1。

在SPH_MATCH_BOOLEAN模式中,不做任何权重估计,每一个匹配项的权重都是1。

在SPH_MATCH_ALL和SPH_MATCH_PHRASE模式中,最终的权值是词组评分的加权和。

在SPH_MATCH_ANY模式中,于前面述两模式的基本思想类似,只是每个数据字段的权重都再加上一个匹配词数目。在那之前,带权的词组相关度被额外乘以一个足够大的数,以便确保任何一个有较大词组评分的数据字段都会使整个匹配的相关度较高,即使该数据字段的权重比较低。

在SPH_MATCH_EXTENDED模式中,最终的权值是带权的词组评分和BM25权重的和,再乘以1000并四舍五入到整数。

这个行为将来会被修改,以便使MATCH_ALL和MATCH_ANY这两个模式也能使用BM25算法。这将使词组评分相同的搜索结果片断得到改进,这在只有一个词的查询中尤其有用。

关键的思想(对于除布尔模式以外的全部模式中)是子词组的匹配越好则评分越高,精确匹配(匹配整个词组)评分最高。作者的经验是,这种基于词组相似性的评分方法可以提供比任何单纯的统计模型(比如其他搜索引擎中广泛使用的BM25)明显更高的搜索质量。

5.5. 表达式,函数,运算符

Sphinx允许你在SphinxQL和SphinxAPI中使用任意的表达式,可以涉及属性的值,内部属性(文档编号和相关度),算术运算符,内置的函数和用户自定义的函数。本章节是支持的运算符和函数的说明文档。下面是用于快速查看的完整参考列表。

5.5.1. 运算符

  • 算术运算符: +, -, *, /, %, DIV, MOD

  • 标准算术运算符。计算可以以三种不同的精度进行:(a) 单精度32位IEEE754浮点值(默认情况),(b) 32位有符号整数,(c) 64位有符号整数。如果没有任何返回浮点数的操作,表达式分析器会自动切换到整数模式,否则使用默认的浮点模式。比如,对于表达式“a+b”,如果两个参数都是32位整数的,则它会被以32位整数模式计算,如果两个参数都是整数,而其中一个是64位的,则以64位整数模式计算,否则以浮点模式计算。然而表达式“a/b”或者“sqrt(a)”却总是使用浮点模式计算,因为这些操作返回非整数的结果,要让前者使用整数模式,可以使用IDIV(a,b)或者a DIV b。另外,如果两个参数都是32位的,表达式“a*b”并不会自动提升到64位模式。要想强制64位模式,可以用BIGINT()。(但要注意的是,如果表达式中同时有浮点数,那么BIGINT()的命运就是简单地被忽略)

  • 比较运算符: <, > <=, >=, =, <>

  • 比较操作符(比如=和<=)在条件为真时返回1.0,否则返回0.0。例如(a=b)+3在属性“a”与属性“b”相等时返回4,否则返回3。与MySQL不同,相等性比较符(即=和<>)中引入了一个小的阈值(默认是1e-6)。如果被比较的两个值的差异在阈值之内,则二者被认为相等。

  • 布尔运算符: AND, OR, NOT

  • 布尔操作符(AND,OR,NOT)是在版本Coreseek 3.1/Sphinx 0.9.9-rc2中引入的,其行为与一般的布尔操作没有两样。它们全部是左结合,而且比之其他操作符,它们有最低的优先级,其中NOT的优先级比AND 和OR高,但仍旧低于所有其他操作符。AND和OR有相同的优先级,因此建议使用括号来避免在复杂的表达式中出现混乱。

  • 位运算符: &, |

  • 这些运算符分别进行按位AND和OR。操作数 必须是整数类型。版本1.10-beta引入。

5.5.2. 数值函数

  • ABS( x )

  • 返回 x 的绝对值。

  • CEIL( x )

  • 返回大于或等于该 x 的最小整数。

  • COS( x )

  • 返回 x 的余弦。

  • EXP( x )

  • 返回e的X乘方后的值(e=2.718...作为自然对数的底)。

  • FLOOR( x )

  • 返回小于或等于该 x 的最大整数。

  • IDIV( x, y )

  • 返回 x 整除 y 的结果。两个参数都必须是整数类型。

  • LN( x )

  • 返回 x 的自然对数 (自然对数的底为e=2.718...).

  • LOG10( x )

  • 返回 x 的常用对数 (底为10).

  • LOG2( x )

  • 返回 x 的二进制对数 (底为2).

  • MAX( x, y )

  • 返回 x, y 中较大的参数.

  • MIN( x, y )

  • 返回 x, y 中较小的参数.

  • POW( x, y )

  • 返回 x 的 y 次方的冥。

  • SIN( x )

  • 返回 x 的正弦.

  • SQRT( x )

  • 返回 x 的平方根.

5.5.3. 日期和时间函数

  • DAY( t )

  • 根据当前时区,返回时间戳 t 对应的所在月日期(范围在1.. 31 )。版本2.0.1-beta引入。

  • MONTH( t )

  • 根据当前时区,返回时间戳 t 对应的月份(范围在1.. 12 )。版本2.0.1-beta引入。

  • NOW()

  • 将当前时间戳作为整数返回。版本0.9.9-rc1引入。

  • YEAR( t )

  • 根据当前时区,返回时间戳 t 对应的年份(范围在1969.. 2038 )。版本2.0.1-beta引入。

  • YEARMONTH( t )

  • 根据当前时区,返回时间戳 t 对应的整数年份和月份(范围在196912..203801 )。版本2.0.1-beta引入。

  • YEARMONTHDAY()

  • 根据当前时区,返回时间戳 t 对应的整数年份、月份和日期(范围在19691231..20380119 )。版本2.0.1-beta引入。

5.5.4. 类型转换函数

  • BIGINT()

  • 它将它的整型参数强行提升到64位,而对浮点参数无效。引入它是为了可以强制某些表达式(如“a*b”)用64位模式计算,即使所有的参数都是32位的。 版本0.9.9-rc1引入。

  • SINT()

  • 强制将32位无符号整数重新诠释为有符号数,并提示道64位(因为32位类型是无符号的)。通过以下的例子很容易明白:通常1-2的计算结果(32位无符号整数)为4294967295,但是SINT(1-2)的计算结果为-1。 版本2.0.1-beta引入。

5.5.5. 比较函数

  • IF( expr, x, y )

  • IF() 行为与其对应的MySQL的有所不同。它接受3个参数,检查第一个参数 expr 是否为0.0,若非零则返回第二个参数 x ,为零时则返回第三个参数 y 。注意,与比较操作符不同,IF()使用阈值!因此在第一个参数中使用比较结果是安全的,但使用算术运算符则可能产生意料之外的结果。比如,下面两个调用会产生不同的结果,虽然在逻辑上他们是等价的:

    IF ( sqrt(3)*sqrt(3)-3<>0, a, b )
    IF ( sqrt(3)*sqrt(3)-3, a, b )

    在第一种情况下,由于有阈值,比较操作符<>返回0.0(逻辑假),于是IF()总是返回‘b’。在第二种情况下,IF()函数亲自擅自在没有阈值的情况下将同样的 sqrt(3)*sqrt(3)-3与零值做比较。但由于浮点数运算的精度问题,该表达式的结果与0值会有微小的差异,因此该值与零值的相等比较不会通过,上述第二种情况中IF()会返回‘a’做为结果。

  • IN( expr, val1, ... )

  • 版本0.9.9-rc1引入了函数IN(expr, val1, val2, …),它需要两个或更多参数,如果第一个参数与后续任何一个参数(val1到valN)相等则返回1,否则返回0。目前,所有被测试是否相等的参数(不包 括expr本身)必须是常量。(支持任意表达式在技术上是可以实现的,未来我们会这么做)。这些常量经过预先排序,测试相等时可以使用二元查找以提高效 率,因此即使参数列表很长IN()也还可以提供较高的速度。自版本0.9.9-rc2始,第一个参数可以是一个MVA多值属性,这种情况下只要MVA中的 任何一个值与后面列表中的任何一个值相等IN()就返回1。 从版本2.0.1-beta开始,IN()也支持IN(expr,@uservar)语法以检查其值是否在给定的全局用户变量列表中。

  • INTERVAL( expr, point1, ...)

  • 版本0.9.9-rc1引入了函数INTERVAL(expr, point1, point2, point3, …),它接受2个或更多参数,返回第一个小于第一个参数expr的参数的下标:如果expr<point1,返回0;如果 point1<=expr<point2,返回1,一次类推。显然,必须有point1<point2<…<pointN 才能保证这个函数正确工作。

5.5.6. 其它函数

  • CRC32( s )

  • 返回字符串 s 的CRC32值. 版本2.0.1-beta引入。

  • GEODIST( lat1, long1, lat2, long2 )

  • 版本0.9.9-rc2引入函数GEODIST(lat1, long1, lat2, long2),它根据坐标计算两个指定点之间的地表距离。请注意经纬度都要以角度为单位,而结果是以米为单位的。四个参数都可以是任意表达式。当其中一对 参数引用的是文档属性对而另一对参数是常数,系统会自动选择一条优化的路径。

5.6. 排序模式

可使用如下模式对搜索结果排序:

  • SPH_SORT_RELEVANCE 模式, 按相关度降序排列(最好的匹配排在最前面)

  • SPH_SORT_ATTR_DESC 模式, 按属性降序排列 (属性值越大的越是排在前面)

  • SPH_SORT_ATTR_ASC 模式, 按属性升序排列(属性值越小的越是排在前面)

  • SPH_SORT_TIME_SEGMENTS 模式, 先按时间段(最近一小时/天/周/月)降序,再按相关度降序

  • SPH_SORT_EXTENDED 模式, 按一种类似SQL的方式将列组合起来,升序或降序排列。

  • SPH_SORT_EXPR 模式,按某个算术表达式排序。

SPH_SORT_RELEVANCE忽略任何附加的参数,永远按相关度评分排序。所有其余的模式都要求额外的排序子句,子句的语法跟具体的模式有关。 SPH_SORT_ATTR_ASC, SPH_SORT_ATTR_DESC以及SPH_SORT_TIME_SEGMENTS这三个模式仅要求一个属性名。 SPH_SORT_RELEVANCE模式等价于在扩展模式中按"@weight DESC, @id ASC"排序,SPH_SORT_ATTR_ASC 模式等价于"attribute ASC, @weight DESC, @id ASC",而SPH_SORT_ATTR_DESC 等价于"attribute DESC, @weight DESC, @id ASC"。

SPH_SORT_TIME_SEGMENTS 模式

在SPH_SORT_TIME_SEGMENTS模式中,属性值被分割成“时间段”,然后先按时间段排序,再按相关度排序。

时间段是根据搜索发生时的当前时间戳计算的,因此结果随时间而变化。所说的时间段有如下这些值:

  • 最近一小时

  • 最近一天

  • 最近一周

  • 最近一月

  • 最近三月

  • 其他.

时间段的分法固化在搜索程序中了,但如果需要,也可以比较容易地改变(需要修改源码)。

这种模式是为了方便对Blog日志和新闻提要等的搜索而增加的。使用这个模式时,处于更近时间段的记录会排在前面,但是在同一时间段中的记录又根据相关度排序-这不同于单纯按时间戳排序而不考虑相关度。

SPH_SORT_EXTENDED mode

在 SPH_SORT_EXTENDED 模式中,您可以指定一个类似SQL的排序表达式,但涉及的属性(包括内部属性)不能超过5个,例如:

@relevance DESC, price ASC, @id DESC

只要做了相关设置,不管是内部属性(引擎动态计算出来的那些属性)还是用户定义的属性就都可以使用。内部属性的名字必须用特殊符号@开头,用户属性按原样使用就行了。在上面的例子里,@relevance@id是内部属性,而price是用户定义属性。

可用的内置属性:

  • @id (匹配文档的 ID)

  • @weight (匹配权值)

  • @rank (等同 weight)

  • @relevance (等同 weight)

  • @random (随机顺序返回结果)

@rank  @relevance 只是 @weight 的别名.

SPH_SORT_EXPR 模式

表达式排序模式使您可以对匹配项按任何算术表达式排序,表达式中的项可以是属性值,内部属性(@id和@weight),算术运算符和一些内建的函数。例如:

$cl->SetSortMode ( SPH_SORT_EXPR,
	"@weight + ( user_karma + ln(pageviews) )*0.1" );

在表达式中支持的运算符和函数请参看专门的章节第 5.5 节 “表达式,函数,运算符”.

5.7. 结果分组(聚类)

有时将搜索结果分组(或者说“聚类”)并对每组中的结果计数是很有用的-例如画个漂亮的图来展示每个月有多少的blog日志,或者把Web搜索结果按站点分组,或者把找到的论坛帖子按其作者分组等等。

理论上,这可以分两步实现:首先在Sphinx中做全文检索,再在SQL服务器端对得到的ID分组。但是现实中在大结果集(10K到10M个匹配)上这样做通常会严重影响性能。

为避免上述问题,Sphinx提供了一种“分组模式”,可以用API调用SetGroupBy()来开启。在分组时,根据group-by值给匹配项赋以一个分组。这个值用下列内建函数之一根据特定的属性值计算:

  • SPH_GROUPBY_DAY, 从时间戳中按YYYYMMDD格式抽取年、月、日;

  • SPH_GROUPBY_WEEK, 从时间戳中按YYYYNNN格式抽取年份和指定周数(自年初计起)的第一天;

  • SPH_GROUPBY_MONTH, 从时间戳中按YYYYMM格式抽取月份;

  • SPH_GROUPBY_YEAR, 从时间戳中按YYYY格式抽取年份;

  • SPH_GROUPBY_ATTR, 使用属性值自身进行分组.

最终的搜索结果中每组包含一个最佳匹配。分组函数值和每组的匹配数目分别以“虚拟”属性 @group  @count 的形式返回.

结果集按group-by排序子句排序,语法与SPH_SORT_EXTENDED 排序子句的语法相似。除了@id@weight,分组排序子句还包括:

  • @group (groupby函数值),

  • @count (组中的匹配数目).

默认模式是根据groupby函数值降序排列,即按照 "@group desc".

排序完成时,结果参数total_found会包含在整个索引上匹配的组的总数目。

注意: 分组操作在固定的内存中执行,因此它给出的是近似结果;所以total_found报告的数目可能比实际给出的个分组数目的和多。@count也可能被低估。要降低不准确性,应提高max_matches。如果max_matches允许存储找到的全部分组,那结果就是百分之百准确的。

例如,如果按相关度排序,同时用SPH_GROUPBY_DAY函数按属性"published"分组,那么:

  • 结果中包含每天的匹配结果中最相关的那一个,如果那天有记录匹配的话,

  • 结果中还附加给出天的编号和每天的匹配数目,

  • 结果以天的编号降序排列(即最近的日子在前面).

从版本0.9.9-rc2开始, 当使用GROUP BY时,可以通过SetSelect() API调用聚合函数 (AVG(), MIN(), MAX(), SUM())

5.8. 分布式搜索

为提高可伸缩性,Sphnix提供了分布式检索能力。分布式检索可以改善查询延迟问题(即缩短查询时间)和提高多服务器、多CPU或多核环境下的吞吐率(即每秒可以完成的查询数)。这对于大量数据(即十亿级的记录数和TB级的文本量)上的搜索应用来说是很关键的。

其关键思想是对数据进行水平分区(HP,Horizontally partition),然后并行处理。

分区不能自动完成,您需要

  • 在不同服务器上设置Sphinx程序集(indexersearchd)的多个实例;

  • 让这些实例对数据的不同部分做索引(并检索);

  • searchd的一些实例上配置一个特殊的分布式索引;

  • 然后对这个索引进行查询.

这个特殊索引只包括对其他本地或远程索引的引用,因此不能对它执行重新建立索引的操作,相反,如果要对这个特殊索引进行重建,要重建的是那些被这个索引被引用到的索引。

searchd收到一个对分布式索引的查询时,它做如下操作

  1. 连接到远程代理;

  2. 执行查询;

  3. (在远程代理执行搜索的同时)对本地索引进行查询;

  4. 接收来自远程代理的搜索结果;

  5. 将所有结果合并,删除重复项;

  6. 将合并后的结果返回给客户端.

在应用程序看来,普通索引和分布式索引完全没有区别。 也就是说,分布式索引对应用程序而言是完全透明的,实际上也无需知道查询使用的索引是分布式的还是本地的。 (就算是在0.9.9之中, Sphinx也不支持以其他的方式来结合分布式索引进行搜索, 也许在将来会去掉该限制.)

任一个searchd实例可以同时做为主控端(master,对搜索结果做聚合)和从属端(只做本地搜索)。这有如下几点好处:

  1. 集群中的每台机器都可以做为主控端来搜索整个集群,搜索请求可以在主控端之间获得负载平衡,相当于实现了一种HA(high availability,高可用性),可以应对某个节点失效的情况。

  2. 如果在单台多CPU或多核机器上使用,一个做为代理对本机进行搜索的searchd实例就可以利用到全部的CPU或者核。

如果在单台多CPU或多核机器上使用,一个做为代理对本机进行搜索的searchd实例就可以利用到全部的CPU或者核。

5.9. 搜索服务(searchd) 查询日志格式

在版本2.0.1-beta中支持下述的两种查询日志格式。以前的版本只支持自定义的纯文本格式,目前也仍然是默认的日志格式。 然而,虽然它可能会方便人工监测和审查,但难以回放以便做性能测试,它只能 记录搜索查询而不能记录搜索请求的其他信息,也不总是包含完整的搜索查询数据等。 默认纯文本格式也很难(有时不可能)为性能测试的目的而回放。新的 sphinxql 格式则缓解了这个问题,其目的是完整和自动化的记录信息,即使降低了简洁和可读性也无所谓。

5.9.1. 纯文本日志格式

默认情况下,searchd 将全部成功执行的搜索查询都记录在查询日志文件中。以下是一个类似记录文件的例子:

[Fri Jun 29 21:17:58 2007] 0.004 sec [all/0/rel 35254 (0,20)] [lj] test
[Fri Jun 29 21:20:34 2007] 0.024 sec [all/0/rel 19886 (0,20) @channel_id] [lj] test

日志格式如下

[query-date] query-time [match-mode/filters-count/sort-mode
    total-matches (offset,limit) @groupby-attr] [index-name] query

匹配模式(match-mode)可以是如下值之一

  • "all" 代表 SPH_MATCH_ALL 模式;

  • "any" 代表 SPH_MATCH_ANY 模式;

  • "phr" 代表 SPH_MATCH_PHRASE 模式;

  • "bool" 代表 SPH_MATCH_BOOLEAN 模式;

  • "ext" 代表 SPH_MATCH_EXTENDED 模式;

  • "ext2" 代表 SPH_MATCH_EXTENDED2 模式;

  • "scan" 代表使用了完整扫描模式,这可能是由于设置了SPH_MATCH_FULLSCAN模式导致的,也可能是因为查询是空的。 (参见文档 匹配模式)

排序模式(sort-mode)可以取如下值之一:

  • "rel" 代表 SPH_SORT_RELEVANCE 模式;

  • "attr-" 代表 SPH_SORT_ATTR_DESC 模式;

  • "attr+" 代表 SPH_SORT_ATTR_ASC 模式;

  • "tsegs" 代表 SPH_SORT_TIME_SEGMENTS 模式;

  • "ext" 代表 SPH_SORT_EXTENDED 模式.

此外,如果searchd 启动的时候带参数 --iostats,那么在列出被搜索的全部索引后还会给出一块数据。

一个查询日志项看起来就像这样:

[Fri Jun 29 21:17:58 2007] 0.004 sec [all/0/rel 35254 (0,20)] [lj]
   [ios=6 kb=111.1 ms=0.5] test

多出来的这块数据是关于搜索中执行的I/O操作的信息,包括执行的I/O操作次数、从索引文件中读取数据的kb数和I/O操作占用的时间(尽管这个时间还包括一个后台处理组件所占用的,但主要是I/O时间)

5.9.2. SphinxQL 日志格式

这是版本2.0.1-beta引入的新的日志格式,其目标是开始以一种简单的格式去自动记录所有的一切(例如为了自动回放)。 新的格式可以通过配置文件中的query_log_format指令来启用, 或者在访问过程中通过SphinxQL语句SET GLOBAL query_log_format=... 进行来回切换。在新的格式中,前面的例子将如下所示:(为了方便阅读进行了换行,但是在实际的日志中每行一个查询)

/* Fri Jun 29 21:17:58.609 2007 2011 conn 2 wall 0.004 found 35254 */
SELECT * FROM lj WHERE MATCH('test') OPTION ranker=proximity;

/* Fri Jun 29 21:20:34 2007.555 conn 3 wall 0.024 found 19886 */
SELECT * FROM lj WHERE MATCH('test') GROUP BY channel_id
OPTION ranker=proximity;

请注意, 所有的请求都将以此格式记录, 包括那些通过SphinxAPI和SphinxSE发送的请求,而不只是那些通过SphinxQL 发送的请求。 还需要注意的是,这种日志记录方式仅适合纯文本日志格式,如果使用'syslog'记录则不会工作。

SphinxQL日志格式相对默认的纯文本日志格式的特点如下:

  • 所有类型的请求都会被记录。 (该工作还在继续和完善.)

  • 全部报表数据将被尽可能的记录。

  • 错误和提醒也会被记录。

  • 该日志可以通过SphinxQL自动回放。

  • 额外的性能计数器(目前,记录了每个Agent的分布式查询时间)被记录。

每个请求(包括SphinxAPI和SphinxQL )都正好对应一个日志记录行。 所有的请求类型,包括INSERT, CALL SNIPPETS等,都会被记录(但 编写本手册时,这方面的工作还在继续和完善)。 每个记录行都是有效的SphinxQL语句,可以用于完整的重构请求,但是请求记录如果太大则需要考虑性能原因而缩短。 其他信息,例如性能计数器等会被作为注释记录在请求后。

5.10. MySQL 协议支持与 SphinxQL

Sphinx的searchd守护程序从 版本0.9.9-rc2开始支持MySQL二进制网络协议,并且能够通过标准的MySQL API访问。例如,“mysql”命令行程序可以很好地工作。以下是用MySQL客户端对Sphinx进行查询的例子:

$ mysql -P 9306
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 1
Server version: 0.9.9-dev (r1734)

Type 'help;' or '\h' for help. Type '\c' to clear the buffer.

mysql> SELECT * FROM test1 WHERE MATCH('test') 
    -> ORDER BY group_id ASC OPTION ranker=bm25;
+------+--------+----------+------------+
| id   | weight | group_id | date_added |
+------+--------+----------+------------+
|    4 |   1442 |        2 | 1231721236 |
|    2 |   2421 |      123 | 1231721236 |
|    1 |   2421 |      456 | 1231721236 |
+------+--------+----------+------------+
3 rows in set (0.00 sec)

请注意mysqld甚至根本没有在测试机上运行。所有事情都是searchd自己搞定的。

新的访问方法是对原生API的一种补充,原生API仍然完美可用。事实上,两种访问方法可以同时使用。另外,原生API仍旧是默认的访问方法。MySQL协议支持需要经过额外的配置才能启用。当然这只需要更动一行配置文件,加入一个协议为mysql41的监听器(listener)就可以了:

listen = localhost:9306:mysql41

如果仅仅支持这个协议但不支持SQL语法,那没什么实际意义。因此Sphinx现在还支持SQL的一个很小的子集,我们给这个子集起个绰号,叫 SphinxQL。它支持在所有类型的索引上进行标准SELECT查询,支持在RT索引上使用INSERT、REPLACE、DELETE来更改信息,以 及更多的处理。 详细的SphinQL指南请查看 第 7 章 SphinxQL 指南.

5.11. 批量查询

多查询,或者批量查询,用于一次发送多条查询到Sphinx(更专业的说法是再一次网络请求中)。

目前有两个方法来实现批量查询,他们是 AddQuery()  RunQueries()。 你也可以通过SphinxQL来执行批量查询,参见 第 7.18 节 “多结果集查询(批量查询)”. (事实上,常规的Query() 调用在内部实现上是一次单独的AddQuery()调用紧跟RunQueries()调用。) AddQuery()会保持前面的API调用对查询设置的当前状态,并且记住这些查询。RunQueries()实际上发送所有记住的查询,并依次返回多个结果集。 对于查询本身没有任何限制,但在一次批量查询中会进行查询数目的完成检查 (参见 第 11.4.25 节 “max_batch_queries:最大批量查询”).

为什么要使用批量查询呢? 通常是是为了提升性能。 首先,在一次批量查询中将多个请求一次发送到searchd 而不是由一个接一个的发送,或多或少会减少网络通信。 其次,更重要的是,在批量查询中发送多条查询时,searchd会执行某些内部优化。随着时间的推移逐渐增加了新的优化措施,在可能的情况下将所有的查询打包为批量查询是有意义的,因此简单地升级Sphinx到一个新的版本将自动启用新的优化措施。 在处理时如果没有任何可能的批量查询优化可应用,查询在内部将被一个接一个的处理。

为什么(或者说什么时候)不使用批量查询呢? 批量查询要求一次处理中的查询都是相互独立的,但是有时候并非如此。 也就是说,有时候查询B是基于查询A的结果的,只能在查询A运行完成以后才能生成新的查询。例如,你可能需要在主索引查不到任何结果的时候,再从第二个索 引查询显示结果。或者,有时候需要根据第一次查询的结果数去指定第二次查询的结果偏移量。诸如此类情况下,你不得不使用多次分开的查询(或者多次分开的批 量查询)。

截止版本0.9.10,有两个主要的优化措施需要注意: 公共查询优化(从版本0.9.8开始提供);公共子树优化(从版本0.9.10开始提供)。

公共查询优化 是指 searchd发现批量查询中的查询仅排序和分组设置不同时,将只执行一次搜索。 例如,如果一次批量查询包含三个查询,查询关键字全部都是“ipod nano”,但是第一个查询取按照价格排序前十的结果,第二个查询按照供应商分组并取评分最高的前五个供货商,而第三个查询取最高价格的,则针 对"ipod nano"的全文查询仅执行一次,但其结果会重复使用来建立三个不同的结果集。

从这个优化措施得到好处的一个典型的案例就是所谓的faceted分面搜索。事实上,分面搜索可以通过执行一定数量的查询,一来获取搜索结果本身,同时进行相同全文关键字的一些其他查询,如根据不同的分组设置所有必要的分组结果(前三作者,前五供货商等等)。而这些只需要保持全文检索和过滤设置相同,公共查询优化将会触发并大幅提高查询性能。

公共子树优化 更加有趣。 它能让 searchd 分析利用批量全文查询中的相似之处。它可以识别所有查询中的公共全文查询部分(子树),并且在查询中缓存其结果。例如,我们可以看下面的批量查询:

barack obama president
barack obama john mccain
barack obama speech

他们有一个共同的部分是两个单词("barack obama"),这将被计算一次,然后缓存并在查询之间共享。这个工作就是公共子树优化来做的。每个查询的缓存大小由subtree_docs_cache subtree_hits_cache指令严格控制(使得即使缓存匹配了“i am”的超多文档也不会耗尽内存和导致服务服务器挂掉)

下面演示的代码(使用PHP )展示了在同一查询中使用3种不同 排序方式:

require ( "sphinxapi.php" );
$cl = new SphinxClient ();
$cl->SetMatchMode ( SPH_MATCH_EXTENDED2 );

$cl->SetSortMode ( SPH_SORT_RELEVANCE );
$cl->AddQuery ( "the", "lj" );
$cl->SetSortMode ( SPH_SORT_EXTENDED, "published desc" );
$cl->AddQuery ( "the", "lj" );
$cl->SetSortMode ( SPH_SORT_EXTENDED, "published asc" );
$cl->AddQuery ( "the", "lj" );
$res = $cl->RunQueries();

如何确认批量查询的请求真的被优化了呢? 如果优化过,在他们各自的查询日志将有一个“倍数“区段 特别之处有多少个查询是一起处理的:

[Sun Jul 12 15:18:17.000 2009] 0.040 sec x3 [ext2/0/rel 747541 (0,20)] [lj] the
[Sun Jul 12 15:18:17.000 2009] 0.040 sec x3 [ext2/0/ext 747541 (0,20)] [lj] the
[Sun Jul 12 15:18:17.000 2009] 0.040 sec x3 [ext2/0/ext 747541 (0,20)] [lj] the

请注意 "x3" 区段. 这意味着该查询是优化的并且在同一批次中处理了3个查询。作为参考,没有进行批量查询处理的多次查询的通常日志会是下面这样:

[Sun Jul 12 15:18:17.062 2009] 0.059 sec [ext2/0/rel 747541 (0,20)] [lj] the
[Sun Jul 12 15:18:17.156 2009] 0.091 sec [ext2/0/ext 747541 (0,20)] [lj] the
[Sun Jul 12 15:18:17.250 2009] 0.092 sec [ext2/0/ext 747541 (0,20)] [lj] the

请注意每个查询多时间查询案件是由一个因素改善 的1.5倍至2.3倍,这取决于在特定的排序模式。 其实在这两方面, 常见的查询优化和共同子树,还有的报告和3倍 甚至更多的改进,而且从生产情况的,而不仅仅是 综合测试。 值得提醒的是批量查询方式中每个查询的时间会改善1.5倍到2.3倍,具体如何取决于特定的排序模式。其实,不仅在综合测试中,公共查询优化和公共子树优化都会提升3倍或者更多的改善,在实际生产运营环境中也是如此。

5.12. 字符串排序规则

Sphinx从版本2.0.1-beta开始支持字符串排序,而排序规则势必会影响字符串属性的比较。Sphinx在进行字符串属性的ORDER BY或者GROUP BY操作时所进行的字符串比较,与字符集的编码设定和策略有关。

在索引时,字符串属性仅仅是存储起来,没有字符集 或语言信息附加给他们。 这没关系,Sphinx 只需要存储并逐字返回字符串给调用的应用程序。 但是如果你让Sphinx进行字符串值排序,这个请求就会产生歧义。

首先,单字节(ASCII码或ISO - 8859 - 1或Windows - 1251 )字符串 处理不同于UTF - 8编码的字符串(可能 每一个字符包含可变数目的字节)。 因此,我们需要知道字符集的类型,以便能够正确地解析每个原始字节为有意义的字符。

第二,我们还需要知道特定语言的 字符串排序规则。 例如,当排序根据美国规则并 在en_US区域时,重音字符' ï'(带分音符的小写字母i ) 在一些地方应该放在'Z '的寿面。 然而,根据法国规则排序 并在fr_FR区域的语言环境中时,它应该放在'i'和'J'之间。 而一些 其他语言规则可以选择忽略所有的口音,使' ï' 和'i'可以任意混合。

第三位,但并非最不重要的,我们也许需要区分在某些场景下大小写相关的排序,而在其他一些情况下大小写无关的排序。

排序规则结合以上所述:字符集, 语言规则, ,大小写区分。Sphinx目前提供以下四种 排序规则。

  1. libc_ci

  2. libc_cs

  3. utf8_general_ci

  4. binary

前两个排序规则依赖于几个标准C库(libc)的调用, 因此,可以支持任何在您的系统中安装的语言环境。他们分别提供对大小写无关和大小写敏感的排序规则支持。 默认情况下,会使用C语言环境,按位进行逐字节对比。你可以通过使用collation_libc_locale指令来制定一个在当前系统中可用的其他语言环境设置来改变它。还可以通过locale命令来获得当前系统上可用的语言环境列表:

$ locale -a
C
en_AG
en_AU.utf8
en_BW.utf8
en_CA.utf8
en_DK.utf8
en_GB.utf8
en_HK.utf8
en_IE.utf8
en_IN
en_NG
en_NZ.utf8
en_PH.utf8
en_SG.utf8
en_US.utf8
en_ZA.utf8
en_ZW.utf8
es_ES
fr_FR
POSIX
ru_RU.utf8
ru_UA.utf8

不同系统上的语言环境列表可能有所不同。请参考操作系统的文档,以安装更多所需的语言环境。

utf8_general_ci  binary 语言环境是Sphinx内建支持的。第一个是UTF-8数据的通用字符串校对类型(没有针对任何语言进行定制)。它与MySQL的 utf8_general_ci 排序规则类似。第二个是简单的字节对比。

字符集校对可以在每次会话过程中通过SphinxQL的 SET collation_connection语句来覆盖全局设置。所有后续的SphinxQL查询都将使用该字符集校对。SphinxAPI和SphinxSE查询都将使用由collation_server配置指令专门指定的服务器默认字符集校对。当前,Sphinx的默认字符集校对为 libc_ci

排序规则会影响所有的字符串属性比较操作,包括在ORDER BY 和 GROUp By中的使用,根据所选择的排序规则将返回不同的排序或者分组结果。

5.13. 用户自定义函数 (UDF)

从版本2.0.1-beta开始,Sphinx支持用户自定义函数,或者简称为UDF。 他们可以被searchd动态的加载或者卸载,而不用重启驻守进程, 并可以在搜索时用在表达式中。UDF的功能如下所述:

  • 函数可以接受整数 (32-bit 与 64-bit), 浮点数, 字符串, 或者MVA参数.

  • 函数可以返回整数或者浮点数值.

  • 函数可以检查参数数量、类型和名称,以及引发错误.

  • 目前仅支持简单的函数(即非聚合的).

用户自定义函数需要你的系统支持动态库调用(也就是共享对象)。 大部分现代操作系统都支持,包括Linux、Windows、MacOS、Solaris、BSD以及其他的。 (内部测试是在Linux和Windows上进行的。)UDF库需要放置在有plugin_dir 指令专门指定的目录中,并且服务需要配置为workers = threads模式。库文件不能使用相对路径。 一旦库文件成功编译构建并拷贝到正确的位置,就可以使用CREATE FUNCTION and DROP FUNCTION语句来分别动态的安装或者反安装这些函数。 单个库可以包含多个函数。每个库会在第一次安装其中的某个函数时加载,并在反安装库中的所有函数后被卸载。

库函数将实现可被SQL语句调用的UDF,他们需要遵循C调用规则,以及简单的命名约定。 Sphinx源代码发行包中包含了一个简单的示例文件, src/udfexample.c, 其中定义了一些简单的函数来演示如何使用证书、字符串以及MVA参数;你可以使用其中的某一个为样板来创建你的新函数。 它包含了UDF接口的头文件 src/sphinxudf.h, 其中定义了所需的类型和结构。sphinxudf.h头是独立的,也就是说不需要Sphinx源代码的任何其他部分就可以编译。

你想要用在SELECT语句中的任何一个函数,都需要至少两个相应的C/C++函数:初始化调用和函数本身的调用。 你也可以根据需要定义反初始化调用以便函数需要在查询后进行清理。 SQL中的函数名是大小写无辜啊的,但是C函数不是,他们需要全部为小写。 函数名称错误将会阻止UDF调入。 你还需要特别注意编译时的调用约定,包括参数顺序和类型,以及主调用函数的返回类型。 任何疏忽都可能导致服务崩溃,最好的情况也许是返回了不可预期的结果。 最后但并非最不重要的,所有的函数都必须是线程安全的。

假设因为因为测试的原因,你在SphinxQL中的UDF起名为MYFUNC。初始化、主调用与卸载函数需要按照以下方式命名并使用对应的参数:

/// 初始化
/// 查询初始化时调用called once during query initialization
/// returns 0 on success
/// returns non-zero and fills error_message buffer on failure
int myfunc_init ( SPH_UDF_INIT * init, SPH_UDF_ARGS * args,
                  char * error_message );

/// 主调用
/// 返回计算的结果
/// 给error_flag写入非零值表示出现错误
RETURN_TYPE myfunc ( SPH_UDF_INIT * init, SPH_UDF_ARGS * args,
                     char * error_flag );

/// 可选的卸载函数
/// 调用一次清理一次以完成查询处理
void myfunc_deinit ( SPH_UDF_INIT * init );

上面提到的两个结构体, SPH_UDF_INIT  SPH_UDF_ARGS src/sphinxudf.h街头头文件中定义并说明。 主调用的RETURN_TYPE 可以是以下之一:

  • int 表示函数返回值为 INT.

  • sphinx_int64_t 表示函数返回值为 BIGINT.

  • float 表示函数返回值为 FLOAT.

他们的调用顺序如下. myfunc_init() 在查询初始化时调用一次。它可以返回非零值表示失败;在此情况下查询将不会被执行,并可以通过error_message缓冲区来返回错误信息。然后,myfunc()会在每行处理中调用,myfunc_deinit()则在查询结束时调用。myfunc()可以通过写入非零值到error_flag来表示错误,此时它将不再被序列中的行调用,而是用默认值0来取代。Sphinx可能会也可能不会选择提前终止此类查询,目前不保证一定会如何选择。