概述

全文搜索(或者文本搜索)提供了确定满足一个查询的自然语言文档的能力,并可以选择将它们按照与查询的相关度排序。最常用的搜索类型是找到所有包含给定查询词的文档并按照它们与查询的相似性顺序返回它们。查询相似性的概念非常灵活并且依赖于特定的应用。最简单的搜索认为查询是一组词而相似性是查询词在文档中的频度。

文本搜索操作符已经在数据库中存在很多年了。本数据库中对文本数据类型提供了~~LIKEILIKE操作符,但是它们缺少现代信息系统所要求的很多基本属性:

  • 即使对英语也缺乏语言的支持。正则表达式是不够的,因为它们不能很容易地处理派生词,例如satisfiessatisfy。你可能会错过包含satisfies的文档,尽管你可能想要在对于satisfy的搜索中找到它们。可以使用OR来搜索多个派生形式,但是这样做太罗嗦也容易出错(有些词可能有数千种派生)。

  • 它们不提供对搜索结果的排序(排名),这使它们面对数以千计被找到的文档时变得无效。

  • 它们很慢因为没有索引支持,因此它们必须为每次搜索处理所有的文档。

全文索引允许文档被预处理并且保存一个索引用于以后快速的搜索。预处理包括:

  • 将文档解析成记号*。标识出多种类型的记号是有所帮助的,例如数字、词、复杂的词、电子邮件地址,这样它们可以被以不同的方式处理。原则上记号分类取决于相关的应用,但是对于大部分目的都可以使用一套预定义的分类。本数据库中使用一个解析器*来执行这个步骤。其中提供了一个标准的解析器,并且为特定的需要也可以创建定制的解析器。

  • 将记号转换成词位*。和一个记号一样,一个词位是一个字符串,但是它已经被正规化,这样同一个词的不同形式被变成一样。例如,正规化几乎总是包括将大写字母转换成小写形式,并且经常涉及移除后缀(例如英语中的ses)。这允许搜索找到同一个词的变体形式,而不需要冗长地输入所有可能的变体。此外,这个步骤通常会消除停用词,它们是那些太普通的词,它们对于搜索是无用的(简而言之,记号是文档文本的原始片段,而词位是那些被认为对索引和搜索有用的词)。本数据库中使用词典*来执行这个步骤。已经提供了多种标准词典,并且为特定的需要也可以创建定制的词典。

  • 为搜索优化存储预处理好的文档。例如,每一个文档可以被表示为正规化的词位的一个有序数组。与词位一起,通常还想要存储用于近似排名的位置信息,这样一个包含查询词更“密集”区域的文档要比那些包含分散的查询词的文档有更高的排名。

词典允许对记号如何被正规化进行细粒度的控制。使用合适的词典,你可以:

  • 定义不应该被索引的停用词。

  • 使用 Ispell 把同义词映射到一个单一词。

  • 使用一个分类词典把短语映射到一个单一词。

  • 使用一个 Ispell 词典把一个词的不同变体映射到一种规范的形式。

  • 使用 Snowball 词干分析器规则将一个词的不同变体映射到一种规范的形式。

我们提供了一种数据类型tsvector来存储预处理后的文档,还提供了一种类型tsquery来表示处理过的查询。有很多函数和操作符可以用于这些数据类型,其中最重要的是匹配操作符@@。全文搜索可以使用索引来加速。

什么是一个文档

一个document是在一个全文搜索系统中进行搜索的单元,例如,一篇杂志文章或电子邮件消息。文本搜索引擎必须能够解析文档并存储词位(关键词)与它们的父文档之间的关联。随后,这些关联会被用来搜索包含查询词的文档。

对于本数据库中的搜索,一个文档通常是一个数据库表中一行内的一个文本形式的域,或者可能是这类域的一个组合(连接),这些域可能存储在多个表或者是动态获取。换句话说,一个文档可能从用于索引的不同部分构建,并且它可能被作为一个整体存储在某个地方。例如:

    SELECT title || ' ' ||  author || ' ' ||  abstract || ' ' || body AS document
    FROM messages
    WHERE mid = 12;

    SELECT m.title || ' ' || m.author || ' ' || m.abstract || ' ' || d.body AS document
    FROM messages m, docs d
    WHERE m.mid = d.did AND m.mid = 12;

注意

实际上在这些例子查询中,coalesce应该被用来防止一个单一NULL属性导致整个文档的一个NULL结果。另一种存储文档的可能性是作为文件系统中的简单文本文件。在这种情况下,数据库可以被用来存储全文索引并执行搜索,并且某些唯一标识符可以被用来从文件系统检索文档。但是,从数据库的外面检索文件要求超级用户权限或者特殊函数支持,因此这种方法通常不如把所有数据放在本数据库中内部方便。另外,把所有东西放在数据库内部允许方便地访问文档元数据来协助索引和现实。对于文本搜索目的,每一个文档必须被缩减成预处理后的tsvector格式。搜索和排名被整个在一个文档的tsvector表示上执行 — 只有当文档被选择来显示给用户时才需要检索原始文本。我们因此经常把tsvector说成是文档,但是当然它只是完整文档的一种紧凑表示。

基本文本匹配

本数据库中的全文搜索基于匹配操作符@@,它在一个tsvector(文档)匹配一个tsquery(查询)时返回true。哪种数据类型写在前面没有影响:

    SELECT 'a fat cat sat on a mat and ate a fat rat'::tsvector @@ 'cat & rat'::tsquery;
     ?column?
    ----------
     t

    SELECT 'fat & cow'::tsquery @@ 'a fat cat sat on a mat and ate a fat rat'::tsvector;
     ?column?
    ----------
     f

正如以上例子所建议的,一个tsquery并不只是一个未经处理的文本,顶多一个tsvector是这样。一个tsquery包含搜索术语,它们必须是已经正规化的词位,并且可以使用 AND 、OR、NOT 以及 FOLLOWED BY 操作符结合多个术语。有几个函数to_tsqueryplainto_tsquery以及phraseto_tsquery可用于将用户书写的文本转换为正确的tsquery,它们会主要采用正则化出现在文本中的词的方法。相似地,to_tsvector被用来解析和正规化一个文档字符串。因此在实际上一个文本搜索匹配可能看起来更像:

    SELECT to_tsvector('fat cats ate fat rats') @@ to_tsquery('fat & rat');
     ?column?
    ----------
     t

注意如果这个匹配被写成下面这样它将不会成功:

    SELECT 'fat cats ate fat rats'::tsvector @@ to_tsquery('fat & rat');
     ?column?
    ----------
     f

因为这里不会发生词rats的正规化。一个tsvector的元素是词位,它被假定为已经正规化好,因此rats不匹配rat@@操作符也支持text输出,它允许在简单情况下跳过从文本字符串到tsvectortsquery的显式转换。可用的变体是:

    tsvector @@ tsquery
    tsquery  @@ tsvector
    text @@ tsquery
    text @@ text

前两种我们已经见过。形式text @@ tsquery等价于to_tsvector(x) @@ y。形式text @@ text等价于to_tsvector(x) @@ plainto_tsquery(y)。在tsquery中,&(AND)操作符指定它的两个参数都必须出现在文档中才表示匹配。类似地,|(OR)操作符指定至少一个参数必须出现,而!(NOT)操作符指定它的参数出现才能匹配。例如,查询fat & ! rat匹配包含fat但不包含rat的文档。在<->(FOLLOWED BY) tsquery操作符的帮助下搜索可能的短语,只有该操作符的参数的匹配是相邻的并且符合给定顺序时,该操作符才算是匹配。例如:

    SELECT to_tsvector('fatal error') @@ to_tsquery('fatal <-> error');
     ?column?
    ----------
     t

    SELECT to_tsvector('error is not fatal') @@ to_tsquery('fatal <-> error');
     ?column?
    ----------
     f

FOLLOWED BY 操作符还有一种更一般的版本,形式是<``N``>,其中N是一个表示匹配词位位置之间的差。<1><->相同,而<2>允许刚好一个其他词位出现在匹配之间,以此类推。当有些词是停用词时,phraseto_tsquery函数利用这个操作符来构造一个能够匹配多词短语的tsquery。例如:

    SELECT phraseto_tsquery('cats ate rats');
           phraseto_tsquery
    -------------------------------
     'cat' <-> 'ate' <-> 'rat'

    SELECT phraseto_tsquery('the cats ate the rats');
           phraseto_tsquery
    -------------------------------
     'cat' <-> 'ate' <2> 'rat'

一种有时候有用的特殊情况是,<0>可以被用来要求两个匹配同一个词的模式。圆括号可以被用来控制tsquery操作符的嵌套。如果没有圆括号,|的计算优先级最低,然后从低到高依次是&<->!。值得注意的是,当 AND/OR/NOT 操作符在一个 FOLLOWED BY 操作符的参数中时,它们表示与不在那些参数中时不同的含义,因为在 FOLLOWED BY 中匹配的准确位置是有意义的。例如,通常!x仅匹配在任何地方都不包含x的文档。但如果y不是紧接在一个x后面,!x <-> y就会匹配那个y,在文档中其他位置出现的x不会阻止匹配。另一个例子是,x & y通常仅要求xy均出现在文档中的某处,但是(x & y) <-> z要求xy在紧挨着z之前的同一个位置匹配。因此这个查询的行为会不同于x <-> z & y <-> z,它将匹配一个含有两个单独序列x z以及y z的文档(这个特定的查询一点用都没有,因为xy不可能在同一个位置匹配,但是对于前缀匹配模式之类的更复杂的情况,这种形式的查询就会有用武之地)。

配置

前述的都是简单的文本搜索例子。正如前面所提到的,全文搜索功能包括做更多事情的能力:跳过索引特定词(停用词)、处理同义词并使用更高级的解析,例如基于空白之外的解析。这个功能由文本搜索配置控制。本数据库中有多种语言的预定义配置,并且你可以很容易地创建你自己的配置(psql 的\dF命令显示所有可用的配置)。在安装期间一个合适的配置将被选择并且 default_text_search_config 也被相应地设置在postgresql.conf中。如果你正在对整个集簇使用相同的文本搜索配置,你可以使用在postgresql.conf中使用该值。要在集簇中使用不同的配置但是在任何一个数据库内部使用同一种配置,使用ALTER DATABASE ... SET。否则,你可以在每个会话中设置default_text_search_config

依赖一个配置的每一个文本搜索函数都有一个可选的regconfig参数,因此要使用的配置可以被显式指定。只有当这个参数被忽略时,default_text_search_config才被使用。为了让建立自定义文本搜索配置更容易,一个配置可以从更简单的数据库对象来建立。本数据库中的文本搜索功能提供了四类配置相关的数据库对象:

  • 文本搜索解析器将文档拆分成记号并分类每个记号(例如,作为词或者数字)。

  • 文本搜索词典将记号转变成正规化的形式并拒绝停用词。

  • 文本搜索模板提供位于词典底层的函数(一个词典简单地指定一个模板和一组用于模板的参数)。

  • 文本搜索配置选择一个解析器和一组用于将解析器产生的记号正规化的词典。

文本搜索解析器和模板是从低层 C 函数构建而来,因此它要求 C 编程能力来开发新的解析器和模板,并且还需要超级用户权限来把它们安装到一个数据库中(在本数据库中发布的contrib/区域中有一些附加的解析器和模板的例子)。由于词典和配置只是对底层解析器和模板的参数化和连接,不需要特殊的权限来创建一个新词典或配置。创建定制词典和配置的例子将在本章稍后的部分给出。