3. 建立索引
3.1. 数据源
索引的数据可以来自各种各样不同的来源:SQL数据库、纯文本、HTML文件、邮件等等。从Sphinx/Coreseek的视角看,索引数据是一个结构化的文档的集合,其中每个文档是字段的集合,这和SQL数据库的视角有所不同,在那里,每一行代表一个文档,每一列代表一个字段。
由于数据来源的不同,需要不同的代码来获取数据、处理数据以供Sphinx/Coreseek进行索引的建立。这种代码被称之为数据源驱动程序(简称:驱动或数据源)。
在本文撰写时,Sphinx/Coreseek中包括MySQL和PostgreSQL数据源的驱动程序,这些驱动使用数据库系统提供的C/C++原生接口连接到数据库服务器并获取数据。此外,Sphinx/Coreseek还提供了额外的被称为xmlpipe的数据源驱动,该驱动运行某个具体的命令,并从该命令的stdout
中读入数据。数据的格式在 Section 3.8, “xmlpipe 数据源” 中有介绍。经过发展,Coreseek还提供了具有特色的Python数据源驱动,可以使用Python编写数据获取脚本自定义数据源,从而得以获取任何已知世界和未知世界的数据。
如果确有必要,一个索引的数据可以来自多个数据源。这些数据将严格按照配置文件中定义的顺序进行处理。所有从这些数据源获取到的文档将被合并,共同产生一个索引,如同他们来源于同一个数据源一样。
3.2. 属性
属性是附加在每个文档上的额外的信息(值),可以在搜索的时候用于过滤和排序。
搜索结果通常不仅仅是进行文档的匹配和相关度的排序,经常还需要根据其他与文档相关联的值,对结果进行额外的处理。例如,用户可能需要对新闻检索结果依次按日期和相关度排序,检索特定价格范围内的产品,检索某些特定用户的blog日志,或者将检索结果按月分组。为了高效地完成上述工作,Sphinx允许给文档附加一些额外的属性,并把这些值存储在全文索引中,以便在对全文匹配结果进行过滤、排序或分组时使用。
属性与字段不同,不会被全文索引。他们仅仅是被存储在索引中,属性进行全文检索式不可能的。如果要对属性进行全文检索,系统将会返回一个错误。
例如,如果column被设置为属性,就不能使用扩展表达式@column 1
去匹配column为1的文档;如果数字字段按照普通的方式被索引,那么就可以这样来匹配。
属性可用于过滤,或者限制返回的数据,以及排序或者 结果分组; 也有可能是完全基于属性排序的结果, 而没有任何搜索相关功能的参与. 此外, 属性直接从搜索服务程序返回信息, 而被索引的文本内容则没有返回.
论坛帖子表是一个很好的例子。假设只有帖子的标题和内容这两个字段需要全文检索,但是有时检索结果需要被限制在某个特定的作者的帖子或者属于某个子论坛的帖子中(也就是说,只检索在SQL表的author_id和forum_id这两个列上有特定值的那些行),或者需要按post_date列对匹配的结果排序,或者根据post_date列对帖子按月份分组,并对每组中的帖子计数。
为实现这些功能,可以将上述各列(除了标题和内容列)作为属性做索引,之后即可使用API调用来设置过滤、排序和分组。以下是一个例子:
示例: sphinx.conf 片段:
... 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):
// only search posts by author whose ID is 123 $cl->SetFilter ( "author_id", array ( 123 ) ); // only search posts in sub-forums 1, 3 and 7 $cl->SetFilter ( "forum_id", array ( 1,3,7 ) ); // sort found posts by posting date in descending order $cl->SetSortMode ( SPH_SORT_ATTR_DESC, "post_date" );
可以通过名字来指示特定的属性,并且这个名字是大小写无关的(注意:直到目前为止,Sphinx还不支持中文作为属性的名称)。属性并不会被全文索引,他们只是按原封不动的存储在索引文件中。目前支持的属性类型如下:
-
无符号整数(1-32位宽);
-
UNIX 时间戳(timestamps);
-
浮点值(32位,IEEE 754单精度);
-
字符串序列 (尤其是计算出的整数值);
-
多值属性 MVA( multi-value attributes ) (32位无符号整型值的变长序列).
由各个文档的全部的属性信息构成了一个集合,它也被称为文档信息 docinfo. 文档信息可以按如下两种方式之一存储:
-
与全文索引数据分开存储(“外部存储”,在
.spa
文件中存储), 或者 -
在全文索引数据中,每出现一次文档ID 就出现相应的文档信息(“内联存储”,在
.spd
文件中存储)
当采用外部存储方式时,searchd
总是在RAM中保持一份.spa
文件的拷贝(该文件包含所有文档的所有文档信息)。这是主要是为了提高性能,因为磁盘的随机访问太慢了。相反,内联存储并不需要任何额外的RAM,但代价是索引文件的体积大大地增加了;请注意,全部属性值在文档ID出现的每一处都被复制了一份,而文档ID出现的次数恰是文档中不同关键字的数目。仅当有一个很小的属性集、庞大的文本数据集和受限的RAM时,内联存储才是一个可考虑的选择。在大多数情况下,外部存储可令建立索引和检索的效率都大幅提高。
检索时,采用外部存储方式产生的的内存需求为 (1+number_of_attrs)*number_of_docs*4字节,也就是说,带有两个属性和一个时间戳的1千万篇文档会消耗(1+2+1)*10M*4 = 160 MB的RAM。这是每个检索的守护进程(PER DAEMON)消耗的量,而不是每次查询,searchd
仅在启动时分配160MB的内存,读入数据并在不同的查询之间保持这些数据。子进程并不会对这些数据做额外的拷贝。
3.3. 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().
3.4. 索引
为了快速地相应响应查询,Sphinx需要从文本数据中建立一种为查询做优化的特殊的数据结构。这种数据结构被称为索引(index);而建立索引的过程也叫做索引或建立索引(indexing)。
不同的索引类型是为不同的任务设计的。比如,基于磁盘的B-Tree存储结构的索引可以更新起来比较简单(容易向已有的索引插入新的文档),但是搜起来就相当慢。因此Sphinx的程序架构允许轻松实现多种不同的索引类型。
目前在Sphinx中实现的唯一一种索引类型是为最优化建立索引和检索的速度而设计的。随之而来的代价是更新索引相当的很慢。理论上讲,更新这种索引甚至可能比从头重建索引还要慢。不过大多数情况下这可以靠建立多个索引来解决索引更新慢的问题,细节请参考 Section 3.11, “实时索引更新”.
实现更多的索引类型支持,已列入计划,其中包括一种可以实时更新的类型。
每个配置文件都可以按需配置足够多的索引。indexer
工具可以将它们同时重新索引(如果使用了–all
选项)或者仅更新明确指出的一个。searchd
工具会为所有被指明的索引提供检索服务,而客户端可以在运行时指定使用那些索引进行检索。
3.5. 源数据的限制
Sphinx/Coreseek索引的源数据有一些限制,其中最重要的一条是:
所有文档的ID必须是唯一的无符号非零整数(根据Sphinx构造时的选项,可能是32位或64位)
如果不满足这个要求,各种糟糕的情况都可能发生。例如,Sphinx/Coreseek建立索引时可能在突然崩溃,或者由于冲突的文档ID而在索引结果中产生奇怪的结果。也可能,一只重达1000磅的大猩猩最后跳出你的电脑,向你扔臭蛋。我告诉过你咯!
3.6. 字符集、大小写转换和转换表
当建立索引时,Sphinx从指定的数据源获得文本文档,将文本分成词的集合,再对每个词做大小写转换,于是“Abc”,“ABC”和“abc”都被当作同一个词(word,或者更学究一点,词项term)
为了正确完成上述工作,Sphinx需要知道:
-
源文本是什么编码的;
-
那些字符是字母,哪些不是;
-
哪些字符需要被转换,以及被转换成什么.
这些都可以用 charset_type
和 charset_table
选项为每个索引单独配置. charset_type
指定文档的编码是单字节的(SBCS)还是UTF-8的。在Coreseek中,如果通过charset_dictpath设置中文词典启动了中文分词模式后,则可以使用GBK及BIG5的编码;但是在内部实现中,任然是预先转换成UTF-8编码在进行处理的. charset_table
则指定了字母类字符到它们的大小写转换版本的对应表,没有在这张表中出现的字符被认为是非字母类字符,并且在建立索引和检索时被当作词的分割符来看待。
注意,尽管默认的转换表并不包含空格符 (ASCII code 0x20, Unicode U+0020) , 但是这么做是 完全合法的. 这在某些情况下可能有用,比如在对tag云构造索引的时候,这样一个用空格分开的词集就可以被当作一个单独的查询项了.
默认转换表目前包括英文和俄文字符。请您提交您为其他语言撰写的转换表!
在Coreseek中,启用中文分词后,系统会使用MMSeg内置的码表(被硬编码在MMSeg的程序中),因此,charset_table在启用分词后将失效。
3.7. SQL 数据源 (MySQL, PostgreSQL)
对于所有的基于SQL驱动,建立索引的过程如下:
-
连接到数据库;
-
执行预查询 (参见 Section 9.1.11, “sql_query_pre”) ,以便完成所有必须的初始设置,比如为MySQL连接设置编码;
-
执行主查询 (参见 Section 9.1.12, “sql_query”) ,其返回的的数据将被索引;
-
执行后查询 (see Section 9.1.23, “sql_query_post”) ,以便完成所有必须的清理工作;
-
关闭到数据库的连接;
-
对短语进行排序 (或者学究一点, 索引类型相关的后处理);
-
再次建立到数据库的连接;
-
执行后索引查询 (参见 Section 9.1.24, “sql_query_post_index”) i,以便完成所有最终的清理善后工作;
-
再次关闭到数据库的连接.
大多数参数是很直观的,例如数据库的用户名、主机、密码。不过,还有一些细节上的问题需要讨论。
区段查询
索引系统需要通过主查询来获取全部的文档信息,一种简单的实现是将整个表的数据读入内存,但是这可能导致整个表被锁定并使得其他操作被阻止(例如:在MyISAM格式上的INSERT操作),同时,将浪费大量内存用于存储查询结果,诸如此类的问题吧。 为了避免出现这种情况,Sphinx/Coreseek支持一种被称为 区段查询的技术. 首先,Sphinx/Coreseek从数据库中取出文档ID的最小值和最大值,将由最大值和最小值定义自然数区间分成若干份,一次获取数据,建立索引。现举例如下:
Example 1. 区段查询示例:
# in sphinx.conf sql_query_range = SELECT MIN(id),MAX(id) FROM documents sql_range_step = 1000 sql_query = SELECT * FROM documents WHERE id>=$start AND id<=$end
如果这个表(documents)中,字段ID的最小值和最大值分别是1 和2345,则sql_query将执行3次:
-
将
$start
替换为1,并且将$end
替换为 1000; -
将
$start
替换为1001,并且将$end
替换为 2000; -
将
$start
替换为2001,并且将$end
替换为 2345.
显然,这对于只有2000行的表,分区查询与整个读入没有太大区别,但是当表的规模扩大到千万级(特别是对于MyISAM格式的表),分区区段查询将提供一些帮助。
后查询(sql_post) vs. 索引后查询(sql_post_index)
后查询和索引后查询的区别在于,当Sphinx获取到全部文档数据后,立即执行后查询,但是构建索引的过程仍然may因为某种原因失败。在另一方面,当索引后查询被执行时,可以理所当然的认为索引已经成功构造完了。因为构造索引可能是个漫长的过程,因此对与数据库的连接在执行后索引操作后被关闭,在执行索引后操作前被再次打开。
3.8. xmlpipe 数据源
xmlpipe 数据源是处于让用户能够将现有数据嵌入Sphinx而无需开发新的数据源驱动的目的被设计和提供的。它将每篇文档限制为只能包括两个可全文索引的字段,以及只能包括两个属性。xmlpipe数据源已经被废弃,在Section 3.9, “xmlpipe2 数据源”中描述了xmlpipe的替代品xmlpipe2数据源。对于新的数据,建议采用xmlpipe2。
为了使用xmlpipe,需要将配置文件改为类似如下的样子:
source example_xmlpipe_source { type = xmlpipe xmlpipe_command = perl /www/mysite.com/bin/sphinxpipe.pl }
indexer
实用程序将要运行 xmlpipe_command
, 所指定的命令,而后读取其向标准输出stdout
上输出的数据,并对之进行解析并建立索引。 严格的说,是索引系统打开了一个与指定命令相连的管道,并从这个管道读取数据。
indexer 实用程序假定在从标准输入读入的XML格式的数据中中存在一个或更多的文档。下面是一个包括两个文档的文档数据流的例子:
Example 2. XMLpipe 文档数据流
<document> <id>123</id> <group>45</group> <timestamp>1132223498</timestamp> <title>test title</title> <body> this is my document body </body> </document> <document> <id>124</id> <group>46</group> <timestamp>1132223498</timestamp> <title>another test</title> <body> this is another document </body> </document>
遗留的xmlpipe数据驱动使用内置的解析器来解析xml文档,这个解析器的速度非常快,但是并没有提供对XML格式完整支持。这个解析器需要文档中必须包括全部的字段,并且严格按照例子中给出的格式给出,而且字段的出现顺序需要严格按照例子中给出的顺序。仅有一个字段timestamp
是可选的,它的缺省值为1。
3.9. xmlpipe2 数据源
xmlpipe2使你可以用另一种自定义的XML格式向Sphinx传输任意文本数据和属性数据。数据模式(即数据字段的集合或者属性集)可以由XML流本身指定,也可以在配置文件中数据源的配置部分中指定。
在对xmlpipe2数据源做索引时,索引器运行指定的命令,打开一个连接到前述命令标准输出的管道,并等待接受具有正确格式的XML数据流。以下是一个数据流的样本:
Example 3. xmlpipe2 文档流
<?xml version="1.0" encoding="utf-8"?> <sphinx:docset> <sphinx:schema> <sphinx:field name="subject"/> <sphinx:field name="content"/> <sphinx:attr name="published" type="timestamp"/> <sphinx:attr name="author_id" type="int" bits="16" default="1"/> </sphinx:schema> <sphinx:document id="1234"> <content>this is the main content <![CDATA[[and this <cdata> entry must be handled properly by xml parser lib]]></content> <published>1012325463</published> <subject>note how field/attr tags can be in <b class="red">randomized</b> order</subject> <misc>some undeclared element</misc> </sphinx:document> <!-- ... more documents here ... --> </sphinx:docset>
任意多的数据字段和属性都是允许的。数据字段和属性在同一文档元素中出现的先后顺序没有特别要求。。单一字段数据的最大长度有限制,超过2MB的数据会被截短到2MB(但这个限制可以在配置文件中数据源部分中修改)。
XML数据模式(Schema),即数据字段和属性的完整列表,必须在任何文档被分析之前就确定。这既可以在配置文件中用xmlpipe_field
和xmlpipe_attr_XXX
选项指定,也可以就在数据流中用<sphinx:schema>元素指定。 <sphinx:schema>元素是可选的,但如果出现,就必须是<sphinx:docset>元素的第一个子元素。如果没有在数据流中内嵌的数据模式定义,配置文件中的相关设置就会生效,否则数据流内嵌的设置被优先采用。
未知类型的标签(既不是数据字段,也不是属性的标签)会被忽略,但会给出警告。在上面的例子中,<misc>标签会被忽略。所有嵌入在其他标签中的标签及其属性都会被无视(例如上述例子中嵌入在<subject>标签中的<b>标签)
支持输入数据流的何种字符编码取决于系统中是否安装了iconv
. xmlpipe2是用 libexpat
解析器解析的,该解析器内置对 US-ASCII, UTF-8, UTF-8 和一些 UTF-16 变体的支持. Sphinx/Coreseek的 configure
脚本也会检查 libiconv
是否存在并使用它来处理其他的字符编码。 libexpat
也隐含的要求在Sphinx/Coreseek端使用UTF-8,因为它返回的分析过的数据总是UTF-8的。
xmlpipe2可以识别的XML元素(标签)(以及前述元素可用的属性)如下:
-
sphinx:docset 顶级元素,用于标明并包括xmlpipe2文档.
-
sphinx:schema 可选元素,它要么是sphinx:docset的第一个子元素,要么干脆不出现。声明文档的模式。包括数据字段和属性的声明。若此元素出现,则它会覆盖配置文件中对数据源的设定.
-
sphinx:field 可选元素,sphinx:schema的子元素。声明一个全文数据字段。唯一可识别的属性是“name”,它指定了字段的名称,后续数据文档中具有此名称的元素的数据都被当作待检索的全文数据对待.
-
sphinx:attr O可选元素,sphinx:schema的子元素。用于声明具体属性。其已知的属性有: “name”,设定该属性名称,后续文档中具有该名称的元素应被当作一个属性对待。. “type”,设定该属性的类型。可能的类型包括 “int”, “timestamp”, “str2ordinal”, “bool”, “float” 和 “multi”. “bits”,设定“int”型属性的宽度,有效值为1到32. “default”,设定该属性的默认值,若后续文档中没有指定这个属性,则使用此默认值。
-
sphinx:document 必须出现的元素,必须是sphinx:docset的子元素。包含任意多的其他元素,这些子元素带有待索引的数据字段和属性值,而这些数据字段或属性值既可以是用sphinx:field和sphinx:attr元素声明的,也可以在配置文件中声明。唯一的已知属性是“id”,它必须包含一个唯一的整型的文档ID。
3.10. Python 数据源
Coreseek支持使用Python编写数据源脚本,从而可以很方便的扩展Sphinx/Coreseek的功能,来轻易的从任何Python可以操作的地方获取需要进行检索的数据。当前,Python几乎支持所有的SQL数据库以及NoSql存储系统,可以查看Python DatabaseInterfaces获得详细列表。
python #用于配置Python数据源程序的PYTHONPATH { path = /usr/local/coreseek/etc/pysource path = /usr/local/coreseek/etc/pysource/csft_demo } source sourcename { type = python #数据类型 name = csft_demo.MainSource #调用的python的类名称 }
在以上配置中,对应的Python数据源脚本,为/usr/local/coreseek/etc/pysource/csft_demo/__init__.py,执行索引操作时,将从该脚本获取数据,请查看Section 10.3, “Python数据源程序接口”了解细节。
3.11. 实时索引更新
有这么一种常见的情况:整个数据集非常大,以至于难于经常性的重建索引,但是每次新增的记录却相当地少。一个典型的例子是:一个论坛有1000000个已经归档的帖子,但每天只有1000个新帖子。
在这种情况下可以用所谓的“主索引+增量索引”(main+delta)模式来实现“近实时”的索引更新。
这种方法的基本思路是设置两个数据源和两个索引,对很少更新或根本不更新的数据建立主索引,而对新增文档建立增量索引。在上述例子中,那1000000个已经归档的帖子放在主索引中,而每天新增的1000个帖子则放在增量索引中。增量索引更新的频率可以非常快,而文档可以在出现几分种内就可以被检索到。
确定具体某一文档的分属那个索引的分类工作可以自动完成。一个可选的方案是,建立一个计数表,记录将文档集分成两部分的那个文档ID,而每次重新构建主索引时,这个表都会被更新。
Example 4. 全自动的即时更新
# 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
设置成空也不行,因为在继承来的数据源上第一次运行这个指令的时候,继承来的所有值都会被清空,这样编码设置的部分也会丢失。因此需要再次显式调用编码设置查询。
3.12. 索引合并
合并两个已有的索引比重新对所有数据做索引更有效率,而且有时候必须这样做(例如在“主索引+增量索引”分区模式中应合并主索引和增量索引,而不是简单地重新索引“主索引对应的数据)。因此indexer
有这个选项。合并索引一般比重新索引快,但在大型索引上仍然不是一蹴而就。基本上,待合并的两个索引都会被读入内存一次,而合并后的内容需要写入磁盘一次。例如,合并100GB和1GB的两个索引将导致202GB的IO操作(但很可能还是比重新索引少)
基本的命令语法如下:
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
:
indexer --merge main delta --merge-dst-range deleted 0 0
这个开关允许您在合并过程中对目标索引实施过滤。过滤器可以有多个,只有满足全部过滤条件的文档才会在最终合并后的索引中出现。在上述例子中,过滤器只允许“deleted”为0的那些条件通过,而去除所有标记为已删除(“deleted”)的记录(可以通过调用UpdateAttributes()设置文档的属性)。