# 全文检索 · 数据库自带的搜索引擎

> PostgreSQL 内置全文检索，不必外挂 Elasticsearch:tsvector 分词 + 词干归一，tsquery 表达查询，@@ 匹配，ts_rank 打分，GIN 索引让百万文档亚秒响应。比 LIKE '%x%' 强在能词干归一、忽略停用词、按相关性排序。

- 作者：David（道雾轩）
- 专栏：PostgreSQL 特性详解（https://daiw.org/manual/postgresql.md）
- 最后更新：2026-07-13
- 原文：https://daiw.org/manual/postgresql/full-text-search
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 全文检索 · 数据库自带的搜索引擎

「站内搜索」几乎是每个应用都要的功能，很多人一上来就想到 Elasticsearch。但对相当一部分场景，PostgreSQL **内置的全文检索**就够了——省掉一个需要单独运维、还要和主库做数据同步的外部系统。

它凭什么比 `LIKE '%keyword%'` 强？`LIKE` 只会做字面子串匹配：搜 `search` 找不到 `searching`，搜到的结果也没有「哪条更相关」的排序，而且 `%` 开头的模糊匹配根本用不上索引，大表上就是全表扫描。全文检索把这些短板一次补齐：**词干归一、忽略停用词、相关性排序、还能走索引。**

## 两个核心类型：tsvector 与 tsquery

全文检索建立在两个类型上：

- **`tsvector`** —— 文档**分词 + 词形归一**后的结果：一串「词元(lexeme)+ 位置」。
- **`tsquery`** —— 查询表达式，支持 `&`（与）、`|`（或）、`!`（非）、`<->`（紧邻）等运算符。

用 `to_tsvector` 把正文变成检索向量，你会看到它做了什么：

```sql
SELECT to_tsvector('english', 'PostgreSQL JSONB is a binary JSON type with rich indexing');
-- 'binari':6 'index':11 'json':7 'jsonb':2,3 'postgresql':1 'rich':10 'type':8
```

留意几处「归一化」：`binary` → `binari`、`indexing` → `index`（词干提取）；`is` / `a` / `with` 这些**停用词被丢掉**了；每个词还记了在原文里的**位置**（`jsonb` 出现在第 2、3 位）。这就是它和字面匹配的本质差别——匹配的是「词」，不是「字符串」。

## @@ 匹配与 GIN 索引

用 `@@` 运算符匹配 `tsvector` 和 `tsquery`，再建 **GIN 索引**加速：

```sql
CREATE INDEX idx_tsv ON docs USING gin (tsv);

-- 同时含 indexes 和 json(注意 indexes 会被归一成 index)
SELECT title FROM docs WHERE tsv @@ to_tsquery('english', 'indexes & json');
-- -> PostgreSQL JSONB

-- 前缀 / 词根匹配:replicat:* 能命中 replication、replicate
SELECT title FROM docs WHERE tsv @@ to_tsquery('english', 'replicat:*');
```

GIN 在这里又一次登场（数组、JSONB 用的也是它）：它为每个词元建「词元 → 文档」的倒排表，所以 `@@` 查询直接查词元对应的文档列表，百万级文档也能亚秒响应。

## 词组查询：`<->` 紧邻

有时你要的不是「两个词都出现」，而是「两个词**挨着**出现」——比如搜 `execution plan` 这个词组，不想匹配到「execution ...（隔很远）... plan」。`<->` 运算符(FOLLOWED BY)干这个：

```sql
SELECT title FROM docs WHERE tsv @@ to_tsquery('english', 'execution <-> plan');
-- -> ['Query Planning']   ← 只匹配 execution 紧跟 plan 的文档
```

`tsvector` 里记的**位置**信息，正是为 `<->` 服务的——数据库知道两个词元在原文里的距离。

## 相关性排序与高亮

搜索的最后一步是「哪条最相关排最前」+「把命中词标出来」。`ts_rank` 打分、`ts_headline` 高亮：

```sql
SELECT title, ts_rank(tsv, q) AS score,
       ts_headline('english', body, q) AS snippet
FROM docs, to_tsquery('english', 'json & indexing') q
WHERE tsv @@ q ORDER BY score DESC;
```

```text
[0.061] PostgreSQL JSONB
        JSONB is a binary <b>JSON</b> type with rich <b>indexing</b>
[0.038] Indexes Deep Dive
        BRIN, GIN and GIST <b>indexes</b> for very different shapes of data
```

`ts_rank` 综合词频与位置给出相关性分数，`ts_headline` 自动截取命中片段并用 `<b>` 包裹关键词——一个像样的搜索结果页，数据库直接帮你拼好了。

## 让 tsvector 自动跟正文同步

正文会改，检索向量得跟着变。**PG 12 起推荐用生成列**（见 [生成列一章](https://daiw.org/manual/postgresql/range-generated)）自动维护，替代过去写触发器的做法：

```sql
ALTER TABLE docs ADD COLUMN tsv tsvector
  GENERATED ALWAYS AS (to_tsvector('english', body)) STORED;
-- body 一改,tsv 自动重算,永不脱节
```

## 中文怎么办

<Callout type="warn">
  内置的分词器**不会切中文**——中文没有空格，默认 parser 会把一整句当成一个词元。中文全文检索需要装分词扩展：**`zhparser`**（基于 SCWS）或 **`pg_jieba`**（结巴分词）。装好后 `to_tsvector('chinese', ...)` 才能正确分词。这是自建 PG 中文搜索绕不开的一步。近年也有 `pg_search`（基于 BM25）等新扩展在补齐中文与相关性排序的短板。
</Callout>

## 横向对比

| 能力 | PostgreSQL | MySQL `FULLTEXT` | Elasticsearch |
| --- | --- | --- | --- |
| 内置 | 是 | 是(InnoDB 5.6+) | 独立服务 |
| 词干归一 | 内置多语言词典 | 默认不做 | 强（分析器丰富） |
| 相关性打分 | `ts_rank` | 基础 | 强（BM25，可调） |
| 中文分词 | 需 `zhparser` / `pg_jieba` | `ngram` 解析器 | 需 IK 等插件 |
| 事务一致 | **与主库同事务** | 与主库同事务 | 需同步，有延迟 |
| 适合规模 | 中小到中大 | 中小 | 超大 / 复杂检索 |

<Callout type="info">
  怎么选？**搜索只是应用的一个附属功能、数据量中等、要和业务数据强一致**——用 PG 内置，省一个系统。**搜索是核心、要复杂的相关性调优 / 聚合分析 / 超大规模**——上 Elasticsearch。别一上来就 ES：很多项目的「站内搜索」用 PG 全文检索足矣，还免了主库到 ES 的同步延迟与一致性烦恼。
</Callout>

## 应用场景

- **站内搜索**：文章、商品、文档标题正文检索，`ts_rank` 排序 + `ts_headline` 高亮。
- **日志 / 工单检索**：结合 `tsvector` 生成列，按内容搜历史工单。
- **标签 / 关键词联想**：`prefix:*` 前缀匹配做搜索建议。
- **多字段加权**：用 `setweight` 给标题比正文更高权重，让标题命中排更前。

## 小结与下一章

PostgreSQL 的全文检索把「搜索引擎」的核心能力——分词、词干、停用词、相关性、高亮——做成了内置类型 + GIN 索引，足以覆盖大量「不必动用 Elasticsearch」的场景。中文场景记得装分词扩展。

查询表达力这一站走完。下一站进入**规模与性能**：当表从几千行长到几亿行，怎么让它依然查得快、管得动。第一课是选对索引——PostgreSQL 有六种，各有所长。👉 [索引类型大全：六种索引怎么选](https://daiw.org/manual/postgresql/indexes)。

## 参考文献

- [1] PostgreSQL 官方文档.「Chapter 12. Full Text Search.」https://www.postgresql.org/docs/current/textsearch.html —— `tsvector` / `tsquery` / 配置 / 索引全解。
- [2] PostgreSQL 官方文档.「12.3.3. GIN and GiST Index Types.」https://www.postgresql.org/docs/current/textsearch-indexes.html —— 全文检索的索引选择。
- [3] PostgreSQL 官方文档.「9.13. Text Search Functions and Operators.」https://www.postgresql.org/docs/current/functions-textsearch.html —— `@@`、`ts_rank`、`ts_headline`、`<->`。
