PostgreSQL 全文搜索实战(TypeScript + Drizzle + Bun)
不用引入 Elasticsearch——PostgreSQL 内置 tsvector 全文搜索在百万级数据上毫秒响应。本文用 Bun + Drizzle ORM + tsvector 构建完整搜索方案,含中文分词注意事项与性能优化。
PostgreSQL 的 tsvector/tsquery 提供数据库原生全文搜索:给文本列建 tsvector 生成列 + GIN 索引,@@ plainto_tsquery() 匹配查询,百万文档毫秒响应。相比 ILIKE 模糊匹配,它支持分词、相关性排序与高亮——零额外基础设施。
为什么先考虑数据库原生方案
提到搜索就想到 Elasticsearch/Algolia 是惯性思维。PG 全文搜索的优势:数据不出库(无同步管道、无一致性问题)、毫秒级响应、与事务/权限体系一体。只有需要复杂相关度调优、聚合分析、多语言高级分词时才值得上专用搜索引擎。
ILIKE 为什么不够
SELECT * FROM posts WHERE content ILIKE '%关键词%';
ILIKE 的问题:全表扫描(索引用不上)、无分词("数据库系统"匹配不到"数据库")、无相关性排序。数据上万后体验崩塌。
tsvector 实战
-- 添加 tsvector 生成列并建索引
ALTER TABLE posts ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (to_tsvector('english', coalesce(title,'') || ' ' || coalesce(content,''))) STORED;
CREATE INDEX posts_search_idx ON posts USING GIN (search_vector);
-- 查询
SELECT title, ts_rank(search_vector, query) AS rank
FROM posts, plainto_tsquery('english', 'database performance') query
WHERE search_vector @@ query
ORDER BY rank DESC;
生成列自动维护(插入更新时重新分词),GIN 索引让查询走索引而非全表扫描,ts_rank 提供相关性评分。
TypeScript 侧集成(Drizzle)
import { sql } from 'drizzle-orm';
async function searchPosts(keyword: string) {
return db.execute(sql`
SELECT id, title,
ts_rank(search_vector, q) AS rank,
ts_headline('english', content, q) AS highlight
FROM posts, plainto_tsquery('english', ${keyword}) q
WHERE search_vector @@ q
ORDER BY rank DESC
LIMIT 20
`);
}
ts_headline 生成带 <b> 高亮标记的摘要片段——搜索结果的"关键词上下文"效果开箱就有。
中文搜索的特殊处理
PG 内置分词器按空格切词,对中文无效(整段文本成一个词)。中文场景方案:
- zhparser 扩展:基于结巴分词的 PG 扩展,自建 Postgres 可安装,创建中文配置
to_tsvector('zh', content); - pg_jieba:另一个中文分词扩展选择;
- 托管 PG 不支持自定义扩展时,退而求其次用
pg_trgm(三元组匹配,中文可用但相关度弱)或应用层分词后存关键词列。
性能要点
- GIN 索引是前提(百万级无索引照样慢);
- 生成列用 STORED(写入时计算,查询零开销);
- 大结果集配合
LIMIT+ 分页,rank 计算只发生在命中集上; - 写入极重的表注意 GIN 索引的写放大,可用 fastupdate 延迟合并平衡。
用实际查询验证数据访问方案
PostgreSQL 全文搜索实战(TypeScript + Drizzle + Bun)的选择最终要落到查询次数、SQL 耗时与连接占用。应用接入观测云 APM 时,先核对 SDK 是否支持实际使用的数据库驱动,不能因为使用了某个 ORM 就假设全部查询自动可见。未覆盖的查询可手动插桩;PostgreSQL 等服务端数据库另配实例采集。SQL 和绑定参数上报前须脱敏,再按相同接口比较 N+1 查询与慢查询的变化。
接入文档:Node.js APM 接入 · PostgreSQL 采集 · 日志采集。
常见问题(FAQ)
Q:PG 全文搜索能替代 Elasticsearch 吗?
中等规模(千万文档内)、标准搜索需求(关键词+排序+高亮)可以。ES 的不可替代性在:复杂相关度调优、聚合分析、跨集群水平扩展、成熟的拼音/纠错生态。从 PG 起步,瓶颈出现再迁 ES——架构演进的标准路径。
Q:怎么支持"拼音搜索"?
PG 层面没有内置方案。常见做法:应用层把标题转拼音存冗余列(title_pinyin),一并纳入 tsvector 内容;或专门的需求交给搜索引擎。
Q:Drizzle 里能建生成列吗?
Drizzle Schema 对生成列的支持在完善中,稳妥做法是迁移文件里手写 SQL(drizzle-kit generate 后编辑迁移文件),查询层用 sql 模板——控制力与类型安全兼得。
参考文档
本文依据公开文档整理,示例需按项目依赖版本验证;不将文档中的性能描述视为本项目实测结果。