设为首页收藏本站

LUPA开源社区

 找回密码
 注册
文章 帖子 博客
LUPA开源社区 首页 业界资讯 技术文摘 查看内容

Postgres的全文搜索已经足够好了

2014-10-14 11:35| 发布者: joejoe0332| 查看: 3870| 评论: 0|原作者: DavidWTF, daxiang, 小猪猪0406, 无若, warrior_by|来自: oschina

摘要: 搜索是项非常重要的功能,所以像elasticsearch和SOLR这样的基于lucene的工具变得很流行。它们都很棒。但使用这些大规模“杀伤性”的搜索武器前,你可能需要来点轻量级的,但又足够好的搜索工具。 ...


归类

当你创建了一个你想要的搜索引擎用来搜索相关的结果(根据相关性归类)的时候,归类可以是基于许多因素的,它的文档大致解释了这些(归类依据)内容。

归类试图处理特定的上下文搜索, 因此有许多个配对的时候,相关性最高的那个会被排在第一个位置。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语义,你将会遇到该方法的性能问题。



酷毙

雷人

鲜花

鸡蛋

漂亮
  • 快毕业了,没工作经验,
    找份工作好难啊?
    赶紧去人才芯片公司磨练吧!!

最新评论

关于LUPA|人才芯片工程|人才招聘|LUPA认证|LUPA教育|LUPA开源社区 ( 浙B2-20090187 浙公网安备 33010602006705号   

返回顶部