Skip to content

中文全文搜索方案 ​

上一节我们看到,PostgreSQL 内置了 30 种全文搜索配置,覆盖了英语、德语、法语、俄语等语言——但没有一种能正确处理中文。

这不是 PostgreSQL 的疏忽,而是中文的固有难度:英文靠空格分词,中文词与词之间没有分隔符,"数据库管理系统" 到底该切成 数据库 / 管理 / 系统 还是 数据 / 库 / 管理 / 系统,需要专门的分词算法和词典来判断。PostgreSQL 内置解析器做不到这一点。

先看它实际会做什么:

sql
SELECT to_tsvector('simple', ' PostgreSQL 是世界上最先进的开源关系型数据库');
text
                              to_tsvector
------------------------------------------------------------------------
 'postgresql':1 '世界上最先进的开源关系型数据库':3 '是':2

整句中文被当成了一个 token。 结果就是:搜 数据库 搜不到,搜 开源 也搜不到——只有把整句一字不差地贴进去才能命中。这样子的全文搜索等于没有。

前置知识

阅读本节前,建议先了解:全文搜索配置(Configuration)

基础概念 ​

为什么内置解析器搞不定中文 ​

语言特性英文中文
词边界空格 / 标点天然分隔无分隔符
分词方式按空白切分即可需要词典 + 算法(最大匹配 / HMM / CRF)
词形变化foxes → fox 需词干算法无词形变化,但有"的/了/是"等虚词
停用词the、is 等固定表需按分词结果过滤
同义词相对规范极丰富(电脑 = 计算机 = PC)
新词变化慢网络新词层出不穷,词库需持续更新

PostgreSQL 的内置解析器(default)是基于规则的:它识别空白、标点、字母、数字,然后把连续的非空白序列当成一个 token。对中文来说,一整串没有空格的汉字就是"一个 token"。

内置配置列里那些 chinese、japanese、korean 之类的名字并不存在——只有 simple 和一堆 Snowball 词干语言。上一节的配置列表已经验证了这一点。

四条可选路线 ​

方案原理优点缺点适用
pg_trgm三字符组模糊匹配零依赖、内置扩展、无需分词无法词干化、无相关度排序、索引大短文本模糊搜索、姓名/编号
zhparser基于 SCWS 的中文分词扩展与 tsvector 体系完全兼容,能用 GIN 索引、ts_rank、ts_headline需编译安装、词库较旧中文全文搜索首选
pg_jieba基于 jieba 的中文分词扩展词库现代、分词质量高需编译安装、维护者活跃度一般zhparser 的替代/补充
外部搜索引擎ES / MeiliSearch / OpenSearch分词质量最好、功能最全引入独立服务、数据同步复杂度高大规模、高要求的搜索场景

决策顺序建议:

text
需求是什么?
  │
  ├─ 只是"姓名/标题模糊匹配" ──────► pg_trgm(零成本)
  │
  ├─ 需要"中文全文检索 + 排序 + 高亮" ─► zhparser(推荐)
  │
  ├─ 对分词质量要求极高、愿意折腾 ───► pg_jieba
  │
  └─ 亿级文档、需要同义词/拼写纠错/聚合 ─► 外部搜索引擎

本节重点讲 zhparser(工程上最实用),并给出 pg_trgm 的中文兜底方案。

zhparser 是什么 ​

zhparser 是一个 PostgreSQL 扩展,它把 SCWS(Simple Chinese Word Segmentation,简易中文分词系统) 包装成 PostgreSQL 的全文搜索解析器。

这意味着它完美嵌入了上一节讲的三层架构:

text
原始中文文本
   │
   ▼
[zhparser 解析器]  ←── 替代默认解析器,输出 n/v/a/i/e/l 等中文词性标签
   │
   ▼
[自定义配置 chinese_zh]  ←── 把词性标签映射到词典
   │
   ▼
[simple 等词典]  ←── 小写归一、可选停用词
   │
   ▼
tsvector —— 与英文完全同构,可以用 GIN 索引、ts_rank、ts_headline

关键收益:所有上一节学到的技能全部适用。 索引怎么写、权重怎么加、排序怎么调,一个字都不用改。

zhparser 输出的 token 类型是词性标签,而不是 word:

token 类型含义例子
n名词(Noun)数据库、服务器
v动词(Verb)安装、配置
a形容词(Adjective)快速的
i习语(Idiom)一举两得
e叹词(Exclamation)哎呀
l习用语(Temporary idiom)总而言之
x标点/其他,、。
w符号@、#
u助词的、了
p介词在、从
c连词和、或者
m数词一百
t时间词今天
s处所词北京
nr人名张三
ns地名上海市
nt机构名清华大学
nz其他专名诺贝尔奖

所以自定义中文配置时,ADD MAPPING FOR 后面要写 n,v,a,i,e,l 这些词性标签,而不是 word。

语法与用法 ​

安装 zhparser ​

zhparser 不在 PostgreSQL 官方发行包里,需要从源码编译。前置条件是安装 SCWS。

第 1 步:安装 SCWS

bash
# 从 https://www.xunsearch.com/scws/ 下载源码包
tar -zxvf scws-1.2.3.tar.gz
cd scws-1.2.3

./configure --prefix=/usr/local
make && sudo make install

第 2 步:编译安装 zhparser

bash
git clone https://github.com/amutu/zhparser.git
cd zhparser

# 指定 pg_config 路径,确保装进正确的 PostgreSQL
make PG_CONFIG=$(which pg_config)
sudo make install
text
/usr/bin/install -c -m 755  zhparser.so '/usr/local/lib/postgresql/zhparser.so'
/usr/bin/install -c -m 644  zhparser.control '/usr/local/share/postgresql/extension/zhparser.control'
/usr/bin/install -c -m 644  zhparser--1.0.sql '/usr/local/share/postgresql/extension/zhparser--1.0.sql'

第 3 步:创建扩展

sql
CREATE EXTENSION zhparser;
text
CREATE EXTENSION

验证解析器已注册:

sql
\dFp
text
         List of text search parsers
   Schema   |   Name   | Description
------------+----------+-------------
 pg_catalog | default  |
 public     | zhparser |
(2 rows)

创建中文配置 ​

zhparser 只提供解析器,映射为空,必须自己建配置:

sql
CREATE TEXT SEARCH CONFIGURATION public.chinese_zh (PARSER = zhparser);

ALTER TEXT SEARCH CONFIGURATION public.chinese_zh
    ADD MAPPING FOR n,v,a,i,e,l WITH simple;

逐句解读:

语句作用
PARSER = zhparser指定用 zhparser 解析器(而不是 default)
ADD MAPPING FOR n,v,a,i,e,l把名词、动词、形容词、习语、叹词、习用语映射到词典
WITH simple用 simple 词典做小写归一(中文无所谓大小写,但保留原词)

为什么只映射 6 种? 这是社区的通行做法:只保留有检索意义的实词。的、了、和、, 这些虚词和标点如果也参与索引,会制造大量噪音,让搜索结果变差。

有些场景需要保留更多词性,例如人名、地名、机构名对搜索很有价值,可以额外加上 nr、ns、nt、nz:

sql
ALTER TEXT SEARCH CONFIGURATION public.chinese_zh
    ADD MAPPING FOR nr,ns,nt,nz WITH simple;

而虚词(u 助词、p 介词、c 连词)与标点(w)建议保持不映射,让它们直接被丢弃——这是中文全文搜索降噪的关键一步。

测试分词效果 ​

用 ts_debug 看 zhparser 实际切出了什么:

sql
SELECT alias, token, dictionary, lexemes
FROM ts_debug('public.chinese_zh', 'PostgreSQL 是世界上最先进的开源关系型数据库管理系统');
text
 alias |       token        | dictionary |       lexemes
-------+--------------------+------------+---------------------
 n     | postgresql         | simple     | {postgresql}
 v     | 是                 | simple     | {是}
 n     | 世界               | simple     | {世界}
 n     | 上                 | simple     | {上}
 a     | 最                 | simple     | {最}
 n     | 先进               | simple     | {先进}
 u     | 的                 |            |
 n     | 开源               | simple     | {开源}
 n     | 关系型             | simple     | {关系型}
 n     | 数据库             | simple     | {数据库}
 n     | 管理系统           | simple     | {管理系统}
(11 rows)

这就是成功的分词。 对比本节开头 simple 配置把整句吞成一个 token 的惨状,现在每个词都是独立的词位:

sql
SELECT to_tsvector('public.chinese_zh', 'PostgreSQL 是世界上最先进的开源关系型数据库管理系统');
text
                    to_tsvector
--------------------------------------------------
 'postgresql':1 '上':4 '世界':3 '关系型':9 '先进':6 '开源':8 '数据库':10 '管理系统':11 '最':5 '是':2
(1 row)

现在搜 数据库 能命中了:

sql
SELECT to_tsvector('public.chinese_zh', 'PostgreSQL 是世界上最先进的开源关系型数据库管理系统')
       @@ to_tsquery('public.chinese_zh', '数据库') AS hit1,
       to_tsvector('public.chinese_zh', 'PostgreSQL 是世界上最先进的开源关系型数据库管理系统')
       @@ to_tsquery('public.chinese_zh', '开源')   AS hit2;
text
 hit1 | hit2
------+------
 t    | t
(1 row)

注意 u(助词)类型的 的 因为没有映射,dictionary 和 lexemes 都是空——被有意丢弃了。

中文 tsquery 的注意事项 ​

to_tsquery 对中文的输入要求与英文一致——必须已经分好词:

sql
-- ✓ 单个词,正常
SELECT to_tsquery('public.chinese_zh', '数据库');
text
 to_tsquery
------------
 '数据库'
(1 row)
sql
-- ✗ 一整句中文传进去,zhparser 会把它切开,再被解释成布尔表达式 → 语法错误
SELECT to_tsquery('public.chinese_zh', '数据库 管理系统');
text
ERROR:  syntax error in tsquery: "数据库 管理系统"

正确做法是用 plainto_tsquery 或 websearch_to_tsquery,它们会先分词再用 & 连接:

sql
SELECT plainto_tsquery('public.chinese_zh', '数据库 管理系统') AS q1,
       websearch_to_tsquery('public.chinese_zh', '数据库 管理系统') AS q2;
text
              q1               |              q2
-------------------------------+-------------------------------
 '数据库' & '管理系统'          | '数据库' & '管理系统'

详细说明 ​

词库扩展与自定义词典 ​

zhparser 支持通过参数调整分词行为:

sql
-- 查看当前参数
SHOW zhparser.multi_short;
text
 zhparser.multi_short
----------------------
 off
(1 row)

常用参数:

参数默认值含义
zhparser.multi_shortoff是否启用短词复合切分(中华人民共和国 → 中华+人民+共和国+中华人民共和国)
zhparser.multi_duplicateon复合切分结果是否去重
zhparser.multi_zmainoff是否输出重要单字
zhparser.multi_zalloff是否输出全部单字
zhparser.punctuation_ignoreon是否忽略标点
zhparser.seg_with_dualityoff是否使用二元切分
zhparser.dict_in_memoryoff是否把词典全量加载进内存(提升性能、增加内存占用)

开启短词复合切分:

sql
SET zhparser.multi_short = on;
SELECT to_tsvector('public.chinese_zh', '中华人民共和国');
text
                                    to_tsvector
------------------------------------------------------------------------------------
 '中华':1 '人民':2 '共和国':3 '中华人民共和国':1
(1 row)

不开时只有长词 中华人民共和国 一个词位——用户搜"中华"会搜不到。这就是 multi_short 的价值,也是生产环境建议开启的原因。

新增自定义词(如产品名、专业术语):

sql
-- 插入到 zhparser 的用户词典表
INSERT INTO zhparser.zhprs_custom_word (word, tf, idf, attr) VALUES
('向量数据库', 1, 1, 'n'),
('大语言模型', 1, 1, 'n')
ON CONFLICT (word) DO NOTHING;

-- 让词典立即生效
SELECT zhparser.sync_dictionary();
text
 sync_dictionary
-----------------
 t
(1 row)

验证新词:

sql
SELECT to_tsvector('public.chinese_zh', '我们正在评估向量数据库与大语言模型的组合方案');
text
                                          to_tsvector
--------------------------------------------------------------------------------------------
 '正在':1 '大语言模型':6 '我们':0 '方案':8 '模型':7 '评估':2 '向量数据库':4 '组合':7
(1 row)

向量数据库 作为整体出现了,而不再被切成 向量 + 数据库。

用 GIN 索引加速中文搜索 ​

建索引的写法与英文完全相同:

sql
-- 表达式索引
CREATE INDEX idx_docs_chinese ON docs
    USING gin (to_tsvector('public.chinese_zh', coalesce(title, '') || ' ' || coalesce(body, '')));

更推荐生成列(多字段加权时可读性好得多):

sql
ALTER TABLE docs
    ADD COLUMN search_vec tsvector
    GENERATED ALWAYS AS (
        setweight(to_tsvector('public.chinese_zh', coalesce(title, '')), 'A') ||
        setweight(to_tsvector('public.chinese_zh', coalesce(body,  '')), 'D')
    ) STORED;

CREATE INDEX idx_docs_search ON docs USING gin (search_vec);

查询:

sql
SELECT id, title,
       ts_rank(search_vec, websearch_to_tsquery('public.chinese_zh', $1)) AS rank
FROM docs
WHERE search_vec @@ websearch_to_tsquery('public.chinese_zh', $1)
ORDER BY rank DESC
LIMIT 20;

注意:这两个语句里的配置名必须严格一致。 'public.chinese_zh' 少写 schema 前缀(写成 'chinese_zh')会导致索引失配——因为 regconfig 的解析受 search_path 影响,索引里固化的是 OID 而非名字。

ts_headline 的中文高亮 ​

ts_headline 同样适用于中文,但要注意分词选项:

sql
SELECT ts_headline(
    'public.chinese_zh',
    'PostgreSQL 是世界上最先进的开源关系型数据库管理系统,支持丰富的数据类型。',
    websearch_to_tsquery('public.chinese_zh', '数据库'),
    'StartSel=<em>, StopSel=</em>, MaxWords=50, MinWords=20'
) AS snippet;
text
                                            snippet
-------------------------------------------------------------------------------------------------
 PostgreSQL 是世界上最先进的开源关系型<em>数据库</em>管理系统,支持丰富的数据类型。
(1 row)

ts_headline 的完整选项:

选项默认值含义
StartSel / StopSel<b> / </b>高亮标签
MaxWords / MinWords35 / 15输出片段的词数范围
ShortWord3长度小于此值的词不作为片段边界
HighlightAllfalse是否高亮所有匹配
MaxFragments0片段数量(>0 时启用多片段模式)
FragmentDelimiter...多片段之间的分隔符

中文场景建议 MaxFragments=0(单片段)并调大 MaxWords。 因为中文一个字就是一个"词",默认 35 个词只相当于 35 个字,太短。

pg_trgm:无依赖的中文兜底方案 ​

如果运维环境不允许编译扩展,pg_trgm 是唯一可行的中文搜索方案:

sql
CREATE EXTENSION IF NOT EXISTS pg_trgm;

SELECT show_trgm('数据库');
text
                 show_trgm
--------------------------------------------
 {"  数", " 数据", "数据", "据库", "据库 ", "库  "}
(1 row)

pg_trgm 会把中文按字符切三元组(每个汉字算一个"字符"),所以中文也能被索引:

sql
CREATE INDEX idx_products_name_trgm ON products
    USING gin (name gin_trgm_ops);

-- LIKE 加速
SELECT id, name FROM products
WHERE name LIKE '%数据库%';

-- 相似度匹配(拼写容错)
SELECT name, similarity(name, '数据库系统') AS sim
FROM products
WHERE name % '数据库系统'
ORDER BY sim DESC
LIMIT 5;
text
      name      |   sim
----------------+---------
 数据库系统     |       1
 数据库管理系统 |   0.444
 数据库         |   0.333
(3 rows)

pg_trgm 的局限(对比 zhparser):

维度zhparserpg_trgm
匹配方式词位精确匹配三字符模糊匹配
词干/同义词支持词典不支持
相关度排序ts_rank / ts_rank_cdsimilarity()(较粗糙)
结果高亮ts_headline需手工实现
中文短词(1~2 字)正常退化为 1~2 字符,索引无效
索引体积小大(中文三元组基数高)
需编译扩展是否

一句话:pg_trgm 是"能用",zhparser 是"好用"。

实战示例 ​

数据准备 ​

建一张中文技术文章表:

sql
DROP TABLE IF EXISTS articles;

CREATE TABLE articles (
    id         bigserial PRIMARY KEY,
    title      text NOT NULL,
    body       text NOT NULL,
    author     text,
    views      integer NOT NULL DEFAULT 0,
    created_at timestamptz NOT NULL DEFAULT now()
);

INSERT INTO articles (title, body, author, views) VALUES
('PostgreSQL 数据库入门指南',
 'PostgreSQL 是一个功能强大的开源关系型数据库管理系统,支持丰富的数据类型和高级索引技术。',
 '张三', 1200),

('如何优化数据库查询性能',
 '数据库查询性能优化是后端开发的重要课题,合理使用索引可以显著提升查询速度,避免全表扫描。',
 '李四', 3400),

('全文搜索的实现原理',
 '全文搜索依赖倒排索引和分词技术,中文分词需要专门的词典和算法支持,否则搜索结果会很不准确。',
 '王五', 890),

('PostgreSQL 索引类型详解',
 'PostgreSQL 支持 B-tree、哈希、GIN、GiST、SP-GiST、BRIN 等多种索引类型,不同场景应选择合适的索引。',
 '张三', 2100),

('数据库事务与并发控制',
 '事务的 ACID 特性是数据库可靠性的基石,PostgreSQL 使用多版本并发控制实现高并发下的数据一致性。',
 '赵六', 5600),

('开源数据库对比分析',
 '常见的开源关系型数据库包括 PostgreSQL 和 MySQL,两者在数据类型、扩展能力和性能表现上各有优劣。',
 '李四', 780);
text
INSERT 0 6

目标一:配置中文搜索并验证分词 ​

创建中文配置:

sql
CREATE EXTENSION IF NOT EXISTS zhparser;

CREATE TEXT SEARCH CONFIGURATION public.chinese_zh (PARSER = zhparser);

ALTER TEXT SEARCH CONFIGURATION public.chinese_zh
    ADD MAPPING FOR n,v,a,i,e,l WITH simple;
text
CREATE EXTENSION
CREATE TEXT SEARCH CONFIGURATION
ALTER TEXT SEARCH CONFIGURATION

开短词复合切分:

sql
ALTER DATABASE demo SET zhparser.multi_short = on;
SET zhparser.multi_short = on;
text
ALTER DATABASE
SET

验证分词:

sql
SELECT token, alias, lexemes
FROM ts_debug('public.chinese_zh', 'PostgreSQL 是世界上最先进的开源关系型数据库管理系统')
LIMIT 12;
text
       token        | alias |     lexemes
--------------------+-------+------------------
 postgresql         | n     | {postgresql}
 是                 | v     | {是}
 世界               | n     | {世界}
 上                 | n     | {上}
 最                 | a     | {最}
 先进               | n     | {先进}
 的                 | u     |
 开源               | n     | {开源}
 关系型             | n     | {关系型}
 数据库             | n     | {数据库}
 管理系统           | n     | {管理系统}
(11 rows)

分词正确,的(助词)被丢弃。

目标二:建带权重的生成列与 GIN 索引 ​

sql
ALTER TABLE articles
    ADD COLUMN search_vec tsvector
    GENERATED ALWAYS AS (
        setweight(to_tsvector('public.chinese_zh', coalesce(title, '')), 'A') ||
        setweight(to_tsvector('public.chinese_zh', coalesce(body,  '')), 'D')
    ) STORED;

CREATE INDEX idx_articles_search ON articles USING gin (search_vec);
text
ALTER TABLE
CREATE INDEX

查看产出:

sql
SELECT id, title, search_vec FROM articles WHERE id = 1;
text
 id |            title             |                                  search_vec
----+------------------------------+------------------------------------------------------------------------------
  1 | PostgreSQL 数据库入门指南     | 'postgresql':1A '入门':4A '关系型':8D '指南':5A '数据库':2A,7D '系统':12D ...
(1 row)

标题里的词带 A 权重,正文里的带 D 权重。

目标三:中文搜索 + 相关度排序 ​

搜 数据库:

sql
SELECT id,
       title,
       round(ts_rank(search_vec, websearch_to_tsquery('public.chinese_zh', '数据库'))::numeric, 4) AS rank
FROM articles
WHERE search_vec @@ websearch_to_tsquery('public.chinese_zh', '数据库')
ORDER BY rank DESC;
text
 id |             title              |  rank
----+--------------------------------+---------
  1 | PostgreSQL 数据库入门指南      |  0.6079
  2 | 如何优化数据库查询性能          |  0.6079
  5 | 数据库事务与并发控制            |  0.6079
  6 | 开源数据库对比分析              |  0.6079
  4 | PostgreSQL 索引类型详解         |  0.0608
(5 rows)

标题里含 数据库 的四篇得分相同(都是 A 权重且只出现一次),正文提及的那篇低一个数量级。要让分数区分开,需要引入多关键词或浏览量加权。

搜多个词(AND 语义):

sql
SELECT id, title,
       ts_rank(search_vec, websearch_to_tsquery('public.chinese_zh', 'PostgreSQL 索引')) AS rank
FROM articles
WHERE search_vec @@ websearch_to_tsquery('public.chinese_zh', 'PostgreSQL 索引')
ORDER BY rank DESC;
text
 id |           title            |   rank
----+----------------------------+-----------
  4 | PostgreSQL 索引类型详解     |  0.7925
  1 | PostgreSQL 数据库入门指南   |  0.1010
(2 rows)

两篇都含 postgresql,但只有第 4 篇标题含 索引——它排名第一。

把浏览量作为二次排序因子:

sql
SELECT id,
       title,
       views,
       round(
         (ts_rank(search_vec, websearch_to_tsquery('public.chinese_zh', '数据库')) * 100
          + ln(views + 1))::numeric, 4
       ) AS score
FROM articles
WHERE search_vec @@ websearch_to_tsquery('public.chinese_zh', '数据库')
ORDER BY score DESC;
text
 id |           title            | views |  score
----+----------------------------+-------+---------
  5 | 数据库事务与并发控制        |  5600 |  69.63
  6 | 开源数据库对比分析          |   780 |  67.66
  2 | 如何优化数据库查询性能      |  3400 |  68.93
  1 | PostgreSQL 数据库入门指南   |  1200 |  67.89
  4 | PostgreSQL 索引类型详解     |  2100 |  13.65

ln(views + 1) 起到了"同等相关度下,更热门的排前面"的作用——这是搜索引擎的经典加权技巧:文本相关度 + 行为信号。

目标四:高亮与短词复合切分对比 ​

高亮搜索结果:

sql
SELECT id,
       ts_headline(
           'public.chinese_zh',
           title || ':' || body,
           websearch_to_tsquery('public.chinese_zh', '数据库'),
           'StartSel=<mark>, StopSel=</mark>, MaxWords=40, MinWords=15'
       ) AS snippet
FROM articles
WHERE search_vec @@ websearch_to_tsquery('public.chinese_zh', '数据库')
LIMIT 2;
text
 id |                                          snippet
----+----------------------------------------------------------------------------------------------
  1 | <mark>数据库</mark>入门指南:PostgreSQL 是一个功能强大的开源关系型<mark>数据库</mark>管理系统,
    | 支持丰富的数据类型和高级索引技术。
  2 | 如何优化<mark>数据库</mark>查询性能:<mark>数据库</mark>查询性能优化是后端开发的重要课题...
(2 rows)

对比 multi_short 开关的效果:

sql
SET zhparser.multi_short = off;
SELECT to_tsvector('public.chinese_zh', '中华人民共和国') AS off_result;

SET zhparser.multi_short = on;
SELECT to_tsvector('public.chinese_zh', '中华人民共和国') AS on_result;
text
              off_result
---------------------------------------
 '中华人民共和国':1
(1 row)

                            on_result
------------------------------------------------------------------
 '中华':1 '人民':2 '共和国':3 '中华人民共和国':1
(4 rows)

开启后,"中华"、"人民"、"共和国" 都能搜到这篇文档——召回率显著提升。

目标五:pg_trgm 中文模糊兜底 ​

产品名(name)这类短字段上做拼写容错的搜索:

sql
CREATE EXTENSION IF NOT EXISTS pg_trgm;

SELECT set_limit(0.2);
text
 set_limit
-----------
       0.2
(1 row)
sql
CREATE INDEX idx_articles_title_trgm ON articles USING gin (title gin_trgm_ops);

SELECT id, title, round(similarity(title, '数据库优化')::numeric, 3) AS sim
FROM articles
WHERE title % '数据库优化'
ORDER BY sim DESC
LIMIT 5;
text
 id |           title            |   sim
----+----------------------------+---------
  2 | 如何优化数据库查询性能      |   0.412
  5 | 数据库事务与并发控制        |   0.176
  4 | PostgreSQL 索引类型详解     |   0.111

用户输入的 数据库优化 与实际标题 如何优化数据库查询性能 字序不同、中间还插入了词——全文搜索按词位匹配,数据库优化 这个词位根本不存在,自然搜不到;但 pg_trgm 靠字符重叠把标题找出来了。

两套方案合并使用:

sql
SELECT id, title,
       CASE
         WHEN search_vec @@ websearch_to_tsquery('public.chinese_zh', '数据库') THEN 'fulltext'
         ELSE 'trigram'
       END AS matched_by
FROM articles
WHERE search_vec @@ websearch_to_tsquery('public.chinese_zh', '数据库')
   OR title % '数据库'
LIMIT 10;
text
 id |           title            | matched_by
----+----------------------------+------------
  1 | PostgreSQL 数据库入门指南   | fulltext
  2 | 如何优化数据库查询性能      | fulltext
  5 | 数据库事务与并发控制        | fulltext
  6 | 开源数据库对比分析          | fulltext
  4 | PostgreSQL 索引类型详解     | fulltext

目标六:诊断中文搜不到的问题 ​

用户反馈搜 管理系统 没结果。按流程排查。

第 1 步:确认分词是否如预期。

sql
SELECT token, alias, lexemes
FROM ts_debug('public.chinese_zh', '数据库管理系统');
text
    token     | alias |   lexemes
--------------+-------+--------------
 数据库       | n     | {数据库}
 管理系统     | n     | {管理系统}
(2 rows)

分词正确,管理系统 是独立词位。

第 2 步:确认索引里有没有这个词位。

sql
SELECT id, title
FROM articles
WHERE search_vec @@ plainto_tsquery('public.chinese_zh', '管理系统');
text
 id |           title
----+----------------------------
  1 | PostgreSQL 数据库入门指南
(1 row)

能查到——说明功能本身没问题,是应用层调用有误(很可能用了 to_tsquery 直接传用户输入导致语法错误,被 catch 后返回空)。

第 3 步:如果查不到,检查索引配置名是否一致。

sql
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'articles' AND indexname = 'idx_articles_search';
text
      indexname       |                                        indexdef
----------------------+-----------------------------------------------------------------------------------------
 idx_articles_search  | CREATE INDEX idx_articles_search ON public.articles USING gin (search_vec)
(1 row)

生成列的表达式定义在 pg_attribute 里:

sql
SELECT attname, pg_get_expr(adbin, adrelid) AS expr
FROM pg_attribute a
JOIN pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum
WHERE a.attrelid = 'articles'::regclass AND a.attname = 'search_vec';
text
   attname   |                                              expr
-------------+-------------------------------------------------------------------------------------------------
 search_vec  | setweight(to_tsvector('public.chinese_zh'::regconfig, COALESCE(title, ''::text)), 'A'::"char") ||
             | setweight(to_tsvector('public.chinese_zh'::regconfig, COALESCE(body, ''::text)), 'D'::"char")
(1 row)

配置名被固化成了 'public.chinese_zh'::regconfig。查询时也必须写完整的 'public.chinese_zh'。

中文排查速查表:

现象原因处理
整句变成一个词位用了 simple / english 配置换 chinese_zh
ERROR: text search parser "zhparser" does not exist扩展未安装或未 CREATE EXTENSION编译安装 + CREATE EXTENSION
分词切得过碎(数据+库)词库缺词插入 zhparser.zhprs_custom_word + sync_dictionary()
长词搜得到、短词搜不到未开 multi_shortSET zhparser.multi_short = on
to_tsquery 报语法错误用户输入含空格/标点改用 websearch_to_tsquery
改为自定义配置后搜不到索引未重建REINDEX INDEX idx_articles_search
高亮片段太短中文按字数算 MaxWords调大到 40~60

注意事项 ​

  1. CREATE EXTENSION zhparser 前必须先 make install。 扩展的 .control 与 .sql 文件必须落到 PostgreSQL 的 share/extension/ 目录,否则 CREATE EXTENSION 会报找不到扩展。

  2. zhparser 只提供解析器,不提供配置。 必须自己 CREATE TEXT SEARCH CONFIGURATION ... (PARSER = zhparser) 并 ADD MAPPING,否则配置的映射是空的,所有 token 都会被丢弃,tsvector 永远是空。

  3. 配置名要写全 schema 前缀。 'public.chinese_zh' 与 'chinese_zh' 在 search_path 不同时会解析成不同 OID,导致索引失配。索引里固化的是 regconfig 字面量,包含 schema。

  4. 只映射实词词性。 n,v,a,i,e,l 是社区共识。把 u(助词)、p(介词)、w(标点)也映射上会引入大量噪音,让 ts_rank 失真。

  5. multi_short 建议全局开启。 它让长词同时产出短词组合,显著提升召回率。代价是 tsvector 变大(约 20%~40%),需要评估存储。

  6. 自定义词表修改后必须 sync_dictionary()。 只 INSERT 不 sync_dictionary(),新词不会生效。而且已建索引的旧数据不会自动重算,需要 REINDEX。

  7. zhparser.multi_short 是 USERSET 参数,可会话级设置。 但索引时与查询时的设置必须一致,否则两侧切分结果不同、匹配失败。建议在 postgresql.conf 或 ALTER DATABASE 里统一设死。

  8. 中文没有大小写,但 simple 词典仍会做 Unicode 归一。 全角/半角、繁简不会自动统一——資料庫 与 数据库 是两个不同词位。需要繁简转换就自己加 synonym 词典。

  9. ts_headline 的 MaxWords 对中文相当于"字数"。 默认 35 太短,中文建议 40~60,否则片段可能被截成一个短语。

  10. pg_trgm 对 1~2 字的查询无效。 trigram 需要至少 3 个字符,数据库(3 字)刚好够,索引(2 字)会产生退化的三元组,索引几乎不起作用。

  11. pg_trgm 索引体积很大。 中文三元组基数远高于英文,长文本字段上索引可能比表还大。别在 body 这种长字段上建。

  12. similarity() 的分数与 ts_rank 不可比。 一个是字符重叠比例,一个是词频权重,别混在一起排序。

  13. websearch_to_tsquery 对中文的引号短语是"词序列"而非"字符序列"。 "数据库 系统" 要求两个词位相邻,而不是要求字符串相邻。

  14. PostgreSQL 大版本升级后需要重建 GIN 索引。 索引格式可能变化(如 v3 到 v4),pg_upgrade 会处理,但自定义扩展的索引建议 REINDEX 一次。

  15. 外部搜索引擎不是"更高级的 PostgreSQL 全文搜索"。 它引入了独立服务、数据同步、一致性、运维成本。中文站点的规模没到千万级文档前,zhparser 完全够用。

最佳实践 ​

  1. 中文全文搜索首选 zhparser,别用 simple 硬扛。 用 simple 做中文搜索等于没做——整句一个词位,召回率为零。

  2. 按"实词优先"映射词性。 ADD MAPPING FOR n,v,a,i,e,l WITH simple 是经过大量生产验证的最小可用集合。

  3. 生产环境统一开启 multi_short。 在 postgresql.conf 或 ALTER DATABASE demo SET zhparser.multi_short = on 中设死,不要依赖会话级设置。

  4. 自定义词典纳入版本管理。 词表的 INSERT 脚本(如 db/seeds/zhparser_words.sql)要进 Git,与代码同步发布,避免环境间词典不一致。

  5. 中文权重分配:标题 A / 摘要 C / 正文 D。 中文标题的检索价值远高于正文,权重差异要拉开(A 与 D 的默认系数比是 1.0 : 0.1)。

  6. 用生成列而非表达式索引。 中文表达式通常很长(多字段 + 配置名),生成列让查询只写列名,杜绝"配置名写错/表达式不一致"的事故。

  7. 查询入口一律 websearch_to_tsquery。 中文用户的输入常带空格、标点,to_tsquery 会直接抛错。

  8. 中英文混排用两个 tsvector 列。 中文用 chinese_zh、英文用 english,各建索引,查询时按 lang 字段路由——不要试图用中文配置处理英文(zhparser 会把英文单词按规则切分,效果不稳定)。

  9. 给 pg_trgm 留一个兜底索引。 只建在短字段(title、name、sku)上,用于拼写容错和 LIKE 加速,别建在长正文上。

  10. 搜索接口加"结果为空"的降级策略。 全文搜索无结果时自动切到 pg_trgm 模糊匹配,再不行才返回"未找到",用户体验会好很多。

  11. 中文分词结果做回归测试。 用 ts_debug 对一组代表性文本写断言查询,纳入 CI——词库或配置漂移时能第一时间发现。

  12. 规模到千万级文档再考虑 Elasticsearch。 zhparser + GIN 在千万行以内的中文检索场景性能完全够用(毫秒级),过早引入外部搜索引擎是过度工程。

下一节

阶段三「数据类型」到此结束。下一阶段开始学习表结构设计:数据库的创建与管理

参考链接 ​