全文搜索配置与词典
前几节我们一直用 to_tsvector('english', body) 这种写法,其中的 'english' 就是一个文本搜索配置(text search configuration)。它决定了「一段文本怎么被切成词元、每个词元怎么被归一化、哪些词算停用词、哪些词该被同义词替换」——换句话说,同一个句子在不同配置下会产出完全不同的 tsvector。
很多「搜不到」的线上问题,根源不在索引、不在 SQL,而在配置:用默认的 simple 配置做英文站,于是 running 与 run 匹配不上;中文站没装分词扩展,整段中文被当成一个词元;同义词表放错文件路径,于是「手机 / mobile / phone」互相召不回。
本节把配置的内部结构拆开讲:解析器(parser)→ 词典(dictionary)→ 配置(configuration) 三层模型,五个内置词典模板的差异与适用场景,从零创建一套自定义配置的完整流程,以及配套的调试函数与系统目录。
前置知识
阅读本节前,建议先了解:搜索排名与权重(ts_rank)
基础概念
三层模型:parser / dictionary / configuration
PostgreSQL 的文本搜索不是「一个黑盒函数」,而是三层职责分明的组件:
原文 "The running dogs barked"
│
▼
1. Parser(解析器)
把字符串切成 token,并给每个 token 打上「类型标签」
the → asciiword / running → asciiword
│ ( token 类型, 原文 )
▼
2. Configuration(配置)
为每种 token 类型指定「词典列表」
asciiword → {english_stem}
│ ( 词典列表, 原文 )
▼
3. Dictionary(词典)
逐个词典尝试归一化,按顺序取结果
running → run / the → ∅(停用词丢弃)
│
▼
'bark':3 'dog':2 'run':1四类对象的关系可以这样记:
| 组件 | 作用 | 类比 |
|---|---|---|
| parser | 决定「怎么切」+「切出来的 token 叫什么类型」 | 词法分析器(lexer) |
| template | 词典的「算法实现」,定义某类词典能做什么 | 类(class) |
| dictionary | template 的一个具体实例,带自己的参数与数据文件 | 对象(instance) |
| configuration | 把 token 类型映射到词典列表 | 配置文件 |
只有 configuration 是我们日常直接使用的对象——to_tsvector('english', ...) 里的 'english' 就是一个 configuration 的名字。parser 与 template 属于底层实现,普通用户极少需要新建。
token 类型决定一切
解析器切出的每个 token 都带一个类型标签,配置里映射的就是这些标签。默认解析器(pg_catalog.default)会产出 23 种类型,常用的有:
token 类型 含义 示例
──────────────── ──────────────────────────── ──────────────────
word 字母开头、含数字或下划线的词 foo_bar
asciiword 纯 ASCII 字母词 running
numword 字母数字混合、以字母结尾 abc123
hword 复合词的一部分 38-51-36
asciihword ASCII 复合词 foo-bar
hword_asciipart 复合词中的 ASCII 片 bar
email 邮箱地址 a@b.com
url / url_path URL 及其路径部分 http://x.com/p
file 文件路径 /etc/passwd
float / int 浮点数 / 整数 3.14 / 42
host 主机名 example.com
version 版本号 8.4.3
blank / tag 空白符 / 标签(XML 等)
protocol / entity 协议前缀(HTTP)/ HTML 实体 &
uint / sfloat 无符号整数 / 科学计数法 1 / 1e-3同一个原始片段可能被重复切出多个 token。 例如 foo-bar 会同时产出 asciihword(整体)与 hword_asciipart(foo、bar 两片),这样无论用户搜 foo-bar、foo bar 还是 foobar,都有机会命中。用 ts_debug('english', 'foo-bar') 可以看到这几行。
词典的四种返回值
这是理解词典行为的核心。一个词典处理某个 token 时,只可能返回四种结果之一:
返回值 含义
────────────────────────────────────────────────────────────
1. 词元数组 {"run"} 正常归一化,产出新词元
2. 单个带 TSL_FILTER 的词元 「过滤词典」:改写后交给下一个词典
3. 空数组 {} 「是停用词,丢弃,不用再看后面的词典」
4. NULL 「不认识这个词,交给下一个词典试试」3 和 4 的区别至关重要:返回 {} 是「到此为止(已消化)」,返回 NULL 是「继续问下一个」。这直接决定词典列表的排列顺序——把 simple 放在最前面,它会吃掉所有词,后面的 ispell、snowball 永远轮不到。
停用词会「吃掉」位置编号
停用词不只是「被忽略」,它会留下位置空洞:
SELECT to_tsvector('english', 'in the list of stop words'); 'list':3 'stop':5 'word':6位置编号是 3, 5, 6——1、2、4 被跳过了,因为 in、the、of 是停用词。位置编号按原文 token 顺序分配,停用词占掉一个位置号后被丢弃。后果有两个:短语搜索的距离计算会变(<N> 跨度里含停用词),ts_rank_cd 的得分也会变——官方给出的对照是 0.05(含停用词)对 0.1(不含),同一个查询分数直接减半。
为什么要理解这一层
业务上必须动配置层的四类场景:
| 需求 | 需要动的东西 |
|---|---|
| 「手机 / mobile / phone」互通 | synonym 词典 + .syn 文件 |
| 「建立 / 建于 / 建」归一到同一词元 | Snowball(英文)、分词扩展(中文) |
屏蔽 的、了、是 等噪音词 | STOPWORDS 停用词表 |
| 「机器学习 / ML」短语级替换 | thesaurus 词典 + .ths 文件 |
语法与用法
psql 元命令:查看现有配置
psql 提供了四个元命令(\dF 系列),全部支持加 + 看更多细节:
\dF [ PATTERN ] 列出配置
\dFd [ PATTERN ] 列出词典(dictionary)
\dFp [ PATTERN ] 列出解析器(parser)
\dFt [ PATTERN ] 列出模板(template)
\dF+ [ PATTERN ] 列配置的详细信息(含 token → 词典映射表)
\dFd+ [ PATTERN ] 列词典的详细信息(含参数值)PATTERN 支持正则,也可以写成 schema.name 分别限定模式与对象名,例如 \dF+ russian、\dFd+ pg_catalog.english_stem。
查看某个配置的 token → 词典映射
这是排查「为什么这个词被这样处理」的第一站:
\dF+ english Text search configuration "pg_catalog.english"
Parser: "pg_catalog.default"
Token | Dictionaries
-------------+-----------------------------
asciihword | english_stem
asciiword | english_stem
email | simple
file | simple
float | simple
host | simple
hword | english_stem
hword_asciipart | english_stem
hword_numpart | simple
hword_part | english_stem
int / numhword / numword / sfloat / uint | simple
url / url_path / version | simple
word | english_stem读法:asciiword 这类「真正的词」交给 english_stem(Snowball 英语词干词典)处理;而 email、url、int 这些「不该被词干化」的类型交给 simple(只做小写化 + 查停用词)。这个设计很合理——你不会希望 http://x.com 被词干化。
查看模板:只有五个内置模板
\dFt List of text search templates
Schema | Name | Description
------------+------------+---------------------------------------
pg_catalog | ispell | ispell dictionary
pg_catalog | simple | simple dictionary: just lower case and check for stopword
pg_catalog | snowball | snowball stemmer
pg_catalog | synonym | synonym dictionary: replace word by its synonym
pg_catalog | thesaurus | thesaurus dictionary: phrase by phrase substitution只有 5 个内置模板。 注意这里没有 stop 模板——「停用词」不是一个独立模板,而是 simple 词典模板的一个参数(STOPWORDS)。所谓「停用词词典」通常就是用 TEMPLATE = simple 加一个 STOPWORDS 文件实现的。
查看解析器与词典
\dFp # 只有一个:pg_catalog | default | default word parser
\dFd # 一批 xxx_stem(Snowball)+ simple
\dFd+ pg_catalog.simple # 看某个词典的 Owner / Default? / Description\dFp+ default 会列出解析器的 5 个方法(prsd_start / prsd_nexttoken / prsd_end / prsd_headline / prsd_lextype)与全部 23 种 token 类型。
创建自定义词典
-- 最小形式:使用模板默认行为
CREATE TEXT SEARCH DICTIONARY my_dict (
TEMPLATE = pg_catalog.simple
);
-- 带停用词表
CREATE TEXT SEARCH DICTIONARY public.simple_dict (
TEMPLATE = pg_catalog.simple,
STOPWORDS = english
);STOPWORDS = english 指向文件 $SHAREDIR/tsearch_data/english.stop。参数值写的是文件名(不含扩展名),文件名不能带路径分隔符。
创建自定义配置
有两种起点:完全从零,或从已有配置复制。
-- 方式一:从零(只指定解析器)
CREATE TEXT SEARCH CONFIGURATION public.my_cfg (
PARSER = pg_catalog.default
);
-- 方式二:复制已有配置(推荐)
CREATE TEXT SEARCH CONFIGURATION public.my_cfg (
COPY = pg_catalog.english
);从零创建的配置没有任何 token 映射,to_tsvector 会返回空向量,必须逐个 ADD MAPPING。而 COPY 会把源配置的解析器和全部 token 映射一起复制过来,是「改一点点」思路下最省事的方式。
修改映射:ADD / ALTER / DROP MAPPING
-- 新增映射(该 token 类型原来没有映射时用 ADD)
ALTER TEXT SEARCH CONFIGURATION public.my_cfg
ADD MAPPING FOR asciiword WITH my_dict, english_stem;
-- 替换已有映射
ALTER TEXT SEARCH CONFIGURATION public.my_cfg
ALTER MAPPING FOR asciiword WITH my_dict, english_ispell, english_stem;
-- 批量替换「所有已有映射中的某个词典」
ALTER TEXT SEARCH CONFIGURATION public.my_cfg
ALTER MAPPING REPLACE english_stem WITH snowball_stem;
-- 删除映射(该 token 类型不再被索引)
ALTER TEXT SEARCH CONFIGURATION public.my_cfg
DROP MAPPING FOR email, url, url_path, sfloat, float;ALTER MAPPING 与 ADD MAPPING 的区别:前者要求该 token 类型已有映射,后者要求没有。写错会报 mapping for token type ... already exists 或 ... does not exist。
使用与设置配置
-- 显式指定配置名(推荐)
SELECT to_tsvector('public.my_cfg', 'The running dogs');
SELECT to_tsquery('public.my_cfg', 'running');
-- 省略配置名 → 走 default_text_search_config
SELECT to_tsvector('The running dogs');-- 会话级
SET default_text_search_config = 'public.my_cfg';
SHOW default_text_search_config;
-- 数据库级(持久化到该库的 pg_db_role_setting)
ALTER DATABASE mydb SET default_text_search_config = 'public.my_cfg';
-- 全局(postgresql.conf):default_text_search_config = 'pg_catalog.english'
-- 读取当前值
SELECT current_setting('default_text_search_config'); current_setting
-----------------
pg_catalog.english关键点:默认配置只影响「省略配置名」的那些调用。 显式写 to_tsvector('english', ...) 时,default_text_search_config 完全不起作用。这一点极其重要——建索引时用什么配置,查询时就必须用什么配置,否则两边产出的词元对不上,索引形同虚设。
详细说明
simple 模板:最小可用的归一化
simple 做两件事:转小写 + 查停用词表。没有词干化,没有同义词。
CREATE TEXT SEARCH DICTIONARY public.simple_dict (
TEMPLATE = pg_catalog.simple,
STOPWORDS = english
);
SELECT ts_lexize('public.simple_dict', 'YeS'); -- {yes} 小写化生效
SELECT ts_lexize('public.simple_dict', 'The'); -- {} 停用词(不是 NULL!)simple 模板的参数:
| 参数 | 默认 | 作用 |
|---|---|---|
STOPWORDS | 无 | 停用词文件名(不含 .stop),不指定则不停用任何词 |
Accept | true | 输入词在停用词表中时,返回 {}(true)还是 NULL(false) |
CREATE TEXT SEARCH DICTIONARY public.passive_simple (
TEMPLATE = pg_catalog.simple,
STOPWORDS = english,
Accept = false
);
SELECT ts_lexize('public.passive_simple', 'The'); -- 返回 NULLAccept = false 什么时候用? 只有当你把 simple 放在词典列表末尾之前、希望停用词判断失败时继续往下走时才合理。放在列表最后时,false 会让「停用词」变成「未知词」,两者最终都被丢弃,效果相同但语义更绕——所以官方建议只在必要时用。
snowball 模板:词干化(stemming)
Snowball 词典做词干化:把单词归约到词根,让不同形态互相匹配。
CREATE TEXT SEARCH DICTIONARY public.my_english_stem (
TEMPLATE = pg_catalog.snowball,
language = english,
stopwords = english
);| 参数 | 说明 |
|---|---|
language | 必需,必须填 Snowball 支持的语言名(english、russian、french…) |
stopwords | 可选,停用词文件名 |
Snowball 先查停用词表,再词干化(官方 12.6.1 明确说明;ispell 的顺序正好相反)。
SELECT to_tsvector('english', 'runs running ran'); 'ran':3 'run':1 'run':2runs 与 running 都归到 run,但 ran 是不规则变形,Snowball 不认得,保持原样——这是 Snowball 词干化的固有局限:它只做规则后缀剥离,不查词典。
用 simple 收尾的重要性:即使前面所有词典都对某个 token 返回 NULL,列表末尾放一个不设停用词的 simple 词典,至少能保证「小写化后进入索引」。否则该 token 会被完全丢弃。
ispell 模板:拼写词典
ispell 基于 Ispell/Hunspell 格式的词典文件,能干三件事:识别词典中已有的词形、按词缀规则识别派生词、做停用词过滤。
CREATE TEXT SEARCH DICTIONARY public.english_ispell (
TEMPLATE = ispell,
DictFile = english,
AffFile = english,
StopWords = english
);
SELECT ts_lexize('public.english_ispell', 'cats'); -- {cat}| 参数 | 对应文件(位于 $SHAREDIR/tsearch_data/) |
|---|---|
DictFile | <name>.dict |
AffFile | <name>.affix |
StopWords | <name>.stop |
Ispell 先归一化(查词典 + 词缀),再查停用词表,与 Snowball 的顺序相反。
从 Hunspell .oxt 包提取词典:OpenOffice/LibreOffice 的语言包是 .oxt(本身是 ZIP)文件,解压后取出 .aff 与 .dic,改名为 .affix 与 .dict 放到 $SHAREDIR/tsearch_data/:
unzip en_US.oxt -d en_us_extract
cp en_us_extract/en_US.aff en_us.affix
cp en_us_extract/en_US.dic en_us.dict
cp en_us.affix en_us.dict "$(pg_config --sharedir)/tsearch_data/"CREATE TEXT SEARCH DICTIONARY public.english_hunspell (
TEMPLATE = ispell,
DictFile = en_us,
AffFile = en_us,
Stopwords = english
);Ispell 词典文件体量远大于 Snowball,但词干化质量更好(它知道 ran → run),代价是内存占用与加载时间。
synonym 模板:同义词替换
同义词词典把词替换成规范词,解决「手机 / 移动电话 / mobile / phone」的召回问题。
CREATE TEXT SEARCH DICTIONARY public.my_synonym (
TEMPLATE = synonym,
SYNONYMS = my_synonyms
);| 参数 | 必需 | 说明 |
|---|---|---|
SYNONYMS | 是 | 文件名(不含 .syn),位于 $SHAREDIR/tsearch_data/ |
CaseSensitive | 否 | 默认 false。为 false 时,文件里的词与输入 token 都折叠为小写后再比对 |
.syn 文件格式:每行一条,第一个词是「被替换词」,后面全部是它对应的同义词(含它自己)。
# my_synonyms.syn
postgres pgsql
postgresql pgsql
postgre pgsql
gogle googl
indices index*#开头是注释,空行忽略。- 反斜杠
\转义(行尾\会续行)。 - 行尾的
*是前缀标记:表示它是一个前缀而非完整词。
* 标记的行为差异是本节最容易踩的坑:
场景 * 的行为
─────────────────────────────────────────────────────────────
to_tsvector() 中 忽略 *,直接把词当同义词
to_tsquery() 中 生成前缀匹配标记 index:*完整示例:
SELECT ts_lexize('public.my_synonym', 'indices'); -- {index}
CREATE TEXT SEARCH CONFIGURATION public.tst (COPY = pg_catalog.simple);
ALTER TEXT SEARCH CONFIGURATION public.tst
ALTER MAPPING FOR asciiword WITH public.my_synonym;
SELECT to_tsvector('public.tst', 'indices'); -- 'index':1
SELECT to_tsquery('public.tst', 'indices'); -- 'index':*同义词词典只是「前面挡一层」,它把 Paris 换成小写规范形之后,后面的词干词典还会继续处理。对比 ts_debug('english', 'Paris') 的输出:默认 dictionaries 是 {english_stem}、lexemes 是 {pari};加上同义词词典后 dictionaries 变成 {my_synonym,english_stem}、dictionary(实际命中者)是 my_synonym、lexemes 变成 {paris}。词典列表的排列原则:越窄、越具体的词典放越前面,越通用的放最后。
thesaurus 模板:短语级替换
同义词词典只能处理单个词;词库模板(thesaurus)处理的是短语——「一个由若干词组成的样本短语 → 一个或多个索引词」。
CREATE TEXT SEARCH DICTIONARY public.thesaurus_simple (
TEMPLATE = thesaurus,
DictFile = mythesaurus,
Dictionary = pg_catalog.english_stem
);| 参数 | 必需 | 说明 |
|---|---|---|
DictFile | 是 | 文件名(不含 .ths) |
Dictionary | 是 | subdictionary,用来把样本词归一化后再与 .ths 中的样本短语比对 |
CaseSensitive | 否 | 默认 false |
.ths 文件格式为 sample word(s) : indexed word(s),每行一条:
# mythesaurus.ths
supernova stars : sn
stars : sn
? one ? two : swsw? 表示该位置可以是任意停用词:规则 ? one ? two : swsw 能匹配 a one the two、the one a two 等。
匹配规则:最长匹配优先(能匹配 3 个词的规则优先于只匹配 1 个词的规则);长度相同取文件中靠后的定义;样本词必须被 subdictionary 认识,否则规则不生效;indexed word 前加 * 表示跳过 subdictionary、直接按原样输出。
词库词典的改动要求重建索引
thesaurus 会改变词元在文档中的位置编号,因此官方明确要求:任何 thesaurus 参数(包括 .ths 文件内容)的变化,都必须 REINDEX 相关索引。其他词典类型(增删停用词)通常不强制重建索引。
过滤词典与 unaccent
「过滤词典」是一个特殊概念:它不产出词元,而是把词元改写后交给列表里的下一个词典继续处理,带 TSL_FILTER 标志。内置的典型例子是 unaccent 扩展提供的词典:
CREATE EXTENSION IF NOT EXISTS unaccent;
ALTER TEXT SEARCH CONFIGURATION public.my_cfg
ALTER MAPPING FOR word, asciiword WITH unaccent, english_stem;这样 café 会先被 unaccent 改写成 cafe,再交给 english_stem 词干化。过滤词典可以放在列表的任意位置,但放在最后没有意义(没有下一个词典接收它改写的词元)。
和「列表内位置」直接相关的,是整条词典列表的排序:官方给出的通用建议是:最窄、最具体的放前面,最通用的放后面——thesaurus(短语)→ synonym(同义词)→ ispell(拼写词典)→ snowball stemmer(词干化)→ simple(兜底)。官方示例:
ALTER TEXT SEARCH CONFIGURATION astro_en
ADD MAPPING FOR asciiword WITH astrosyn, english_ispell, english_stem;调试函数:ts_debug()
ts_debug() 是排查配置问题的第一工具,签名如下:
ts_debug([ config regconfig, ]
document text,
OUT alias text,
OUT description text,
OUT token text,
OUT dictionaries regdictionary[],
OUT dictionary regdictionary,
OUT lexemes text[])
returns setof record每个 token 返回一行。 各列含义:
列名 含义
───────────── ────────────────────────────────────────────────
alias token 类型短名(对应配置映射表里的 Token 列)
description token 类型的长描述
token 解析器切出的原始文本片段
dictionaries 该 token 类型在配置里被指定的全部词典
dictionary 实际「认领」了这个 token 的词典(没有则为 NULL)
lexemes 该词典产出的词元数组
({} 表示识别为停用词;NULL 表示没有词典认领)SELECT * FROM ts_debug('english', 'a fat cat sat on a mat - it ate a fat rats'); alias | description | token | dictionaries | dictionary | lexemes
-----------+----------------+-------+----------------+--------------+---------
asciiword | Word, all ASCII| a | {english_stem} | english_stem | {}
asciiword | Word, all ASCII| fat | {english_stem} | english_stem | {fat}
...三个关键读法: lexemes = {} 表示这个词是停用词(a、it 都被丢弃);lexemes = {fat} 表示正常产出;alias = blank 且 dictionaries = {} 表示解析器识别出了空白符,但配置没有为 blank 指定词典,于是无声丢弃。
如果某个 token 的 dictionary 是 NULL,说明配置里没有任何词典认领这个 token 类型——通常是 ADD MAPPING 时漏了这个类型。
调试函数:ts_lexize()
ts_lexize() 直接测试单个词典,跳过配置与解析器:
ts_lexize(dict regdictionary, token text) returns text[]SELECT ts_lexize('english_stem', 'running'); -- {run}
SELECT ts_lexize('public.my_synonym', 'postgres'); -- {pgsql}返回值的三种形态,与前面「词典的四种返回值」对应: {lexeme} 正常归一化、{} 停用词(已消化)、NULL 词典不认识这个词。另外,ts_lexize 的输入是单个 token,不要传整句(传整句只会按整串处理,结果不可预期)。
停用词文件位置与重读
所有词典的配置数据文件都放在 $SHAREDIR/tsearch_data/ 下(不是 postgresql.conf 所在目录,也不是 pgdata):
pg_config --sharedir # 例如 /usr/local/pgsql/share $SHAREDIR/tsearch_data/english.stop 停用词(词库)
$SHAREDIR/tsearch_data/my_synonyms.syn 同义词
$SHAREDIR/tsearch_data/mythesaurus.ths 词库(短语替换)
$SHAREDIR/tsearch_data/english.dict ispell 词典主文件
$SHAREDIR/tsearch_data/english.affix ispell 词缀规则Debian/Ubuntu 的 apt 安装通常在 /usr/share/postgresql/18/。
词典文件的编码必须是 UTF-8
官方明确警告(12.6.1):词典配置文件必须是 UTF-8 编码。服务器在读取时会把它转换成当前数据库的编码。用 GBK/GB18030 保存的中文停用词表会导致加载失败或乱码。
改动词典配置文件后必须通知服务器重读
每个会话只在首次使用某个配置时读取一次词典文件,之后结果被缓存。直接修改 .stop/.syn/.ths 文件不会立即生效,必须对词典执行一次 ALTER TEXT SEARCH DICTIONARY(哪怕参数写回原值)来触发重读:
ALTER TEXT SEARCH DICTIONARY public.simple_dict ( Accept = true );系统目录:词典与配置的底层存储
前面所有 \dF* 元命令读的都是系统目录。PG 18 有四个核心目录:
| 目录 | 存什么 | 关键列 |
|---|---|---|
pg_ts_config | 文本搜索配置 | oid、cfgname、cfgnamespace、cfgowner、cfgparser |
pg_ts_dict | 词典实例 | oid、dictname、dictnamespace、dictowner、dicttemplate、dictinitoption |
pg_ts_parser | 解析器 | oid、prsname、prsnamespace、prsstart、prstoken、prsend、prsheadline、prslextype |
pg_ts_template | 词典模板 | oid、tmplname、tmplnamespace、tmplinit、tmpllexize |
pg_ts_config 的映射关系不在本表里,而在第五个目录 pg_ts_config_map 中(这是最容易忽略的一点):
-- 列出某个配置的全部 token → 词典映射
SELECT m.maptokentype, m.mapseqno, m.mapdict::regdictionary AS dict
FROM pg_ts_config_map m
WHERE m.mapcfg = 'english'::regconfig
ORDER BY m.maptokentype, m.mapseqno;
-- 我建的配置都在哪个 schema?用的是哪个解析器?
SELECT n.nspname, c.cfgname, p.prsname
FROM pg_ts_config c
JOIN pg_namespace n ON n.oid = c.cfgnamespace
JOIN pg_ts_parser p ON p.oid = c.cfgparser
ORDER BY n.nspname, c.cfgname;
-- 每个词典用的是哪个模板、带什么参数?
SELECT d.dictname, t.tmplname, d.dictinitoption
FROM pg_ts_dict d
JOIN pg_ts_template t ON t.oid = d.dicttemplate
ORDER BY t.tmplname, d.dictname;pg_ts_parser、pg_ts_template 的注释里都明确写着:这两类对象只能用 C 语言级函数实现,创建它们仅限超级用户。所以普通应用中「新建解析器/模板」几乎不会发生。
实战示例
示例一:中文分词(前置说明)
PostgreSQL 内置没有中文配置。\dF 的输出里只有 simple 和一批 Snowball 语言(arabic、danish… russian、swedish 等),没有 chinese。原因是 Snowball 不支持中文词干化,中文需要分词而非词干化。解决方案是安装分词扩展(zhparser 或 pg_jieba)并自建配置,这部分在下一节 中文全文搜索方案 展开。这里先给出配置侧骨架(以 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, d, t WITH simple;示例二:为 PostgreSQL 技术站建一套自定义配置
需求:把 postgres/postgresql/pgsql 归一到 pg;启用英文词干化;去掉 email、url 等无意义类型的索引。
第一步:准备同义词文件 $SHAREDIR/tsearch_data/pg_dict.syn:
postgres pg
pgsql pg
postgresql pg第二步:创建配置与词典:
CREATE TEXT SEARCH CONFIGURATION public.pg ( COPY = pg_catalog.english );
CREATE TEXT SEARCH DICTIONARY public.pg_dict (
TEMPLATE = synonym,
SYNONYMS = pg_dict
);
CREATE TEXT SEARCH DICTIONARY public.english_ispell (
TEMPLATE = ispell,
DictFile = english,
AffFile = english,
StopWords = english
);第三步:替换映射并删除多余类型:
ALTER TEXT SEARCH CONFIGURATION public.pg
ALTER MAPPING FOR asciiword, asciihword, hword_asciipart, word, hword, hword_part
WITH public.pg_dict, public.english_ispell, pg_catalog.english_stem;
ALTER TEXT SEARCH CONFIGURATION public.pg
DROP MAPPING FOR email, url, url_path, sfloat, float;第四步:验证。SELECT * FROM ts_debug('public.pg', 'PostgreSQL is great') 会输出三行有效 token:PostgreSQL 被 pg_dict(列表第一个词典)认领、产出 pg;is 被 english_ispell 认出是停用词、产出 {};great 也被 english_ispell 认领。
第五步:设为默认(可选):SET default_text_search_config = 'public.pg';
示例三:用 ts_lexize 快速回归测试词典
不建索引、不起表,纯函数级测试:
SELECT ts_lexize('public.pg_dict', 'PostgreSQL') AS syn,
ts_lexize('english_stem', 'running') AS stem,
ts_lexize('english_stem', 'ran') AS irregular,
ts_lexize('english_stem', 'the') AS stopword; syn | stem | irregular | stopword
--------+-------+-----------+----------
{pg} | {run} | {ran} | {}一眼看出三个事实:同义词生效、规则变形被词干化、不规则变形(ran)没有被词干化、停用词返回空数组。这套断言可以直接搬进 CI。
注意事项
配置文件必须是 UTF-8。 这是官方 12.6.1 的第一条 Caution。用 GBK / GB18030 / UTF-16 保存的词典文件在加载时会失败或产生乱码。
改了词典文件不会自动生效。 会话只在首次使用时读一次,必须 ALTER TEXT SEARCH DICTIONARY 触发重读:
ALTER TEXT SEARCH DICTIONARY public.simple_dict ( Accept = true ); -- 触发重读CREATE TEXT SEARCH CONFIGURATION(无 COPY)产出的配置是空的。 从零建配置后直接使用,会返回空向量,任何查询都搜不到:
-- 危险:返回空向量
CREATE TEXT SEARCH CONFIGURATION public.empty_cfg (PARSER = pg_catalog.default);
-- 正确:从已有配置复制,或补上映射
CREATE TEXT SEARCH CONFIGURATION public.good_cfg (COPY = pg_catalog.english);ADD MAPPING 与 ALTER MAPPING 不能互换。 token 类型已有映射时用 ADD 会报 mapping for token type ... already exists;没有映射时用 ALTER 会报 mapping for token type ... does not exist。
词典列表顺序写错会让后面的词典永远不生效。 simple(无 STOPWORDS)放在最前面会吃掉一切:
-- 错误!english_stem 永远不会被调用
ALTER TEXT SEARCH CONFIGURATION t ALTER MAPPING FOR asciiword WITH simple, english_stem;
-- 正确:具体的在前,通用的在后
ALTER TEXT SEARCH CONFIGURATION t ALTER MAPPING FOR asciiword WITH my_synonym, english_ispell, english_stem;thesaurus 改动后必须重建索引。 它影响词元位置,与其他词典不同:REINDEX INDEX idx_articles_fts;
同义词文件里行尾的 * 在 to_tsvector() 中会被忽略。 这是有意设计(索引侧不需要前缀标记),不要以为写错了:index* 经 to_tsvector 得到 'index':1(不带 *),经 to_tsquery 得到 'index':*(带 *)。
停用词不是「不存在」,它会占掉位置编号。 依赖位置的查询(<N>、ts_rank_cd)结果会受影响——to_tsvector('english', 'in the list of stop words') 的位置是 3, 5, 6,不是 1, 2, 3。
建索引与查询必须用同一个配置。 这是最隐蔽的错误来源——两边配置不同,词元对不上,索引完全失效但不报错:
-- 索引用 english 建
CREATE INDEX idx ON t USING gin (to_tsvector('english', body));
-- 查询却用了 simple → 走不了索引,且结果错误
SELECT * FROM t WHERE to_tsvector('simple', body) @@ to_tsquery('simple', 'running');不要用 simple 配置做英文站。 它不做词干化,running 搜不到 run:to_tsvector('simple', 'running') 得到 'running',而 to_tsvector('english', 'running') 得到 'run'。
Ispell 词典文件体量大,注意内存与加载时间。 首次加载 .dict/.affix 可能占用数十 MB 共享内存。
最佳实践
优先「复制现成配置」而不是从零创建。
CREATE TEXT SEARCH CONFIGURATION x (COPY = pg_catalog.english)能一次性继承解析器与全部 token 映射,只在需要改的地方ALTER MAPPING,故障面最小。字典列表遵循「具体 → 通用」顺序,末尾必须有兜底。 推荐的英文站顺序是
thesaurus → synonym → ispell → snowball stemmer → simple;末尾保留一个不带停用词的simple或词干词典,保证任何 token 至少有小写化后的表示。建配置前先用
\dF+ english看清默认映射表。 明确每个 token 类型当前由哪个词典负责,只改动你真正关心的几行,避免误伤url、email等特殊类型。把配置做成可脚本化的迁移文件。 词典文件属于数据库外部状态(放在
$SHAREDIR/tsearch_data/),不随pg_dump走。把.syn/.ths/.stop的生成与CREATE TEXT SEARCH ...语句都写进版本控制,否则新环境恢复数据库后配置会「缺文件」:
CREATE TEXT SEARCH DICTIONARY IF NOT EXISTS public.pg_dict (
TEMPLATE = synonym, SYNONYMS = pg_dict
);改完词典文件,一定执行一次
ALTER TEXT SEARCH DICTIONARY触发重读。 可以在应用部署脚本里带上一句(参数写回原值即可),否则线上会长期使用旧缓存。配置与索引成对版本化。 凡是会改变词元的改动(换词典模板、改同义词、改 thesaurus、改
default_text_search_config),都必须同步REINDEX相关索引,并把「配置版本号」记进部署记录,便于定位不一致。default_text_search_config只作为「便利开关」,生产 SQL 一律显式写配置名。 显式写名字才能让「索引侧」与「查询侧」在代码里一眼可见地一致,也避免不同环境(不同ALTER DATABASE SET)行为漂移。用
ts_debug()+ts_lexize()建立词典回归测试。 至少固化「一批输入词 → 期望词元」的断言,覆盖同义词、词干化、停用词、前缀标记四类情形,改动词典后跑一遍。停用词表保持精简。 过度停用会损失精度(
not、no被丢弃后,否定查询彻底失效)。建议只在ts_stat统计中确认「确实高频且无区分度」的词才加进.stop。中文场景不要用内置配置硬撑。 PostgreSQL 18 内置没有中文配置,
\dF里没有chinese。中文必须安装分词扩展后自建配置,详见 中文全文搜索方案。建立配置对象的命名规范与 schema 归属。 自定义配置、词典统一放在业务 schema(如
public)下并加项目前缀(public.pg、public.zh_cfg),避免与pg_catalog内置对象混淆,也便于\dF public.*一次性查看。thesaurus 谨慎使用、单独备份。 它是唯一「改内容就要求重建索引」的词典类型,且
.ths文件不会进pg_dump。启用前先评估索引规模与可接受的REINDEX窗口。
下一节
继续学习:GIN / GiST 全文索引
参考链接
- PostgreSQL 官方文档:Dictionaries(12.6,词典模板、停用词、同义词、词库、ts_lexize)
- PostgreSQL 官方文档:Configuration Example(12.7,完整配置示例)
- PostgreSQL 官方文档:Testing and Debugging Text Search(12.8,ts_debug)
- PostgreSQL 官方文档:psql Support(12.10,\dF / \dFd / \dFp / \dFt)
- PostgreSQL 官方文档:Parsers(12.5,解析器与 token 类型)
- PostgreSQL 官方文档:CREATE TEXT SEARCH DICTIONARY
- PostgreSQL 官方文档:CREATE TEXT SEARCH CONFIGURATION
- PostgreSQL 官方文档:ALTER TEXT SEARCH CONFIGURATION(ADD / ALTER / DROP MAPPING)
- PostgreSQL 官方文档:pg_ts_config / pg_ts_dict / pg_ts_parser / pg_ts_template 系统目录
- PostgreSQL 官方文档:Text Search Functions and Operators(9.13,ts_debug / ts_lexize 签名)