当构建一个Web应用时,经常被要求加上搜索功能。其实有时候我们也不知道我要搜索个啥,反正就是要有这个功能。搜索确实很重要的特性,这也是为什么像Elasticsearch和Solr这样基于Lucene的数据库越来越流行。这两个是好用,但是在构建Web应用时,有时候感觉像是杀鸡用牛刀。所以我们需要选择轻量好用的东西来搜索。

如何定义足够好用? 足够好用的搜索引擎需要有以下几点特征:

  • Stemming 关键词,词干
  • Ranking/Boost 排名和权重分配
  • 多语言支持
  • 错误纠正/模糊查询
  • 口音支持 类似我们的一二三四声

非常幸运,PostgreSql支持以上所有特征。

这篇文章旨在帮助以下的人群:

  • 用了PG数据库,不想再加一个搜索引擎
  • 用了其他数据库,想有一个更好的全文检索体验

在这片文章,我们将基于以下表和数据逐步的阐述PG中的全文搜索功能。

CREATE TABLE author(
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
); CREATE TABLE post(
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
author_id INT NOT NULL references author(id)
); CREATE TABLE tag(
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
); CREATE TABLE posts_tags(
post_id INT NOT NULL references post(id),
tag_id INT NOT NULL references tag(id)
); INSERT INTO author (id, name)
VALUES (1, 'Pete Graham'),
(2, 'Rachid Belaid'),
(3, 'Robert Berry'); INSERT INTO tag (id, name)
VALUES (1, 'scifi'),
(2, 'politics'),
(3, 'science'); INSERT INTO post (id, title, content, author_id)
VALUES (1, 'Endangered species',
'Pandas are an endangered species', 1 ),
(2, 'Freedom of Speech',
'Freedom of speech is a necessary right', 2),
(3, 'Star Wars vs Star Trek',
'Few words from a big fan', 3); INSERT INTO posts_tags (post_id, tag_id)
VALUES (1, 3),
(2, 2),
(3, 1);

以上是一个比较典型的Blog类型的数据库。有post, posttitlecontentpost通过外键与author建立关系。post也有自己的多个tag

什么是Full-Text Search全文检索

首先让我们看看定义

In text retrieval, full-text search refers to techniques for searching a single computer-stored document or a collection in a full-text database. The full-text search is distinguished from searches based on metadata or on parts of the original texts represented in databases.

-- Wikipedia

这段定义指出了一个重要的概念-document。当你执行一个查询的时候,你实际上是为了寻找一些具有实际含义的实体。那些就是documents。PostgreSQL文档解释的有点耐人寻味

A document is the unit of searching in a full-text search system; for example, a magazine article or email message.

-- Postgres documentation

document 能够跨越多个表,它代表的是一个逻辑上的独立个体。

建立我们的document

在上一节我们介绍了document的概念,document和我们的表结构没关系,它只和数据有关系。以我们的数据库为例,一个document可以有以下几个field组成:

  • post.title
  • post.content
  • postauthor.name
  • 所有与该post相关的tag.name

    通过以下查询,我们可以建立一个document的二维表数据
SELECT post.title || ' ' ||
post.content || ' ' ||
author.name || ' ' ||
coalesce((string_agg(tag.name, ' ')), '') 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; document
--------------------------------------------------
Endangered species Pandas are an endangered species Pete Graham politics
Freedom of Speech Freedom of speech is a necessary right Rachid Belaid politics
Star Wars vs Star Trek Few words from a big fan Robert Berry politics

在查询语句中,我们通过postauthor进行group, 通过string_aggtag.name进行聚合。同时我们用了coalesce来防止聚合之后tag出现null

此时,我们的document是一个简单的长字符串,当然对我们起不到什么作用。我们需要通过to_tsvector()对这个长字符串操作一顿。

SELECT to_tsvector(post.title) ||
to_tsvector(post.content) ||
to_tsvector(author.name) ||
to_tsvector(coalesce((string_agg(tag.name, ' ')), '')) 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; document
--------------------------------------------------
'endang':1,6 'graham':9 'panda':3 'pete':8 'polit':10 'speci':2,7
'belaid':12 'freedom':1,4 'necessari':9 'polit':13 'rachid':11 'right':10 'speech':3,6
'berri':13 'big':10 'fan':11 'polit':14 'robert':12 'star':1,4 'trek':5 'vs':3 'war':2 'word':7
(3 rows)

这个查询以tsvector的形式回传我们的document,这种格式特别适合来做full-text search。让我们在试一下将一个简单的字符串转化成tsvector类型

SELECT to_tsvector('Try not to become a man of success, but rather try to become a man of value');

query will return the following result:

                             to_tsvector
----------------------------------------------------------------------
'becom':4,13 'man':6,15 'rather':10 'success':8 'tri':1,11 'valu':17
(1 row)

见证奇迹的时刻到啦。 首先有一些单词是来自于原句子的,但是有一些单词变化了(try变成了tri)。同时所有单词后面都带了数字,为啥子?

一个tsvector是由一组不同的词元组成。词元是分词之后的变种,就是被操作过了。操作的专业名词叫做normalization。这波操作主包含大小写转化,后缀移除等等。搜索意味着要面对多重选项,减少一些选项便于引擎去搜搜。单词后面带的数字表示词元原来在的位置。比如man就是在6和15。您可以自个儿数数对不对。

默认情况下,Postgres在to_tsvector中用的是english的配置。它会直接忽略非英文的单词。

Querying

现在,我们知道如何构建一个文档。但是我们的目的是为了搜索。我们用@@来对tsvector的数据进行搜索。官方文档走一波。让我们看看一些例子:

> select to_tsvector('If you can dream it, you can do it') @@ 'dream';
?column?
----------
f
(1 row) > select to_tsvector('It''s kind of fun to do the impossible') @@ 'impossible'; ?column?
----------
t
(1 row)

上面主要是简单的查一下是否存在这个关键词。接下去的例子可以展示强制类型转化和to_tsquery这个函数的区别。

SELECT 'impossible'::tsquery, to_tsquery('impossible');
tsquery | to_tsquery
--------------+------------
'impossible' | 'imposs'
(1 row)

to_tsquery函数用于写一些简单的搜索语句。支持布尔操作符&(AND), |(OR), !(NOT)。在to_tsquery也可以用进行表达式优先级的调整。

> SELECT to_tsvector('If the facts don't fit the theory, change the facts') @@ to_tsquery('! fact');

 ?column?
----------
f
(1 row) > SELECT to_tsvector('If the facts don''t fit the theory, change the facts') @@ to_tsquery('theory & !fact'); ?column?
----------
f
(1 row) > SELECT to_tsvector('If the facts don''t fit the theory, change the facts.') @@ to_tsquery('fiction | theory'); ?column?
----------
t
(1 row)

同时可以用:*进行startwith搜索。

> SELECT to_tsvector('If the facts don''t fit the theory, change the facts.') @@ to_tsquery('theo:*');

 ?column?
----------
t
(1 row)

现在我们知道如何做一个full-text search。我们回到我们的数据库尝试对我们文档进行查询。

SELECT pid, p_title
FROM (SELECT post.id as pid,
post.title as p_title,
to_tsvector(post.title) ||
to_tsvector(post.content) ||
to_tsvector(author.name) ||
to_tsvector(coalesce(string_agg(tag.name, ' '))) 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_search
WHERE p_search.document @@ to_tsquery('Endangered & Species'); pid | p_title
-----+--------------------
1 | Endangered species

语言支持

Posgres内置了一些语言: Danish, Dutch, English, Finnish, French, German, Hungarian, Italian, Norwegian, Portuguese, Romanian, Russian, Spanish, Swedish, Turkish.(没看到中文,这段就不说了)

Accented Character

音调的不同导致意思也不同,(没有内置中文,这段就不说了)

Ranking

构建一个搜索引擎,您会希望你的查询结果是根据相关度来排序的。与documents的排名相关的指标在下面这段引用中解释的很清楚。

Ranking attempts to measure how relevant documents are to a particular query, so that when there are many matches the most relevant ones can be shown first. PostgreSQL provides two predefined ranking functions, which take into account lexical, proximity, and structural information; that is, they consider how often the query terms appear in the document, how close together the terms are in the document, and how important is the part of the document where they occur.

-- PostgreSQL documentation

在PostgreSQL中提供了两个函数来进行相关度调整。分别是ts_rank()setweight()

函数setweight允许给予tsvector一个权重,权重的值可以是A,B,C,D

SELECT pid, p_title
FROM (SELECT post.id as pid,
post.title as p_title,
setweight(to_tsvector(post.title), 'A') ||
setweight(to_tsvector(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_search
WHERE p_search.document @@ to_tsquery('english', 'Endangered & Species')
ORDER BY ts_rank(p_search.document, to_tsquery('english', 'Endangered & Species')) DESC;

在上面这个查询中,我们给予不同的tsvector不同的权重,A权重的post.titleB权重的post.content更加重要。依此类推。这意味着如果我们查一个词Alice。Adocument在标题中有Alice,Bdocument在内容中有Alice。那A文档会在前。

ts_rank函数会回传tsquerytsvector的相关度,用一个小数来表示。

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)

但是,相关度这个概念实际上有点模糊且与实际APP需求紧密连接的。不同的APP需要不同的排名方式。你可以自己加上一些参数上去。比如你加一个根据作者年龄的排序,你可以ORDER BY ts_rank(p_search.document, to_tsquery('english', 'Endangered & Species')) * author.age DESC。最后怎么算还是由你自己决定。

(Optimization and Indexing)优化和索引

优化搜索非常简单明了。因为PostgreSQL支持基于Index的函数。所以你可以创建一个GIN index作用于tsvector函数。

CREATE INDEX idx_fts_post ON post
USING gin((setweight(to_tsvector(language::regconfig, title),'A') ||
setweight(to_tsvector(language::regconfig, content), 'B'))); -- If this throws an IMMUTABLE error then you can use this workaround CREATE OR REPLACE FUNCTION gin_fts_fct(title text, content text, language text)
RETURNS tsvector
AS
$BODY$
SELECT setweight(to_tsvector($3::regconfig, $1), 'A') || setweight(to_tsvector($3::regconfig, $1), 'B');
$BODY$
LANGUAGE sql
IMMUTABLE; CREATE INDEX idx_fts_post ON post USING gin(gin_fts_fct(title, content, language));

选择GIN还是GiST索引?这要根据你的读写量以及数据量来决定

As a rule of thumb, GIN indexes are best for static data because lookups are faster. For dynamic data, GiST indexes are faster to update. Specifically, GiST indexes are very good for dynamic data and fast if the number of unique words (lexemes) is under 100,000 while GIN indexes will handle 100,000+ lexemes better but are slower to update.

-- Postgres doc : Chap 12 Full Text Search

在这个例子中,我们将采用GIN。但是你可以根据你的情况决定使用什么数据。在document中,我们还有一个问题。document是跨表的,且各表字段拥有不同权重。为了得到更好的性能,我们通过triggers或者materialized view的方式进行数据重组。不过您不需要总是重组数据。在一些情况下,您可以加一些基于index的函数。还可以通过tsvector_update_trigger(...)或者tsvector_update_trigger_column(...)来重组数据。查看文档来获取更多细节。另外如果可以接受短暂延迟的话。用Materialized View是一个不错的选择。

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 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

然后可以通过REFRESH MATERIALIZED VIEW search_index;的方式来重建索引。

首先我们建立索引

CREATE INDEX idx_fts_search ON search_index USING gin(document);

然后查询也会变得更加简单

SELECT id as post_id, title
FROM search_index
WHERE document @@ to_tsquery('english', 'Endangered & Species')
ORDER BY ts_rank(p_search.document, to_tsquery('english', 'Endangered & Species')) DESC;

Mispelling

英文没啥用, 中文的分词不知道实现了pg_trgm没有。。

总结

Posgres全文搜索功能还是相当棒和快的。有了它,你就不用装其他的搜索引擎了。Posgres像不像一颗幸福的子弹。。。也许不是,如果您的业务围着搜索转啊转的话。

有一些功能我没讲,但是您基本上也用不到。以我个人而言,我希望Posgres的全文搜索还有一些功能能够加进来。

  • 更多的内置语言
  • 和Lucene搞基
  • 更好的排名和权重分配解决方案
  • 模糊查询再叼一点

    总的来说,Posgres再全文搜索上肯定是没有ElasticSearch和SOLR那么先进。。。

Postgres全文搜索功能的更多相关文章

  1. 使用ElasticSearch服务从MySQL同步数据实现搜索即时提示与全文搜索功能

    最近用了几天时间为公司项目集成了全文搜索引擎,项目初步目标是用于搜索框的即时提示.数据需要从MySQL中同步过来,因为数据不小,因此需要考虑初次同步后进行持续的增量同步.这里用到的开源服务就是Elas ...

  2. 如何在Web前端实现CAD图文字全文搜索功能之技术分享

    现状 在CAD看图过程中我们经常会需要用到查找文字的功能,在AutoCAD软件查找一个文字时,可以通过打开左下角输入命令find,输入查找的文字,然后设置查找范围,就可以搜索到需要查询的文字.但在We ...

  3. GoldenDict词典的超级实用高级玩法----全文搜索功能

    快捷键: Ctrl+Shift+F 菜单进入:搜索--全文搜索 模式:正则表达式 比如:我想知道地道的英文表达    请xx天/周/年假 给搜索框写入正则表达式   请(.)(天|周|年)假 就可以了 ...

  4. 为Jekyll+GitHub Pages添加全文搜索功能

    动态演示如下: [上传失败, 请自行搜索原文] 源码库: program-in-chinese/team_website 找到此JS工具: christian-fei/Simple-Jekyll-Se ...

  5. 使用ElasticSearch实现搜索时即时提示与全文搜索功能

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  6. 08: Django使用haystack借助Whoosh实现全文搜索功能

    参考文章01:http://python.jobbole.com/86123/ 参考文章02: https://segmentfault.com/a/1190000010866019 参考官网自定制v ...

  7. MySQL 全文搜索支持, mysql 5.6.4支持Innodb的全文检索和类memcache的nosql支持

    背景:搞个个人博客的全文搜索得用like啥的,现在mysql版本号已经大于5.6.4了也就支持了innodb的全文搜索了,刚查了下目前版本号都到MySQL Community Server 5.6.1 ...

  8. 如何在MySQL中获得更好的全文搜索结果

    如何在MySQL中获得更好的全文搜索结果 很多互联网应用程序都提供了全文搜索功能,用户可以使用一个词或者词语片断作为查询项目来定位匹配的记录.在后台,这些程序使用在一个SELECT 查询中的LIKE语 ...

  9. SQL Server 全文搜索 配置、查询初体验

    原文:SQL Server 全文搜索 配置.查询初体验 一.使用SQL Server全文搜索配置 要使用SQL Server的全文搜索服务,需要进行如下配置. 1.开启全文搜索服务: 2.开启数据库的 ...

随机推荐

  1. iOS开发-程序的生命周期

    为了更好的管理程序,了解程序的生命周期是很有必要的. 运行结果: 1.首次启动: 2015-05-26 17:33:28.362 Live[4858:214241] 程序开始 2015-05-26 1 ...

  2. asp.net mvc 使用 Autocomplete 实现类似百度,谷歌动态搜索条提示框。

    Autocomplete是一个Jquery的控件,用法比较简单. 大家先看下效果: 当文本框中输入内容,自动检索数据库给出下拉框进行提示功能. 需要用此控件大家先到它的官方网站进行下载最新版本: ht ...

  3. 在Python中用Request库模拟登录(四):哔哩哔哩(有加密,有验证码)

    !已失效! 抓包分析 获取验证码 获取加密公钥 其中hash是变化的,公钥key不变 登录 其中用户名没有被加密,密码被加密. 因为在获取公钥的时候同时返回了一个hash值,推测此hash值与密码加密 ...

  4. SpringData ES中一些底层原理的分析

    之前写过一篇SpringData ES 关于字段名和索引中的列名字不一致导致的查询问题,顺便深入学习下Spring Data Elasticsearch. Spring Data Elasticsea ...

  5. Java 面试知识点解析(六)——数据库篇

    前言: 在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Java 知识点进行复习和学习一番,大 ...

  6. oracle+mybatis 使用动态Sql在要insert的字段不确定的情况下实现批量insert

    最近做项目遇到一个挺操蛋的问题,由于业务的关系,DB的数据表无法确定,在使用过程中字段可能会增加,这样在insert时给我造成了很大的困扰. 先来看一下最终我是怎么实现的: <insert id ...

  7. Django的ORM操作

    ORM操作 select * from tb where id > 1 # 对应关系 models.tb.objects.filter(id__gt=1) models.tb.objects.f ...

  8. C++中虚拟继承

    多重继承 在多重继承中,基类的构造函数的调用次序即不受派生类构造函数初始化列表中出现的基类构造函数的影响,也不受基类在构造函数初始化列表中的出现次序的影响,它按照基类在类派生列表中的出现次序依次调用相 ...

  9. C++11中map的用法

    最全的c++map的用法 1. map最基本的构造函数:map<string ,int>mapstring; map<int,string >mapint;map<sri ...

  10. 显著性检测(saliency detection)评价指标之KL散度距离Matlab代码实现

    步骤1:先定义KLdiv函数: function score = KLdiv(saliencyMap, fixationMap) % saliencyMap is the saliency map % ...