Sphinx基础

什么是Sphinx

Sphinx 是SQL Phrase Index的缩写;Sphinx是一个在GPLv2下分发的全文检索引擎;一般而言,Sphinx是一个独立的全文搜索引擎;可以非常容易的与SQL数据库和脚本语言集成;搜索API支持PHP、Python、Perl、Rudy和Java,并且也可以用作MySQL存储引擎.

Sphinx 的主要特性:

  • 索引和搜索性能优异;
  • 先进的索引和查询工具 (灵活且功能丰富的文本分析器,查询语言,以及多种不同的排序方式等等);
  • 先进的结果集分析处理 (SELECT 可以使用表达式, WHERE, ORDER BY, GROUP BY 等对全文搜索结果集进行过滤);
  • 实践证实可扩展性支持数十亿文档记录,TB级别的数据,以及每秒数千次查询;
  • 易于集成SQL和XML数据源,并可使用SphinxAPI、SphinxQL或者SphinxSE搜索接口
  • 易于通过分布式搜索进行扩展

Sphinx 的详细特性:

  • 高速的索引建立(在当代CPU上,峰值性能可达到10 ~ 15MB/秒);
  • 高性能的搜索 (在1.2G文本,100万条文档上进行搜索,支持高达每秒150~250次查询);
  • 高扩展性 (最大的索引集群超过30亿条文档,最繁忙时刻的查询峰值达到每天5千万次);
  • 提供了优秀的相关度算法,基于短语相似度和统计(BM25)的复合Ranking方法;
  • 支持分布式搜索功能;
  • 提供文档片段(摘要以及高亮)生成功能;
  • 内建支持SphinxAPI和SphinxQL搜索箭口,也可作为MySQL的存储引擎提供搜索服务;
  • 支持布尔、短语、词语相似度等多种检索模式;
  • 文档支持多个全文检索字段(缺省配置下,最大不超过32个);
  • 文档支持多个额外的属性信息(例如:分组信息,时间戳等);
  • 支持查询停止词;
  • 支持词形学处理;
  • 支持特殊词汇处理;
  • 支持单一字节编码和UTF-8编码;
  • 内建支持英语、俄语、捷克语词干化处理; 对 法语,西班牙语,葡萄牙语,意大利语,罗马尼亚语,德国,荷兰,瑞典,挪威,丹麦,芬兰,匈牙利等语言 的支持可通过第三方的 libstemmer 库 建立);
  • 原生的MySQL支持(同时支持MyISAM 、InnoDB、NDB、Archive等所有类型的数据表 );
  • 原生的PostgreSQL 支持;
  • 原生的ODBC兼容数据库支持 (MS SQL, Oracle, 等) ;
  • …多达50多项其他功能没有在此一一列出,请参阅API和配置手册!

数据源

索引的数据可以来自各种各样不同的来源:SQL数据库、纯文本、HTML文件、邮件等等。从CoreSeek/Sphinx的视角看,索引数据是一个结构化的文档的集合,其中每个文档是字段的集合,这种结构更利于SQL数据获取,其中每一行代表一个文档,每一列代表一个字段。

由于数据来源的不同,需要不同的代码来获取数据、处理数据以供CoreSeek/Sphinx进行索引的建立。这种代码被称之为数据源驱动程序(简称:驱动或数据源)。

在本文撰写时,CoreSeek/Sphinx中包括MySQL和PostgreSQL数据源的驱动程序,这些驱动使用数据库系统提供的C/C++原生接口连接到数据库服务器并获取数据。此外,CoreSeek/Sphinx还提供了额外的被称为xmlpipe的数据源驱动,该驱动运行某个具体的命令,并从该命令的stdout中读入数据。数据的格式在 第 3.8 节 “xmlpipe 数据源” 中有介绍。经过长足的发展,Coreseek还提供了具有特色的Python数据源驱动,可以使用Python编写数据获取脚本自定义数据源,从而得以获取任何已知世界和未知世界的数据。

如果确有必要,一个索引的数据可以来自多个数据源。这些数据将严格按照配置文件中定义的顺序进行处理。所有从这些数据源获取到的文档将被合并,共同产生一个索引,如同他们来源于同一个数据源一样。

属性

属性是附加在每个文档上的额外的信息(值),可以在搜索的时候用于过滤和排序。

搜索结果通常不仅仅是进行文档的匹配和相关度的排序,经常还需要根据其他与文档相关联的值,对结果进行额外的处理。例如,用户可能需要对新闻检索结果依次按日期和相关度排序,检索特定价格范围内的产品,检索某些特定用户的blog日志,或者将检索结果按月分组。为了高效地完成上述工作,Sphinx允许给文档附加一些额外的属性,并把这些值存储在全文索引中,以便在对全文匹配结果进行过滤、排序或分组时使用。

属性与字段不同,不会被全文索引。他们仅仅是被存储在索引中,属性进行全文检索式不可能的。如果要对属性进行全文检索,系统将会返回一个错误。

例如,如果column被设置为属性,就不能使用扩展表达式@column 1去匹配column为1的文档;如果数字字段按照普通的方式被索引,那么就可以这样来匹配。

属性可用于过滤,或者限制返回的数据,以及排序或者 结果分组; 也有可能是完全基于属性排序的结果, 而没有任何搜索相关功能的参与. 此外, 属性直接从搜索服务程序返回信息, 而被索引的文本内容则没有返回.

论坛帖子表是一个很好的例子。假设只有帖子的标题和内容这两个字段需要全文检索,但是有时检索结果需要被限制在某个特定的作者的帖子或者属于某个子论坛的帖子中(也就是说,只检索在SQL表的author_id和forum_id这两个列上有特定值的那些行),或者需要按post_date列对匹配的结果排序,或者根据post_date列对帖子按月份分组,并对每组中的帖子计数。

为实现这些功能,可以将上述各列(除了标题和内容列)作为属性做索引,之后即可使用API调用来设置过滤、排序和分组。以下是一个例子:

示例: sphinx.conf 片段:

1
2
3
4
5
6
7
...
sql_query = SELECT id, title, content, \
author_id, forum_id, post_date FROM my_forum_posts
sql_attr_uint = author_id
sql_attr_uint = forum_id
sql_attr_timestamp = post_date
...

示例: 应用程序代码 (使用 PHP):

1
2
3
4
5
6
7
8
// 仅搜索ID为123的作者发布的内容
$cl->SetFilter ( "author_id", array ( 123 ) );

// 仅在id为1,3,7的子论坛中搜索
$cl->SetFilter ( "forum_id", array ( 1,3,7 ) );

// 按照发布时间倒序排列获取的结果
$cl->SetSortMode ( SPH_SORT_ATTR_DESC, "post_date" );

可以通过名字来指示特定的属性,并且这个名字是大小写无关的。属性并不会被全文索引,他们只是按原封不动的存储在索引文件中。目前支持的属性类型如下:

  • 无符号整数(1-32位宽);
  • UNIX 时间戳(timestamps);
  • 浮点值(32位,IEEE 754单精度);
  • 字符串序列 (专指尤其计算出来的字符串序列整数值);
  • 字符串 (版本 1.10-beta 开始支持);
  • 多值属性 MVA(32位无符号整型值的变长序列).

由各个文档的全部的属性信息构成了一个集合,它也被称为文档信息 docinfo. 文档信息可以按如下两种方式之一存储:

  • 与全文索引数据分开存储(“外部存储extern”,在.spa文件中存储), 或者
  • 在全文索引数据中,每出现一次文档ID 就出现相应的文档信息(“内联存储inline”,在.spd文件中存储)

在大多数情况下,外部存储可令建立索引和检索的效率都大幅提高。

检索时,采用外部存储方式产生的的内存需求为 (1+属性总数)文档总数4字节,也就是说,带有两个属性和一个时间戳的1千万篇文档会消耗(1+2+1)10M4 = 160 MB的RAM。这是每个检索的守护进程(PER DAEMON)消耗的量,而不是每次查询,searchd仅在启动时分配160MB的内存,读入数据并在不同的查询之间保持这些数据。子进程并不会对这些数据做额外的拷贝。

MVA (多值属性)

多值属性MVA (multi-valued attributes)是文档属性的一种重要的特例,MVA使得向文档附加一系列的值作为属性的想法成为可能。这对文章的tags,产品类别等等非常有用。MVA属性支持过滤和分组(但不支持分组排序)。

目前MVA列表项的值被限制为32位无符号整数。列表的长度不受限制,只要有足够的RAM,任意个数的值都可以被附加到文档上(包含MVA值的.spm文件会被searchd预缓冲到RAM中)。MVA的源数据来源既可以是一个单独的查询,也可以是文档属性,参考 sql_attr_multi中的来源类型。在第一种情况中,该查询须返回文档ID和MVA值的序对;而在第二种情况中,该字段被分析为整型值。对于多值属性的输入数据的顺序没有任何限制,在索引过程中这些值会自动按文档ID分组(而相同文档ID下的数据也会排序)。

在过滤过程中,MVA属性中的任何一个值满足过滤条件,则文档与过滤条件匹配(因此通过排他性过滤的文档不会包含任何被禁止的值)。按MVA属性分组时,一篇文档会被分到与多个不同MVA值对应的多个组。例如,如果文档集只包含一篇文档,它有一个叫做tag的MVA属性,该属性的值是5、7和11,那么按tag的分组操作会产生三个组,它们的@count都是1,@groupby键值分别是5、7和11。还要注意,按MVA分组可能会导致结果集中有重复的文档:因为每篇文文档可能属于不同的组,而且它可能在多个组中被选为最佳结果,这会导致重复的ID。由于历史原因,PHP API对结果集的行进行按文档ID的有序hash,因此用PHP API进行对MVA属性的分组操作时你还需要使用 SetArrayResult().

索引

为了快速地相应响应查询,Sphinx需要从文本数据中建立一种为查询做优化的特殊的数据结构。这种数据结构被称为索引(index);而建立索引的过程也叫做索引或建立索引(indexing)。

不同的索引类型是为不同的任务设计的。比如,基于磁盘的B-Tree存储结构的索引可以更新起来比较简单(容易向已有的索引插入新的文档),但是搜起来就相当慢。

目前在Sphinx中实现的唯一一种索引类型是为最优化建立索引和检索的速度而设计的。随之而来的代价是更新索引相当的很慢。理论上讲,更新这种索引甚至可能比从头重建索引还要慢。不过大多数情况下这可以靠建立多个索引来解决索引更新慢的问题,细节请参考 第 3.11 节 “实时索引更新”.

每个配置文件都可以按需配置足够多的索引。indexer 工具可以将它们同时重新索引(如果使用了–all选项)或者仅更新明确指出的一个。 searchd工具会为所有被指明的索引提供检索服务,而客户端可以在运行时指定使用那些索引进行检索。

字符集、大小写转换和转换表

当建立索引时,Sphinx从指定的数据源获得文本文档,将文本分成词的集合,再对每个词做大小写转换,于是“Abc”,“ABC”和“abc”都被当作同一个词(word,或者更学究一点,词项term)

为了正确完成上述工作,Sphinx需要知道:

  • 源文本是什么编码的;
  • 那些字符是字母,哪些不是;
  • 哪些字符需要被转换,以及被转换成什么.

这些都可以用 charset_type 和 charset_table 选项为每个索引单独配置. charset_type 指定文档的编码是单字节的(SBCS)还是UTF-8的。在Coreseek中,如果通过charset_dictpath设置中文词典启动了中文分词模式后,不仅可以使用UTF-8编码的,还可以使用GBK及BIG5的编码(需要编译时提供iconv支持);但是在内部实现中,仍然是预先转换成UTF-8编码在进行处理的. charset_table 则指定了字母类字符到它们的大小写转换版本的对应表,没有在这张表中出现的字符被认为是非字母类字符,并且在建立索引和检索时被当作词的分割符来看待。

注意,尽管默认的转换表并不包含空格符 (ASCII code 0x20, Unicode U+0020) , 但是这么做是 完全合法的。 这在某些情况下可能有用,比如在对tag云构造索引的时候,这样一个用空格分开的词集就可以被当作一个单独的查询项了.

实时索引更新(作者注:然并卵)

当前主要有两种方法来维护全文索引的内容是最新的。请注意,但是,这两种处理方法 的任务是应对全文数据更新,而不是 属性更新。从版本0.9.8开始支持即时的属性更新。参看 UpdateAttributes() API 调用了解详情。

方法一,使用基于磁盘的索引,手动分区,然后定期重建较小的分区(被称为“增量”)。通过尽可能的减小重建部分的大小,可以将平均索引滞后时间降低到30~60秒。在0.9.x版本中,这是唯一可用的方法。在一个巨大的文档集上,这可能是最有效的一种方法。参看第 3.12 节 “增量索引更新”了解详情。

方法二,版本1.x(从版本1.10-beta开始)增加了实时索引(简写为Rt索引)的支持,用于及时更新全文数据。在RT索引上的更新,可以在1~2毫秒(0.001-0.002秒)内出现在搜索结果中。然而,RT实时索引在处理较大数据量的批量索引上效率并不高。参看 第 4 章 RT实时索引 了解详情。

增量索引更新

有这么一种常见的情况:整个数据集非常大,以至于难于经常性的重建索引,但是每次新增的记录却相当地少。一个典型的例子是:一个论坛有1000000个已经归档的帖子,但每天只有1000个新帖子。

在这种情况下可以用所谓的“主索引+增量索引”(main+delta)模式来实现“近实时”的索引更新。

这种方法的基本思路是设置两个数据源和两个索引,对很少更新或根本不更新的数据建立主索引,而对新增文档建立增量索引。在上述例子中,那1000000个已经归档的帖子放在主索引中,而每天新增的1000个帖子则放在增量索引中。增量索引更新的频率可以非常快,而文档可以在出现几分种内就可以被检索到。

确定具体某一文档的分属那个索引的分类工作可以自动完成。一个可选的方案是,建立一个计数表,记录将文档集分成两部分的那个文档ID,而每次重新构建主索引时,这个表都会被更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# in MySQL
CREATE TABLE sph_counter
(
counter_id INTEGER PRIMARY KEY NOT NULL,
max_doc_id INTEGER NOT NULL
);

# in sphinx.conf
source main
{
# ...
sql_query_pre = SET NAMES utf8
sql_query_pre = REPLACE INTO sph_counter SELECT 1, MAX(id) FROM documents
sql_query = SELECT id, title, body FROM documents \
WHERE id<=( SELECT max_doc_id FROM sph_counter WHERE counter_id=1 )
}

source delta : main
{
sql_query_pre = SET NAMES utf8
sql_query = SELECT id, title, body FROM documents \
WHERE id>( SELECT max_doc_id FROM sph_counter WHERE counter_id=1 )
}

index main
{
source = main
path = /path/to/main
# ... all the other settings
}

# note how all other settings are copied from main,
# but source and path are overridden (they MUST be)
index delta : main
{
source = delta
path = /path/to/delta
}

请注意,上例中我们显示设置了数据源delta的sql_query_pre选项,覆盖了全局设置。必须显示地覆盖这个选项,否则对delta做索引的时候也会运行那条REPLACE查询,那样会导致delta源中选出的数据为空。可是简单地将delta的sql_query_pre设置成空也不行,因为在继承来的数据源上第一次运行这个指令的时候,继承来的所有值都会被清空,这样编码设置的部分也会丢失。因此需要再次显式调用编码设置查询。

索引合并

合并两个已有的索引比重新对所有数据做索引更有效率,而且有时候必须这样做(例如在“主索引+增量索引”分区模式中应合并主索引和增量索引,而不是简单地重新索引“主索引对应的数据)。因此indexer有这个选项。合并索引一般比重新索引快,但在大型索引上仍然不是一蹴而就。基本上,待合并的两个索引都会被读入内存一次,而合并后的内容需要写入磁盘一次。例如,合并100GB和1GB的两个索引将导致202GB的IO操作(但很可能还是比重新索引少)

基本的命令语法如下:

1
indexer --merge DSTINDEX SRCINDEX [--rotate]

SRCINDEX的内容被合并到DSTINDEX中,因此只有DSTINDEX索引会被改变。 若DSTINDEX已经被searchd于提供服务,则–rotate参数是必须的。 最初设计的使用模式是,将小量的更新从SRCINDEX合并到DSTINDEX中。 因此,当属性被合并时,一旦出现了重复的文档ID,SRCINDEX中的属性值更优先(会覆盖DSTINDEX中的值)。 不过要注意,“旧的”关键字在这个过程中并不会被自动删除。 例如,在DSTINDEX中有一个叫做“old”的关键字与文档123相关联,而在SRCINDEX中则有关键字“new”与同一个文档相关,那么在合并后用这两个关键字都能找到文档123。 您可以给出一个显式条件来将文档从DSTINDEX中移除,以便应对这种情况,相关的开关是–merge-dst-range:

1
indexer --merge main delta --merge-dst-range deleted 0 0

这个开关允许您在合并过程中对目标索引实施过滤。过滤器可以有多个,只有满足全部过滤条件的文档才会在最终合并后的索引中出现。在上述例子中,过滤器只允许“deleted”为0的那些条件通过,而去除所有标记为已删除(“deleted”)的记录(可以通过调用UpdateAttributes()设置文档的属性)。

这个开关允许您在合并过程中对目标索引实施过滤。过滤器可以有多个,只有满足全部过滤条件的文档才会在最终合并后的索引中出现。在上述例子中,过滤器只允许“deleted”为0的那些条件通过,而去除所有标记为已删除(“deleted”)的记录(可以通过调用UpdateAttributes()设置文档的属性)。

RT实时索引

RT实时索引概览

RT实时索引使用注意事项

RT实时索引原理

二进制日志

搜索##

SphinxQL 指南(作者注:很重要,更何况search工具在新版后不这么好用了)

sphinx.conf/csft.conf 配置选项参考


参考资料: