引言:為什么不用LIKE?
當(dāng)我們需要在文章、帖子或產(chǎn)品描述中搜索關(guān)鍵詞時,第一個蹦進(jìn)腦海的可能是SQL的LIKE
或ILIKE
操作符:
SELECT * FROM posts WHERE body LIKE '%postgres%';
這種方式簡單但問題很多:
性能極差:%postgres%
這種前導(dǎo)通配符無法利用索引,會導(dǎo)致全表掃描。
不夠智能:它只能進(jìn)行嚴(yán)格的字符匹配。搜索"run"
,不會找到"running"
或"ran"
。
沒有相關(guān)性排序:它只能告訴你“有”或“沒有”,無法告訴你哪條結(jié)果更相關(guān)。
而PostgreSQL內(nèi)置的全文搜索(Full-Text Search, FTS) 就是為了解決這些問題而生的。
一、核心概念:文檔、向量與查詢
PostgreSQL的FTS將搜索過程抽象為三步:
文檔(Document):待搜索的文本單元??梢允且黄恼?、一條評論,或者由多個字段組合而成的文本(如 title || ' ' || body
)。
向量(tsvector):對文檔進(jìn)行處理后的產(chǎn)物。它:
解析和分詞:將文本拆分成一個個獨(dú)立的 token(詞位)。
標(biāo)準(zhǔn)化:轉(zhuǎn)換為小寫。
去除停用詞:移除a
, the
, is
等常見但無搜索意義的詞。
提取詞干:將單詞變?yōu)槠湓~根形式(如 running
-> run
)。
最終存儲的是(詞位, 位置)
形式的列表。
查詢(tsquery):代表用戶要搜索的內(nèi)容。它也可以進(jìn)行詞干提取,并支持邏輯操作符(&
與, |
或, !
非)。
搜索的本質(zhì),就是判斷 @@
操作符兩邊的 tsvector
和 tsquery
是否匹配。
二、逐行詳解示例SQL
拆解以下SQL語句:
SELECT title, body
FROM posts
WHERE to_tsvector('english', body) @@ to_tsquery('english', 'postgres & optimization');
to_tsvector('english', body)
功能:生成一個 tsvector
。
參數(shù)1:'english'
:這是一個文本搜索配置(Text Search Configuration)。它決定了使用哪種語言的規(guī)則進(jìn)行分詞、停用詞處理和詞干提取。PostgreSQL支持多種語言(如simple
, english
, spanish
, russian
等)。配置的選擇直接影響搜索結(jié)果。
參數(shù)2:body
:需要被處理的文本字段。
結(jié)果示例:假設(shè)body
字段內(nèi)容是 "PostgreSQL provides powerful optimization tools."
,經(jīng)過處理后會變成: 'power':5 'optim':6 'postgresql':1 'provid':2 'tool':7
??梢钥吹?,provides
變成了provid
(詞干提?。?,the
被移除(停用詞)。
to_tsquery('english', 'postgres & optimization')
功能:生成一個 tsquery
。
參數(shù)1:'english'
:同樣需要指定配置,確保查詢詞和處理文檔時使用相同的規(guī)則(例如,optimization
也會被提取詞干為optim
)。
參數(shù)2:'postgres & optimization'
:查詢字符串。&
表示同時包含這兩個詞。
結(jié)果:一個代表包含詞干 'postgres' 且 包含詞干 'optim'
的查詢對象。
@@
操作符
三、超越基礎(chǔ):排名與高亮
僅僅找到匹配的文檔還不夠,我們通常需要最好的結(jié)果排在前面。
相關(guān)性排名(Ranking): 使用ts_rank
函數(shù)。
SELECT title, body,
ts_rank(to_tsvector('english', body),
to_tsquery('english', 'postgres & optimization')) AS rank
FROM posts
WHERE to_tsvector('english', body) @@ to_tsquery('english', 'postgres & optimization')
ORDER BY rank DESC;
ts_rank
會根據(jù)詞位出現(xiàn)的頻率、位置等因素計算一個相關(guān)性分?jǐn)?shù),然后通過ORDER BY rank DESC
將最相關(guān)的結(jié)果排在頂部。
結(jié)果高亮(Highlighting): 使用ts_headline
函數(shù)。
SELECT title,
ts_headline('english', body,
to_tsquery('english', 'postgres & optimization')) AS headline
FROM posts
WHERE ...;
ts_headline
會在結(jié)果中環(huán)繞匹配到的詞加上諸如<b>...</b>
這樣的HTML標(biāo)簽,讓用戶在上下文中一眼就看到為什么這條結(jié)果被匹配上了,體驗(yàn)堪比專業(yè)搜索引擎。
四、性能優(yōu)化:GiST/GIN 索引
在WHERE
子句中使用函數(shù)調(diào)用(如to_tsvector(...)
)通常會導(dǎo)致無法使用索引。為了讓全文搜索飛起來,必須創(chuàng)建專門的索引。
首選GIN索引,它專為這種“多值列”(一個tsvector
里包含很多個詞位)的查詢而優(yōu)化。
ALTER TABLE posts
ADD COLUMN body_tsvector tsvector
GENERATED ALWAYS AS (to_tsvector('english', COALESCE(body, ''))) STORED;
CREATE INDEX idx_posts_body_fts ON posts USING GIN(body_tsvector);
SELECT title, body
FROM posts
WHERE body_tsvector @@ to_tsquery('english', 'postgres & optimization');
五、與Elasticsearch的比較
特性 | PostgreSQL FTS | Elasticsearch |
---|
部署復(fù)雜度 | 零成本,內(nèi)置,無需額外部署 | 需要單獨(dú)部署和維護(hù)另一個分布式系統(tǒng) |
數(shù)據(jù)一致性 | 強(qiáng)一致,搜索和事務(wù)在同一數(shù)據(jù)庫,無延遲 | 最終一致,數(shù)據(jù)同步有延遲(取決于刷新間隔) |
功能豐富度 | 支持基礎(chǔ)到中級的需求(分詞、排名、高亮) | 極其豐富,專業(yè)的分布式搜索引擎,支持聚合分析、同義詞、拼音搜索等 |
性能與擴(kuò)展性 | 單機(jī)性能優(yōu)秀,可通過分片擴(kuò)展 | 為大規(guī)模分布式搜索和水平擴(kuò)展而生 |
結(jié)論:如何選擇?
總結(jié)
PostgreSQL的全文搜索是一個被嚴(yán)重低估的“隱藏寶石”。它提供了一個在** simplicity(簡單性)** 和 power(功能) 之間絕佳平衡的解決方案。對于許多不需要Elasticsearch這種“重武器”的應(yīng)用場景來說,它完全足夠且是更優(yōu)雅的選擇。
通過本文的介紹,希望你能在你的下一個項(xiàng)目中輕松上手這項(xiàng)強(qiáng)大功能!
討論點(diǎn):
大家在項(xiàng)目中是用PG的全文搜索還是ES?為什么做出這個選擇?
在處理中文全文搜索時,有什么好的配置或分詞方案推薦嗎?(這是一個常見痛點(diǎn),可以引出zhparser
等擴(kuò)展的討論)