← 中文资源

简体中文版本

用全文检索替代 LIKE 全表扫描

建立生成式 tsvector 列并用 GIN 索引,改用 websearch_to_tsquery 与相关度排序,取代前导通配符 LIKE。

阅读英文原文

概述

col ILIKE '%term%' 会让 PostgreSQL 检查每一行,因为 B-tree 索引按前缀有序,前导通配符无法利用。大表上的搜索框因此成为系统里最贵的查询,而且内容越多越慢。全文检索换了一种思路:把文本归一化为词元,存入 tsvector,再用 GIN 索引,使匹配开销与命中数量成正比而不是与表大小成正比。

步骤

  1. 增加生成式 tsvector 列,并给标题更高权重:
ALTER TABLE articles
  ADD COLUMN search_document tsvector
  GENERATED ALWAYS AS (
    setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
    setweight(to_tsvector('english', coalesce(body, '')), 'B')
  ) STORED;
  1. 建立 GIN 索引:CREATE INDEX articles_search_idx ON articles USING gin (search_document);
  2. 使用 websearch_to_tsquery 接受用户输入,它支持引号短语、or 和前置减号,不会因标点报语法错误。
  3. 用 ts_rank 排序并限制返回条数,再用 ts_headline 只对当前页生成摘要。

验证

查询 postgres index tuning 应返回按相关度排序的前 20 条,EXPLAIN (ANALYZE, BUFFERS) 中应出现 Bitmap Index Scan on articles_search_idx,buffer 读取量与命中数相关而不是与表大小相关。摘要查询应只返回一个带 <mark> 标签的片段。

注意事项

  • 全文检索匹配词元而非子串,post 不会命中 postgres,需要前缀查询或 pg_trgm 支持拼写容错。
  • english 配置会做词干化,代码标识符建议用 simple 配置单独索引,再用 OR 合并查询。
  • 添加 STORED 生成列会重写整张表,大表需安排在维护窗口或改为分批回填加触发器维护。
  • ts_headline 需要重新解析文档,开销较高,只对返回页生成,不要对所有命中行调用。
  • 生成列会增加写入成本,多语言混排内容需要按行选择配置或拆分列。