受支持版本: 当前版本 (18) / 17 / 16 / 15 / 14
开发版本: devel

Chapter 12. 全文搜索

12.1. 介绍 #

全文搜索(或简称文本搜索)提供了识别满足 查询条件的自然语言文档的能力, 并且可按它们与查询的相关度进行排序。最常见的搜索类型,是找出所有包含给定 查询词的文档,并按它们与查询的相似性 排序返回。查询相似性的概念都很灵活, 取决于具体应用。最简单的搜索把查询视为一组词,把 相似性视为查询词在文档中的出现频率。

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

  • 缺少语言学支持,即便对英语也是如此。正则表达式并不足够,因为它们难以轻松 处理词形变化,例如 satisfiessatisfy。 搜索 satisfy 时,你大概也希望找到包含 satisfies 的文档,但实际上可能会漏掉它们。虽然可以用 OR 搜索多个派生形式,但这样既繁琐又容易出错 (有些词甚至可能有数千种派生形式)。

  • 它们不会对搜索结果排序(排名),因此当找到成千上万条匹配文档时就难以实用。

  • 它们往往很慢,因为没有索引支持,所以每次搜索都必须处理全部文档。

全文索引允许先对文档进行预处理,并保存索引以供后续快速搜索。 预处理包括:

  • 将文档解析成词元。识别出不同类别的词元 很有帮助,例如数字、单词、复合词、电子邮件地址等,这样就可以分别处理。 原则上,词元类别取决于具体应用,但在大多数场景中,一套预定义的类别已经足够。 PostgreSQL使用解析器执行这一步。 系统提供了标准解析器,也可以根据特定需求创建自定义解析器。

  • 将词元转换成词位。词位和词元一样都是字符串, 但它已经过正规化,使同一个词的不同形式归为一致。例如, 正规化几乎总是包含把大写字母折叠为小写,也常常会去掉后缀 (如英语中的 ses)。这样,搜索时 无需繁琐地输入所有可能的变体,就能匹配同一个词的不同形式。此外,这一步通常 还会去除停用词,也就是那些过于常见、对搜索没有帮助的词。 (简言之,词元是文档文本的原始片段,而词位则是被认为适合用于索引和搜索的词。) PostgreSQL使用词典执行这一步。 系统提供了多种标准词典,也可以按需创建自定义词典。

  • 以适合搜索的形式存储预处理后的文档。例如,每个文档都可以表示为 由正规化词位组成的有序数组。除词位之外,通常还希望存储位置信息,以便用于 邻近排名;这样,查询词分布更密集的文档 会比查询词分散出现的文档得到更高的排名。

词典允许对词元的正规化方式进行细粒度控制。借助合适的词典,你可以:

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

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

  • 使用分类词典把短语映射为同一个词。

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

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

系统提供了数据类型 tsvector 用于存储预处理后的文档,也提供了 数据类型 tsquery 用于表示处理过的查询 (Section 8.11)。围绕这两种数据类型还有许多函数和 操作符(Section 9.13),其中最重要的是匹配操作符 @@,我们将在Section 12.1.2中介绍。 全文搜索还可以借助索引加速(Section 12.9)。

12.1.1. 什么是文档? #

文档是全文搜索系统中的搜索单位,例如一篇杂志文章或一封电子邮件。文本搜索引擎必须能够解析文档,并保存词位(关键字)与其所属文档之间的关联。随后,就可以利用这些关联来搜索包含查询词的文档。

PostgreSQL 中进行搜索时,文档通常是数据库表某一行中的一个文本字段,或者是这类字段的组合(串接),这些字段可能分布在多个表里,也可能是动态取得的。换句话说,文档可以由不同部分拼接而成以供索引,而且未必会以一个整体存储在任何地方。例如:

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;

Note

实际上,这些示例查询中应使用 coalesce,以避免某个单独的 NULL 属性导致整个文档结果变成 NULL

另一种做法是把文档存储为文件系统中的普通文本文件。在这种情况下,数据库可以用于保存全文索引并执行搜索,而文档本身则借助某个唯一标识符从文件系统中取回。不过,从数据库外部检索文件需要超级用户权限或特殊函数支持,因此这种方法通常不如把所有数据都保存在 PostgreSQL 内部方便。此外,把所有内容都放在数据库中,也便于访问文档元数据来辅助索引和展示。

为了进行文本搜索,每个文档都必须被化简为预处理后的 tsvector 格式。搜索和排名完全基于文档的 tsvector 表示来执行 — 只有当文档被选中并准备展示给用户时,才需要取回原始文本。因此,我们常常把 tsvector 直接称作文档,但它当然只是完整文档的一种紧凑表示。

12.1.2. 基本文本匹配 #

PostgreSQL中,全文搜索基于匹配操作符@@。如果一个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 操作符把多个术语组合起来。(语法细节见Section 8.11.2。)to_tsqueryplainto_tsqueryphraseto_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) @@ ytext @@ 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 的文档;但 !x <-> y 会在 y 前面没有紧邻一个 x 时匹配,文档其他位置出现的 x 并不会阻止匹配。再例如,x & y 通常只要求 xy 都在文档某处出现,而 (x & y) <-> z 则要求 xy 在同一位置匹配,并且紧挨在 z 之前。因此,这个查询的行为不同于 x <-> z & y <-> z,后者会匹配同时包含两个独立序列 x zy z 的文档。(按字面写出时,这个特定查询其实没有什么用,因为 xy 不可能在同一位置匹配;但在更复杂的场景中,例如前缀匹配模式,这种形式就可能有用。)

12.1.3. 配置 #

前述的都是简单的文本搜索示例。正如前面所提到的,全文搜索功能包括做更多事情的能力:跳过索引特定词(停用词)、处理同义词并使用更高级的解析,例如基于空白之外的解析。这个功能由文本搜索配置控制。PostgreSQL中有多种语言的预定义配置,并且你可以很容易地创建你自己的配置(psql\dF命令显示所有可用的配置)。

在安装期间会选择一个合适的配置,并据此在postgresql.conf中设置default_text_search_config。如果整个集簇都使用同一种文本搜索配置,你可以直接使用postgresql.conf中的这个值。若要在整个集簇中使用不同配置,但保证每个数据库内部使用同一种配置,可以使用ALTER DATABASE ... SET。否则,你也可以在每个会话中设置default_text_search_config

依赖一个配置的每一个文本搜索函数都有一个可选的regconfig参数,因此要使用的配置可以被显式指定。只有当这个参数被忽略时,default_text_search_config才被使用。

为了让建立自定义文本搜索配置更容易,一个配置可以从更简单的数据库对象来建立。PostgreSQL的文本搜索功能提供了四类配置相关的数据库对象:

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

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

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

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

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

提交更正

如果您发现文档中有不正确的内容、与您使用特定功能的经验不符或需要进一步说明,请使用此表单来报告文档问题。