實戰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
。舉個復雜的例子:
('sql & (java | python)');
to_tsquery
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
或許更加適合你。