中文全文搜索方案(pg_jieba / zhparser)
PostgreSQL 内置了三十多种全文搜索配置,覆盖了英语、德语、法语、俄语、阿拉伯语等——但没有一种能正确处理中文。原因不在于 PostgreSQL 偷懒,而在于中文的书写方式:英文靠空格分词,full text search 天然是三个词;中文词与词之间没有分隔符,数据库管理系统 究竟该切成 数据库 / 管理 / 系统 还是 数据 / 库 / 管理 / 系统,需要专门的算法加词典来判断。
内置解析器做不到这一点。它按「连续的非空白、非标点字符序列」切 token,一串汉字会被整段当成一个 token。结果是:搜 数据库 搜不到,搜 开源 也搜不到,只有把整句一字不差地贴进去才能命中——这样的全文搜索等于没有。
要解决中文搜索,必须在解析器这一层做替换:引入一个能把中文切成词、并给每个词打上词性标签的解析器。工程上可用的是 zhparser(基于 SCWS)与 pg_jieba(基于 cppjieba),把它们提供的解析器接上自定义配置后,tsvector、GIN 索引、ts_rank、ts_headline 这些上一节学过的技能全部照用不改。
本节讲清中文为什么必须先分词、两个扩展的安装与配置方式、各自的 GUC 参数与自定义词典机制、中文停用词与同义词的落地写法、中英混排处理、与 pg_trgm 的组合策略,以及升级重建索引这类容易踩的坑。
前置知识
阅读本节前,建议先阅读:GIN / GiST 全文索引
基础概念
中文为什么不能靠空格分词
| 语言特性 | 英文 | 中文 |
|---|---|---|
| 词边界 | 空格 / 标点天然分隔 | 无分隔符 |
| 分词方式 | 按空白切分即可 | 需要词典 + 算法(机械匹配 / HMM / CRF) |
| 词形变化 | foxes → fox 需要词干算法 | 无词形变化,但有 的、了、是 等虚词噪音 |
| 停用词 | the、is 等固定表 | 需按分词结果过滤 |
| 同义词 | 相对规范 | 极丰富(电脑 = 计算机 = PC) |
| 新词 | 变化慢 | 网络新词层出不穷,词库需持续更新 |
内置解析器(pg_catalog.default)是基于规则的:它识别空白、标点、字母、数字,然后把连续的非空白序列当成一个 token。对中文来说,一整串没有空格的汉字就是「一个 token」。
内置解析器实际会做什么
SELECT to_tsvector('simple', 'PostgreSQL 是世界上最先进的开源关系型数据库'); to_tsvector
------------------------------------------------------------------------
'postgresql':1 '世界上最先进的开源关系型数据库':3 '是':2注意 是 被单独切了出来——因为它两侧有空格,规则解析器正好在空格处切开了;而 世界上最先进的开源关系型数据库 整块成了一个词位。关键词一个都搜不到。
同理,\dF 里列出的配置只有 simple 和一批 Snowball 语言(arabic、danish、english … russian、swedish),没有 chinese:Snowball 提供的是「词干化(stemming)」,而中文需要的是「分词(word segmentation)」,这是两个不同的问题。
分词与词干化是两件事
上一节讲的词典层做的是归一化:把 running 变成 run,把 手机 变成 移动电话。前提是「词」已经存在。
中文缺的恰恰是这一步:token 还没被切出来,词典层无从下手。所以中文方案必须落在解析器层——这也是 zhparser / pg_jieba 与普通词典扩展的本质区别:它们提供的是 TEXT SEARCH PARSER(解析器),不是 TEXT SEARCH DICTIONARY(词典)。
接入后仍然沿用上一节的三层模型,只是第一层被替换掉了:
原始中文文本
│
▼
[zhparser / jieba 解析器] ← 替换 pg_catalog.default,输出 n/v/a/... 等词性标签
│
▼
[自定义配置 zh_cfg] ← 把词性标签映射到词典列表
│
▼
[simple / 自定义词典] ← 小写归一、可选停用词、可选同义词
│
▼
tsvector —— 与英文完全同构,可建 GIN 索引、可用 ts_rank / ts_headline三条可选路线
| 方案 | 原理 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| pg_trgm | 三字符组模糊匹配 | 零依赖、PG 官方扩展、无需分词 | 无词干化、无相关度排序、索引偏大 | 短文本模糊搜索、姓名 / 编号 |
| zhparser | 基于 SCWS 的中文分词解析器 | 与 tsvector 体系完全兼容,可用 GIN / ts_rank / ts_headline | 需编译安装、词库相对陈旧 | 中文全文检索首选 |
| pg_jieba | 基于 cppjieba 的中文分词解析器 | 词库现代、分词质量好 | 需编译安装、需 C++11 工具链 | zhparser 的替代或补充 |
| 外部搜索引擎 | ES / OpenSearch / MeiliSearch | 分词最好、功能最全 | 独立服务、数据同步复杂 | 亿级文档、搜索是核心业务 |
需求是什么?
│
├─ 只是"姓名/标题模糊匹配" ─────────► pg_trgm(零依赖)
│
├─ 需要"中文全文检索 + 排序 + 高亮" ─► zhparser
│
├─ 对分词质量要求更高、工具链允许 ───► pg_jieba
│
└─ 文档量巨大、需要同义词/纠错/聚合 ─► 外部搜索引擎关键前提:两者都是「解析器」
无论选哪个扩展,最终产出的都是标准 tsvector。这意味着:
- 索引写法与英文完全一致:
USING GIN (fts)。 - 排序函数与英文完全一致:
ts_rank/ts_rank_cd。 - 高亮函数与英文完全一致:
ts_headline。 - 权重标记与英文完全一致:
setweight(..., 'A')。
唯一的差异是配置名:查询与索引必须使用同一个中文配置,例如 public.zh_cfg 或 jiebacfg。
语法与用法
安装 zhparser(编译)
zhparser 不在 PostgreSQL 官方发行包里,必须从源码编译。它依赖 SCWS,所以要先装 SCWS:
# 1. 安装 SCWS(编译并安装到 /usr/local)
wget -q -O - http://www.xunsearch.com/scws/down/scws-1.2.3.tar.bz2 | tar xf -
cd scws-1.2.3 && ./configure && make install
# 2. 拉取 zhparser 源码
git clone https://github.com/amutu/zhparser.git
cd zhparser
# 3. 编译安装(SCWS 不在 /usr/local 时需指定 SCWS_HOME)
make && sudo make install
# SCWS_HOME=/usr make && sudo make install
# 4. 多版本 PostgreSQL 共存时,显式指定目标版本的 pg_config
# PG_CONFIG=/usr/lib/postgresql/18/bin/pg_config make && sudo make install装完后可以用 pg_config --pkglibdir / pg_config --sharedir 确认扩展文件是否落到了 PostgreSQL 的安装目录。
创建扩展与中文配置
扩展由超级用户创建,每个数据库装一次:CREATE EXTENSION zhparser;(成功时返回 CREATE EXTENSION)。
再基于它建一个中文配置。注意 ADD MAPPING 后面要写词性标签(n、v、a…),而不是英文配置里的 asciiword / word:
CREATE TEXT SEARCH CONFIGURATION public.zh_cfg (PARSER = zhparser);
ALTER TEXT SEARCH CONFIGURATION public.zh_cfg
ADD MAPPING FOR n, v, a, i, e, l WITH simple;这一步的含义是:名词、动词、形容词、成语、感叹词、习用语这六类 token 交给 simple 词典处理(小写归一,不做词干化)。其他类型(如标点、虚词)没有映射,会被直接丢弃——这正是我们想要的。
验证分词:ts_parse 与 ts_debug
ts_parse() 看解析器的原始切分结果:
SELECT * FROM ts_parse('zhparser', 'hello world! 2010年保障房建设在全国范围内获全面启动'); tokid | token
-------+-------
101 | hello
101 | world
117 | !
101 | 2010
113 | 年
118 | 保障
110 | 房建
118 | 设在
110 | 全国
110 | 范围
102 | 内
118 | 获
97 | 全面
118 | 启动
(14 rows)ts_debug() 看「token → 词典 → 词位」的全链路:
SELECT alias, token, dictionary, lexemes
FROM ts_debug('public.zh_cfg', 'PostgreSQL 是世界上最先进的开源关系型数据库')
WHERE alias <> 'blank'; alias | token | dictionary | lexemes
-------+------------+------------+------------
x | PostgreSQL | simple | {postgresql}
v | 是 | simple | {是}
n | 世界 | simple | {世界}
f | 上 | simple | {上}
a | 最 | simple | {最}
a | 先进 | simple | {先进}
u | 的 | |
j | 开源 | |
n | 关系型 | simple | {关系型}
n | 数据库 | simple | {数据库}
(10 rows)不同 zhparser / SCWS 版本的分词结果与词性标注可能略有差异,实际以
ts_debug()输出为准。
判断三点:
lexemes非空 → 该词会进索引,可被搜到。lexemes为空且dictionary有值 → 该 token 命中了停用词,占位但不进索引。dictionary为NULL→ 该 token 类型在配置里没有映射,直接被丢弃(上面u、j两行就是)。
看 tsvector 长什么样
沿用上面的配置(只映射 n,v,a,i,e,l):
SELECT to_tsvector('public.zh_cfg', 'PostgreSQL 是世界上最先进的开源关系型数据库');
-- to_tsvector
-- ---------------------------------------------------------------------------------
-- '是':1 '世界':2 '最':3 '先进':4 '关系型':5 '数据库':6对照前面的 ts_debug() 输出逐个看:
PostgreSQL(x)、上(f)、的(u)、开源(j)在映射里没有位置,被整段丢弃,位置编号里也不占号。是、世界、最、先进、关系型、数据库进索引,位置连续。
原本搜不到的 数据库、关系型 现在都能搜到了。映射列表就是「哪些 token 值得进索引」的白名单:漏掉某类词性,表现就是「这个词怎么都搜不到」。比如想让 开源 可检索,就把它所属的 j 补进映射:
ALTER TEXT SEARCH CONFIGURATION public.zh_cfg
ADD MAPPING FOR j WITH simple;排查手段永远是同一个:用 ts_debug() 看这个 token 的 alias 是什么,再确认该 alias 是否出现在 \dF+ public.zh_cfg 的映射列表里。
查询:必须用同一个配置
SELECT to_tsquery('public.zh_cfg', '保障房资金压力');
-- '保障' <-> '房' <-> '资金' <-> '压力'这里有个和英文不同的行为,必须点明:to_tsquery 在英文下要求你手写 & / |,而中文串里没有这些符号,整串会被当成一个 token 交给分词器。分词器切出的多个词属于同一个 token,PostgreSQL 会用短语操作符 <-> 把它们串起来——因为中文连写的多个词在原句中本来就是相邻的。
这带来一个容易踩的坑:to_tsquery 只认显式运算符,空格分隔的多个裸词会直接报语法错误:
-- 多个词之间必须有 & | ! <->,否则整段被当成一个 token 之外的语法被拒绝
SELECT to_tsquery('public.zh_cfg', '数据库 性能');
-- ERROR: syntax error in tsquery: "数据库 性能"
SELECT to_tsquery('public.zh_cfg', '数据库 & 性能'); -- '数据库' & '性能'
SELECT to_tsquery('public.zh_cfg', '数据库 <-> 性能'); -- '数据库' <-> '性能'plainto_tsquery 与 phraseto_tsquery 没有这个限制:它们不解析输入里的运算符,把整段文本当作一个 morph,运算符分别固定为 & 与 <->:
SELECT plainto_tsquery('public.zh_cfg', '数据库 性能'); -- '数据库' & '性能'
SELECT phraseto_tsquery('public.zh_cfg', '数据库 性能'); -- '数据库' <-> '性能'生产环境直接处理用户输入时,推荐 websearch_to_tsquery():它同样不报语法错误,并额外支持双引号短语与 - 排除:
SELECT websearch_to_tsquery('public.zh_cfg', '"数据库" -mysql');
-- '数据库' & !'mysql'| 函数 | 运算符来源 | 用户输入含 &/|/!/括号 | 双引号 | - 排除 | | --- | --- | --- | --- | --- | | to_tsquery | 用户手写 | 会被解析(可能报错) | 不支持 | 不支持 | | plainto_tsquery | 固定 & | 全部当普通字符 | 不支持 | 不支持 | | phraseto_tsquery | 固定 <-> | 全部当普通字符 | 不支持 | 不支持 | | websearch_to_tsquery | 固定 &/<->/! | 全部当普通字符 | 支持 | 支持 |
结论:给用户输入的查询,一律用 websearch_to_tsquery;只有你自己拼 SQL 字面量时才用 to_tsquery。
安装 pg_jieba(编译)
pg_jieba 需要 C++11(gcc 4.8+),用 CMake 构建:
git clone https://github.com/jaiminpan/pg_jieba
cd pg_jieba
git submodule update --init --recursive # 拉取 cppjieba 子模块
mkdir build && cd build
cmake ..
make && sudo make install
# 自定义安装路径的 PostgreSQL
# cmake -DCMAKE_PREFIX_PATH=/PATH/TO/PGSQL_INSTALL_DIR ..同样由超级用户建立扩展:CREATE EXTENSION pg_jieba;
pg_jieba 的四套预置配置
CREATE EXTENSION pg_jieba 之后会自动出现四个配置,可按场景直接选用:
| 配置名 | 算法 | 特点 | 建议 |
|---|---|---|---|
jiebamp | 最大概率(MP) | 切出的词偏长、偏整 | 词语边界清晰的长词 |
jiebahmm | 隐马尔可夫(HMM) | 能发现未登录词,但可能切出怪词 | 探索性场景 |
jiebacfg | MP + HMM 混合 | 兼顾两者,最通用 | 生产默认首选 |
jiebaqry | 混合 + 全模式 | 同一串文本尽量多切,召回高 | 短查询词、搜索建议 |
用法与普通配置完全一致:
SELECT to_tsvector('jiebacfg', '是拖拉机学院手扶拖拉机专业的');
-- '专业':5 '会':13 '拖拉机':2 '学院':3 '手扶拖拉机':4 '档':1
SELECT to_tsquery('jiebacfg', '保障房资金压力');
-- '保障' & '房' & '资金' & '压力'设为默认配置
确认效果满意后,可以让 default_text_search_config 指向中文配置,省去每次都写配置名:
ALTER DATABASE mydb SET default_text_search_config = 'public.zh_cfg';也可以写进 postgresql.conf 的 default_text_search_config(影响整个实例的新连接)。生产 SQL 仍建议显式写配置名,理由与上一节相同:显式才能让「索引侧」和「查询侧」在代码里一眼比对,也避免不同环境行为漂移。
详细说明
zhparser 的 GUC 参数
zhparser 用一组 GUC 参数控制分词行为。默认值全部为 false(关):
| 参数 | 作用域 | 默认 | 含义 |
|---|---|---|---|
zhparser.punctuation_ignore | USERSET | off | 忽略标点等特殊符号,不输出标点 token |
zhparser.seg_with_duality | USERSET | off | 闲散单字自动以二字分词法聚合 |
zhparser.multi_short | USERSET | off | 短词复合:同时输出短词与整词 |
zhparser.multi_duality | USERSET | off | 散字二元复合 |
zhparser.multi_zmain | USERSET | off | 重要单字复合 |
zhparser.multi_zall | USERSET | off | 全部单字复合 |
zhparser.dict_in_memory | BACKEND | off | 词典全量载入内存(每后端一份私有副本) |
zhparser.extra_dicts | BACKEND | 空 | 额外词典文件名列表(逗号分隔) |
两个作用域区别很关键:
USERSET参数可在会话里随时SET,立即生效。BACKEND参数必须在 backend 启动前设置(写进postgresql.conf后reload,新建连接才生效),会话内SET无效。
multi_short 是生产环境最该考虑打开的一个。 不开时,中华人民共和国 只切出一个长词;用户搜 中华 搜不到:
SET zhparser.multi_short = on;
SELECT to_tsvector('public.zh_cfg', '中华人民共和国');
-- '中华':1 '人民':2 '共和国':3 '中华人民共和国':1zhparser 自定义词典:表驱动方式
zhparser 2.1 起提供了一张数据库级的自定义词表(\d zhparser.zhprs_custom_word 可看结构),需要超级用户权限,改动后要手动同步:
-- 新增词(可指定 TF / IDF / 词性)
INSERT INTO zhparser.zhprs_custom_word (word, tf, idf, attr)
VALUES ('向量数据库', 1.0, 1.0, 'n');
-- 删除/屏蔽一个词:把词性设为 '!',即使核心词库里存在该词也视为无效
INSERT INTO zhparser.zhprs_custom_word (word, attr) VALUES ('某个不该出现的词', '!');写完必须同步,并重新建立连接才能生效:
SELECT sync_zhprs_custom_word(); -- 返回 1 row
-- 重连后再验证
SELECT * FROM ts_parse('zhparser', '保障房资金压力');
-- 118 | 保障
-- 110 | 房
-- 120 | 资金压力zhparser 自定义词典:文件方式
若词典内容较多,用文件方式更合适——zhparser.extra_dicts 指定 $SHAREDIR/tsearch_data/ 下的文件,逗号分隔,排在后面的优先级更高:
# postgresql.conf(BACKEND 级,必须 reload + 新建连接)
zhparser.extra_dicts = 'dict_extra.txt,mydict.xdb'文本词典格式为每行一条,字段以空格或制表符分隔,只有「词语」是必需的:
# 词语 TF IDF 词性
新词条 12.0 2.2 n
向量数据库 1.0 1.0 n.txt为文本格式,.xdb为二进制格式(用 SCWS 自带的scws-gen-dict从 txt 转换,效率更高,适合大词典)。- 以
#或分号开头的行为注释。 - 词性写
!表示该词作废。 - 官方建议 txt 词典不要过大——它每次会监测文件修改时间并转成 xdb 放在临时目录。
pg_jieba 的 GUC 参数
pg_jieba 的参数需要通过 shared_preload_libraries 预加载才会生效,且修改后需要重启:
# postgresql.conf
shared_preload_libraries = 'pg_jieba.so' # 修改需重启| 参数 | 修改方式 | 含义 |
|---|---|---|
pg_jieba.hmm_model | 需重启 | HMM 模型文件 |
pg_jieba.base_dict | 需重启 | 基础词典文件 |
pg_jieba.user_dict | 需重启 | 用户词典文件名列表(逗号分隔,不含 .dict 后缀),文件须位于 tsearch_data 目录 |
pg_jieba 自定义词典:jieba_user.dict
pg_jieba 的用户词典是一个固定文件名的文件,放进 PostgreSQL 的 tsearch_data 目录即可:
cd $(pg_config --sharedir)/tsearch_data
cp /path/to/your_dict jieba_user.dict文件格式为每行一个词,可选带词频与词性:
云计算
韩玉鉴赏
蓝翔 nz
区块链 10 nz多个词典文件时,用 pg_jieba.user_dict 列出名字(不含 .dict):
# postgresql.conf
pg_jieba.user_dict = 'jieba_user,my_dict'中文停用词
中文里 的、了、是、在、和 这类虚词出现频率极高却几乎无区分度,应该过滤掉。做法与英文完全一致:建一个带 STOPWORDS 的 simple 词典。
第一步:在 $SHAREDIR/tsearch_data/ 下创建停用词文件(必须是 UTF-8 编码):
# zh_stop.stop
的
了
是
在
和
就
也
都
而
及
与
着
或
把
被第二步:创建词典,并把它放在词典列表的最前面:
CREATE TEXT SEARCH DICTIONARY public.zh_stop (
TEMPLATE = simple,
STOPWORDS = zh_stop
);
ALTER TEXT SEARCH CONFIGURATION public.zh_cfg
ALTER MAPPING FOR n, v, a, i, e, l WITH zh_stop, simple;验证:用 ts_debug() 看没被过滤掉的词,或直接看 tsvector 里 是、最 这类词是否消失:
SELECT to_tsvector('public.zh_cfg', '数据库 是 世界上 最 先进 的 开源 数据库');
-- '世界':3 '先进':6 '开源':7 '数据库':1,8是、最、上 等噪音词已被过滤。
注意:停用词不是「不存在」,它会占掉位置编号。依赖位置的查询(<N> 短语跨度、ts_rank_cd)会受影响,这与英文配置的行为一致。vl 类词一旦被过滤,否定查询(!是)也就失去意义——停用词表要克制。
中文同义词
synonym 词典同样支持中文。$SHAREDIR/tsearch_data/zh_syn.syn:
# 第一个词是"规范词",后面全部是它的同义词(含它自己)
计算机 电脑 微机
数据库 db database
人工智能 ai创建词典并加入映射:
CREATE TEXT SEARCH DICTIONARY public.zh_syn (
TEMPLATE = synonym,
SYNONYMS = zh_syn
);
ALTER TEXT SEARCH CONFIGURATION public.zh_cfg
ALTER MAPPING FOR n, v, a, i, e, l WITH zh_stop, zh_syn, simple;SELECT to_tsvector('public.zh_cfg', '我买了一台电脑'); to_tsvector
-------------------------------------------
'买':3 '一台':4 '计算机':5电脑 被归一成了 计算机,搜 计算机 也能命中。
短语级替换(机器学习 → ML)要用 thesaurus 词典与 .ths 文件。注意 thesaurus 会改变词元位置,改动后必须重建索引。
中英混排
中文技术文档里混着英文术语与数字是常态(PostgreSQL 支持 JSONB)。处理要点:
- 解析器本身是能切英文的。
ts_parse('zhparser', 'hello world!')能把hello、world切成独立 token(见前文示例)。 - 但配置里必须有对应映射,否则英文 token 会被丢弃。查你安装版本的解析器定义了哪些 token 类型:
-- 部分输出(共 26 行)
SELECT * FROM ts_token_type('zhparser');
-- tokid | alias | description
-- -------+-------+----------------------------
-- 97 | a | adjective,形容词
-- 99 | c | conjunction,连词
-- 118 | v | verb,动词
-- 119 | w | punctuation,标点符号
-- 120 | x | unknown,未知词
-- ...- 用
ts_debug()确认英文 token 的 alias,再补上映射。若英文词被归为x(unknown),就把它加进映射:
ALTER TEXT SEARCH CONFIGURATION public.zh_cfg
ALTER MAPPING FOR n, v, a, i, e, l, x WITH zh_stop, zh_syn, simple;- 英文词干化可以追加
english_stem,但要注意 Snowball 词典「识别一切」,放在列表末尾做兜底正好:
-- 先查已有映射,再逐个 token 类型追加,不要覆盖中文映射
ALTER TEXT SEARCH CONFIGURATION public.zh_cfg
ALTER MAPPING FOR x WITH simple, english_stem;与 pg_trgm 组合
pg_trgm 与全文搜索是两套互不相通的操作符:一套用 @@ + tsquery,另一套用 %、LIKE + similarity()。但它们可以在同一张表上共存,各解决一类子问题。
pg_trgm 按字符(不是词)切三元组,所以中文天然可用:
CREATE EXTENSION IF NOT EXISTS pg_trgm;
SELECT show_trgm('数据库'); show_trgm
----------------------------------------------
{" 数", " 数据", "数据", "据库", "据库 ", "库 "}组合策略:
-- 1. 主检索:分词 + GIN(精确、可排序、可高亮)
SELECT id, title
FROM articles
WHERE fts @@ websearch_to_tsquery('public.zh_cfg', $1)
ORDER BY ts_rank(fts, websearch_to_tsquery('public.zh_cfg', $1)) DESC
LIMIT 20;
-- 2. 兜底:子串模糊匹配,解决"分词切错导致搜不到"与拼写容错
SELECT id, title, similarity(title, $1) AS sim
FROM articles
WHERE title % $1 -- 依赖 gin_trgm_ops 索引
ORDER BY sim DESC
LIMIT 20;| 维度 | 分词方案(zhparser / pg_jieba) | pg_trgm |
|---|---|---|
| 匹配粒度 | 词位精确匹配 | 三字符模糊匹配 |
| 停用词 / 同义词 | 支持 | 不支持 |
| 相关度排序 | ts_rank / ts_rank_cd | similarity()(较粗糙) |
| 结果高亮 | ts_headline | 需自行实现 |
| 1~2 字短查询 | 正常 | 退化,索引几乎无效 |
| 索引体积 | 小 | 大(中文三元组基数高) |
| 需编译扩展 | 是 | 否(官方扩展) |
结论:pg_trgm 解决「能用」,分词方案解决「好用」。两者不是替代关系,是分层关系。
中文高亮
ts_headline() 处理中文时,必须显式传入中文配置,否则它内部会用默认配置重新分词,导致高亮位置错乱:
SELECT ts_headline(
'public.zh_cfg',
'PostgreSQL 是一个功能强大的开源关系型数据库管理系统,支持丰富的数据类型。',
websearch_to_tsquery('public.zh_cfg', '数据库'),
'StartSel=<em>, StopSel=</em>, MaxWords=100, MinWords=20'
); ts_headline
--------------------------------------------------------------------------------------
PostgreSQL 是一个功能强大的开源关系型<em>数据库</em>管理系统,支持丰富的数据类型。中文要调大 MaxWords:中文一个字就是一个「词」,默认 35 太短(只相当于 35 个字),片段容易被截断得莫名其妙。建议 MaxWords 设到 80~150 并保持 MaxFragments=0(单片段)。
实战示例
数据准备
DROP TABLE IF EXISTS articles;
CREATE TABLE articles (
id bigserial PRIMARY KEY,
title text NOT NULL,
body text NOT NULL,
author text,
created_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO articles (title, body, author) VALUES
('PostgreSQL 数据库入门指南',
'PostgreSQL 是一个功能强大的开源关系型数据库管理系统,支持丰富的数据类型和高级索引技术。',
'张三'),
('如何优化数据库查询性能',
'数据库查询性能优化是后端开发的重要课题,合理使用索引可以显著提升查询速度,避免全表扫描。',
'李四'),
('全文搜索的实现原理',
'全文搜索依赖倒排索引和分词技术,中文分词需要专门的词典和算法支持,否则搜索结果会很不准确。',
'王五'),
('数据库事务与并发控制',
'事务的 ACID 特性是数据库可靠性的基石,PostgreSQL 使用多版本并发控制实现高并发下的数据一致性。',
'赵六'),
('开源数据库对比分析',
'常见的开源关系型数据库包括 PostgreSQL 和 MySQL,两者在扩展能力和性能表现上各有优劣。',
'李四');
-- 输出:INSERT 0 5示例一:建配置并验证分词
CREATE EXTENSION IF NOT EXISTS zhparser;
CREATE TEXT SEARCH CONFIGURATION public.zh_cfg (PARSER = zhparser);
ALTER TEXT SEARCH CONFIGURATION public.zh_cfg
ADD MAPPING FOR n, v, a, i, e, l WITH simple;
-- 依次输出:CREATE EXTENSION / CREATE TEXT SEARCH CONFIGURATION / ALTER TEXT SEARCH CONFIGURATION开短词复合切分(建议写进 postgresql.conf 或 ALTER DATABASE,保证所有会话一致):
-- 错误写法:数据库名不能是函数调用
ALTER DATABASE current_database() SET zhparser.multi_short = on;
-- ERROR: syntax error at or near "("
-- 正确写法:先确认库名再执行
ALTER DATABASE mydb SET zhparser.multi_short = on;验证:
SELECT token, alias, dictionary, lexemes
FROM ts_debug('public.zh_cfg', '数据库查询性能优化')
WHERE alias <> 'blank';
-- token | alias | dictionary | lexemes
-- ----------+-------+------------+-----------
-- 数据库 | n | simple | {数据库}
-- 查询 | v | simple | {查询}
-- 性能 | n | simple | {性能}
-- 优化 | v | simple | {优化}
-- (4 rows)示例二:生成列 + GIN 索引
强烈建议用「存储生成列」而不是表达式索引:查询侧只写 fts @@ q,从根上消除「查询表达式与索引表达式不一致」这类静默失效。
ALTER TABLE articles
ADD COLUMN fts tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('public.zh_cfg', coalesce(title, '')), 'A') ||
setweight(to_tsvector('public.zh_cfg', coalesce(body, '')), 'B')
) STORED;
CREATE INDEX idx_articles_fts ON articles USING GIN (fts);
-- 输出:ALTER TABLE / CREATE INDEX注意配置名必须带 schema 前缀且两边完全一致。 regconfig 的解析受 search_path 影响,索引里固化的是 OID,写 'zh_cfg' 与 'public.zh_cfg' 在某些 search_path 下会解析成不同对象。
示例三:中文检索 + 相关度排序
SELECT id, title, ts_rank(fts, q) AS rank
FROM articles, websearch_to_tsquery('public.zh_cfg', '数据库 性能') AS q
WHERE fts @@ q
ORDER BY rank DESC
LIMIT 10;
-- id | title | rank
-- ----+-------------------------+----------
-- 2 | 如何优化数据库查询性能 | 0.607927
-- 1 | PostgreSQL 数据库入门指南| 0.243491
-- 4 | 数据库事务与并发控制 | 0.243491
-- 5 | 开源数据库对比分析 | 0.243491
-- (4 rows)确认走了索引,而不是全表扫描:
EXPLAIN (ANALYZE, COSTS OFF)
SELECT id FROM articles
WHERE fts @@ websearch_to_tsquery('public.zh_cfg', '数据库');
-- QUERY PLAN
-- --------------------------------------------------------------
-- Limit
-- -> Bitmap Heap Scan on articles
-- Recheck Cond: (fts @@ websearch_to_tsquery('public.zh_cfg'::regconfig, '数据库'::text))
-- -> Bitmap Index Scan on idx_articles_fts
-- Index Cond: (fts @@ websearch_to_tsquery('public.zh_cfg'::regconfig, '数据库'::text))
-- (5 rows)示例四:新增专有名词并让索引吃到
业务词(产品名、内部术语)必须进词典,否则会被切碎:
-- 1. 加词
INSERT INTO zhparser.zhprs_custom_word (word, tf, idf, attr)
VALUES ('向量数据库', 1.0, 1.0, 'n');
-- 2. 同步(返回 1 row)
SELECT sync_zhprs_custom_word();3. 重新建立连接(同步结果通过词典文件加载,老连接仍用旧词典)。
4. 重建索引——这一步不能省。 生成列的表达式会在写入时求值,表里已存在的 fts 值不会因为词典变化而自动更新:
-- 先刷新已存在的生成列值
UPDATE articles SET title = title; -- UPDATE 5
-- 再重建索引(并发重建,避免长时间阻塞写入)
REINDEX INDEX CONCURRENTLY idx_articles_fts;验证新词生效:
SELECT to_tsvector('public.zh_cfg', '我们正在评估向量数据库方案');
-- to_tsvector
-- ------------------------------------------------
-- '向量数据库':4 '我们':1 '正在':2 '评估':3 '方案':5向量数据库 作为整体出现,不再被切成 向量 + 数据库。
示例五:pg_trgm 兜底
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_articles_title_trgm ON articles USING GIN (title gin_trgm_ops);
-- 用户输入"数据裤"(错别字),分词方案搜不到,trgm 兜底
SELECT id, title, similarity(title, '数据裤') AS sim
FROM articles
WHERE title % '数据裤'
ORDER BY sim DESC
LIMIT 5;
-- id | title | sim
-- ----+---------------------------+----------
-- 5 | 开源数据库对比分析 | 0.230769
-- 1 | PostgreSQL 数据库入门指南 | 0.187500
-- (2 rows)注意事项
编译安装是硬门槛。 zhparser 需要先装 SCWS(纯 C,无外部依赖,但要从源码编译),pg_jieba 需要 C++11 工具链与 CMake 子模块。托管数据库(如部分云厂商的 RDS)通常不允许自定义扩展,此时只能退回 pg_trgm,或在应用层做分词后拼接 tsquery。
升级 PostgreSQL 大版本后必须重建相关索引。 扩展的 .so 需针对新版本重新编译;即使能加载,索引内部结构也可能不兼容。标准动作是 REINDEX:
-- 大版本升级后
REINDEX INDEX CONCURRENTLY idx_articles_fts;改词典后索引不会自动更新。 这是中文场景最常见的「改了没效果」原因。链条是:改词典文件 / zhprs_custom_word 表 → sync_zhprs_custom_word()(zhparser)→ 重连 → 刷新生成列 → REINDEX。少任何一步都得不到预期结果。
不要把 zhparser.extra_dicts / dict_in_memory 写在会话里。 它们是 BACKEND 级参数,必须写进 postgresql.conf 并 reload 后新建连接才生效:
-- 无效!BACKEND 级参数不能在会话里 SET
SET zhparser.dict_in_memory = on;停用词文件必须是 UTF-8。 用 GBK / GB18030 保存的中文停用词文件在加载时会失败或产生乱码。这与上一节对词典文件的约束是同一条。
thesaurus(.ths)改动后一定要重建索引。 它影响词元位置信息,与其他词典不同——其他词典改动往往只影响新写入的数据,thesaurus 改动会让已有索引与新规则对不上。
两种方案不要在同一个数据库里混用同一配置名。 zhparser 的 PARSER = zhparser 与 pg_jieba 的 jiebacfg 解析器不同,token 类型集合也不同。混用会导致「部分词有时搜到、有时搜不到」的诡异现象。
中文配置的 ADD MAPPING 列表要按业务裁剪。 把 w(标点)、u(助词)、m(数词)等全都映射进去,只会让索引变大、噪音变多。只映射真正有检索价值的类型。
DANGER
升级扩展版本后必须重建索引,且要覆盖全部分区/子表。 扩展升级会改变分词结果,旧的 tsvector 与新词典不再一致,表现为「同一个查询,部分行能搜到、部分行搜不到」。分区表用 REINDEX TABLE 时代价很大,务必安排在维护窗口,或对每个分区单独 REINDEX INDEX CONCURRENTLY。
最佳实践
先定方案再建模,不要上线后再改。 索引表达式一旦确定(用
public.zh_cfg还是jiebacfg),改动意味着全表重建索引。建模阶段就确认:装 zhparser / pg_jieba 还是用pg_trgm兜底。优先用「存储生成列 + GIN」,而不是表达式索引。 生成列把
tsvector物化成表的一部分,查询侧只写fts @@ q;任何词典变动只需UPDATE刷新生成列 +REINDEX,逻辑集中在一处。用
setweight合并多字段,只建一个 GIN 列。 标题给A、正文给B,一个fts列同时承载检索与排序:
ALTER TABLE articles ADD COLUMN fts tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('public.zh_cfg', coalesce(title,'')), 'A') ||
setweight(to_tsvector('public.zh_cfg', coalesce(body, '')), 'B')
) STORED;打开
zhparser.multi_short,并写进postgresql.conf或ALTER DATABASE。 短词复合能显著提升「用户搜短词」的召回;用会话级SET会造成不同连接行为不一致。把配置名写成带 schema 的完整形式。
'public.zh_cfg'而不是'zh_cfg',避免search_path变化导致索引静默失配。建立「词典回归测试」。 用
ts_debug()/ts_parse()固化一批「输入串 → 期望词元」的断言,覆盖:分词是否正确、停用词是否过滤、同义词是否归一、专有名词是否整体保留。改词典后跑一遍。专有名词必须进词典,且进词典后要刷新生成列并重建索引。 顺序记为:加词 →
sync_zhprs_custom_word()→ 重连 →UPDATE刷生成列 →REINDEX CONCURRENTLY。pg_trgm作为兜底,不作为主检索。 主链路用分词 + GIN(可排序、可高亮、索引小),trgm 索引只用于「短串模糊匹配」与「分词切错时的补救」。两个索引并存是可接受的,但不要在WHERE里混用两套操作符去指望走同一个索引。ts_rank的调用放在WHERE之后、LIMIT之前,并且不要试图给它建索引。 排名函数只做重排序,不参与索引查找;排名慢说明候选集太大,应优化WHERE条件。ts_headline只对最终页调用,并且必须显式传中文配置。 它读原始文档、不走索引;MaxWords在中文下要显著调大(80~150),否则片段会被截断得没有可读性。停用词表保持精简,宁少勿多。 过度停用会损失精度——
不、无、非这类否定词被过滤后,否定查询彻底失效。建议先用统计找出「确实高频且无区分度」的词再加。为扩展升级准备维护窗口与回滚方案。 大版本升级或扩展升级后,标准动作是「重编译扩展 → 刷新生成列 →
REINDEX CONCURRENTLY全部分区」。把这套流程脚本化,并预先评估索引体积与重建耗时。
下一节
继续学习:FDW 概述
参考链接
- PostgreSQL 官方文档:Parsers(12.5,解析器机制与内置 token 类型)
- PostgreSQL 官方文档:Configuration Example(12.7,自定义配置完整示例)
- PostgreSQL 官方文档:Testing and Debugging Text Search(12.8,ts_debug / ts_parse / ts_lexize)
- PostgreSQL 官方文档:Dictionaries(12.6,simple / synonym / thesaurus 词典与停用词)
- PostgreSQL 官方文档:Tables and Indexes(12.2,生成列与 GIN 索引)
- PostgreSQL 官方文档:Additional Features(12.4,setweight 与 tsvector 拼接)
- PostgreSQL 官方文档:pg_trgm(中文兜底的模糊匹配方案)
- GitHub:zhparser(基于 SCWS 的 PostgreSQL 中文分词解析器)
- GitHub:pg_jieba(基于 cppjieba 的 PostgreSQL 中文分词解析器)
- GitHub:SCWS 简体中文分词系统(hightman/scws)