中文全文搜索方案
上一节我们看到,PostgreSQL 内置了 30 种全文搜索配置,覆盖了英语、德语、法语、俄语等语言——但没有一种能正确处理中文。
这不是 PostgreSQL 的疏忽,而是中文的固有难度:英文靠空格分词,中文词与词之间没有分隔符,"数据库管理系统" 到底该切成 数据库 / 管理 / 系统 还是 数据 / 库 / 管理 / 系统,需要专门的分词算法和词典来判断。PostgreSQL 内置解析器做不到这一点。
先看它实际会做什么:
SELECT to_tsvector('simple', ' PostgreSQL 是世界上最先进的开源关系型数据库'); 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 | 分词质量最好、功能最全 | 引入独立服务、数据同步复杂度高 | 大规模、高要求的搜索场景 |
决策顺序建议:
需求是什么?
│
├─ 只是"姓名/标题模糊匹配" ──────► pg_trgm(零成本)
│
├─ 需要"中文全文检索 + 排序 + 高亮" ─► zhparser(推荐)
│
├─ 对分词质量要求极高、愿意折腾 ───► pg_jieba
│
└─ 亿级文档、需要同义词/拼写纠错/聚合 ─► 外部搜索引擎本节重点讲 zhparser(工程上最实用),并给出 pg_trgm 的中文兜底方案。
zhparser 是什么
zhparser 是一个 PostgreSQL 扩展,它把 SCWS(Simple Chinese Word Segmentation,简易中文分词系统) 包装成 PostgreSQL 的全文搜索解析器。
这意味着它完美嵌入了上一节讲的三层架构:
原始中文文本
│
▼
[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
# 从 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
git clone https://github.com/amutu/zhparser.git
cd zhparser
# 指定 pg_config 路径,确保装进正确的 PostgreSQL
make PG_CONFIG=$(which pg_config)
sudo make install/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 步:创建扩展
CREATE EXTENSION zhparser;CREATE EXTENSION验证解析器已注册:
\dFp List of text search parsers
Schema | Name | Description
------------+----------+-------------
pg_catalog | default |
public | zhparser |
(2 rows)创建中文配置
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;逐句解读:
| 语句 | 作用 |
|---|---|
PARSER = zhparser | 指定用 zhparser 解析器(而不是 default) |
ADD MAPPING FOR n,v,a,i,e,l | 把名词、动词、形容词、习语、叹词、习用语映射到词典 |
WITH simple | 用 simple 词典做小写归一(中文无所谓大小写,但保留原词) |
为什么只映射 6 种? 这是社区的通行做法:只保留有检索意义的实词。的、了、和、, 这些虚词和标点如果也参与索引,会制造大量噪音,让搜索结果变差。
有些场景需要保留更多词性,例如人名、地名、机构名对搜索很有价值,可以额外加上 nr、ns、nt、nz:
ALTER TEXT SEARCH CONFIGURATION public.chinese_zh
ADD MAPPING FOR nr,ns,nt,nz WITH simple;而虚词(u 助词、p 介词、c 连词)与标点(w)建议保持不映射,让它们直接被丢弃——这是中文全文搜索降噪的关键一步。
测试分词效果
用 ts_debug 看 zhparser 实际切出了什么:
SELECT alias, token, dictionary, lexemes
FROM ts_debug('public.chinese_zh', 'PostgreSQL 是世界上最先进的开源关系型数据库管理系统'); 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 的惨状,现在每个词都是独立的词位:
SELECT to_tsvector('public.chinese_zh', 'PostgreSQL 是世界上最先进的开源关系型数据库管理系统'); to_tsvector
--------------------------------------------------
'postgresql':1 '上':4 '世界':3 '关系型':9 '先进':6 '开源':8 '数据库':10 '管理系统':11 '最':5 '是':2
(1 row)现在搜 数据库 能命中了:
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; hit1 | hit2
------+------
t | t
(1 row)注意 u(助词)类型的 的 因为没有映射,dictionary 和 lexemes 都是空——被有意丢弃了。
中文 tsquery 的注意事项
to_tsquery 对中文的输入要求与英文一致——必须已经分好词:
-- ✓ 单个词,正常
SELECT to_tsquery('public.chinese_zh', '数据库'); to_tsquery
------------
'数据库'
(1 row)-- ✗ 一整句中文传进去,zhparser 会把它切开,再被解释成布尔表达式 → 语法错误
SELECT to_tsquery('public.chinese_zh', '数据库 管理系统');ERROR: syntax error in tsquery: "数据库 管理系统"正确做法是用 plainto_tsquery 或 websearch_to_tsquery,它们会先分词再用 & 连接:
SELECT plainto_tsquery('public.chinese_zh', '数据库 管理系统') AS q1,
websearch_to_tsquery('public.chinese_zh', '数据库 管理系统') AS q2; q1 | q2
-------------------------------+-------------------------------
'数据库' & '管理系统' | '数据库' & '管理系统'详细说明
词库扩展与自定义词典
zhparser 支持通过参数调整分词行为:
-- 查看当前参数
SHOW zhparser.multi_short; zhparser.multi_short
----------------------
off
(1 row)常用参数:
| 参数 | 默认值 | 含义 |
|---|---|---|
zhparser.multi_short | off | 是否启用短词复合切分(中华人民共和国 → 中华+人民+共和国+中华人民共和国) |
zhparser.multi_duplicate | on | 复合切分结果是否去重 |
zhparser.multi_zmain | off | 是否输出重要单字 |
zhparser.multi_zall | off | 是否输出全部单字 |
zhparser.punctuation_ignore | on | 是否忽略标点 |
zhparser.seg_with_duality | off | 是否使用二元切分 |
zhparser.dict_in_memory | off | 是否把词典全量加载进内存(提升性能、增加内存占用) |
开启短词复合切分:
SET zhparser.multi_short = on;
SELECT to_tsvector('public.chinese_zh', '中华人民共和国'); to_tsvector
------------------------------------------------------------------------------------
'中华':1 '人民':2 '共和国':3 '中华人民共和国':1
(1 row)不开时只有长词 中华人民共和国 一个词位——用户搜"中华"会搜不到。这就是 multi_short 的价值,也是生产环境建议开启的原因。
新增自定义词(如产品名、专业术语):
-- 插入到 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(); sync_dictionary
-----------------
t
(1 row)验证新词:
SELECT to_tsvector('public.chinese_zh', '我们正在评估向量数据库与大语言模型的组合方案'); to_tsvector
--------------------------------------------------------------------------------------------
'正在':1 '大语言模型':6 '我们':0 '方案':8 '模型':7 '评估':2 '向量数据库':4 '组合':7
(1 row)向量数据库 作为整体出现了,而不再被切成 向量 + 数据库。
用 GIN 索引加速中文搜索
建索引的写法与英文完全相同:
-- 表达式索引
CREATE INDEX idx_docs_chinese ON docs
USING gin (to_tsvector('public.chinese_zh', coalesce(title, '') || ' ' || coalesce(body, '')));更推荐生成列(多字段加权时可读性好得多):
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);查询:
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 同样适用于中文,但要注意分词选项:
SELECT ts_headline(
'public.chinese_zh',
'PostgreSQL 是世界上最先进的开源关系型数据库管理系统,支持丰富的数据类型。',
websearch_to_tsquery('public.chinese_zh', '数据库'),
'StartSel=<em>, StopSel=</em>, MaxWords=50, MinWords=20'
) AS snippet; snippet
-------------------------------------------------------------------------------------------------
PostgreSQL 是世界上最先进的开源关系型<em>数据库</em>管理系统,支持丰富的数据类型。
(1 row)ts_headline 的完整选项:
| 选项 | 默认值 | 含义 |
|---|---|---|
StartSel / StopSel | <b> / </b> | 高亮标签 |
MaxWords / MinWords | 35 / 15 | 输出片段的词数范围 |
ShortWord | 3 | 长度小于此值的词不作为片段边界 |
HighlightAll | false | 是否高亮所有匹配 |
MaxFragments | 0 | 片段数量(>0 时启用多片段模式) |
FragmentDelimiter | ... | 多片段之间的分隔符 |
中文场景建议 MaxFragments=0(单片段)并调大 MaxWords。 因为中文一个字就是一个"词",默认 35 个词只相当于 35 个字,太短。
pg_trgm:无依赖的中文兜底方案
如果运维环境不允许编译扩展,pg_trgm 是唯一可行的中文搜索方案:
CREATE EXTENSION IF NOT EXISTS pg_trgm;
SELECT show_trgm('数据库'); show_trgm
--------------------------------------------
{" 数", " 数据", "数据", "据库", "据库 ", "库 "}
(1 row)pg_trgm 会把中文按字符切三元组(每个汉字算一个"字符"),所以中文也能被索引:
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; name | sim
----------------+---------
数据库系统 | 1
数据库管理系统 | 0.444
数据库 | 0.333
(3 rows)pg_trgm 的局限(对比 zhparser):
| 维度 | zhparser | pg_trgm |
|---|---|---|
| 匹配方式 | 词位精确匹配 | 三字符模糊匹配 |
| 词干/同义词 | 支持词典 | 不支持 |
| 相关度排序 | ts_rank / ts_rank_cd | similarity()(较粗糙) |
| 结果高亮 | ts_headline | 需手工实现 |
| 中文短词(1~2 字) | 正常 | 退化为 1~2 字符,索引无效 |
| 索引体积 | 小 | 大(中文三元组基数高) |
| 需编译扩展 | 是 | 否 |
一句话:pg_trgm 是"能用",zhparser 是"好用"。
实战示例
数据准备
建一张中文技术文章表:
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);INSERT 0 6目标一:配置中文搜索并验证分词
创建中文配置:
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;CREATE EXTENSION
CREATE TEXT SEARCH CONFIGURATION
ALTER TEXT SEARCH CONFIGURATION开短词复合切分:
ALTER DATABASE demo SET zhparser.multi_short = on;
SET zhparser.multi_short = on;ALTER DATABASE
SET验证分词:
SELECT token, alias, lexemes
FROM ts_debug('public.chinese_zh', 'PostgreSQL 是世界上最先进的开源关系型数据库管理系统')
LIMIT 12; token | alias | lexemes
--------------------+-------+------------------
postgresql | n | {postgresql}
是 | v | {是}
世界 | n | {世界}
上 | n | {上}
最 | a | {最}
先进 | n | {先进}
的 | u |
开源 | n | {开源}
关系型 | n | {关系型}
数据库 | n | {数据库}
管理系统 | n | {管理系统}
(11 rows)分词正确,的(助词)被丢弃。
目标二:建带权重的生成列与 GIN 索引
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);ALTER TABLE
CREATE INDEX查看产出:
SELECT id, title, search_vec FROM articles WHERE id = 1; id | title | search_vec
----+------------------------------+------------------------------------------------------------------------------
1 | PostgreSQL 数据库入门指南 | 'postgresql':1A '入门':4A '关系型':8D '指南':5A '数据库':2A,7D '系统':12D ...
(1 row)标题里的词带 A 权重,正文里的带 D 权重。
目标三:中文搜索 + 相关度排序
搜 数据库:
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; id | title | rank
----+--------------------------------+---------
1 | PostgreSQL 数据库入门指南 | 0.6079
2 | 如何优化数据库查询性能 | 0.6079
5 | 数据库事务与并发控制 | 0.6079
6 | 开源数据库对比分析 | 0.6079
4 | PostgreSQL 索引类型详解 | 0.0608
(5 rows)标题里含 数据库 的四篇得分相同(都是 A 权重且只出现一次),正文提及的那篇低一个数量级。要让分数区分开,需要引入多关键词或浏览量加权。
搜多个词(AND 语义):
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; id | title | rank
----+----------------------------+-----------
4 | PostgreSQL 索引类型详解 | 0.7925
1 | PostgreSQL 数据库入门指南 | 0.1010
(2 rows)两篇都含 postgresql,但只有第 4 篇标题含 索引——它排名第一。
把浏览量作为二次排序因子:
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; 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.65ln(views + 1) 起到了"同等相关度下,更热门的排前面"的作用——这是搜索引擎的经典加权技巧:文本相关度 + 行为信号。
目标四:高亮与短词复合切分对比
高亮搜索结果:
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; id | snippet
----+----------------------------------------------------------------------------------------------
1 | <mark>数据库</mark>入门指南:PostgreSQL 是一个功能强大的开源关系型<mark>数据库</mark>管理系统,
| 支持丰富的数据类型和高级索引技术。
2 | 如何优化<mark>数据库</mark>查询性能:<mark>数据库</mark>查询性能优化是后端开发的重要课题...
(2 rows)对比 multi_short 开关的效果:
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; off_result
---------------------------------------
'中华人民共和国':1
(1 row)
on_result
------------------------------------------------------------------
'中华':1 '人民':2 '共和国':3 '中华人民共和国':1
(4 rows)开启后,"中华"、"人民"、"共和国" 都能搜到这篇文档——召回率显著提升。
目标五:pg_trgm 中文模糊兜底
产品名(name)这类短字段上做拼写容错的搜索:
CREATE EXTENSION IF NOT EXISTS pg_trgm;
SELECT set_limit(0.2); set_limit
-----------
0.2
(1 row)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; id | title | sim
----+----------------------------+---------
2 | 如何优化数据库查询性能 | 0.412
5 | 数据库事务与并发控制 | 0.176
4 | PostgreSQL 索引类型详解 | 0.111用户输入的 数据库优化 与实际标题 如何优化数据库查询性能 字序不同、中间还插入了词——全文搜索按词位匹配,数据库优化 这个词位根本不存在,自然搜不到;但 pg_trgm 靠字符重叠把标题找出来了。
两套方案合并使用:
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; id | title | matched_by
----+----------------------------+------------
1 | PostgreSQL 数据库入门指南 | fulltext
2 | 如何优化数据库查询性能 | fulltext
5 | 数据库事务与并发控制 | fulltext
6 | 开源数据库对比分析 | fulltext
4 | PostgreSQL 索引类型详解 | fulltext目标六:诊断中文搜不到的问题
用户反馈搜 管理系统 没结果。按流程排查。
第 1 步:确认分词是否如预期。
SELECT token, alias, lexemes
FROM ts_debug('public.chinese_zh', '数据库管理系统'); token | alias | lexemes
--------------+-------+--------------
数据库 | n | {数据库}
管理系统 | n | {管理系统}
(2 rows)分词正确,管理系统 是独立词位。
第 2 步:确认索引里有没有这个词位。
SELECT id, title
FROM articles
WHERE search_vec @@ plainto_tsquery('public.chinese_zh', '管理系统'); id | title
----+----------------------------
1 | PostgreSQL 数据库入门指南
(1 row)能查到——说明功能本身没问题,是应用层调用有误(很可能用了 to_tsquery 直接传用户输入导致语法错误,被 catch 后返回空)。
第 3 步:如果查不到,检查索引配置名是否一致。
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'articles' AND indexname = 'idx_articles_search'; indexname | indexdef
----------------------+-----------------------------------------------------------------------------------------
idx_articles_search | CREATE INDEX idx_articles_search ON public.articles USING gin (search_vec)
(1 row)生成列的表达式定义在 pg_attribute 里:
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'; 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_short | SET zhparser.multi_short = on |
to_tsquery 报语法错误 | 用户输入含空格/标点 | 改用 websearch_to_tsquery |
| 改为自定义配置后搜不到 | 索引未重建 | REINDEX INDEX idx_articles_search |
| 高亮片段太短 | 中文按字数算 MaxWords | 调大到 40~60 |
注意事项
CREATE EXTENSION zhparser前必须先make install。 扩展的.control与.sql文件必须落到 PostgreSQL 的share/extension/目录,否则CREATE EXTENSION会报找不到扩展。zhparser只提供解析器,不提供配置。 必须自己CREATE TEXT SEARCH CONFIGURATION ... (PARSER = zhparser)并ADD MAPPING,否则配置的映射是空的,所有 token 都会被丢弃,tsvector永远是空。配置名要写全 schema 前缀。
'public.chinese_zh'与'chinese_zh'在search_path不同时会解析成不同 OID,导致索引失配。索引里固化的是regconfig字面量,包含 schema。只映射实词词性。
n,v,a,i,e,l是社区共识。把u(助词)、p(介词)、w(标点)也映射上会引入大量噪音,让ts_rank失真。multi_short建议全局开启。 它让长词同时产出短词组合,显著提升召回率。代价是tsvector变大(约 20%~40%),需要评估存储。自定义词表修改后必须
sync_dictionary()。 只INSERT不sync_dictionary(),新词不会生效。而且已建索引的旧数据不会自动重算,需要REINDEX。zhparser.multi_short是 USERSET 参数,可会话级设置。 但索引时与查询时的设置必须一致,否则两侧切分结果不同、匹配失败。建议在postgresql.conf或ALTER DATABASE里统一设死。中文没有大小写,但
simple词典仍会做 Unicode 归一。 全角/半角、繁简不会自动统一——資料庫与数据库是两个不同词位。需要繁简转换就自己加synonym词典。ts_headline的MaxWords对中文相当于"字数"。 默认 35 太短,中文建议 40~60,否则片段可能被截成一个短语。pg_trgm对 1~2 字的查询无效。 trigram 需要至少 3 个字符,数据库(3 字)刚好够,索引(2 字)会产生退化的三元组,索引几乎不起作用。pg_trgm索引体积很大。 中文三元组基数远高于英文,长文本字段上索引可能比表还大。别在body这种长字段上建。similarity()的分数与ts_rank不可比。 一个是字符重叠比例,一个是词频权重,别混在一起排序。websearch_to_tsquery对中文的引号短语是"词序列"而非"字符序列"。"数据库 系统"要求两个词位相邻,而不是要求字符串相邻。PostgreSQL 大版本升级后需要重建 GIN 索引。 索引格式可能变化(如 v3 到 v4),
pg_upgrade会处理,但自定义扩展的索引建议REINDEX一次。外部搜索引擎不是"更高级的 PostgreSQL 全文搜索"。 它引入了独立服务、数据同步、一致性、运维成本。中文站点的规模没到千万级文档前,zhparser 完全够用。
最佳实践
中文全文搜索首选 zhparser,别用
simple硬扛。 用simple做中文搜索等于没做——整句一个词位,召回率为零。按"实词优先"映射词性。
ADD MAPPING FOR n,v,a,i,e,l WITH simple是经过大量生产验证的最小可用集合。生产环境统一开启
multi_short。 在postgresql.conf或ALTER DATABASE demo SET zhparser.multi_short = on中设死,不要依赖会话级设置。自定义词典纳入版本管理。 词表的
INSERT脚本(如db/seeds/zhparser_words.sql)要进 Git,与代码同步发布,避免环境间词典不一致。中文权重分配:标题 A / 摘要 C / 正文 D。 中文标题的检索价值远高于正文,权重差异要拉开(
A与D的默认系数比是1.0 : 0.1)。用生成列而非表达式索引。 中文表达式通常很长(多字段 + 配置名),生成列让查询只写列名,杜绝"配置名写错/表达式不一致"的事故。
查询入口一律
websearch_to_tsquery。 中文用户的输入常带空格、标点,to_tsquery会直接抛错。中英文混排用两个
tsvector列。 中文用chinese_zh、英文用english,各建索引,查询时按lang字段路由——不要试图用中文配置处理英文(zhparser 会把英文单词按规则切分,效果不稳定)。给
pg_trgm留一个兜底索引。 只建在短字段(title、name、sku)上,用于拼写容错和LIKE加速,别建在长正文上。搜索接口加"结果为空"的降级策略。 全文搜索无结果时自动切到
pg_trgm模糊匹配,再不行才返回"未找到",用户体验会好很多。中文分词结果做回归测试。 用
ts_debug对一组代表性文本写断言查询,纳入 CI——词库或配置漂移时能第一时间发现。规模到千万级文档再考虑 Elasticsearch。 zhparser + GIN 在千万行以内的中文检索场景性能完全够用(毫秒级),过早引入外部搜索引擎是过度工程。
下一节
阶段三「数据类型」到此结束。下一阶段开始学习表结构设计:数据库的创建与管理
参考链接
- PostgreSQL 官方文档:Full Text Search
- PostgreSQL 官方文档:Controlling Text Search
- PostgreSQL 官方文档:CREATE TEXT SEARCH CONFIGURATION
- PostgreSQL 官方文档:pg_trgm