PostgreSQL 如何做全文搜索?
文本搜索操作符已经在数据库中存在很多年了。PostgreSQL对文本数据类型提供了~、~*、LIKE和ILIKE操作符,但是它们缺少现代信息系统所要求的很多基本属性:
- 即使对英语也缺乏语言的支持。正则表达式是不够的,因为它们不能很容易地处理派生词,例如satisfies和satisfy。你可能会错过包含satisfies的文档,尽管你可能想要在对于satisfy的搜索中找到它们。可以使用OR来搜索多个派生形式,但是这样做太罗嗦也容易出错(有些词可能有数千种派生)。
- 它们不提供对搜索结果的排序(排名),这使它们面对数以千计被找到的文档时变得无效。
- 它们很慢因为没有索引支持,因此它们必须为每次搜索处理所有的文档。
全文索引允许文档被预处理并且保存一个索引用于以后快速的搜索。预处理包括:
- 将文档解析成记号。标识出多种类型的记号是有所帮助的,例如数字、词、复杂的词、电子邮件地址,这样它们可以被以不同的方式处理。原则上记号分类取决于相关的应用,但是对于大部分目的都可以使用一套预定义的分类。PostgreSQL使用一个解析器来执行这个步骤。其中提供了一个标准的解析器,并且为特定的需要也可以创建定制的解析器。
- 将记号转换成词位。和一个记号一样,一个词位是一个字符串,但是它已经被正规化,这样同一个词的不同形式被变成一样。例如,正规化几乎总是包括将大写字母转换成小写形式,并且经常涉及移除后缀(例如英语中的s或es)。这允许搜索找到同一个词的变体形式,而不需要冗长地输入所有可能的变体。此外,这个步骤通常会消除停用词,它们是那些太普通的词,它们对于搜索是无用的(简而言之,记号是文档文本的原始片段,而词位是那些被认为对索引和搜索有用的词)。PostgreSQL使用词典来执行这个步骤。已经提供了多种标准词典,并且为特定的需要也可以创建定制的词典。
- 为搜索优化存储预处理好的文档。例如,每一个文档可以被表示为正规化的词位的一个有序数组。与词位一起,通常还想要存储用于近似排名的位置信息,这样一个包含查询词更“密集”区域的文档要比那些包含分散的查询词的文档有更高的排名。
词典允许对记号如何被正规化进行细粒度的控制。使用合适的词典,你可以:
- 定义不应该被索引的停用词。
- 使用Ispell把同义词映射到一个单一词。
- 使用一个分类词典把短语映射到一个单一词。
- 使用一个Ispell词典把一个词的不同变体映射到一种规范的形式。
- 使用Snowball词干分析器规则将一个词的不同变体映射到一种规范的形式。
我们提供了一种数据类型tsvector来存储预处理后的文档,还提供了一种类型tsquery来表示处理过的查询。有很多函数和操作符可以用于这些数据类型,其中最重要的是匹配操作符@@。全文搜索可以使用索引来加速。
什么是一个文档?
一个document是在一个全文搜索系统中进行搜索的单元,例如,一篇杂志文章或电子邮件消息。文本搜索引擎必须能够解析文档并存储词位(关键词)与它们的父文档之间的关联。随后,这些关联会被用来搜索包含查询词的文档。
对于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 mid = did AND mid = 12;
注意
实际上在这些例子查询中,coalesce应该被用来防止一个单一NULL属性导致整个文档的一个NULL结果。
另一种存储文档的可能性是作为文件系统中的简单文本文件。在这种情况下,数据库可以被用来存储全文索引并执行搜索,并且某些唯一标识符可以被用来从文件系统检索文档。但是,从数据库的外面检索文件要求超级用户权限或者特殊函数支持,因此这种方法通常不如把所有数据放在PostgreSQL内部方便。另外,把所有东西放在数据库内部允许方便地访问文档元数据来协助索引和现实。
对于文本搜索目的,每一个文档必须被缩减成预处理后的tsvector格式。搜索和排名被整个在一个文档的tsvector表示上执行 — 只有当文档被选择来显示给用户时才需要检索原始文本。我们因此经常把tsvector说成是文档,但是当然它只是完整文档的一种紧凑表示。
基本文本匹配
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 操作符结合多个术语。有几个函数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的文档。但如果y不是紧接在一个x后面,!x <-> y就会匹配那个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命令显示所有可用的配置)。
在安装期间一个合适的配置将被选择并且default_text_search_config也被相应地设置在postgresql.conf中。如果你正在对整个集簇使用相同的文本搜索配置,你可以使用在postgresql.conf中使用该值。要在集簇中使用不同的配置但是在任何一个数据库内部使用同一种配置,使用ALTER DATABASE ... SET。否则,你可以在每个会话中设置default_text_search_config。
依赖一个配置的每一个文本搜索函数都有一个可选的regconfig参数,因此要使用的配置可以被显式指定。只有当这个参数被忽略时,default_text_search_config才被使用。
为了让建立自定义文本搜索配置更容易,一个配置可以从更简单的数据库对象来建立。PostgreSQL的文本搜索功能提供了四类配置相关的数据库对象:
- 文本搜索解析器将文档拆分成记号并分类每个记号(例如,作为词或者数字)。
- 文本搜索词典将记号转变成正规化的形式并拒绝停用词。
- 文本搜索模板提供位于词典底层的函数(一个词典简单地指定一个模板和一组用于模板的参数)。
- 文本搜索配置选择一个解析器和一组用于将解析器产生的记号正规化的词典。
文本搜索解析器和模板是从低层 C 函数构建而来,因此它要求 C 编程能力来开发新的解析器和模板,并且还需要超级用户权限来把它们安装到一个数据库中(在PostgreSQL发布的contrib/区域中有一些附加的解析器和模板的例子)。由于词典和配置只是对底层解析器和模板的参数化和连接,不需要特殊的权限来创建一个新词典或配置。创建定制词典和配置的例子将在本章稍后的部分给出。
搜索一个表
可以在没有一个索引的情况下做一次全文搜索。一个简单的查询将打印每一个行的title,这些行在其body域中包含词friend:
SELECT title
FROM pgweb
WHERE to_tsvector('english', body) @@ to_tsquery('english', 'friend');
这将还会找到相关的词例如friends和friendly,因为这些都被约减到同一个正规化的词位。
以上的查询指定要使用english配置来解析和正规化字符串。我们也可以忽略配置参数:
SELECT title
FROM pgweb
WHERE to_tsvector(body) @@ to_tsquery('friend');
这个查询将使用由default_text_search_config设置的配置。
一个更复杂的例子是选择 10 个最近的文档,要求它们在title或body中包含create和table:
SELECT title
FROM pgweb
WHERE to_tsvector(title || ' ' || body) @@ to_tsquery('create & table')
ORDER BY last_mod_date DESC
LIMIT 10;
为了清晰,我们忽略coalesce函数调用,它可能需要被用来查找在这两个域之中包含NULL的行。
尽管这些查询可以在没有索引的情况下工作,大部分应用会发现这种方法太慢了,除了偶尔的临时搜索。实际使用文本搜索通常要求创建一个索引。
创建索引
我们可以创建一个GIN索引来加速文本搜索:
CREATE INDEX pgweb_idx ON pgweb USING GIN(to_tsvector('english', body));
注意这里使用了to_tsvector的双参数版本。只有指定了一个配置名称的文本搜索函数可以被用在表达式索引中。这是因为索引内容必须是没有被default_text_search_config影响的。如果它们被影响,索引内容可能会不一致因为不同的项可能包含被使用不同文本搜索配置创建的tsvector,并且没有办法猜测哪个是哪个。也没有可能正确地转储和恢复这样的一个索引。
由于to_tsvector的双参数版本被使用在上述的索引中,只有一个使用了带有相同配置名的双参数版to_tsvector的查询引用才能使用该索引。即,WHERE to_tsvector('english', body) @@ 'a & b' 可以使用该索引,但WHERE to_tsvector(body) @@ 'a & b'不能。这保证一个索引只能和创建索引项时所用的相同配置一起使用。
可以建立更复杂的表达式索引,在其中配置名被另一个列指定,例如:
CREATE INDEX pgweb_idx ON pgweb USING GIN(to_tsvector(config_name, body));
这里config_name是pgweb表中的一个列。这允许在同一个索引中有混合配置,同时记录哪个配置被用于每一个索引项。例如,如果文档集合包含不同语言的文档,这就可能会有用。同样,要使用索引的查询必须被措辞成匹配,例如WHERE to_tsvector(config_name, body) @@ 'a & b'。
索引甚至可以连接列:
CREATE INDEX pgweb_idx ON pgweb USING GIN(to_tsvector('english', title || ' ' || body));
另一种方法是创建一个单独的tsvector列来保存to_tsvector的输出。若要使此列与其源数据保持自动更新,用存储生成的列。这个例子是title和body的连接,使用coalesce来保证当其他域为NULL时一个域仍然能留在索引中:
ALTER TABLE pgweb
ADD COLUMN textsearchable_index_col tsvector
GENERATED ALWAYS AS (to_tsvector('english', coalesce(title, '') || ' ' || coalesce(body, ''))) STORED;
然后我们创建一个GIN索引来加速搜索:
CREATE INDEX textsearch_idx ON pgweb USING GIN(textsearchable_index_col);
现在我们准备好执行一个快速的全文搜索了:
SELECT title
FROM pgweb
WHERE textsearchable_index_col @@ to_tsquery('create & table')
ORDER BY last_mod_date DESC
LIMIT 10;
单独列方法相对于表达式索引的一个优势在于,它不必为了利用索引而在查询中显式地指定文本搜索配置。如上述例子所示,查询可以依赖default_text_search_config。另一个优势是搜索将会更快,因为它不必重做to_tsvector调用来验证索引匹配(在使用 GiST 索引时这一点比使用 GIN 索引时更重要;)。表达式索引方法更容易建立,但是它要求更少的磁盘空间,因为tsvector表示没有被显式地存储下来。
解析文档
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函数在内部调用了一个解析器,它把文档文本分解成记号并且为每一种记号分配一个类型。对于每一个记号,会去查询一个词典列表,该列表会根据记号的类型而变化。第一个识别记号的词典产生一个或多个正规化的词位来表示该记号。例如,rats变成rat是因为一个词典识别到该词rats是rat的复数形式。一些词会被识别为停用词,这将导致它们被忽略,因为它们出现得太频繁以至于在搜索中起不到作用。在我们的例子中有a、on和it是停用词。如果在列表中没有词典能识别该记号,那它将也会被忽略。在这个例子中标点符号-就属于这种情况,因为事实上没有词典会给它分配记号类型(空间符号),即空间记号不会被索引。对于解析器、词典以及要索引哪些记号类型是由所选择的文本搜索配置决定的。可以在同一个数据库中有多种不同的配置,并且有用于很多种语言的预定义配置。在我们的例子中,我们使用用于英语的默认配置english。
函数setweight可以被用来对tsvector中的项标注一个给定的权重,这里一个权重可以是四个字母之一:A、B、C或D。这通常被用来标记来自文档不同部分的项,例如标题对正文。稍后,这种信息可以被用来排名搜索结果。
因为to_tsvector(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连接操作符||合并在一起。
解析查询
PostgreSQL提供了函数to_tsquery、plainto_tsquery、phraseto_tsquery以及websearch_to_tsquery用来把一个查询转换成tsquery数据类型。to_tsquery提供了比plainto_tsquery和phraseto_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输入的一般规则。区别在于基本的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)布尔操作符被插入到留下来的词之间。
例子:
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:逻辑或将被转换成|操作符。
- -:逻辑非操作符,被转换成!操作符。
示例:
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)
排名搜索结果
排名处理尝试度量文档和一个特定查询的接近程度,这样当有很多匹配时最相关的那些可以被先显示。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 年在期刊 "Information Processing and Management" 上的文章 "Relevance Ranking for One to Three Term Queries" 文章中有描述。覆盖密度类似于
ts_rank
排名,不过它会考虑匹配词位相互之间的接近度。这个函数要求词位的位置信息来执行其计算。因此它会忽略
tsvector
中任何“被剥离的”词位。如果在输入中有未被剥离的词位,结果将会是零。
对这两个函数,可选的权重参数提供了为词实例赋予更多或更少权重的能力,这种能力是依据它们被标注的情况的。权重数组指定每一类词应该得到多重的权重,按照如下的顺序:
{D-权重, C-权重, B-权重, A-权重}
如果没有提供权重,那么将使用这些默认值:
{0.1, 0.2, 0.4, 1.0}
通常权重被用来标记来自文档特别区域的词,如标题或一个初始的摘要,这样它们可以被认为比来自文档正文的词更重要或更不重要。
由于一个较长的文档有更多的机会包含一个查询术语,因此考虑文档的尺寸是合理的,例如一个一百个词的文档中有一个搜索词的五个实例而零一个一千个词的文档中有该搜索词的五个实例,则前者比后者更相关。两种排名函数都采用一个整数正规化选项,它指定文档长度是否影响其排名以及如何影响。该整数选项控制多个行为,因此它是一个位掩码:你可以使用|指定一个或多个行为(例如,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因而很慢。不幸的是,这几乎不可能避免,因为实际查询常常导致巨大数目的匹配。
加亮结果
要表示搜索结果,理想的方式是显示每一个文档的一个部分并且显示它是怎样与查询相关的。通常,搜索引擎显示文档片段时会对其中的搜索术语进行标记。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对。可用的选项是:
- StartSel、StopSel:用来定界文档中出现的查询词的字符串,这用来把它们与其他被引用的词区分开。如果这些字符串包含空格或逗号,你必须把它们加上双引号。
- MaxWords、MinWords:这些数字决定要输出的最长和最短 headline。
- ShortWord:长度小于等于这个值的词将被从一个 headline 的开头或结尾处丢掉。默认值三消除普通英语文章。
- HighlightAll:布尔标志,如果为true整个文档将被用作 headline,并忽略前面的三个参数。
- MaxFragments:要显示的文本引用或片段的最大数量。默认值零选择一种非片段倾向的 headline 生成方法。一个大于零的值选择基于片段的 headline 生成。这种方法找到有尽可能多查询词的文本片段并且展开查询词周围的那些片段。结果是查询词会靠近每个片段的中间并且在其两侧都有词。每一个片段将是最多MaxWords并且长度小于等于ShortWord的词被从每个片段的开头或结尾丢弃。如果不是所有的查询词都在该文档中找到,文档中第一个MinWords的单一片段将被显示。
- FragmentDelimiter:当多于一个片段被显示时,片段将被这个字符串所分隔。
这些选项名称不区分大小写。任何未指定的选项将收到这些默认值:
StartSel=<b>, StopSel=</b>,
MaxWords=35, MinWords=15, ShortWord=3, HighlightAll=FALSE,
MaxFragments=0, FragmentDelimiter=" ... "
例如:
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('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',
'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('query & similarity'),
'StartSel = <, StopSel = >');
ts_headline
-------------------------------------------------------
containing given <query> terms
and return them in order of their <similarity> to the
<query>.
ts_headline使用原始文档,而不是一个tsvector摘要,因此它可能很慢并且应该被小心使用。