重音字符
当你建立一个搜索引擎支持多种语言时你也需要考虑重音问题。在许多语言中重音非常重要,可以改变这个词的含义。Postgres附带一个unaccent扩展去调用 unaccentuate内容是有用处的。
- CREATE EXTENSION unaccent;SELECT unaccent('èéêë');
- unaccent----------
- eeee
- (1 row)
让我们添加一些重音的你内容到我们的post表中。
- INSERT INTO post (id, title, content, author_id, language)
- VALUES (4, 'il était une fois', 'il était une fois un hôtel ...', 2,'french')
如果我们想要忽略重音在我们建立文档时,之后我们可以简单做到以下几点:
- SELECT to_tsvector(post.language, unaccent(post.title)) ||
- to_tsvector(post.language, unaccent(post.content)) ||
- to_tsvector('simple', unaccent(author.name)) ||
- to_tsvector('simple', unaccent(coalesce(string_agg(tag.name, ' '))))JOIN author ON author.id = post.author_idJOIN posts_tags ON posts_tags.post_id = posts_tags.tag_idJOIN tag ON author.id = post.author_idGROUP BY p.id
这样工作的话,如果有更多错误的空间它就有点麻烦。 我们还可以建立一个新的文本搜索配置支持无重音的字符。
- CREATE TEXT SEARCH CONFIGURATION fr ( COPY = french );ALTER TEXT SEARCH CONFIGURATION fr ALTER MAPPINGFOR hword, hword_part, word WITH unaccent, french_stem;
当我们使用这个新的文本搜索配置,我们可以看到词位
- SELECT to_tsvector('french', 'il était une fois');
- to_tsvector-------------
- 'fois':4
- (1 row)SELECT to_tsvector('fr', 'il était une fois');
- to_tsvector--------------------
- 'etait':2 'fois':4
- (1 row)
这给了我们相同的结果,第一作为应用unaccent并且从结果建立tsvector。
- SELECT to_tsvector('french', unaccent('il était une fois'));
- to_tsvector--------------------
- 'etait':2 'fois':4
- (1 row)
词位的数量是不同的,因为il était une在法国是一个无用词。这是一个问题让这些词停止在我们的文件吗?我不这么认为etait不是一个真正的无用词而是拼写错误。
- SELECT to_tsvector('fr', 'Hôtel') @@ to_tsquery('hotels') as result;
- result--------
- t
- (1 row)
如果我们为每种语言创建一个无重音的搜索配置,这样我们的post可以写入并且我们保持这个值在post.language的中,然后我们可以保持以前的文档查询。
- SELECT to_tsvector(post.language, post.title) ||
- to_tsvector(post.language, post.content) ||
- to_tsvector('simple', author.name) ||
- to_tsvector('simple', coalesce(string_agg(tag.name, ' ')))JOIN author ON author.id = post.author_idJOIN posts_tags ON posts_tags.post_id = posts_tags.tag_idJOIN tag ON author.id = post.author_idGROUP BY p.id
如果你需要为每种语言创建无重音的文本搜索配置由Postgres支持,然后你可以使用gist
我们当前的文档大小可能会增加,因为它可以包括无重音的无用词但是我们并没有关注重音字符查询。这可能是有用的如有人用英语键盘搜索法语内容。
归类
当你创建了一个你想要的搜索引擎用来搜索相关的结果根据相关性归类)的时候,归类可以是基于许多因素的,它的文档大致解释了这些归类依据)内容。
归类试图处理特定的上下文搜索, 因此有许多个配对的时候,相关性最高的那个会被排在第一个位置。PostgreSQL提供了两个预定义归类函数,它们考虑到了词法解释,接近度和结构信息;他们考虑到了在上下文中的词频,如何接近上下文中的相同词语,以及在文中的什么位置出现和其重要程度。
-- PostgreSQL documentation
通过PostgreSQL提供的一些函数得到我们想要的相关性结果,在我们的例子中我们将会使用他们中的2个:ts_rank() 和 setweight() 。
函数setweight允许我们通过tsvector函数给重要程度权)赋值;值可以是'A', 'B', 'C' 或者 'D'。
- 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()这个函数返回一个浮点数,这个浮点数代表了文档和查询关键词的相关性.
- 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索引.
- 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文档以得到更多详细的信息.
在我们的应用中在结果返回之前存在着一些可接受的延迟. 这是一个使用物化视图将额外索引加载其中的好的情况.
- 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这么简单.
现在我们可以给物化视图添加索引.
- CREATE INDEX idx_fts_search ON search_index USING gin(document);
查询也变得同样简单.
- 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。
- CREATE EXTENSION pg_trgm;
pg_trgm支持N元语法如N==3。N元语法比较有用因为它可以查找相似的字符串,其实,这就是拼写错误的定义 – 一个相似但不正确的单词。
- 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比较合适。 首先,我们需要根据文档创建一个唯一性词位列表,在列表中每一个词位都是唯一的。
- 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表中可以存储多种语言的文本。 一旦创建了这个实体化视图,我们需要添加一个索引来使相似度查询速度更快。
- CREATE INDEX words_idx ON search_words USING gin(word gin_trgm_ops);
幸运的是,搜索引擎中使用的唯一性词位列表不会快速变化,这样我们就无需通过下面脚本经常刷新实体化视图:
- REFRESH MATERIALIZED VIEW unique_lexeme;
一旦我们建立起这个表,查找最接近的匹配是很容易的。
- 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语义,你将会遇到该方法的性能问题。
关于MySQL和RDS远程数据服务)
这在Postgres RDS上能运行吗?
上面所有的示例在RDS上都是可以运行的。 据我所知,RDS搜索特性中唯一的限制是搜索某些数据时需要访问文件系统,如自定义字典,拼写检查程序,同义词,主题词表。 相关信息见亚马逊aws论坛。
我使用的是MYSQL数据库,我可以使用内置的全文本搜索功能吗?
如果是我,我不会去用这个功能。 无需争论,MySQL的全文本搜索功能非常局限。 默认情况,它不支持任何语言的词干提取功能。 我偶然发现一个可以安装的词干提取的函数,但是MYSQL不支持基于索引的函数。
那么你可以做些什么? 鉴于我们上面的讨论,如果 Postgres能够胜任你使用的各个场景,那么考虑下把数据库换为 Postgres。 数据库迁移工作可以通过工具如 py-mysql2pgsql方便地完成。 或者你可以研究一下更高级的解决方案如 SOLR基于 Lucene的全文搜索服务器)和 Elasticsearch基于 Lucene的开源、分布式、 RESTful搜索引擎)。
总结
我们已经了解了基于一个特殊的文档如何构建一个性能良好且支持多语言的文本搜索引擎。 这篇文章只是一个概述,但是它已经给你提供了足够的背景知识和示例,这样你可以开始构建自己的搜索引擎。 在这篇文章中,我也许犯了一些错误,如果你能把错误信息发送到blog@lostpropertyhq.com,我将感激不尽。
Postgres的全文本搜索特性非常好,而且搜索速度足够快。 这可以使你的应用中的数据不断增长,而无需依赖其它工具进行处理。 Postgres的搜索功能是银弹吗? 如果你的核心业务围绕搜索进行,它可能不是的。
它移除了一些特性,但是在大部分场景中你不会用到这些特性。 毫无疑问,你需要认真分析和理解你的需求来决定使用哪种搜索方式。
就我个人而言,我希望Postgres全文本搜索功能继续改善,并新增下面的一些特性:
-
额外的内置语言支持: 汉语,日语...
-
围绕Lucene的外国数据包装程序。 在全文本搜索功能上,Lucene仍然是最优秀的工具,把它集成到Postgres中会有很多好处。
-
更多排名结果的提高或评分特性会是一流的。 Elasticsearch 和 SOLR已经提供了先进的解决方案。
-
进行模糊查询tsquery)时不使用trigram的方式会非常棒。 Elasticsearch 提供了一种非常简单的方式来实现模糊搜索查询。
-
能够通过SQL动态创建和编辑如字典内容、同义词、主题词表的特性,而不再使用把文件添加到文件系统的方式。
Postgres 没有ElasticSearch 和 SOLR 那么先进,毕竟ElasticSearch 和 SOLR是专门进行全文本搜索的工具,而全文本搜索只是PostgresSQL一个比较优秀的特性。
英文原文:Postgres full-text search is Good Enough!
译文出自:http://www.oschina.net/translate/postgres-full-text-search-is-good-enough