DanLevy.net

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 以及任何答案是二元的场景:匹配或不匹配。这些看起来不像“搜索”,但它们值得纳入讨论,因为最糟糕的模式是用模糊或语义搜索去解决那些有正确答案的问题。

选择的关键不在于复杂度,而在于将工具与查询的形状相匹配。

Postgres 搜索工具地图按输入形状和查询意图比较 pg_trgm、全文搜索、pgvector 和混合搜索。根据输入形状选择搜索原语同一个 Postgres 表可以支持全部四种。关键在于将查询与文本匹配。精确词重要含义重要短 / 结构化文本长文本 / 段落模糊pg_trgm姓名、地址、标题、拼写错误、自动补全、部分字符串。正字法相似度:拼写距离。相似pgvector相关条目、重复工单、基于简短描述的推荐。嵌入相似度:含义距离。词法全文搜索文章、文档、日志、支持内容查询词应出现其中。词素、词干提取、排序、布尔过滤。混合FTS + pgvector技术文档和 RAG,用户既提概念性问题又涉及精确符号。同时运行两者,用 RRF 融合排序。从查询意图出发,再检查文本形状
按查询意图(精确 vs. 语义)和文本形状(结构化 vs. 散文)映射的四种 Postgres 搜索原语。同一张表可以承载全部四种索引——选择基于每次查询,而非每张表。

全文搜索的适用场景

对散文进行关键词搜索。 博客文章、文档、产品描述、支持工单、法律文档。全文搜索正是为这种内容形态设计的:对自然语言文本进行索引、排序和检索。

基于关键词的用户查询。 用户输入搜索词、按标签过滤或按关键词浏览。全文搜索原生支持这种意图,无需任何嵌入基础设施。

无需外部依赖的排序结果。 全文搜索索引速度快、确定性强,且无需 API 调用。相关性信号来自词频,并按字段位置加权。

搜索与布尔过滤的组合。 全文搜索能自然地与现有查询逻辑结合:

SELECT * FROM posts
WHERE 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 rank
FROM posts, to_tsquery('english', 'postgres & performance') query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 10;

setweight 分配权重:A(标题)的排序优先级高于 B(正文)。对于大多数内容搜索场景,这就是完整的相关性模型。

全文搜索不擅长的领域


三元组(pg_trgm)的适用场景

pg_trgm 覆盖了全文搜索经常搞不定的尴尬中间地带。

全文搜索将文本切分为词素并做词干化。对于散文这是正确的。但对于名称和短标识符,往往不是这样:

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 score
FROM users
WHERE name % $1 -- % 运算符 = 相似度阈值(默认 0.3)
ORDER BY score DESC
LIMIT 10;

% 运算符使用 pg_trgm.similarity_threshold(默认 0.3,范围 0–1)。对于名称搜索,0.3–0.4 能捕捉拼写错误,同时保持较低的噪声。

自动补全、前缀搜索和包含搜索

-- 自动补全的前缀匹配。三元组 GIN 索引可以帮忙,
-- 但纯左锚定前缀场景下,B-tree 模式索引可能更好。
SELECT name FROM users
WHERE name ILIKE $1 || '%'
ORDER BY name
LIMIT 10;
-- word_similarity 用于较长字符串内的部分匹配
-- ("Johnson" 在 "Andrew Johnson III" 中)
SELECT id, name, word_similarity($1, name) AS score
FROM users
WHERE $1 <% name
ORDER BY score DESC
LIMIT 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_score
FROM posts
WHERE search_vector @@ to_tsquery('simple', $1)
OR title % $1
ORDER BY (ts_rank(search_vector, to_tsquery('simple', $1)) + similarity(title, $1)) DESC
LIMIT 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_refresh
BEFORE INSERT OR UPDATE OF title, body ON posts
FOR 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_refresh
BEFORE INSERT OR UPDATE OF name ON users
FOR 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, name
FROM users
WHERE name_search % unaccent($1)
ORDER BY similarity(name_search, unaccent($1)) DESC
LIMIT 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 rank
FROM posts
WHERE posts @@@ paradedb.fuzzy_phrase(field => 'title', value => 'postgres performnce')
ORDER BY rank DESC
LIMIT 10;

pgvector 增加了稠密向量存储和相似性搜索。当用户描述他们想要什么而不是直接命名时,它是正确的工具——语义搜索、RAG、相关内容推荐、多语言查询。在语义向量搜索与混合策略中有深入介绍。


决策表

搜索内容推荐工具
散文类文章、文档、工单FTS
带拼写错误的人名/公司名pg_trgm
自动补全、前缀搜索pg_trgm
短代码、标识符pg_trgm
日志消息中的关键词FTS
国际化名称pg_trgm + unaccent
大量内容、更好的排名pg_search(ParadeDB BM25)
主键、精确邮箱、IDB-tree 索引
日期、范围、排序列表B-tree 索引
权限、分类、过滤器普通 WHERE 子句
问题、释义、概念pgvector(见下一篇文章)

不确定时:短字符串带拼写变体 → 三元组。长文本关键词查询 → FTS。结构化标识符 → 常规索引。概念性或自然语言查询 → pgvector。


混合搜索:两个信号,一个排名

当像 "withRetry timeout errors" 这样的查询进入搜索框时,它携带两种意图:用户知道的确切符号名(withRetry)和概念性描述(timeout errors)。没有单一原语能同时覆盖两者。并行运行 FTS 和向量搜索——然后通过倒数排名融合(Reciprocal Rank Fusion)合并它们的排名列表——可以做到。

RRF 对每个列表中的每个结果评分 1 / (60 + rank),并在列表间求和。常数 60 削弱了顶部排名的优势,因此一个在两个列表中都排第二的结果可能击败一个在一个列表中获胜而完全错过另一个列表的结果。关键在于,RRF 从不跨方法平均原始分数——FTS 排名和余弦距离是不同货币,不能算术组合。

使用倒数排名融合的混合搜索查询分发到全文搜索和向量搜索,每个生成排名,倒数排名融合将它们合并为一个结果列表。混合搜索是两个诚实的信号,然后合并为一个排名不要平均原始分数。FTS 排名和余弦距离是不同货币。用户查询”withRetrytimeout errors”FTS / BM25精确符号和单词1. API 参考2. 重试指南pgvector概念邻居1. 网络故障2. 重试指南RRF 合并为每个结果赋予其在每个列表中排名的信用。1 / (60 + rank)最终结果最佳命中是精确术语和语义含义一致之处。
查询并行分发到 FTS 和 pgvector。每个生成自己的排名列表。RRF 根据每个文档在每个列表中的位置评分并求和——结果展示两个信号都同意的文档。
-- Hybrid search: FTS + pgvector merged with RRF
WITH 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_score
FROM fts FULL JOIN vec ON fts.id = vec.id
ORDER BY rrf_score DESC
LIMIT 10;

每个分支的 60 个文档候选池(LIMIT 60)是一个常见起点。如果召回率低则扩大;为了速度则缩小。


下一步

Postgres 文本搜索覆盖了很多领域,但它有一个天花板。当用户描述他们想要什么而不是命名它——“帮助我在飞机上睡觉的东西”,“关于作为新工程师调试信心的文章”——词汇和三元组搜索都失败。

那是向量嵌入、语义搜索和混合架构的领域。在语义向量搜索与混合策略中介绍。