Postgres 文本搜索指南 2026
数据库中已有的搜索工具,以及各自发挥价值的时机。
大多数团队只使用一种 Postgres 搜索工具。而掌握全部三种工具的团队,能用更少的复杂度交付更好的搜索——并且避免过早转向一个他们其实还不需要的专用搜索服务,从而省下高昂的绕路成本。
本指南涵盖 Postgres 原生选项的完整集合:每种工具的功能、适用场景,以及如何将它们分层组合。
三种工具
全文搜索(tsvector / GIN 索引)是词法层面的。它将文本切分为词素(lexeme),进行词干提取,然后根据索引匹配查询。“Running” 和 “runs” 归并到同一个词素,“dog” 和 “dogs” 也是如此。排序函数(ts_rank)会奖励查询词出现频率高或位置显著的文档。
三元组(pg_trgm)将字符串拆分为重叠的 3 字符片段,并衡量两个字符串共享的片段比例。“Dan” → " da", "dan", "an "。“Micheal” 和 “Michael” 共享大部分三元组,因此相似度很高。这使得 pg_trgm 在模糊姓名匹配、拼写容错和自动补全方面表现出色——这正是全文搜索表现不佳的领域。
精确匹配索引(B-tree、hash)处理主键、电子邮件地址、ID、SKU 以及任何答案是二元的场景:匹配或不匹配。这些看起来不像“搜索”,但它们值得纳入讨论,因为最糟糕的模式是用模糊或语义搜索去解决那些有正确答案的问题。
选择的关键不在于复杂度,而在于将工具与查询的形状相匹配。
全文搜索的适用场景
对散文进行关键词搜索。 博客文章、文档、产品描述、支持工单、法律文档。全文搜索正是为这种内容形态设计的:对自然语言文本进行索引、排序和检索。
基于关键词的用户查询。 用户输入搜索词、按标签过滤或按关键词浏览。全文搜索原生支持这种意图,无需任何嵌入基础设施。
无需外部依赖的排序结果。 全文搜索索引速度快、确定性强,且无需 API 调用。相关性信号来自词频,并按字段位置加权。
搜索与布尔过滤的组合。 全文搜索能自然地与现有查询逻辑结合:
SELECT * FROM postsWHERE search_vector @@ to_tsquery('english', 'postgres & performance') AND category = 'tutorial' AND published_at > NOW() - INTERVAL '6 months';设置全文搜索
-- 生成列自动保持索引最新ALTER TABLE posts ADD COLUMN search_vector tsvector GENERATED ALWAYS AS ( setweight(to_tsvector('english', coalesce(title, '')), 'A') || setweight(to_tsvector('english', coalesce(body, '')), 'B') ) STORED;
CREATE INDEX posts_search_idx ON posts USING GIN (search_vector);
-- 查询SELECT title, ts_rank(search_vector, query) AS rankFROM posts, to_tsquery('english', 'postgres & performance') queryWHERE search_vector @@ queryORDER BY rank DESCLIMIT 10;setweight 分配权重:A(标题)的排序优先级高于 B(正文)。对于大多数内容搜索场景,这就是完整的相关性模型。
全文搜索不擅长的领域
- 查询中的拼写错误 —— “javascipt” 不会匹配 “javascript”
- 人名、地址、专有名词,这些词的词干化结果不可预测
- 没有特殊配置时,前缀/自动补全无法工作
- 用户描述概念而非直接命名的查询
三元组(pg_trgm)的适用场景
pg_trgm 覆盖了全文搜索经常搞不定的尴尬中间地带。
全文搜索将文本切分为词素并做词干化。对于散文这是正确的。但对于名称和短标识符,往往不是这样:
- 人名(“Dan Levy” → 根据字典和语言配置的不同,词干化结果也不同)
- 公司名、地址、产品标题,这些场景下精确拼写很重要
- 带拼写错误的查询 —— “Micheal Jordan”、“Amaon”、“javascipt”
- 自动补全 / 前缀搜索
- 部分字符串匹配(“son” 匹配 “Johnson”、“Anderson”)
pg_trgm 还与语言无关,这对于来自不同语言背景的名称很重要。全文搜索需要为每种语言配置字典。
模糊名称搜索
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX users_name_trgm_idx ON users USING GIN (name gin_trgm_ops);
-- 搜索 "Michael Jordan" 时也能找到 "Micheal Jordan"SELECT id, name, similarity(name, $1) AS scoreFROM usersWHERE name % $1 -- % 运算符 = 相似度阈值(默认 0.3)ORDER BY score DESCLIMIT 10;% 运算符使用 pg_trgm.similarity_threshold(默认 0.3,范围 0–1)。对于名称搜索,0.3–0.4 能捕捉拼写错误,同时保持较低的噪声。
自动补全、前缀搜索和包含搜索
-- 自动补全的前缀匹配。三元组 GIN 索引可以帮忙,-- 但纯左锚定前缀场景下,B-tree 模式索引可能更好。SELECT name FROM usersWHERE name ILIKE $1 || '%'ORDER BY nameLIMIT 10;
-- word_similarity 用于较长字符串内的部分匹配-- ("Johnson" 在 "Andrew Johnson III" 中)SELECT id, name, word_similarity($1, name) AS scoreFROM usersWHERE $1 <% nameORDER BY score DESCLIMIT 10;三元组 GIN 索引对于 ILIKE '%pattern%' 包含查询和容忍拼写错误的匹配特别有用——这些模式在没有三元组索引时通常会导致全表扫描。
何时使用 pg_trgm 而非全文搜索
| 场景 | 使用 |
|---|---|
| 带拼写错误的人名/公司名搜索 | pg_trgm |
| 自动补全 / 前缀搜索 | pg_trgm(或带前缀查询的全文搜索) |
| 短字符串、标识符、代码 | pg_trgm |
| 散文类文章、文档、工单 | 全文搜索 |
| 日志消息中的关键词搜索 | 全文搜索 |
| 多语言名称搜索 | pg_trgm(与语言无关) |
精确匹配 SQL 的适用场景
有些“搜索”问题根本就不是搜索。
“查找邮箱为 dan@example.com 的用户”是一个等值检查。“查找订单 ORD-12345”是一个主键查找。“按日期排序列出 tutorial 分类下的文章”是一个带过滤条件的查询。这些应该使用 B-tree 或哈希索引。
在这里使用全文搜索或三元组会增加复杂性,但不会提高正确性——对于精确标识符,近似匹配比没有匹配更糟糕。
CREATE INDEX users_email_idx ON users (email);
-- 精确查找:快速且无歧义SELECT id, name FROM users WHERE email = $1;更广泛的教训:对存在正确答案的问题使用近似搜索,属于范畴错误。它会返回某些结果——但这些结果可能自信地给出错误答案。
组合使用这些工具
这些工具可以干净地组合。你不必只选一个。
FTS + pg_trgm:用于容忍关键词拼写错误的搜索框
-- 标题上的三元组相似度捕捉拼写错误;ts_rank 处理正文相关性SELECT id, title, ts_rank(search_vector, to_tsquery('simple', $1)) AS fts_rank, similarity(title, $1) AS trgm_scoreFROM postsWHERE search_vector @@ to_tsquery('simple', $1) OR title % $1ORDER BY (ts_rank(search_vector, to_tsquery('simple', $1)) + similarity(title, $1)) DESCLIMIT 10;FTS + unaccent:用于国际化内容
-- 去除变音符号,使 "José" 能匹配 "Jose"CREATE EXTENSION IF NOT EXISTS unaccent;
CREATE TEXT SEARCH CONFIGURATION public.simple_unaccent (COPY = pg_catalog.simple);
ALTER TEXT SEARCH CONFIGURATION public.simple_unaccent ALTER MAPPING FOR hword, hword_part, word WITH unaccent, simple;
ALTER TABLE posts ADD COLUMN search_vector tsvector;
CREATE TRIGGER posts_search_vector_refreshBEFORE INSERT OR UPDATE OF title, body ON postsFOR EACH ROW EXECUTE FUNCTION tsvector_update_trigger(search_vector, 'public.simple_unaccent', title, body);unaccent + pg_trgm:用于国际化姓名搜索
ALTER TABLE users ADD COLUMN name_search text;
CREATE FUNCTION users_name_search_refresh()RETURNS trigger LANGUAGE plpgsql AS $$BEGIN NEW.name_search := unaccent(coalesce(NEW.name, '')); RETURN NEW;END;$$;
CREATE TRIGGER users_name_search_refreshBEFORE INSERT OR UPDATE OF name ON usersFOR EACH ROW EXECUTE FUNCTION users_name_search_refresh();
CREATE INDEX users_name_search_trgm_idx ON users USING GIN (name_search gin_trgm_ops);
SELECT id, nameFROM usersWHERE name_search % unaccent($1)ORDER BY similarity(name_search, unaccent($1)) DESCLIMIT 10;触发器示例避免了在生成列或索引表达式中使用 unaccent(),因为 PostgreSQL 的不可变性规则在此处很重要。如果你将 unaccent() 封装在自己定义的不可变函数中,请记录你正在接受升级/配置风险。
值得注意的扩展
pg_trgm 随大多数 Postgres 发行版捆绑,但需要显式启用。它是 Postgres 中模糊字符串匹配的基础。
unaccent 在索引和查询前去除变音符号。与 pg_trgm 和 FTS 配合良好,适用于欧洲语言内容。随 Postgres 捆绑。
pg_bigm 将三元组方法扩展为二元组(2 字符切片),显著改善了 CJK(中文、日文、韩文)语言的结果,因为 pg_trgm 在这些语言上表现不佳。需要单独安装,不捆绑。
pg_search(来自 ParadeDB)用基于 Tantivy 的 BM25 索引替代了标准的 GIN / tsvector 栈。它提供了 BM25 评分(通常优于 ts_rank)、FTS 查询内的模糊匹配、分面搜索,以及在大表上显著更快的索引速度。当标准 FTS 开始出现排名或性能瓶颈时,这是一个可插拔的升级路径。
-- pg_search:带模糊匹配的 BM25 全文搜索CREATE INDEX posts_bm25_idx ON posts USING bm25 (id, title, body) WITH (key_field = 'id', text_fields = '{"title": {}, "body": {}}');
-- 使用 BM25 评分 + 模糊匹配查询(捕捉 "javascipt")SELECT id, title, paradedb.score(id) AS rankFROM postsWHERE posts @@@ paradedb.fuzzy_phrase(field => 'title', value => 'postgres performnce')ORDER BY rank DESCLIMIT 10;pgvector 增加了稠密向量存储和相似性搜索。当用户描述他们想要什么而不是直接命名时,它是正确的工具——语义搜索、RAG、相关内容推荐、多语言查询。在语义向量搜索与混合策略中有深入介绍。
决策表
| 搜索内容 | 推荐工具 |
|---|---|
| 散文类文章、文档、工单 | FTS |
| 带拼写错误的人名/公司名 | pg_trgm |
| 自动补全、前缀搜索 | pg_trgm |
| 短代码、标识符 | pg_trgm |
| 日志消息中的关键词 | FTS |
| 国际化名称 | pg_trgm + unaccent |
| 大量内容、更好的排名 | pg_search(ParadeDB BM25) |
| 主键、精确邮箱、ID | B-tree 索引 |
| 日期、范围、排序列表 | B-tree 索引 |
| 权限、分类、过滤器 | 普通 WHERE 子句 |
| 问题、释义、概念 | pgvector(见下一篇文章) |
不确定时:短字符串带拼写变体 → 三元组。长文本关键词查询 → FTS。结构化标识符 → 常规索引。概念性或自然语言查询 → pgvector。
混合搜索:两个信号,一个排名
当像 "withRetry timeout errors" 这样的查询进入搜索框时,它携带两种意图:用户知道的确切符号名(withRetry)和概念性描述(timeout errors)。没有单一原语能同时覆盖两者。并行运行 FTS 和向量搜索——然后通过倒数排名融合(Reciprocal Rank Fusion)合并它们的排名列表——可以做到。
RRF 对每个列表中的每个结果评分 1 / (60 + rank),并在列表间求和。常数 60 削弱了顶部排名的优势,因此一个在两个列表中都排第二的结果可能击败一个在一个列表中获胜而完全错过另一个列表的结果。关键在于,RRF 从不跨方法平均原始分数——FTS 排名和余弦距离是不同货币,不能算术组合。
-- Hybrid search: FTS + pgvector merged with RRFWITH fts AS ( SELECT id, ts_rank(search_vector, query) AS score, ROW_NUMBER() OVER (ORDER BY ts_rank(search_vector, query) DESC) AS rank FROM docs, to_tsquery('english', 'withRetry & timeout') query WHERE search_vector @@ query LIMIT 60),vec AS ( SELECT id, ROW_NUMBER() OVER (ORDER BY embedding <=> $embedding) AS rank FROM docs ORDER BY embedding <=> $embedding LIMIT 60)SELECT COALESCE(fts.id, vec.id) AS id, (COALESCE(1.0 / (60 + fts.rank), 0) + COALESCE(1.0 / (60 + vec.rank), 0)) AS rrf_scoreFROM fts FULL JOIN vec ON fts.id = vec.idORDER BY rrf_score DESCLIMIT 10;每个分支的 60 个文档候选池(LIMIT 60)是一个常见起点。如果召回率低则扩大;为了速度则缩小。
下一步
Postgres 文本搜索覆盖了很多领域,但它有一个天花板。当用户描述他们想要什么而不是命名它——“帮助我在飞机上睡觉的东西”,“关于作为新工程师调试信心的文章”——词汇和三元组搜索都失败。
那是向量嵌入、语义搜索和混合架构的领域。在语义向量搜索与混合策略中介绍。