12.3. 控制文本搜索#
要实现全文搜索,必须有一个函数,可以从文档中创建一个tsvector
,从用户查询中创建一个tsquery
。此外,我们需要按有用顺序返回结果,因此我们需要一个函数来比较文档与查询的相关性。能够很好地显示结果也很重要。PostgreSQL为所有这些函数提供支持。
12.3.1. 解析文档#
PostgreSQL提供函数to_tsvector
,用于将文档转换为tsvector
数据类型。
to_tsvector([ config regconfig, ] document text) returns tsvector
to_tsvector
将文本文档解析为标记,将标记简化为词素,并返回一个tsvector
,其中列出了词素及其在文档中的位置。文档根据指定或默认的文本搜索配置进行处理。这是一个简单的示例
SELECT to_tsvector('english', 'a fat cat sat on a mat - it ate a fat rats');
to_tsvector
-----------------------------------------------------
'ate':9 'cat':3 'fat':2,11 'mat':7 'rat':12 'sat':4
在上面的示例中,我们看到生成的tsvector
不包含单词a
、on
或it
,单词rats
变成rat
,并且忽略了标点符号-
。
函数to_tsvector
在内部调用一个解析器,该解析器将文档文本分解为标记,并为每个标记分配一个类型。对于每个标记,都会查阅字典列表(第 12.6 节),其中列表可以根据标记类型而有所不同。第一个识别标记的字典会发出一个或多个已规范化的词素来表示标记。例如,rats
变成了rat
,因为其中一个字典识别出单词rats
是rat
的复数形式。一些单词被识别为停用词(第 12.6.1 节),这会导致它们被忽略,因为它们出现的频率太高,无法用于搜索。在我们的示例中,这些是a
、on
和it
。如果列表中的任何字典都无法识别标记,那么该标记也会被忽略。在此示例中,标点符号-
发生了这种情况,因为实际上没有为其标记类型(空格符号
)分配任何字典,这意味着空格标记永远不会被编入索引。解析器、字典和要编入索引的标记类型由所选文本搜索配置决定(第 12.7 节)。同一个数据库中可以有多种不同的配置,并且为各种语言提供了预定义的配置。在我们的示例中,我们对英语语言使用了默认配置english
。
函数setweight
可用于使用给定的权重标记tsvector
的条目,其中权重是字母A
、B
、C
或D
之一。这通常用于标记来自文档不同部分的条目,例如标题与正文。稍后,此信息可用于对搜索结果进行排名。
由于to_tsvector
(NULL
) 会返回NULL
,因此建议在字段可能为 null 时使用coalesce
。以下是根据结构化文档创建tsvector
的推荐方法
UPDATE tt SET ti =
setweight(to_tsvector(coalesce(title,'')), 'A') ||
setweight(to_tsvector(coalesce(keyword,'')), 'B') ||
setweight(to_tsvector(coalesce(abstract,'')), 'C') ||
setweight(to_tsvector(coalesce(body,'')), 'D');
此处我们使用setweight
标记完成的tsvector
中每个词素的来源,然后使用tsvector
连接运算符||
合并标记的tsvector
值。(第 12.4.1 节详细介绍了这些操作。)
12.3.2. 解析查询#
PostgreSQL提供函数to_tsquery
、plainto_tsquery
、phraseto_tsquery
和websearch_to_tsquery
,用于将查询转换为tsquery
数据类型。与plainto_tsquery
或phraseto_tsquery
相比,to_tsquery
提供了更多功能,但对输入要求更严格。websearch_to_tsquery
是to_tsquery
的简化版本,具有替代语法,类似于 Web 搜索引擎使用的语法。
to_tsquery([ config regconfig, ] querytext text) returns tsquery
to_tsquery
从*querytext
*创建一个tsquery
值,该值必须由单个标记组成,这些标记由tsquery
运算符&
(AND)、|
(OR)、!
(NOT)和<->
(FOLLOWED BY)分隔,可能使用括号进行分组。换句话说,to_tsquery
的输入必须已经遵循tsquery
输入的一般规则,如第 8.11.2 节中所述。不同之处在于,虽然基本的tsquery
输入按字面意思获取标记,但to_tsquery
使用指定或默认配置将每个标记规范化为词素,并根据配置丢弃任何停用词标记。例如
SELECT to_tsquery('english', 'The & Fat & Rats');
to_tsquery
---------------
'fat' & 'rat'
与基本tsquery
输入一样,可以将权重附加到每个词素,以将其限制为仅匹配这些权重的tsvector
词素。例如
SELECT to_tsquery('english', 'Fat | Rats:AB');
to_tsquery
------------------
'fat' | 'rat':AB
此外,可以将*
附加到词素以指定前缀匹配
SELECT to_tsquery('supern:*A & star:A*B');
to_tsquery
--------------------------
'supern':*A & 'star':*AB
此类词素将匹配tsvector
中以给定字符串开头的任何单词。
to_tsquery
还可以接受带单引号的短语。当配置包含可能触发此类短语的同义词词典时,这主要很有用。在以下示例中,同义词词典包含规则supernovae stars : sn
SELECT to_tsquery('''supernovae stars'' & !crab');
to_tsquery
---------------
'sn' & !'crab'
如果没有引号,to_tsquery
将为未由 AND、OR 或 FOLLOWED BY 运算符分隔的标记生成语法错误。
plainto_tsquery([ config regconfig, ] querytext text) returns tsquery
plainto_tsquery
将未格式化的文本*querytext
*转换为tsquery
值。文本的解析和规范化与to_tsvector
非常相似,然后在保留的单词之间插入&
(AND)tsquery
运算符。
示例
SELECT plainto_tsquery('english', 'The Fat Rats');
plainto_tsquery
-----------------
'fat' & 'rat'
请注意,plainto_tsquery
不会在其输入中识别tsquery
运算符、权重标签或前缀匹配标签
SELECT plainto_tsquery('english', 'The Fat & Rats:C');
plainto_tsquery
---------------------
'fat' & 'rat' & 'c'
在此,所有输入标点符号均已丢弃。
phraseto_tsquery([ config regconfig, ] querytext text) returns tsquery
phraseto_tsquery
的行为与plainto_tsquery
非常相似,不同之处在于它在保留的单词之间插入<->
(FOLLOWED BY)运算符,而不是&
(AND)运算符。此外,停用词不会简单地丢弃,而是通过插入<*
N*>
运算符而不是<->
运算符来考虑。此函数在搜索确切词素序列时很有用,因为 FOLLOWED BY 运算符不仅检查所有词素的存在,还检查词素的顺序。
示例
SELECT phraseto_tsquery('english', 'The Fat Rats');
phraseto_tsquery
------------------
'fat' <-> 'rat'
与plainto_tsquery
类似,phraseto_tsquery
函数不会在其输入中识别tsquery
运算符、权重标签或前缀匹配标签
SELECT phraseto_tsquery('english', 'The Fat & Rats:C');
phraseto_tsquery
-----------------------------
'fat' <-> 'rat' <-> 'c'
websearch_to_tsquery([ config regconfig, ] querytext text) returns tsquery
websearch_to_tsquery
使用替代语法从*querytext
*创建tsquery
值,其中简单的未格式化文本是有效的查询。与plainto_tsquery
和phraseto_tsquery
不同,它还识别某些运算符。此外,此函数永远不会引发语法错误,这使得可以将原始用户提供的输入用于搜索。支持以下语法
未加引号的文本
:不在引号内的文本将转换为由&
运算符分隔的术语,就像由plainto_tsquery
处理一样。"带引号的文本"
:引号内的文本将转换为由<->
运算符分隔的术语,就像由phraseto_tsquery
处理一样。OR
:单词 “or” 将转换为|
运算符。-
:破折号将转换为!
运算符。
其他标点符号将被忽略。因此,与plainto_tsquery
和phraseto_tsquery
一样,websearch_to_tsquery
函数不会在其输入中识别tsquery
运算符、权重标签或前缀匹配标签。
示例
SELECT websearch_to_tsquery('english', 'The fat rats');
websearch_to_tsquery
----------------------
'fat' & 'rat'
(1 row)
SELECT websearch_to_tsquery('english', '"supernovae stars" -crab');
websearch_to_tsquery
----------------------------------
'supernova' <-> 'star' & !'crab'
(1 row)
SELECT websearch_to_tsquery('english', '"sad cat" or "fat rat"');
websearch_to_tsquery
-----------------------------------
'sad' <-> 'cat' | 'fat' <-> 'rat'
(1 row)
SELECT websearch_to_tsquery('english', 'signal -"segmentation fault"');
websearch_to_tsquery
---------------------------------------
'signal' & !( 'segment' <-> 'fault' )
(1 row)
SELECT websearch_to_tsquery('english', '""" )( dummy \\ query <->');
websearch_to_tsquery
----------------------
'dummi' & 'queri'
(1 row)
12.3.3. 搜索结果排名#
排名尝试衡量文档与特定查询的相关性,以便在有多个匹配项时可以首先显示最相关的匹配项。PostgreSQL提供了两个预定义的排名函数,它们考虑了词法、邻近和结构信息;也就是说,它们考虑了查询词在文档中出现的频率、词在文档中的接近程度以及它们出现的文档部分的重要性。但是,相关性的概念很模糊,并且非常特定于应用程序。不同的应用程序可能需要额外的信息进行排名,例如文档修改时间。内置排名函数只是示例。您可以编写自己的排名函数和/或将其结果与其他因素结合起来以满足您的特定需求。
当前可用的两个排名函数是
-
ts_rank([
weights
float4[]
, ]vector
tsvector
,query
tsquery
[,normalization
integer
]) returnsfloat4
根据其匹配词素的频率对向量进行排名。
-
ts_rank_cd([
weights
float4[]
, ]vector
tsvector
,query
tsquery
[,normalization
integer
]) returnsfloat4
此函数计算给定文档向量和查询的覆盖密度排名,如 Clarke、Cormack 和 Tudhope 在期刊“信息处理与管理”1999 年的“一到三个术语查询的相关性排名”中所述。覆盖密度类似于
ts_rank
排名,不同之处在于考虑了匹配词素彼此的邻近性。此函数需要词素位置信息来执行其计算。因此,它会忽略
tsvector
中的任何“stripped”词素。如果输入中没有未剥离的词素,则结果将为零。(有关strip
函数和tsvector
中的位置信息的详细信息,请参见第 12.4.1 节。)
对于这两个函数,可选的*weights
*参数提供了根据单词的标记方式对单词实例进行更多或更少加权的能力。权重数组指定按顺序对每类单词加权的程度
{D-weight, C-weight, B-weight, A-weight}
如果没有提供*weights
*,则使用以下默认值
{0.1, 0.2, 0.4, 1.0}
通常,权重用于标记文档中特殊区域的单词,如标题或初始摘要,以便可以比文档正文中的单词赋予其更多或更少的权重。
由于较长的文档包含查询词条的可能性更大,因此考虑文档大小是合理的,例如,包含搜索词条五个实例的百字文档可能比包含搜索词条五个实例的千字文档更相关。两种排名函数都采用一个整数*normalization
*选项,该选项指定文档的长度是否以及如何影响其排名。整数选项控制多种行为,因此它是一个位掩码:你可以使用|
指定一个或多个行为(例如,2|4
)。
0(默认值)忽略文档长度
1 将排名除以 1 + 文档长度的对数
2 将排名除以文档长度
4 将排名除以范围之间的平均调和距离(这仅由
ts_rank_cd
实现)8 将排名除以文档中唯一单词的数量
16 将排名除以 1 + 文档中唯一单词数量的对数
32 将排名除以其自身 + 1
如果指定了多个标志位,则按列出的顺序应用转换。
需要注意的是,排名函数不使用任何全局信息,因此不可能按有时所期望的那样生成到 1% 或 100% 的公平归一化。归一化选项 32 (rank/(rank+1)
) 可用于将所有排名缩放到零到一范围,但当然这只是表面上的更改;它不会影响搜索结果的排序。
这是一个仅选择排名最高的十个匹配项的示例
SELECT title, ts_rank_cd(textsearch, query) AS rank
FROM apod, to_tsquery('neutrino|(dark & matter)') query
WHERE query @@ textsearch
ORDER BY rank DESC
LIMIT 10;
title | rank
-----------------------------------------------+----------
Neutrinos in the Sun | 3.1
The Sudbury Neutrino Detector | 2.4
A MACHO View of Galactic Dark Matter | 2.01317
Hot Gas and Dark Matter | 1.91171
The Virgo Cluster: Hot Plasma and Dark Matter | 1.90953
Rafting for Solar Neutrinos | 1.9
NGC 4650A: Strange Galaxy and Dark Matter | 1.85774
Hot Gas and Dark Matter | 1.6123
Ice Fishing for Cosmic Neutrinos | 1.6
Weak Lensing Distorts the Universe | 0.818218
这是使用归一化排名的相同示例
SELECT title, ts_rank_cd(textsearch, query, 32 /* rank/(rank+1) */ ) AS rank
FROM apod, to_tsquery('neutrino|(dark & matter)') query
WHERE query @@ textsearch
ORDER BY rank DESC
LIMIT 10;
title | rank
-----------------------------------------------+-------------------
Neutrinos in the Sun | 0.756097569485493
The Sudbury Neutrino Detector | 0.705882361190954
A MACHO View of Galactic Dark Matter | 0.668123210574724
Hot Gas and Dark Matter | 0.65655958650282
The Virgo Cluster: Hot Plasma and Dark Matter | 0.656301290640973
Rafting for Solar Neutrinos | 0.655172410958162
NGC 4650A: Strange Galaxy and Dark Matter | 0.650072921219637
Hot Gas and Dark Matter | 0.617195790024749
Ice Fishing for Cosmic Neutrinos | 0.615384618911517
Weak Lensing Distorts the Universe | 0.450010798361481
排名可能很昂贵,因为它需要查询每个匹配文档的tsvector
,这可能会受到 I/O 限制,因此很慢。不幸的是,几乎不可能避免,因为实际查询通常会导致大量的匹配项。
12.3.4. 突出显示结果#
为了呈现搜索结果,理想的做法是显示每个文档的一部分以及它与查询的关系。通常,搜索引擎会显示带有标记搜索词条的文档片段。PostgreSQL提供了一个实现此功能的函数ts_headline
。
ts_headline([ config regconfig, ] document text, query tsquery [, options text ]) returns text
ts_headline
接受一个文档和一个查询,并返回一个文档摘录,其中查询中的词条被突出显示。具体来说,该函数将使用查询选择相关的文本片段,然后突出显示出现在查询中的所有单词,即使这些单词位置不符合查询的限制。用于解析文档的配置可以通过*config
指定;如果省略config
*,则使用default_text_search_config
配置。
如果指定了*options
字符串,它必须由一个或多个option
=
value
*对的逗号分隔列表组成。可用选项为
MaxWords
、MinWords
(整数):这些数字决定了输出的最长和最短标题。默认值为 35 和 15。ShortWord
(整数):长度小于或等于此值的单词将从标题的开头和结尾处删除,除非它们是查询词。默认值为 3,可消除常见的英语冠词。HighlightAll
(布尔值):如果为true
,则整个文档将用作标题,忽略前三个参数。默认值为false
。MaxFragments
(整数):要显示的文本片段的最大数量。默认值为 0,选择基于非片段的标题生成方法。大于 0 的值选择基于片段的标题生成(见下文)。StartSel
、StopSel
(字符串):用于分隔出现在文档中的查询词的字符串,以将它们与其他摘录的单词区分开来。默认值为 “<b>
” 和 “</b>
”,这适用于 HTML 输出。FragmentDelimiter
(字符串):当显示多个片段时,这些片段将由该字符串分隔。默认值为 “...
”。
这些选项名称不区分大小写。如果字符串值包含空格或逗号,则必须用双引号引起来。
在基于非片段的标题生成中,ts_headline
找到给定*query
*的匹配项,并选择一个进行显示,优先选择在允许的标题长度内具有更多查询词的匹配项。在基于片段的标题生成中,ts_headline
找到查询匹配项,并将每个匹配项拆分为不超过MaxWords
个单词的“片段”,优先选择具有更多查询词的片段,并在可能的情况下“延伸”片段以包括周围的单词。因此,当查询匹配项跨越文档的大部分内容或希望显示多个匹配项时,基于片段的模式更有用。在任一模式中,如果无法识别查询匹配项,则将显示文档中前MinWords
个单词的单个片段。
例如
SELECT ts_headline('english',
'The most common type of search
is to find all documents containing given query terms
and return them in order of their similarity to the
query.',
to_tsquery('english', 'query & similarity'));
ts_headline
------------------------------------------------------------
containing given <b>query</b> terms +
and return them in order of their <b>similarity</b> to the+
<b>query</b>.
SELECT ts_headline('english',
'Search terms may occur
many times in a document,
requiring ranking of the search matches to decide which
occurrences to display in the result.',
to_tsquery('english', 'search & term'),
'MaxFragments=10, MaxWords=7, MinWords=3, StartSel=<<, StopSel=>>');
ts_headline
------------------------------------------------------------
<<Search>> <<terms>> may occur +
many times ... ranking of the <<search>> matches to decide
ts_headline
使用原始文档,而不是tsvector
摘要,因此它可能会很慢,应谨慎使用。