1. 程式人生 > SQL入門教學 >實戰6:PostgreSQL全文檢索功能實戰

實戰6:PostgreSQL全文檢索功能實戰

1. 前言

本小節,我們一起來學習 PostgreSQL 中的一大殺器——FTS(Full Text Search,全文檢索)。

提到全文搜尋,你是否立刻想到了大名鼎鼎的LuceneElasticsearch。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 流程

全文搜尋大致可分為兩部分:

  1. 構建文字對應的索引(倒排索引)
  2. 通過搜尋索引來找到對應的文字

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

這個例子表示,搜尋的內容必須包含sqljava與python中的一種。

3.4 搜尋關鍵句向量化

當然你也可以使用句子來搜尋:

SELECT * FROM to_tsquery('jiebacfg','SQL難道不香嗎?');
        to_tsquery
---------------------------
 'sql' & '難道' & '不香嗎'

在輸入句子的情況下,to_tsquery會自動將句子分詞,然後將其拼接為tsquery

3.5 FTS 總結

我們總結一下 FTS 的使用:

  1. 原始文字,即字串不能被直接搜尋,我們通過 to_tsvector 函式將其向量化為片語,並儲存到某個欄位中,該欄位資料型別為 tsvector。
  2. tsvector 的欄位儲存的是詞與詞序列的元組,需要新建 gin 索引才能使用搜索,下面會介紹。
  3. 搜尋條件,狹義上可以理解成搜尋關鍵字,也需要通過 to_tsquery 來向量化,且型別為 tsquery。
  4. 使用 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我們將每篇文章的titlecontent分別向量化。

由於titlecontent的重要性不一樣,文章的標題明顯比內容資料更加重要,因此setweight設定標題的權重為A,而內容的權重為BA的重要性大於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或許更加適合你。