实战6:PostgreSQL 全文检索功能实战
1. 前言
本小节,我们一起来学习 PostgreSQL 中的一大杀器——FTS
(Full Text Search,全文检索)。
提到全文搜索,你是否立刻想到了大名鼎鼎的Lucene
和Elasticsearch
。Elasticsearch 基于 Lucene ,并为开发者提供丰富的接口和工具,但是这也造成了它日益庞大。
使用它,你得备上一个大的服务器,一个优秀的运维团队,还要承受数据同步的心智负担。但你的需求其实很简单,只是一个小功能搜索,或者一个简单的全站搜索。如果在项目的初期,花费如此大成本在搜索上有些得不偿失。
如果数据库本身就支持全文检索,那该多好啊!没错,PostgreSQL 就支持全文搜索,而且很强大,还支持插件扩展定制。
2. FTS配置库
2.1 PostgreSQL 默认 FTS
PostgreSQL 全文搜索是通过 FTS 配置库来支持的,大多数 PostgreSQL 发行版都自带了 10 个以上的 FTS 配置库,我们可以通过psql
的\dF
命令来查看已安装的配置库:
List of text search configurations
Schema | Name | Description
------------+------------+--------------------------------------------
pg_catalog | arabic | configuration for arabic language
pg_catalog | danish | configuration for danish language
pg_catalog | dutch | configuration for dutch language
pg_catalog | english | configuration for english language
pg_catalog | finnish | configuration for finnish language
pg_catalog | french | configuration for french language
pg_catalog | german | configuration for german language
pg_catalog | hungarian | configuration for hungarian language
.......
可以看到 PostgreSQL 默认已经安装了大量的 FTS 搜索配置库,但是很不幸没有中文配置库
。但好在,PostgreSQL 支持插件的形式来扩展 FTS,所以我们可以使用成熟的扩展库。
2.2 pg_jiebe FTS
jieba
是国内一个颇为著名分词库,如果你是 Python 开发者,那么一定听过它的大名。有贡献者为 PostgreSQL 提供了 jieba 分词插件——pg_jieba,让我们可以在 PostgreSQL 使用到中文全文检索。
如果你想跟着我们一起,完成本节的实战内容,那么请先点开此链接安装 pg_jieba。
如果你安装成功,那么可以通过\dF
命令来找到jieba
相关的分词配置:
public | jiebacfg | Mix segmentation configuration for jieba
public | jiebahmm | Hmm segmentation configuration for jieba
public | jiebamp | MP segmentation configuration for jieba
public | jiebaqry | Query segmentation configuration for jieba
可以看到jieba
提供了4
种分类器,它们分别对应了不同的分词算法,如果你感兴趣,可以查阅相关的资料,这里我们不做过多的介绍,默认使用jiebacfg
即可。
3. 基本使用
3.1 FTS 流程
全文搜索大致可分为两部分:
- 构建文本对应的索引(倒排索引)
- 通过搜索索引来找到对应的文本
3.2 文本向量化
在 FTS 中,原始文本在构建索引之前需要被向量化。原始文本(如:字符串)必须先被向量化后才能通过 FTS 对其检索,向量化后的内容需要存储到一个单独的向量字段中,该向量的数据类型是tsvector
。
PostgreSQL 提供了to_tsvector
函数来将原始文本向量化,如下:
SELECT * FROM to_tsvector('jiebacfg','SQL,你敢吃我俺老孙一棒吗?');
to_tsvector
-------------------------------------------
'sql':1 '一棒吗':9 '吃':5 '敢':4 '老孙':8
tsvector
是由(词,序列)
元组组成的列表,如sql
是原始文本中的第一个词,所以它的序列是1
。
3.3 搜索关键字向量化
有了索引后,我们如何来搜索索引了?
一般情况下,我们是通过关键词
来检索的,那么如何来组织关键词呢?
PostgreSQL 提供了to_tsquery
函数来将词组织成tsquery
向量,然后通过向量去搜索。如下:
SELECT to_tsquery('sql & java');
to_tsquery
----------------
'sql' & 'java'
tsquery
是一种特殊的数据类型,它会将关键词拼接来表示搜索条件,如&
表示搜索的内容必须包含sql和java
。举个复杂的例子:
SELECT to_tsquery('sql & (java | python)');
to_tsquery
-------------------------------
'sql' & ( 'java' | 'python' )
这个例子表示,搜索的内容必须包含sql
和java与python
中的一种。
3.4 搜索关键句向量化
当然你也可以使用句子来搜索:
SELECT * FROM to_tsquery('jiebacfg','SQL难道不香吗?');
to_tsquery
---------------------------
'sql' & '难道' & '不香吗'
在输入句子的情况下,to_tsquery
会自动将句子分词,然后将其拼接为tsquery
。
3.5 FTS 总结
我们总结一下 FTS 的使用:
- 原始文本,即字符串不能被直接搜索,我们通过 to_tsvector 函数将其向量化为词组,并保存到某个字段中,该字段数据类型为 tsvector。
- tsvector 的字段存储的是词与词序列的元组,需要新建 gin 索引才能使用搜索,下面会介绍。
- 搜索条件,狭义上可以理解成搜索关键字,也需要通过 to_tsquery 来向量化,且类型为 tsquery。
- 使用 tsquery 去搜索 tsvector,在下面的部分会介绍到。
4. 实践
接下来,我们以实践的角度来使用和学习一下 FTS。
4.1 文章搜索
假设某个应用有一个文章搜索
功能点,我们将通过 FTS 来实现它。
首先,我们新建文章数据表:
DROP TABLE IF EXISTS article;
CREATE TABLE article
(
id serial PRIMARY KEY,
title varchar(40),
content text
);
id
是每篇文章的唯一标识,title
是标题,content
是文章内容,我们省略了其它信息。然后我们插入几条记录:
INSERT INTO article(id, title, content)
VALUES (1, '科学和人文谁更有意义', '科学和人文谁更有意义,发生了会如何,不发生又会如何。 本人也是经过了深思熟虑,在每个日日夜夜思考这个问题。 一般来讲,我们都必须务必慎重的考虑考虑。 本人也是经过了深思熟虑,在每个日日夜夜思考这个问题。 马云曾经提到过,最大的挑战和突破在于用人,而用人最大的突破在于信任人。我希望诸位也能好好地体会这句话。 既然如此, 科学和人文谁更有意义,发生了会如何,不发生又会如何。 富勒在不经意间这样说过,苦难磨炼一些人,也毁灭另一些人。这启发了我, 塞内加曾经提到过,勇气通往天堂,怯懦通往地狱。这不禁令我深思。 '),
(2, '编程的艺术','对我个人而言,编程的艺术不仅仅是一个重大的事件,还可能会改变我的人生。 编程的艺术,到底应该如何实现。 伏尔泰曾经提到过,坚持意志伟大的事业需要始终不渝的精神。这似乎解答了我的疑惑。 既然如何, 生活中,若编程的艺术出现了,我们就不得不考虑它出现了的事实。 我们不得不面对一个非常尴尬的事实,那就是, 莎士比亚曾经说过,抛弃时间的人,时间也抛弃他。这启发了我, 编程的艺术因何而发生? 要想清楚,编程的艺术,到底是一种怎么样的存在。 编程的艺术的发生,到底需要如何做到,不编程的艺术的发生,又会如何产生。 既然如此, 那么。'),
(3, '生命在于创造','在这种困难的抉择下,本人思来想去,寝食难安。 带着这些问题,我们来审视一下生命在于创造。 我认为, 一般来说, 生命在于创造因何而发生? 可是,即使是这样,生命在于创造的出现仍然代表了一定的意义。 生命在于创造,到底应该如何实现。 问题的关键究竟为何? 生活中,若生命在于创造出现了,我们就不得不考虑它出现了的事实。 生命在于创造因何而发生? 莎士比亚曾经提到过,人的一生是短的,但如果卑劣地过这一生,就太长了。我希望诸位也能好好地体会这句话。');
4.2 构建文章索引
有了标题和内容后,我们需要为每篇文章单独新建一个字段fts
用来表示每篇文章的 tsvector 字段,并且给 fts 字段创建 gin 索引,这样后面就可以通过该字段来搜索文章了。
ALTER TABLE article ADD COLUMN fts tsvector;
UPDATE article
SET fts = setweight(to_tsvector('jiebacfg', title), 'A') ||
setweight(to_tsvector('jiebacfg', content), 'B');
CREATE INDEX article_fts_gin_index ON article USING gin (fts);
在 SQL 语句中,我们首先为article
数据表新增了一个fts
字段,字段类型为tsvector
。有了该字段后,我们需要为该字段赋值,通过to_tsvector
我们将每篇文章的title
和content
分别向量化。
由于title
和content
的重要性不一样,文章的标题明显比内容数据更加重要,因此setweight
设置标题的权重为A
,而内容的权重为B
,A
的重要性大于B
。||
操作符合并向量后将结果赋给fts
。
到此,article 表中新增了一个 fts 字段,字段中是标题和内容词组的列表。最后为 fts 字段我们新建了索引 article_fts_gin_index 来加速我们的搜索效率。
提示: || 操作符是 PostgreSQL 的一个特点,表示连接、合并。
4.3 使用 FTS
接下来,我们便可以使用全文搜索了,搜索条件是文章需包含问题
关键字,如下:
SELECT title FROM article WHERE fts @@ to_tsquery('问题');
title
----------------------
科学和人文谁更有意义
生命在于创造
PostgreSQL 提供@@
操作符来搜索,上面语句将问题
通过to_tsquery
转化为向量后,使用@@
来搜索。从结果中可以看出,与问题
相关的文章有两篇。
注意: 在 article 表中,只有 fts 是 tsvector 字段,因此只有它能使用 @@ 操作符。
我们再尝试一下复杂的搜索,搜索条件是文章必须含有问题
和生命
两个关键字:
SELECT title FROM article WHERE fts @@ to_tsquery('问题 & 生命');
title
--------------
生命在于创造
4.4 完善文章搜索
从结果中可以看到,全文搜索已经可以工作了,但它还不完备,如果更新或者添加文章,内容发生了改变,那么索引也应该随之变化,我们可以使用触发器来解决这个需求点。运行如下 SQL:
DROP TRIGGER IF EXISTS trig_article_insert_update ON article;
CREATE TRIGGER trig_article_insert_update
BEFORE INSERT OR UPDATE OF title,content
ON article
FOR EACH ROW
EXECUTE PROCEDURE tsvector_update_trigger(fts, 'public.jiebacfg', title, content);
有了 trig_article_insert_update 这个触发器后,article 表中插入或 title,content 的更新都会引起 fts 向量的重建,由此一个比较完备的全文检索功能点也就完成了。
我们的全文搜索实战到此就结束了,你完全可以按照这种模式改编成你自己的应用,让它支持炫酷的全文搜索功能。
5. 小结
PostgreSQL
的全文搜索的功能还是非常强大的,本节内容仅仅只是一部分,你可以阅读官方文档获取更多的信息。- 如果你需要强大的全文搜索功能以及数据分析能力,
Elasticsearch
或许更加适合你。