PostgreSQL文本搜索(一)——简介

文本搜索对于数据库系统来说是一个十分重要的功能,它可以在数据库文本文档(包括表中的text字段等)中搜索一个模式,可以是一个或多个单词、短语、短句,并且可以按照相关度排序。最常见的文本搜索是找到所有包含给定查询项的文本文档,然后按照与查询项的相似度排序并返回。查询项和相似度的定义都非常灵活,取决于具体的应用。这个最简单的搜索将查询看作一组单词,将相似度看作查询项在文本文档中的频率。

文本搜索操作符在数据库中已经存在多年。PostgreSQL对文本数据类型有~~*LIKEILIKE运算符,但它们缺乏现代信息系统所需的许多基本特征:

  • 没有语言学支持,包括英语。正则表达式无法轻松处理派生词,例如satisfies和satisfy。如果搜索项为satisfy,绝大部分情况下我们希望搜索的是这个单词的意义,包含satisfies的文档可能会被错过。可以使用OR搜索多个派生形式,但这既繁琐又容易出错(有些词可能有数千个派生词)。
  • 无法提供对于搜索结果的排序,导致在同时找到数千个匹配文档时无效。
  • 由于没有索引支持,速度往往非常慢,每次搜索都必须处理所有文档。

全文索引可以对文档进行预处理并保存索引,以供之后快速搜索。预处理包括以下内容:

  • 对文档进行语法分析,生成许多token(记号),token可以很容易地被分类,如数字、单词、邮件地址等,这使得它们可以有不同的处理方式。原则上token的类型取决于特定的应用,但多数情况下使用一组预定义的类型就够了。PostgreSQL使用一个解析器来完成这一步,提供了一个标准解析器,并且可以根据特定需要创建自定义解析器。
  • 将token转化为lexemes(词位)。lexemes和token一样,也是一个字符,但它已被规范化,同一单词的不同形式被视为相同。例如,规范化几乎总会将大写字母转化为小写字母,并且经常涉及到单词后缀的移除(例如英语中的s或es)。这允许搜索找到同一单词的变体形式,而无需繁琐地输入所有可能的变体。此外,此步骤通常会消除非常常见且对搜索没有帮助的停顿词。(简言之,token是文档文本的原始片段,而lexemes是被认为对索引和搜索有用的词。)PostgreSQL使用词典执行此步骤,提供了各种标准词典,并可根据具体需要创建自定义词典
  • 存储为搜索而优化好的预处理文档。例如,每个文档可以表示为已经规范化的lexemes的排序数组。与lexemes一起,通常需要存储位置信息以用于近似度排序,这样包含更“密集”的查询词区域的文档将被分配比具有分散的查询词的文档有更高的排名。

词典允许细粒度地控制token的规范化方式。使用适当的词典可以:

  • 定义不应被索引的停顿词。
  • 使用Ispell将同义词映射到单个单词。
  • 使用同义短语词典将短语映射到单个单词。
  • 使用Ispell词典将单词的不同变体映射到规范形式。
  • 使用Snowball词干分析器的规则将单词的不同变体映射到规范形式。

数据类型tsvector用于存储预处理文档,类型tsquery用于表示已处理的查询,这些数据类型有许多函数和运算符,其中最重要的是匹配运算符@@,全文搜索可以使用索引来加速。

什么是文档?

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

对于PostgreSQL中的搜索,一个文档通常是数据库表中的一行文本字段,或者可能是这种字段的组合(连接),可能存储在几个表中或动态地获得。换句话说,一个文档可以由不同的部分构成索引,它可能不会作为一个整体存储在任何地方。比如:

-- 并操作( || )是用来防止一个NULL的属性导致整个结果都为NULL
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;

另一种可能性是将文件作为简单的文本文件存储在文件系统中。在这种情况下,数据库可以用来存储全文索引和执行搜索,而一些唯一的标识符可以用来从文件系统中检索文件。然而,从数据库外检索文件需要超级用户权限或特殊功能支持,所以这通常不如将所有数据保存在PostgreSQL内部方便。另外,把所有的东西都保存在数据库里面,可以很容易地访问文件元数据以便索引和显示。

出于文本搜索的目的,每个文档必须被简化成预处理后的tsvector格式。搜索和排名完全是在文档的tsvecto表示上进行的,只有当文档被选中显示给用户时,才需要检索原始文本。因此,我们经常把tsvector说成是文档,但当然它只是完整文档的一个紧凑的表示。

基本的文本匹配

PostgreSQL中的全文搜索基于匹配运算符@@,如果一个tsvector(文档)与一个tsquery(查询)匹配,则返回true,哪个数据类型先被写入并不重要:

SELECT 'a fat cat sat on a mat and ate a fat rat'::tsvector @@ 'cat & rat'::tsquery;
-- tsvector匹配了tsquery中的cat和rat,返回true
 ?column?
----------
 t

SELECT 'fat & cow'::tsquery @@ 'a fat cat sat on a mat and ate a fat rat'::tsvector;
-- tsvector匹配了tsquery中的fat但是没有匹配cow,返回false
 ?column?
----------
 f

正如上面的例子所表明的,和tsvector一样,一个tsquery也不是原始文本。一个tsquery包含搜索词,这些搜索词必须是已经规范化的词组,并且可以使用ANDORNOTFOLLOWED BY操作符组合多个词。有一些函数to_tsquery, plainto_tsquery, phraseto_tsquery可以把用户输入的文本转换为适当的tsquery,主要是通过规范化文本中出现的词。类似地,to_tsvector被用来解析和规范化一个文档字符串。所以在实践中,一个文本搜索匹配看起来更像这样:

SELECT to_tsvector('fat cats ate fat rats') @@ to_tsquery('fat & rat');
-- tsvector匹配了tsquery中的fat和rat
 ?column? 
----------
 t

如果写成以下形式,这个匹配就不会成功:

SELECT 'fat cats ate fat rats'::tsvector @@ to_tsquery('fat & rat');
-- 由于这里是直接输入的tsvector而不是文档,相当于处理后的结果,没有包含tsquery中的rat,所以匹配失败
 ?column? 
----------
 f

因为在这里不会发生对rats这个词的规范化处理。tsvector的元素是假定已经规范化了的lexemes,所以rats与rat不匹配。

@@操作符也支持文本输入,允许在简单情况下跳过文本字符串到tsvectortsquery的显式转换。可用的变体是:

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

其中前两个我们已经在前文看到了。text @@ tsquery等同于to_tsvector(x) @@ ytext @@ text等同于to_tsvector(x) @@ plainto_tsquery(y)

在一个tsquery中,&(AND)运算符指定它的两个参数必须同时出现在文档中才能匹配。同样,|(OR)运算符指定它的参数中至少有一个必须出现,而!(NOT)运算符指定它的参数必须不出现才能匹配。例如,查询fat & ! rat匹配包含fat但不包含rat的文档:

tsquery<->(FOLLOWED BY)操作符帮助下可以搜索短语,该操作符只在其参数相邻且符合给定顺序时才匹配。例如:

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

SELECT to_tsvector('error is not fatal') @@ to_tsquery('fatal <-> error');
-- tsquery中的fatal和error在tsvector中不相邻且顺序错误,返回false
 ?column? 
----------
 f

FOLLOWED BY操作符有一个更通用的版本,其形式为<N>,其中N是一个整数,代表匹配lexemes的位置之差。<1><->相同,而<2>允许在匹配词组之间正好出现一个其他词组,以此类推。phraseto_tsquery函数利用这个操作符来构造一个tsquery,当一些词是停顿词时,它可以匹配一个多字短语。例如:

SELECT phraseto_tsquery('cats ate rats');
-- cat、ate、rat都相邻
       phraseto_tsquery        
-------------------------------
 'cat' <-> 'ate' <-> 'rat'

SELECT phraseto_tsquery('the cats ate the rats');
-- cat和ate相邻,但ate和rat的距离为2,中间有一个其他词
       phraseto_tsquery        
-------------------------------
 'cat' <-> 'ate' <2> 'rat'

一个有时很有用的特殊情况是,<0>可以用来要求两个模式匹配同一个词。

圆括号可以用来控制tsquery操作符的嵌套。如果没有括号,优先级从低到高依次为:~<->&|

值得注意的是,当AND/OR/NOT操作符在FOLLOWED BY操作符的参数范围内时,它们的含义有细微的不同,因为在FOLLOWED BY中匹配的确切位置是重要的。例如,通常情况下!x只匹配不包含x的文档。但是!x <-> y匹配的是不紧跟在x之后的y,在文档的其他地方出现的x并不妨碍匹配。另一个例子是,x & y通常只要求x和y都出现在文档的某个地方,但是(x & y) <-> z要求x和y在同一个地方匹配且紧接在z之前。因此这个查询的行为与x <-> z & y <-> z不同,它将匹配包含两个独立序列x z和y z的文档。(这个具体的查询在书写上是无用的,因为x和y不可能在同一个地方匹配;但在更复杂的情况下,如前缀匹配模式,这种形式的查询可能是有用的。)

文本搜索配置

以上都是简单的文本搜索例子。如前所述,全文搜索功能包括做更多事情的能力:跳过对某些词的索引(停顿词),处理同义词,以及使用复杂的解析,例如不仅基于词间的空格来解析。这种功能是由文本搜索配置控制的。PostgreSQL为许多语言提供了预定义的配置,也可以很容易地创建自己的配置。(psql的\dF命令显示所有可用的配置)。

在安装过程中会选择了一个合适的配置,并且在postgresql.conf中相应地设置了default_text_search_config。如果在整个集群中使用相同的文本搜索配置,可以使用postgresql.conf中的值。要在整个集群中使用不同的配置,但在任何一个数据库中使用相同的配置,则需使用ALTER DATABASE ... SET。或者可以在每个会话中设置一次default_text_search_config

每个依赖于配置的文本搜索功能都有一个可选的regconfig参数,这样就可以明确指定要使用的配置。 default_text_search_config只在省略该参数时使用。

为了更容易建立自定义文本搜索配置,一个配置是由更简单的数据库对象建立起来的。PostgreSQL的文本搜索提供了四种与配置有关的数据库对象:

  • 文本搜索分析器将文件分解为token,并对每个token进行分类(例如分为单词或数字)。
  • 文本搜索词典将token转换为规范化的形式,并拒绝接受停顿词。
  • 文本搜索模板提供了词典的基本功能。(一个词典简单地指定了一个模板和模板的一组参数)。
  • 文本搜索配置选择一个分析器和一组词典,用于规范化分析器产生的token。

文本搜索解析器和模板是由低级别的C函数构建的;因此需要用C语言来开发新的解析器,并且需要超级用户权限来安装到一个数据库。(在PostgreSQL发行版的contrib/目录有一些附加解析器和模板的例子)。由于词典和配置只是将一些底层的分析器和模板参数化并连接在一起,所以不需要特殊权限来创建一个新的词典或配置。创建自定义词典和配置的例子在本章后面出现。

Last Updated:
Contributors: yuesong-feng