PostgreSQL8.3.X新特性-全文搜索(一)
PostgreSQL8.3.X新特性-全文搜索
介绍 全文搜索(或者说是文本搜索)提供了一种可以标识满足某个查询的自然语言文档的能力,并且还可以根据文档的相关性对文档进行排序。最常见的搜索是找出所有包含给出的查询词的文档,并且以它们和查询的相似性的顺序输出。查询和相似性的概念相当灵活,并且和具体的应用有关。最简单的搜索认为查询是一组词的集合,相似性是文档中查询词出现的频率。 文本搜索操作符在数据库里的存在已经有很多年了。PostgreSQL 有 ~,~*,LIKE,和 ILIKE 操作符用于文本数据类型,但是它们缺乏许多现代的信息系统需要的重要功能:
全文索引允许我们可以对文档进行预处理并且可以保存一个事后可以用于快速搜索的索引。预处理包括:
词典允许对如何对记号进行一般化处理有更细粒度的控制。使用合适的词典,我们就可以:
提供了一个数据类型 tsvector 用于存储预处理后的文档,还提供了一个 tsquery 类型用于代表查询(8.3第8.11节)。这些数据类型上有很多函数和操作符可用(8.3第9.13节),最重要的是匹配操作符 @@,我们在8.3第12.1.2节。全文搜索可以通过使用索引加速(8.3第12.9节)。 [编辑] 什么是文档?文档是全文搜索系统里面的搜索的单位;比如,一篇杂志的文章或者一个电子邮件的信息。文本搜索引擎必须能够分析文档并且存储与父文档相关的语意(关键词)。然后,这些关联用于搜索包含查询词的文档。 在 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;
另外一个可能是把文档以简单的文本文件的形式存储在文件系统里。在这种场合下,数据库可以用于存储全文索引并且执行搜索,然后可以用一个唯一标识来从文件系统中检索文档。不过,从数据库外面检索文件要求(数据库)超级用户权限或者是一些特殊的函数支持,所以相比之下,通常把所有数据保存在 PostgreSQL 里面是最方便的。还有,把所有东西都保存在数据库里也让我们很容易访问文档的元数据以帮助索引和现实。 为了使用文本搜索,每个文档都必须缩减成预处理过的 tsvector 格式。搜索和相关性完全都是在文档的 tsvector 形式上进行的 -- 原始的文档只有在需要给用户显示的事后才需要检索。所以,我们通常直接把 tsvector 当做文档,当然了,它其实只是一个完整文档的紧凑表现形式。 [编辑] 基础的文本搜索PostgreSQL 的全文搜索是基于匹配操作符 @@,如果一个 tsvector(文档)匹配一个 tsquery(查询),它就返回真。哪个数据类型写在前头并无所谓: 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操作符组合起来。(细节参阅8.3第8.11节。)有两个函数 to_tsquery 和 plainto_tsquery 可以帮助把用户写的 text 转换成合适的 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 输入,允许在简单情况下省略 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)。 [编辑] 配置上面的都是简单文本的搜索例子。如我们前面说过的,全文搜索的功能包括做更多事情的能力:忽略对某些词的索引(屏蔽词),处理同义词,以及使用复杂的词法分析等。比如,不仅仅基于空白的分词。这些功能是通过文本搜索的配置控制的。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/ 里面有一些附加分析器和模板的例子。)因为字典和配置只是参数和一些下层的分析器和模板的组合,所以创建新的字典或者配置不需要特殊的权限。创建客户化字典和配置的例子在本章后面出现。 表和索引 前面一节的例子演示了使用简单的常量字串进行全文匹配。本节显示如何搜索表数据,还有是如何使用索引。 [编辑] 搜索一个表我们可以不用索引来做全文检索。一个简单的,打印 body 字段里包含有单词 friend 的所有行的标题的查询看上去像这样: SELECT title FROM pgweb WHERE to_tsvector('english',body) @@ to_tsquery('english','friend'); 这样还会找到相关的单词,比如 friends 和 friendly,因为所有这些词都归纳为同一个规范化的语意(lexeme)。 上面的查询声明了使用 english 配置对字串进行分析(分词)和规范化。我们也可以省略配置参数: SELECT title FROM pgweb WHERE to_tsvector(body) @@ to_tsquery('friend'); 这个查询将使用 default_text_search_config 设置的配置参数。 一个更复杂的例子是选取十条最新的、在 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 索引(第 12.9节)来加速文本搜索: CREATE INDEX pgweb_idx ON pgweb USING gin(to_tsvector('english',body)); 请注意我们使用了 2 参数的 to_tsvector 版本。只有声明了配置名称的搜索函数可以在表达式索引(第11.7节)里面使用。这事因为索引内容不能被 default_text_search_config 影响。如果它们被影响,那么索引内容可能就不一致,因为不同的条目可能包含使用不同的文本搜索配置生成的 tsvector,而这样就没有办法猜测是什么了。这样的索引是无法正确转储和恢复的。 因为在上面的索引里面使用了 2 参数的 to_tsvector 版本,所以,只有使用了同样的配置参数的 2 参数版本的 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; UPDATE pgweb SET textsearchable_index_col = to_tsvector('english',coalesce(title,) || coalesce(body,)); 然后我们创建一个 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; 在使用独立字段存储 tsvector 形式的时候,我们需要创建一些触发器以保持 tsvector 字段对 title 和 body 修改的同步。12.4.3节解释了如何实现这个。 分离字段方式比表达式索引方式的一个优点是它不需要在查询里明确声明文本搜索的配置就可以使用索引。如上面例子所示,查询可以依赖于default_text_search_config。另外一个优点是搜索会更快,因为它不需要重新调用 to_tsvector 函数来判断是否和索引匹配。(这点在使用 GiST 索引而不是 GIN 索引的时候更重要;参阅第12.9节。)不过,表达式索引的方法的优点是设置简单,而且它需要的磁盘空间更烧,因为 tsvector 表现形式不用明确存储。 控制文本搜索 要实现文本搜索,我们必须有一个函数从文档中创建一个 tsvector,和一个函数从用户的查询中创建 tsquery。还有,我们需要以有用的方式返回结果,因此我们需要一个函数对比文档和查询之间的相关性。把结果显示得比较漂亮也很重要。PostgreSQL 提供了所有这些函数的支持。 [编辑] 分析文档(分词)PostgreSQL 提供了函数 to_tsvector 用于把文档转换成 tsvector 数据类型。 to_tsvector([ config regconfig,] document text) returns tsvector to_tsvector 把一个文本文档分解成记号,把记号分解成语意(lexeme),然后返回一个 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节),这个词典的列表可能和记号的类型密切相关。第一个识别了该记号的词典发出一个或多个规范化的语意(lexeme)以代表该记号。比如,rats 变成了 rat,因为其中一个词典认出单词 rats 是 rat 的复数形式。有些词会别当作屏蔽词看待(第12.6.1节),这样它们就会被忽略,因为它们出现得太过频繁,对搜索没什么用处。在我们的例子里,屏蔽词是 a,on,和it。如果列表中的词典都没有识别该记号,那么它也会被忽略。在我们的例子里,标点符号就是这么处理的 - 因为实际上没有什么词典给它们标识记号类型(间隔符号),意味着空间记号永远不会被索引。分析器(分词器),词典和需要索引的记号的选择是由选定的文本搜索配置(第12.7节)决定的。我们可以在同一个数据库里有多个不同的配置,预定义的那些配置可以用于多种语言。在我们的例子里,我们用缺省的配置 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 里面标记每个语意(lexeme)的源,然后使用 tsvector 的连接操作符 || 把标记后的 tsvector 值融合起来。(第12.4.1节给出了有关这些操作的细节。) [编辑] 分析查询PostgreSQL 提供了函数 to_tsquery 和 plainto_tsquery 用于把查询转换成 tsquery 数据类型。to_tsquery 提供了获取比 plainto_tsquery 更多特性的机制,但是对输入要求更严。 to_tsquery([ config regconfig,] querytext text) returns tsquery to_tsquery 从一个查询文本创建一个 tsquery 值,它们必须由使用布尔操作符 & (与),|(或)和! (非)分隔的单个记号。这些操作符可以用圆括弧分组。换句话说,给 to_tsquery 的输入必须已经遵守了 tsquery 输入的通用规则,如第8.11节所示。区别是基本的 tsquery 输入以字面值接受输入的记号,而 to_tsquery 使用指定的或者缺省的配置,把每个记号都规范化成一个语意(lexeme),并且抛弃任何配置里规定的屏蔽词。比如: SELECT to_tsquery('english','The & Fat & Rats'); to_tsquery --------------- 'fat' & 'rat' 和基本的 tsquery 输入一样,我们可以给每个语意(lexeme)附加附加权重以限制它之匹配那些权重的 tsvector 语意。比如: SELECT to_tsquery('english','Fat | Rats:AB'); to_tsquery ------------------ 'fat' | 'rat':AB to_tsquery 还接受单引号包围的短语。这个功能在配置包含(thesaurus)知识词典、可能触发这类短语的时候很有效。在下面的例子里,一个包含规则 supernovae stars: sn 的知识词典: SELECT to_tsquery('''supernovae stars'' &!crab'); to_tsquery --------------- 'sn' &!'crab' 如果没有引号,to_tsquery 会生成一个语法错误,因为记号没有被“与”或者“或”操作符分隔。 plainto_tsquery([ config regconfig,] querytext text) returns tsquery plainto_tsquery 把未格式化的文本 querytext 转换成 tsquery。文本会像在 to_tsvector 里那样分析和规范化,然后用布尔操作符 & (与)插入到余下的单词中。 例子: SELECT plainto_tsquery('english','The Fat Rats'); plainto_tsquery ----------------- 'fat' & 'rat' 请注意 plainto_tsquery 不能在其输入中识别布尔操作符或者权重标签: SELECT plainto_tsquery('english','The Fat & Rats:C'); plainto_tsquery --------------------- 'fat' & 'rat' & 'c' 这里,所有输入的标点都被当做间隔符号抛弃。 [编辑] 计算搜索结果相关性相关性是企图评判文档与特定的查询之间是多相关的一个衡量标准,这样,如果有很多匹配的项,那么最相关的就会显示在前头。PostgreSQL提供了两个预定义的相关性函数,它们考虑了词法,相似性和结构信息;也就是说,它们会考虑查询词在文档中出现的频率,这些词在文档中的位置有多靠近,以及它们在文档中的位置有多重要。不过,相关性的概念是很模糊的,并且和应用密切相关。不同的应用可能需要相关性的额外信息,比如,文档的修改时间。内置的相关性函数只是例子。你可以书写你自己的相关性函数和/或把它们的结果跟额外的因素组合起来满足自己特定的需求。 目前可用的两个相关性函数是: ts_rank([ weights float4[],] vector tsvector,query tsquery [,normalization integer ]) returns float4
ts_rank_cd([ weights float4[],normalization integer ]) returns float4
对于这两个函数,我们都可选的权重参数提供根据词自身的(权重)标签来加重或者减轻单词的相关性分量。权重数组声明每个级别的单词有多重,按照下面顺序: {D-weight,C-weight,B-weight,A-weight} 如果没有提供权重,那么使用下面的缺省: {0.1,0.2,0.4,1.0} 通常来说,权重用于把单词标记为来自于文档的哪个趋于,比如标题和开头的摘要,这样就可以以比文档体更重要或更不重要的方式对待它们。 因为更长的文档有更大的机会包含搜索的词,所以把文档尺寸考虑在内也是很合理的,也就是说,假如一个一百个词的文档里,出现了五个搜索词的实例,那么其相关性要比一千个词的文档出现五个实例要更相关。两个相关性函数都接受一个整数的规范化选项,声明一个文档的长度是否影响相关性,以及如何影响。这个整数选项控制好几个行为,所以它是一个位掩码:你可以用 | 声明一个或多个行为(比如,2|4)。
如果提供了多于一个的标志位,那么变换以列出的顺序施加。 我们要注意的是相关性函数并不使用任何全局信息,所以我们不可能生成一个公平的 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,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,options text ]) returns text ts_headline 接受一个文档以及对应的查询,然后返回一个文档的摘要,在摘要里面,查询是高亮的。用于分析文档的配置可以在 config 里面声明;如果忽略 config,那么使用 default_text_search_config 配置。 如果声明了一个可选的字串,那么它必须由以一个逗号分隔的一个或多个“选项=值”对组成。可用的选项有:
任何未声明的选项都会得到下面的缺省值: StartSel=<b>,StopSel=</b>,MaxWords=35,MinWords=15,ShortWord=3,HighlightAll=FALSE 比如: 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 ------------------------------------------------------------ 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 ------------------------------------------------------- given <query> terms and return them in order of their <similarity> to the <query>. ts_headline 使用原始的文档,而不是 tsvector 摘要,所以它可能比较慢,因此要小心使用。一个常见的错误是对每个匹配的文档都调用 ts_headline,而实际上只需要显示十个文档。这个时候 SQL 的子查询可以帮忙;下面是一个例子: SELECT id,ts_headline(body,q),rank FROM (SELECT id,body,q,ts_rank_cd(ti,q) AS rank FROM apod,to_tsquery('stars') q WHERE ti @@ q ORDER BY rank DESC LIMIT 10) AS foo; (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |