归类当你创建了一个你想要的搜索引擎用来搜索相关的结果(根据相关性归类)的时候,归类可以是基于许多因素的,它的文档大致解释了这些(归类依据)内容。 归类试图处理特定的上下文搜索, 因此有许多个配对的时候,相关性最高的那个会被排在第一个位置。PostgreSQL提供了两个预定义归类函数,它们考虑到了词法解释,接近度和结构信息;他们考虑到了在上下文中的词频,如何接近上下文中的相同词语,以及在文中的什么位置出现和其重要程度。 -- PostgreSQL documentation
通过PostgreSQL提供的一些函数得到我们想要的相关性结果,在我们的例子中我们将会使用他们中的2个:ts_rank() 和 setweight() 。 函数setweight允许我们通过tsvector函数给重要程度(权)赋值;值可以是'A', 'B', 'C' 或者 'D'。 1 2 3 4 5 6 | SELECT pid, p_titleFROM ( SELECT post.id as pid,
post.title as p_title,
setweight(to_tsvector(post.language::regconfig, post.title), 'A' ) ||
setweight(to_tsvector(post.language::regconfig, post.content), 'B' ) ||
setweight(to_tsvector( 'simple' , author. name ), 'C' ) ||
setweight(to_tsvector( 'simple' , coalesce (string_agg(tag. name , ' ' ))), 'B' ) as document FROM post JOIN author ON author.id = post.author_id JOIN posts_tags ON posts_tags.post_id = posts_tags.tag_id JOIN tag ON tag.id = posts_tags.tag_id GROUP BY post.id, author.id) p_searchWHERE p_search.document @@ to_tsquery( 'english' , 'Endangered & Species' ) ORDER BY ts_rank(p_search.document, to_tsquery( 'english' , 'Endangered & Species' )) DESC ;
|
上面的查询,我们在文中不同的栏里面赋了不同的权值。post.title的重要程度超过post.content和tag的总和。最不重要的是author.name。
这意味着如果我们搜索关键词“Alice”,那么在题目中包含这个关键词的文档就会排在搜索结果的前面,在此之后是在内容中包含这些关键词的文档,最后才是作者名字中包含这些关键词的文档. 基于对文档各个部分的权重分配ts_rank()这个函数返回一个浮点数,这个浮点数代表了文档和查询关键词的相关性. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | SELECT ts_rank(to_tsvector( 'This is an example of document' ),
to_tsquery( 'example | document' )) as relevancy;
relevancy
0.0607927
(1 row) SELECT ts_rank(to_tsvector( 'This is an example of document' ),
to_tsquery( 'example ' )) as relevancy;
relevancy
0.0607927
(1 row) SELECT ts_rank(to_tsvector( 'This is an example of document' ),
to_tsquery( 'example | unkown' )) as relevancy;
relevancy
0.0303964
(1 row) SELECT ts_rank(to_tsvector( 'This is an example of document' ),
to_tsquery( 'example & document' )) as relevancy;
relevancy
0.0985009
(1 row) SELECT ts_rank(to_tsvector( 'This is an example of document' ),
to_tsquery( 'example & unknown' )) as relevancy;
relevancy
1e-20
(1 row)
|
但是, 相关性的概念是模糊的,而且是与特定的应用相关. 不同的应用可能需要额外的信息来得到想要的排序结果,比如,文档的修改时间. 内建的排序功能如asts_rank只是个例子. 你可以写出自己的排序函数 并且/或者 将得到的结果和其他因素混合来适应你自己的特定需求.
这里说明一下, 如果我们想是新的文章比旧的文章更重要,可以讲ts_rank函数的数值除以文档的年龄+1(为防止被0除).
优化与索引 将一个表中的搜索结果优化为直线前进的. PostgreSQL 支持基于索引的功能,因此你可以用tsvector()函数方便地创建GIN索引. 1 2 3 | CREATE INDEX idx_fts_post ON post
USING gin(setweight(to_tsvector(language, title), 'A' ) ||
setweight(to_tsvector(language, content), 'B' ));
|
GIN还是GiST索引? 这两个索引会成为与他们相关的博文的主题. GiST会导出一个错误的匹配,之后需要一个额外的表行查找来验证得到的匹配. 另一方面, GIN 可以更快地查找但是在创建时会更大更慢. 一个经验, GIN索引适合静态的数据因为查找是迅速的. 对于动态数据, GiST 可以更快的更新. 具体来说, GiST索引在动态数据上是好用的并且如果单独的字(词位)在100,000以下也是快速的,然而GIN 索引在处理100,000词位以上时是更好的但是更新就要慢点了. -- Postgres 文档 : 第12章 全文搜索
在我们的例子中,我们选择GIN。但是这个选择不是一定的,你可以根据你自己的数据来作出决定。
我们的架构例子中有一个问题; 分当时分布在拥有不同权重的不同表中的. 为了更好的运行,通过触发器和物化视图使得数据非规范化是必要的.
我们并非总是需要非规范化并且有时也需要加入基于索引的功能,就像上面所做的那样. 另外你可以通过postgres触发器 功能tsvector_update_trigger(...)或者tsvector_update_trigger_column(...)实现相同表的数据的非规范化.参见Postgres文档以得到更多详细的信息.
在我们的应用中在结果返回之前存在着一些可接受的延迟. 这是一个使用物化视图将额外索引加载其中的好的情况.
1 2 3 4 5 6 | CREATE MATERIALIZED VIEW search_index AS SELECT post.id,
post.title,
setweight(to_tsvector(post.language::regconfig, post.title), 'A' ) ||
setweight(to_tsvector(post.language::regconfig, post.content), 'B' ) ||
setweight(to_tsvector( 'simple' , author. name ), 'C' ) ||
setweight(to_tsvector( 'simple' , coalesce (string_agg(tag. name , ' ' ))), 'A' ) as documentFROM postJOIN author ON author.id = post.author_idJOIN posts_tags ON posts_tags.post_id = posts_tags.tag_idJOIN tag ON tag.id = posts_tags.tag_idGROUP BY post.id, author.id
|
之后重新索引搜索引擎就是定期运行REFRESH MATERIALIZED VIEW search_index这么简单. 现在我们可以给物化视图添加索引. 1 | CREATE INDEX idx_fts_search ON search_index USING gin(document);
|
查询也变得同样简单. 1 | SELECT id as post_id, titleFROM search_indexWHERE document @@ to_tsquery( 'english' , 'Endangered & Species' ) ORDER BY ts_rank(p_search.document, to_tsquery( 'english' , 'Endangered & Species' )) DESC ;
|
如果延迟变得无法忍受,你就应该去研究一下使用触发器的替代方法. 建立文档存储的方式并不唯一;这取决于你文档的情况: 单表、多表,多国语言,数据量 ... Thoughtbot.com 发表了文章"Implementing Multi-Table Full Text Search with Postgres in Rails" 我建议阅读以下.
拼写错误PostgreSQL 提供了一个非常有用的扩展程序pg_trgm。 相关文档见pg_trgm doc。 1 | CREATE EXTENSION pg_trgm;
|
pg_trgm支持N元语法如N==3。N元语法比较有用因为它可以查找相似的字符串,其实,这就是拼写错误的定义 – 一个相似但不正确的单词。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | SELECT similarity( 'Something' , 'something' );
similarity
1
(1 row) SELECT similarity( 'Something' , 'samething' );
similarity
0.538462
(1 row) SELECT similarity( 'Something' , 'unrelated' );
similarity
0
(1 row) SELECT similarity( 'Something' , 'everything' );
similarity
0.235294
(1 row) SELECT similarity( 'Something' , 'omething' );
similarity
0.583333
(1 row)
|
通过上面的示例你可以看到,similarity 函数返回一个表示两个字符串之间相似度的浮点值。 检测拼写错误就是一系列的收集文档中使用的词位、比较词位与输入文本的相似度的过程。 我发现检测拼写错误时,相似度临界值设置为0.5比较合适。 首先,我们需要根据文档创建一个唯一性词位列表,在列表中每一个词位都是唯一的。 1 2 3 4 5 6 7 8 9 | CREATE MATERIALIZED VIEW unique_lexeme ASSELECT word FROM ts_stat( 'SELECT to_tsvector(' simple ', post.title) ||
to_tsvector(' simple ', post.content) ||
to_tsvector(' simple ', author.name) ||
to_tsvector(' simple ', coalesce(string_agg(tag.name, ' ')))
FROM post
JOIN author ON author.id = post.author_id
JOIN posts_tags ON posts_tags.post_id = posts_tags.tag_id
JOIN tag ON tag.id = posts_tags.tag_id
GROUP BY post.id, author.id' );
|
上面的脚本使用word列创建了一个视图,word列内容来自于词位列表。 我们使用simple关键字,这样table表中可以存储多种语言的文本。 一旦创建了这个实体化视图,我们需要添加一个索引来使相似度查询速度更快。 1 | CREATE INDEX words_idx ON search_words USING gin(word gin_trgm_ops);
|
幸运的是,搜索引擎中使用的唯一性词位列表不会快速变化,这样我们就无需通过下面脚本经常刷新实体化视图: 1 | REFRESH MATERIALIZED VIEW unique_lexeme;
|
一旦我们建立起这个表,查找最接近的匹配是很容易的。 1 2 | SELECT word
WHERE similarity(word, 'samething' ) > 0.5 ORDER BY word <-> 'samething' LIMIT 1;
|
这个查询返回的是这样一个语义,它相似度满足(>0.5),再根据输入的samething将其最接近的排在首位。操作符<->返回的是参数间的“距离”,而且是一减去similarity()的值。 当你决定在你的搜索中处理拼写错误的时候,你不会希望看到它(拼写错误)出现在每一个查询中。相反地,当你在搜索无结果时,可以为了拼写错误去查询,并使用查询所提供结果给用户一些建议。如果数据来自于非正式的通讯,例如:社交网络,可能你的数据中会包含拼写错误。你可以通过追加一个类似的语义到你的tsquery中,来获得一个好点的结果。 "Super Fuzzy Searching on PostgreSQL" 是一篇很好的关于为拼写错误和搜索Postgres使用三字母组的参考文章。 在我使用的例子中,使用unique语义的表不会大于2000行,而且我的理解是,如果你有超过1M的文本时使用unique语义,你将会遇到该方法的性能问题。
|