PG 全文搜索实战(9):选型与避坑 · PG FTS vs Elasticsearch
本系列最后一篇。什么时候用 PG 全文搜索够了,什么时候该上 ES,以及一堆真实踩过的坑。
一、PG 全文搜索 vs Elasticsearch
| 维度 | PostgreSQL FTS | Elasticsearch |
|---|---|---|
| 运维成本 | 低(已有 PG,不加组件) | 高(额外集群、同步、监控) |
| 数据一致性 | 强(事务内,实时) | 最终一致(需同步,有延迟) |
| 中文分词 | 需装扩展,词库一般 | 生态成熟(IK 等) |
| 相关性/打分 | 基础够用 | 强大,可深度调优 |
| 聚合分析 | 弱 | 强(facet、聚合) |
| 海量 + 高并发搜索 | 单机瓶颈明显 | 天生分布式 |
| 高亮/纠错/联想 | 基础 | 丰富 |
二、决策建议(实战经验)
优先用 PG FTS,当:
- 数据本来就在 PG,量级在千万级以内(或分区后单区可控)。
- 搜索通常带过滤条件(时间、租户、分类),不是全库任意搜。
- 要求搜索结果和业务数据强一致、实时。
- 团队不想多养一套 ES。
该上 ES,当:
- 数据量上亿且需要全局任意关键词高并发检索。
- 需要复杂的相关性调优、聚合分析、搜索联想/纠错。
- 中文分词要求高,且 PG 装不了合适扩展(如云 RDS 受限)。
- 已经有分表几十上百张,跨表全局搜索用 PG 很吃力(见第 8 章)。
经验法则:别一上来就 ES。先评估 PG FTS + 分区能不能扛,扛不住或明显不合适再上 ES。很多“搜索需求”其实 PG 就够了,省下一套中间件的运维。
三、真实踩坑清单
1. NULL 导致 tsvector 整个为空
-- ❌ title 为 NULL 时,整个 tsvector = NULL,搜不到to_tsvector('english',title||' '||body)-- ✅ 用 coalesce 兜底to_tsvector('english',coalesce(title,'')||' '||coalesce(body,''))2. 建索引配置和查询配置不一致
建索引用english,查询用simple(或中文用了不同配置),结果匹配不上。两边必须完全一致。
3. 表达式索引没命中
对to_tsvector('english', title || ' ' || body)建的索引,查询表达式必须逐字一致(连拼接顺序、空格都要一样)才走索引。推荐用生成列避免这个坑。
4. 用户输入直接喂 to_tsquery 报错
to_tsquery要求合法语法,用户输入a & & b之类会抛异常。接收前端输入一律用websearch_to_tsquery。
5. ts_headline 拖垮查询
对全表做高亮 = 灾难。只对分页后的当前页做(见第 5 章)。
6. 小数据量 EXPLAIN 看不到用索引
数据少时 PG 认为全表扫更快,不用 GIN,这是正常的优化器行为,不是索引坏了。数据量上来自然会用。
7. 大表直接 ADD COLUMN GENERATED 锁表
亿级表一把梭会锁很久。先加空列、分批回填、CONCURRENTLY 建索引(见第 7 章)。
8. 改了词库/配置,老数据不生效
分词配置或自定义词典变更后,历史行的 tsvector 不会自动重算,需要 rebuild(重跑 UPDATE 或重建生成列)。
9. 云 RDS 装不了 zhparser
采购前确认云数据库是否支持中文分词扩展,否则中文搜索会很尴尬。
四、一句话总结全系列
PG 全文搜索 = tsvector(预计算落列)+ GIN 索引 + tsquery 匹配 + ts_rank 排名。
中小规模、数据在 PG、搜索带过滤条件的场景,它完全够用,且省一套 ES 的运维。数据到海量、要全局高并发检索和复杂分析时,再上 ES。
