12.1. 简介#
全文搜索(或简称文本搜索)提供了识别满足查询的自然语言文档的功能,并可以选择按与查询的相关性对它们进行排序。最常见的搜索类型是查找包含给定查询词的所有文档,并按其与查询的相似性进行排序。query
和similarity
的概念非常灵活,具体取决于特定应用程序。最简单的搜索将query
视为一组单词,将similarity
视为查询单词在文档中的频率。
文本搜索运算符已在数据库中存在多年。PostgreSQL针对文本数据类型具有~
、~*
、LIKE
和ILIKE
运算符,但它们缺少现代信息系统所需的许多基本属性
即使对于英语,也没有语言支持。正则表达式不够用,因为它们无法轻松处理派生词,例如
satisfies
和satisfy
。您可能会错过包含satisfies
的文档,即使您在搜索satisfy
时可能希望找到它们。可以使用OR
来搜索多个派生形式,但这很繁琐且容易出错(某些单词可能有多达数千个派生词)。它们不提供搜索结果的排序(排名),当找到数千个匹配文档时,这会使它们无效。
它们往往很慢,因为没有索引支持,因此它们必须针对每次搜索处理所有文档。
全文索引允许对文档进行预处理,并保存索引以供以后快速搜索。预处理包括
将文档解析成 标记。识别各种标记类别很有用,例如数字、单词、复杂单词、电子邮件地址,以便可以对它们进行不同的处理。原则上,标记类别取决于特定应用程序,但对于大多数目的,使用一组预定义的类别就足够了。 PostgreSQL 使用 解析器 来执行此步骤。提供了标准解析器,并且可以针对特定需求创建自定义解析器。
将标记转换为 词素。词素是一个字符串,就像标记一样,但它已被 规范化,以便同一个单词的不同形式变得相同。例如,规范化几乎总是包括将大写字母折叠为小写字母,并且通常涉及删除后缀(例如英语中的
s
或es
)。这允许搜索找到同一个单词的不同形式,而无需繁琐地输入所有可能的变体。此外,此步骤通常会消除 停用词,这些词非常常见,以至于对搜索毫无用处。(简而言之,标记是文档文本的原始片段,而词素被认为对索引和搜索有用。) PostgreSQL 使用 词典 来执行此步骤。提供了各种标准词典,并且可以针对特定需求创建自定义词典。存储经过预处理的文档,以优化搜索。例如,每个文档都可以表示为一个经过排序的规范化词素数组。除了词素之外,通常还需要存储位置信息以用于 邻近排名,以便包含更多 “密集” 查询词区域的文档比具有分散查询词的文档分配更高的排名。
词典允许对标记的规范化方式进行细粒度控制。使用适当的词典,您可以
定义不应编入索引的停用词。
使用 Ispell 将同义词映射到一个单词。
使用同义词库将短语映射到一个单词。
使用 Ispell 词典将单词的不同变体映射到规范形式。
使用 Snowball 词干规则将单词的不同变体映射到规范形式。
提供数据类型tsvector
用于存储预处理文档,以及类型tsquery
用于表示已处理查询(第 8.11 节)。这些数据类型提供了许多函数和运算符(第 9.13 节),其中最重要的一个就是匹配运算符@@
,我们在第 12.1.2 节中介绍了它。可以使用索引加速全文搜索(第 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;
注意
实际上,在这些示例查询中,应使用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 运算符组合多个词。(有关语法详细信息,请参见第 8.11.2 节。)有函数to_tsquery
、plainto_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
输入,允许在简单情况下跳过将文本字符串显式转换为tsvector
或tsquery
。可用的变体有
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
的文档。但!x <-> y
匹配y
,如果它不紧跟在x
之后;文档中其他位置出现的x
不会阻止匹配。另一个示例是x & y
通常仅要求x
和y
都出现在文档中的某个位置,但(x & y) <-> z
要求x
和y
在同一位置匹配,紧跟在z
之前。因此,此查询的行为与x <-> z & y <-> z
不同,后者将匹配包含两个单独序列x z
和y z
的文档。(此特定查询按原样编写时毫无用处,因为x
和y
无法在同一位置匹配;但在诸如前缀匹配模式等更复杂的情况下,此类形式的查询可能有用。)
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/
区域中有一些附加解析器和模板的示例。)由于词典和配置只是对一些基础解析器和模板进行参数化和连接,因此无需特殊权限即可创建新词典或配置。创建自定义词典和配置的示例将在本章后面出现。