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