Skip to content

全文搜索配置与词典 ​

前几节我们一直用 to_tsvector('english', body) 这种写法,其中的 'english' 就是一个文本搜索配置(text search configuration)。它决定了「一段文本怎么被切成词元、每个词元怎么被归一化、哪些词算停用词、哪些词该被同义词替换」——换句话说,同一个句子在不同配置下会产出完全不同的 tsvector。

很多「搜不到」的线上问题,根源不在索引、不在 SQL,而在配置:用默认的 simple 配置做英文站,于是 running 与 run 匹配不上;中文站没装分词扩展,整段中文被当成一个词元;同义词表放错文件路径,于是「手机 / mobile / phone」互相召不回。

本节把配置的内部结构拆开讲:解析器(parser)→ 词典(dictionary)→ 配置(configuration) 三层模型,五个内置词典模板的差异与适用场景,从零创建一套自定义配置的完整流程,以及配套的调试函数与系统目录。

前置知识

阅读本节前,建议先了解:搜索排名与权重(ts_rank)

基础概念 ​

三层模型:parser / dictionary / configuration ​

PostgreSQL 的文本搜索不是「一个黑盒函数」,而是三层职责分明的组件:

text
   原文 "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)
dictionarytemplate 的一个具体实例,带自己的参数与数据文件对象(instance)
configuration把 token 类型映射到词典列表配置文件

只有 configuration 是我们日常直接使用的对象——to_tsvector('english', ...) 里的 'english' 就是一个 configuration 的名字。parser 与 template 属于底层实现,普通用户极少需要新建。

token 类型决定一切 ​

解析器切出的每个 token 都带一个类型标签,配置里映射的就是这些标签。默认解析器(pg_catalog.default)会产出 23 种类型,常用的有:

text
   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 时,只可能返回四种结果之一:

text
   返回值                      含义
   ────────────────────────────────────────────────────────────
   1. 词元数组 {"run"}         正常归一化,产出新词元
   2. 单个带 TSL_FILTER 的词元   「过滤词典」:改写后交给下一个词典
   3. 空数组 {}                「是停用词,丢弃,不用再看后面的词典」
   4. NULL                     「不认识这个词,交给下一个词典试试」

3 和 4 的区别至关重要:返回 {} 是「到此为止(已消化)」,返回 NULL 是「继续问下一个」。这直接决定词典列表的排列顺序——把 simple 放在最前面,它会吃掉所有词,后面的 ispell、snowball 永远轮不到。

停用词会「吃掉」位置编号 ​

停用词不只是「被忽略」,它会留下位置空洞:

sql
SELECT to_tsvector('english', 'in the list of stop words');
text
 '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 系列),全部支持加 + 看更多细节:

text
   \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 → 词典映射 ​

这是排查「为什么这个词被这样处理」的第一站:

bash
\dF+ english
text
     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 被词干化。

查看模板:只有五个内置模板 ​

bash
\dFt
text
            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 文件实现的。

查看解析器与词典 ​

bash
\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 类型。

创建自定义词典 ​

sql
-- 最小形式:使用模板默认行为
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。参数值写的是文件名(不含扩展名),文件名不能带路径分隔符。

创建自定义配置 ​

有两种起点:完全从零,或从已有配置复制。

sql
-- 方式一:从零(只指定解析器)
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 ​

sql
-- 新增映射(该 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。

使用与设置配置 ​

sql
-- 显式指定配置名(推荐)
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');
sql
-- 会话级
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');
text
 current_setting
-----------------
 pg_catalog.english

关键点:默认配置只影响「省略配置名」的那些调用。 显式写 to_tsvector('english', ...) 时,default_text_search_config 完全不起作用。这一点极其重要——建索引时用什么配置,查询时就必须用什么配置,否则两边产出的词元对不上,索引形同虚设。

详细说明 ​

simple 模板:最小可用的归一化 ​

simple 做两件事:转小写 + 查停用词表。没有词干化,没有同义词。

sql
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),不指定则不停用任何词
Accepttrue输入词在停用词表中时,返回 {}(true)还是 NULL(false)
sql
CREATE TEXT SEARCH DICTIONARY public.passive_simple (
    TEMPLATE  = pg_catalog.simple,
    STOPWORDS = english,
    Accept    = false
);

SELECT ts_lexize('public.passive_simple', 'The');   -- 返回 NULL

Accept = false 什么时候用? 只有当你把 simple 放在词典列表末尾之前、希望停用词判断失败时继续往下走时才合理。放在列表最后时,false 会让「停用词」变成「未知词」,两者最终都被丢弃,效果相同但语义更绕——所以官方建议只在必要时用。

snowball 模板:词干化(stemming) ​

Snowball 词典做词干化:把单词归约到词根,让不同形态互相匹配。

sql
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 的顺序正好相反)。

sql
SELECT to_tsvector('english', 'runs running ran');
text
 'ran':3 'run':1 'run':2

runs 与 running 都归到 run,但 ran 是不规则变形,Snowball 不认得,保持原样——这是 Snowball 词干化的固有局限:它只做规则后缀剥离,不查词典。

用 simple 收尾的重要性:即使前面所有词典都对某个 token 返回 NULL,列表末尾放一个不设停用词的 simple 词典,至少能保证「小写化后进入索引」。否则该 token 会被完全丢弃。

ispell 模板:拼写词典 ​

ispell 基于 Ispell/Hunspell 格式的词典文件,能干三件事:识别词典中已有的词形、按词缀规则识别派生词、做停用词过滤。

sql
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/:

bash
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/"
sql
CREATE TEXT SEARCH DICTIONARY public.english_hunspell (
    TEMPLATE  = ispell,
    DictFile  = en_us,
    AffFile   = en_us,
    Stopwords = english
);

Ispell 词典文件体量远大于 Snowball,但词干化质量更好(它知道 ran → run),代价是内存占用与加载时间。

synonym 模板:同义词替换 ​

同义词词典把词替换成规范词,解决「手机 / 移动电话 / mobile / phone」的召回问题。

sql
CREATE TEXT SEARCH DICTIONARY public.my_synonym (
    TEMPLATE = synonym,
    SYNONYMS = my_synonyms
);
参数必需说明
SYNONYMS是文件名(不含 .syn),位于 $SHAREDIR/tsearch_data/
CaseSensitive否默认 false。为 false 时,文件里的词与输入 token 都折叠为小写后再比对

.syn 文件格式:每行一条,第一个词是「被替换词」,后面全部是它对应的同义词(含它自己)。

text
# my_synonyms.syn
postgres    pgsql
postgresql  pgsql
postgre     pgsql
gogle       googl
indices     index*
  • # 开头是注释,空行忽略。
  • 反斜杠 \ 转义(行尾 \ 会续行)。
  • 行尾的 * 是前缀标记:表示它是一个前缀而非完整词。

* 标记的行为差异是本节最容易踩的坑:

text
   场景                        * 的行为
   ─────────────────────────────────────────────────────────────
   to_tsvector() 中            忽略 *,直接把词当同义词
   to_tsquery()  中            生成前缀匹配标记 index:*

完整示例:

sql
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)处理的是短语——「一个由若干词组成的样本短语 → 一个或多个索引词」。

sql
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),每行一条:

text
# 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 扩展提供的词典:

sql
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(兜底)。官方示例:

sql
ALTER TEXT SEARCH CONFIGURATION astro_en
    ADD MAPPING FOR asciiword WITH astrosyn, english_ispell, english_stem;

调试函数:ts_debug() ​

ts_debug() 是排查配置问题的第一工具,签名如下:

text
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 返回一行。 各列含义:

text
   列名           含义
   ─────────────  ────────────────────────────────────────────────
   alias          token 类型短名(对应配置映射表里的 Token 列)
   description    token 类型的长描述
   token          解析器切出的原始文本片段
   dictionaries   该 token 类型在配置里被指定的全部词典
   dictionary     实际「认领」了这个 token 的词典(没有则为 NULL)
   lexemes        该词典产出的词元数组
                  ({} 表示识别为停用词;NULL 表示没有词典认领)
sql
SELECT * FROM ts_debug('english', 'a fat cat sat on a mat - it ate a fat rats');
text
   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() 直接测试单个词典,跳过配置与解析器:

text
ts_lexize(dict regdictionary, token text) returns text[]
sql
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):

bash
pg_config --sharedir      # 例如 /usr/local/pgsql/share
text
   $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(哪怕参数写回原值)来触发重读:

sql
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 中(这是最容易忽略的一点):

sql
-- 列出某个配置的全部 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 为例,安装扩展后可用):

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, d, t WITH simple;

示例二:为 PostgreSQL 技术站建一套自定义配置 ​

需求:把 postgres/postgresql/pgsql 归一到 pg;启用英文词干化;去掉 email、url 等无意义类型的索引。

第一步:准备同义词文件 $SHAREDIR/tsearch_data/pg_dict.syn:

text
postgres    pg
pgsql       pg
postgresql  pg

第二步:创建配置与词典:

sql
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
);

第三步:替换映射并删除多余类型:

sql
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 快速回归测试词典 ​

不建索引、不起表,纯函数级测试:

sql
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;
text
   syn  |  stem | irregular | stopword
--------+-------+-----------+----------
 {pg}   | {run} | {ran}     | {}

一眼看出三个事实:同义词生效、规则变形被词干化、不规则变形(ran)没有被词干化、停用词返回空数组。这套断言可以直接搬进 CI。

注意事项 ​

配置文件必须是 UTF-8。 这是官方 12.6.1 的第一条 Caution。用 GBK / GB18030 / UTF-16 保存的词典文件在加载时会失败或产生乱码。

改了词典文件不会自动生效。 会话只在首次使用时读一次,必须 ALTER TEXT SEARCH DICTIONARY 触发重读:

sql
ALTER TEXT SEARCH DICTIONARY public.simple_dict ( Accept = true );  -- 触发重读

CREATE TEXT SEARCH CONFIGURATION(无 COPY)产出的配置是空的。 从零建配置后直接使用,会返回空向量,任何查询都搜不到:

sql
-- 危险:返回空向量
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)放在最前面会吃掉一切:

sql
-- 错误!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。

建索引与查询必须用同一个配置。 这是最隐蔽的错误来源——两边配置不同,词元对不上,索引完全失效但不报错:

sql
-- 索引用 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 共享内存。

最佳实践 ​

  1. 优先「复制现成配置」而不是从零创建。 CREATE TEXT SEARCH CONFIGURATION x (COPY = pg_catalog.english) 能一次性继承解析器与全部 token 映射,只在需要改的地方 ALTER MAPPING,故障面最小。

  2. 字典列表遵循「具体 → 通用」顺序,末尾必须有兜底。 推荐的英文站顺序是 thesaurus → synonym → ispell → snowball stemmer → simple;末尾保留一个不带停用词的 simple 或词干词典,保证任何 token 至少有小写化后的表示。

  3. 建配置前先用 \dF+ english 看清默认映射表。 明确每个 token 类型当前由哪个词典负责,只改动你真正关心的几行,避免误伤 url、email 等特殊类型。

  4. 把配置做成可脚本化的迁移文件。 词典文件属于数据库外部状态(放在 $SHAREDIR/tsearch_data/),不随 pg_dump 走。把 .syn/.ths/.stop 的生成与 CREATE TEXT SEARCH ... 语句都写进版本控制,否则新环境恢复数据库后配置会「缺文件」:

sql
CREATE TEXT SEARCH DICTIONARY IF NOT EXISTS public.pg_dict (
    TEMPLATE = synonym, SYNONYMS = pg_dict
);
  1. 改完词典文件,一定执行一次 ALTER TEXT SEARCH DICTIONARY 触发重读。 可以在应用部署脚本里带上一句(参数写回原值即可),否则线上会长期使用旧缓存。

  2. 配置与索引成对版本化。 凡是会改变词元的改动(换词典模板、改同义词、改 thesaurus、改 default_text_search_config),都必须同步 REINDEX 相关索引,并把「配置版本号」记进部署记录,便于定位不一致。

  3. default_text_search_config 只作为「便利开关」,生产 SQL 一律显式写配置名。 显式写名字才能让「索引侧」与「查询侧」在代码里一眼可见地一致,也避免不同环境(不同 ALTER DATABASE SET)行为漂移。

  4. 用 ts_debug() + ts_lexize() 建立词典回归测试。 至少固化「一批输入词 → 期望词元」的断言,覆盖同义词、词干化、停用词、前缀标记四类情形,改动词典后跑一遍。

  5. 停用词表保持精简。 过度停用会损失精度(not、no 被丢弃后,否定查询彻底失效)。建议只在 ts_stat 统计中确认「确实高频且无区分度」的词才加进 .stop。

  6. 中文场景不要用内置配置硬撑。 PostgreSQL 18 内置没有中文配置,\dF 里没有 chinese。中文必须安装分词扩展后自建配置,详见 中文全文搜索方案。

  7. 建立配置对象的命名规范与 schema 归属。 自定义配置、词典统一放在业务 schema(如 public)下并加项目前缀(public.pg、public.zh_cfg),避免与 pg_catalog 内置对象混淆,也便于 \dF public.* 一次性查看。

  8. thesaurus 谨慎使用、单独备份。 它是唯一「改内容就要求重建索引」的词典类型,且 .ths 文件不会进 pg_dump。启用前先评估索引规模与可接受的 REINDEX 窗口。

下一节

继续学习:GIN / GiST 全文索引

参考链接 ​