Skip to content

全文搜索配置(Configuration) ​

上一节的 to_tsvector('english', ...) 里那个 'english' 参数,就是本节的主角——全文搜索配置(Text Search Configuration)。

配置决定了"一段文本怎么被切成 token、token 怎么被还原成词位、哪些 token 被丢弃"。同一个字符串在不同配置下会产出完全不同的 tsvector:

sql
SELECT to_tsvector('english', 'The banks were robbed') AS with_english,
       to_tsvector('simple',  'The banks were robbed') AS with_simple;
text
        with_english        |           with_simple
----------------------------+----------------------------------
 'bank':3 'rob':4           | 'banks':2 'robbed':4 'the':1 'were':3
(1 row)

同一句话:english 配置丢掉了 the / were 两个停用词,还把 banks 词干化成 bank、robbed 词干化成 rob;simple 配置什么都没做,原样保留。

这说明全文搜索的正确性从配置开始,而不是从查询开始。 配置选错,后面建索引、写查询、调排序全是在错误的地基上盖楼。

前置知识

阅读本节前,建议先了解:tsvector 与 tsquery

基础概念 ​

全文搜索的三层架构 ​

PostgreSQL 全文搜索不是一个黑盒,它由三个可替换的部件组成,层层递进:

层部件职责内置数量
第一层解析器(Parser)把原始文本拆成 token,并给每个 token 打上类型标签(word、numword、email、blank 等)1 种(default)
第二层词典(Dictionary)接收解析器产出的 token,做归一化(小写、词干、同义词替换)或直接丢弃(停用词)若干(simple、english_stem、synonym、thesaurus 等)
第三层配置(Configuration)把"token 类型 → 词典链"的映射关系绑定起来,对外表现为一个名字30 种

数据流是单向的:

text
原始文本
   │
   ▼
[解析器]  ──►  token 流: ('the', 'word') ('bank', 'word') ('@', 'blank') ...
   │
   ▼
[配置]    ──►  查表:token 类型 'word' 该交给哪些词典?
   │
   ▼
[词典链]  ──►  逐个词典尝试,第一个返回结果的胜出;全部返回 NULL 则丢弃该 token
   │
   ▼
tsvector

关键理解:配置本身不做任何处理,它只是一张路由表。 真正干活的是解析器和词典,配置负责把它们串起来。

内置配置一览 ​

PostgreSQL 18 出厂自带 30 种配置,常用的这些:

配置名基于词典特点适用场景
simplesimple只做小写归一,不过滤停用词、不做词干通用兜底、需要精确保留原词
englishenglish_stem英文停用词 + 词干还原(Snowball)英文文档搜索(最常用)
english_stemenglish_stem只做词干,保留停用词需要搜 the 但要词干化
chinese / japanese 等—部分语言存在但不支持分词见下一节中文方案
german / french / russian ...各语言 Snowball相应语言的停用词 + 词干多语言站点
synonymsynonym专做同义词映射的空配置自定义扩展

查询可用配置:

sql
SELECT cfgname FROM pg_ts_config ORDER BY cfgname;
text
     cfgname
------------------
 arabic
 armenian
 basque
 catalan
 danish
 dutch
 english
 estonian
 finnish
 french
 german
 greek
 hebrew
 hindi
 hungarian
 indonesian
 irish
 italian
 lithuanian
 nepali
 norwegian
 portuguese
 romanian
 russian
 serbian
 simple
 spanish
 swedish
 turkish
 yiddish
(30 rows)

注意:列表里没有 chinese。PostgreSQL 内置解析器不认识中文,这是下一节要专门解决的问题。

解析器产出的 token 类型 ​

理解配置必须先理解"token 类型"。默认解析器会输出这些类型(部分):

token 类型含义例子
word普通单词quick、banks
numword字母数字混合词version2、pg18
asciiword纯 ASCII 单词hello
word (含非 ASCII 词)含 Unicode 字母的词café
numhword / asciihword / hword连字符分隔词的组成部分foo-bar 中的 foo
email电子邮件地址a@b.com
url网址http://x.com
host主机名example.com
float数值3.14
int整数42
uint无符号整数42
sfloat带符号浮点-3.14
blank空白符、\n
tagHTML/XML 标签<b>
protocol协议头http
version版本号8.1.2
entityHTML 实体&amp;

默认解析器只给 word 类 token 配置了词典,所以 email、url、float 等类型的 token 会被直接丢弃——这就是为什么全文搜索默认搜不到邮箱和网址。想让它们进 tsvector,必须显式加映射。

语法与用法 ​

查看配置内容 ​

用元命令或视图查看"某个配置里,哪种 token 类型交给哪些词典":

sql
-- 列出所有配置
\dF

-- 查看 english 配置的全部映射
\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             | simple
 numhword        | simple
 numword         | simple
 sfloat          | simple
 uint            | simple
 url             | simple
 url_path        | simple
 version         | simple
 word            | english_stem

这张表就是配置的全部内容。读法:token 类型 asciiword → 交给 english_stem 词典处理。而 email 走的是 simple 词典(原样小写保留,不走词干)。

咦,前面不是说"默认丢弃 email"吗?其实 english 配置里已经给 email 加了 simple 映射,所以邮箱是能搜的。真正被丢弃的是没有出现在这张表里的类型(如 tag、blank、entity)。

查看配置的解析器与词典来源:

sql
SELECT c.cfgname,
       p.prsname AS parser,
       d.dictname,
       m.maptokentype AS tokentype
FROM pg_ts_config c
JOIN pg_ts_parser p ON p.oid = c.cfgparser
LEFT JOIN pg_ts_config_map m ON m.mapcfg = c.oid
LEFT JOIN pg_ts_dict d ON d.oid = m.mapdict
WHERE c.cfgname = 'english'
ORDER BY m.maptokentype, m.mapseqno
LIMIT 10;
text
 cfgname | parser  |   dictname   | tokentype
---------+---------+--------------+-----------
 english | default | english_stem |        12
 english | default | english_stem |        13
 english | default | english_stem |        14
 english | default | english_stem |        15
 english | default | english_stem |        16
 english | default | simple       |        17
 english | default | english_stem |        18
 english | default | english_stem |        19
 english | default | simple       |        20
 english | default | simple       |        21
(10 rows)

各元命令的作用:

命令作用
\dF列出所有文本搜索配置
\dF+ name列出配置的详细映射(parser + token→dictionary)
\dFd列出所有词典
\dFd+ name列出词典详情(含选项、模板)
\dFp列出所有解析器
\dFp+ name列出解析器详情及其输出的 token 类型
\dFt列出所有词典模板

用 ts_debug 观察分词链路 ​

ts_debug() 是排查全文搜索的第一工具,它把"原文 → token → 词典处理 → 词位"完整链路打印出来:

sql
SELECT alias, description, token, dictionaries, dictionary, lexemes
FROM ts_debug('english', 'PostgreSQL 18 is a great database, see https://a.com');
text
   alias   |   description    |       token        |   dictionaries    |  dictionary  | lexemes
-----------+------------------+--------------------+-------------------+--------------+---------
 asciiword | Word, all ASCII  | PostgreSQL         | {english_stem}    | english_stem | {postgresql}
 blank     | Space symbols    |                    | {}                |              |
 numword   | Word, letters and| 18                 | {english_stem}    | english_stem | {18}
           | digits           |                    |                   |              |
 blank     | Space symbols    |                    | {}                |              |
 asciiword | Word, all ASCII  | is                 | {english_stem}    | english_stem | {}
 blank     | Space symbols    |                    | {}                |              |
 asciiword | Word, all ASCII  | a                  | {english_stem}    | english_stem | {}
 blank     | Space symbols    |                    | {}                |              |
 asciiword | Word, all ASCII  | great              | {english_stem}    | english_stem | {great}
 blank     | Space symbols    |                    | {}                |              |
 asciiword | Word, all ASCII  | database           | {english_stem}    | english_stem | {databas}
 blank     | Space symbols    |                    | {}                |              |
 blank     | Space symbols    | ,                  | {}                |              |
 blank     | Space symbols    |                    |                   |              |
 url       | URL              | https://a.com      | {simple}          | simple       | {https://a.com}
(15 rows)

逐列解读:

列含义关键观察
aliastoken 类型(配置表的键)asciiword、numword、url 是三种不同类型
token解析器切出的原始片段标点 , 也成 token,但类型是 blank
dictionaries该类型配置的词典链{english_stem};空表示无映射(必被丢弃)
dictionary实际命中的那个词典词典链中第一个返回非 NULL 的
lexemes最终生成的词位{} 表示被停用词过滤(is、a);database → databas

lexemes 为空是"搜不到"的直接证据。 用户搜 is 没结果,就是这里显示 {} 造成的。

对比 simple 配置:

sql
SELECT token, dictionary, lexemes
FROM ts_debug('simple', 'The banks were robbed');
text
  token  | dictionary | lexemes
---------+------------+----------
 The     | simple     | {the}
 banks   | simple     | {banks}
 were    | simple     | {were}
 robbed  | simple     | {robbed}
(4 rows)

simple 词典没有停用词表、没有词干算法,只做小写归一,所以四个词原样保留(The → the)。

创建自定义配置 ​

标准用法是"复制一个内置配置再改":

sql
-- 语法
CREATE TEXT SEARCH CONFIGURATION name (
    PARSER = parser_name |
    COPY   = source_config
);

两种写法:

写法含义用途
PARSER = default只指定解析器,映射为空从零开始定义(如 zhparser 场景)
COPY = english复制源配置的解析器和全部映射在既有基础上微调(最常见)

先做一份"英文 + 保留停用词"的配置:

sql
CREATE TEXT SEARCH CONFIGURATION public.english_keep_stop (
    COPY = pg_catalog.english
);

-- 把 word 类 token 的词典从 english_stem 换成 simple
ALTER TEXT SEARCH CONFIGURATION public.english_keep_stop
    ALTER MAPPING FOR asciiword, asciihword, hword_asciipart, word WITH simple;

ALTER MAPPING 有三种动作:

动作语义影响
ADD MAPPING FOR ... WITH ...追加词典保留原有映射并新增;类型已有映射时会报错
ALTER MAPPING FOR ... WITH ...替换词典类型必须已有映射,整体换掉
ALTER MAPPING FOR ... REPLACE ... WITH ...替换词典链中的某个精细手术
DROP MAPPING FOR ...删除映射该类型 token 从此被丢弃

验证效果:

sql
SELECT to_tsvector('public.english_keep_stop', 'The banks were robbed');
text
                     to_tsvector
-----------------------------------------------------
 'banks':2 'robbed':4 'the':1 'were':3
(1 row)

停用词回来了,但词干也没了(simple 不做词干)。词典是整体替换的,不能"只关停用词、保留词干"——因为停用词表是 english_stem 词典模板的一部分。要做这个效果得自建词典,见后文。

会话级默认配置 ​

不显式传配置名时,to_tsvector(text) 使用会话变量:

sql
SHOW default_text_search_config;
text
 default_text_search_config
----------------------------
 pg_catalog.english
(1 row)

可以按需切换:

sql
SET default_text_search_config = 'public.english_keep_stop';
SELECT to_tsvector('The banks were robbed');
text
                     to_tsvector
-----------------------------------------------------
 'banks':2 'robbed':4 'the':1 'were':3
(1 row)

也可以在 postgresql.conf 里改全局默认:

ini
# postgresql.conf
default_text_search_config = 'pg_catalog.english'

不要依赖会话默认配置

它会让"同样的 SQL 在不同会话跑出不同结果",还会让索引失配。生产环境的索引表达式与查询永远显式写配置名。

详细说明 ​

停用词(Stop Words) ​

停用词表是文件,不是数据库对象。路径:

text
$SHAREDIR/tsearch_data/english.stop
bash
# 查看 english 配置用的停用词文件
ls -l $(pg_config --sharedir)/tsearch_data/english.stop
text
-rw-r--r--  1 root  wheel  259  9 22 10:00 /usr/local/share/postgresql/tsearch_data/english.stop

内容是一行一个词:

text
i
me
my
myself
we
our
ours
ourselves
you
your
...

查看数据库里记录了哪些停用词文件被加载:

sql
SELECT dictname, dictinitoption
FROM pg_ts_dict
WHERE dictinitoption LIKE '%stopwords%'
ORDER BY dictname;
text
   dictname    |                    dictinitoption
---------------+-------------------------------------------------------
 danish_stem   | language = 'danish', stopwords = 'danish'
 dutch_stem    | language = 'dutch', stopwords = 'dutch'
 english_stem  | language = 'english', stopwords = 'english'
 finnish_stem  | language = 'finnish', stopwords = 'finnish'
 french_stem   | language = 'french', stopwords = 'french'
 german_stem   | language = 'german', stopwords = 'german'
 ...
 simple        | (null)
(30 rows)

simple 词典没有 stopwords 选项——这就是它不过滤停用词的原因。

想定制停用词表,需要自建词典(不能改内置词典,它们是只读的):

sql
-- 把自定义停用词文件 my_stop.txt 放进 $SHAREDIR/tsearch_data/
CREATE TEXT SEARCH DICTIONARY public.my_stem (
    TEMPLATE = snowball,
    Language  = english,
    StopWords = my_stop
);

-- 建一个只用这个词典的配置
CREATE TEXT SEARCH CONFIGURATION public.my_en ( COPY = pg_catalog.english );
ALTER TEXT SEARCH CONFIGURATION public.my_en
    ALTER MAPPING FOR asciiword, asciihword, word WITH public.my_stem;

my_stop.txt 的格式是一行一词,且必须按字典序排序——PostgreSQL 用二分查找,乱序会导致部分停用词静默失效:

text
# 自定义停用词:保留 the,去掉内部黑话
foo
bar
the

词典模板与选项 ​

词典基于模板(Template)创建,模板决定有哪些可配选项:

模板用途关键选项
simple小写归一,可选停用词StopWords、Accept
snowballSnowball 词干算法,可选停用词Language、StopWords
ispellIspell 词典(最强大)DictFile、AffFile、StopWords
synonym同义词映射Synonyms
thesaurus同义词/词组词典(短语级)DictFile、Dictionary
unaccent去音符Rules
zhparser中文分词(扩展提供)—

查看模板详情:

sql
\dFd english_stem
text
        Text search dictionary "pg_catalog.english_stem"
  Template: snowball
  Init options: language = 'english'
                stopwords = 'english'
                stemdict = 'english'

同义词词典(synonym) ​

synonym 词典用最简单的方式做词映射,文件格式是 词位 同义词1 同义词2 ...:

text
postgres  postgresql pgsql
db        database
sql
CREATE TEXT SEARCH DICTIONARY public.my_synonym (
    TEMPLATE = synonym,
    Synonyms = my_synonym
);

CREATE TEXT SEARCH CONFIGURATION public.my_syn_en ( COPY = pg_catalog.english );
ALTER TEXT SEARCH CONFIGURATION public.my_syn_en
    ALTER MAPPING FOR asciiword WITH public.my_synonym, english_stem;

注意词典链的顺序:my_synonym 在前,english_stem 在后。 词典链是"短路求值"的——my_synonym 命中就返回,不再往下走;没命中才轮到 english_stem。

效果:

sql
SELECT to_tsvector('public.my_syn_en', 'pgsql is a db');
text
                     to_tsvector
-----------------------------------------------------
 'database':4 'postgresql':1
(1 row)

pgsql → postgresql、db → database,且 is 被 english_stem 的停用词过滤掉(my_synonym 未命中时返回 NULL,继续往下走)。

thesaurus 词典(短语级同义词) ​

thesaurus 比 synonym 更强,支持多词短语映射,文件格式:

text
supernovae stars : sn
crab nebulae : crab

指的是"左边出现时替换成右边"。用途举例——把 united states 归一到 usa:

text
united states : usa
great britain : uk

创建时通常要指定一个 fallback 词典:

sql
CREATE TEXT SEARCH DICTIONARY public.my_thesaurus (
    TEMPLATE = thesaurus,
    DictFile = my_thesaurus,
    Dictionary = pg_catalog.english_stem
);

配置与索引的关系 ​

这是本节最容易被忽略、也最容易出事故的一点:配置名是索引表达式的一部分。

sql
-- 建索引
CREATE INDEX idx_body_en ON articles
    USING gin (to_tsvector('english', body));

-- ✓ 完全一致,走索引
SELECT * FROM articles WHERE to_tsvector('english', body) @@ to_tsquery('english', 'postgres');

-- ✗ 用了 simple,表达式不一致,全表扫描
SELECT * FROM articles WHERE to_tsvector('simple', body) @@ to_tsquery('simple', 'postgres');

用 EXPLAIN 确认:

sql
EXPLAIN (COSTS OFF) SELECT id FROM articles WHERE to_tsvector('english', body) @@ websearch_to_tsquery('english', 'postgres');
text
                     QUERY PLAN
-----------------------------------------------------
 Bitmap Heap Scan on articles
   Recheck Cond: (to_tsvector('english'::regconfig, body) @@ '''postgr'''::tsquery)
   ->  Bitmap Index Scan on idx_body_en
         Index Cond: (to_tsvector('english'::regconfig, body) @@ '''postgr'''::tsquery)
(4 rows)

看到 Bitmap Index Scan 就是走对了;出现 Seq Scan + Filter 就是失配。

表达式索引 vs 生成列 的选择:

维度表达式索引生成列(Generated Column)
定义位置CREATE INDEXCREATE TABLE ... GENERATED ALWAYS AS
查询写法必须逐字重复表达式只写列名
多字段加权表达式会很丑定义一次,清晰
额外存储无(只存索引)有(多存一份 tsvector)
更新成本索引随行更新列 + 索引双份更新
适用单列、表达式简单多列、需反复查询、需权重

生成列的写法(表达式必须 IMMUTABLE,必须显式写配置名):

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

CREATE INDEX idx_articles_search ON articles USING gin (search_vec);

查询变得极其干净:

sql
SELECT id, ts_rank(search_vec, websearch_to_tsquery('english', 'postgres')) AS rank
FROM articles
WHERE search_vec @@ websearch_to_tsquery('english', 'postgres')
ORDER BY rank DESC
LIMIT 10;

pg_trgm 与全文搜索的分工 ​

pg_trgm 是另一套索引方案,用三元组(trigram)做模糊匹配:

维度全文搜索(GIN + tsvector)pg_trgm(GIN + gin_trgm_ops)
匹配单位词位3 字符子串
支持的查询词、短语、布尔、前缀LIKE、ILIKE、正则、% 相似度
词形变化支持词干不支持
拼写错误不支持(除非配词典)支持(相似度匹配)
中文需 zhparser直接可用(按字符切)
短词(1~2 字)可搜trigram 退化为 2 字符,效果差
索引大小中等较大(三元组数量多)
排序能力ts_ranksimilarity()

创建方式:

sql
CREATE EXTENSION IF NOT EXISTS pg_trgm;

CREATE INDEX idx_users_name_trgm ON users
    USING gin (name gin_trgm_ops);

-- 模糊查找:相似度 > 0.3 的名字
SELECT name, similarity(name, 'jonh') AS sim
FROM users
WHERE name % 'jonh'
ORDER BY sim DESC
LIMIT 5;
text
  name  |   sim
--------+---------
 john   |   0.571
 johan  |   0.333
 jon    |   0.333
(3 rows)

% 的阈值由 pg_trgm.similarity_threshold 控制(默认 0.3):

sql
SHOW pg_trgm.similarity_threshold;
SET pg_trgm.similarity_threshold = 0.5;

两者可以共存,各建各的索引,查询时用 OR 合并:

sql
SELECT id, title
FROM articles
WHERE search_vec @@ websearch_to_tsquery('english', 'postgres')
   OR title % 'postgrs'   -- 拼写容错兜底
LIMIT 20;

实战示例 ​

数据准备 ​

建一个多语言博客表:

sql
DROP TABLE IF EXISTS blog_posts;

CREATE TABLE blog_posts (
    id         bigserial PRIMARY KEY,
    lang       text NOT NULL DEFAULT 'en',      -- 语言标记
    title      text NOT NULL,
    summary    text,
    body       text NOT NULL,
    tags       text[],
    created_at timestamptz NOT NULL DEFAULT now()
);

INSERT INTO blog_posts (lang, title, summary, body, tags) VALUES
('en', 'Getting Started with PostgreSQL',
       'An introduction to the world''s most advanced open source database.',
       'PostgreSQL is a powerful, open source object-relational database system. '
       'It has more than 35 years of active development and a proven architecture '
       'that has earned it a strong reputation for reliability and data integrity.',
       ARRAY['postgresql', 'database', 'tutorial']),

('en', 'Indexing Strategies for Full-Text Search',
       'How to make text search fast on millions of documents.',
       'Full text search in PostgreSQL relies on GIN indexes over tsvector columns. '
       'Choosing the right configuration and index type is critical for performance. '
       'A poorly configured index can be slower than a sequential scan.',
       ARRAY['index', 'search', 'performance']),

('en', 'Understanding Database Normalization',
       'Reduce redundancy and improve data integrity.',
       'Database normalization organizes columns and tables to minimize redundancy. '
       'The first three normal forms are the most commonly applied in practice.',
       ARRAY['database', 'design']),

('en', 'PostgreSQL versus MySQL',
       'A practical comparison for backend engineers.',
       'Both PostgreSQL and MySQL are excellent databases. PostgreSQL tends to offer '
       'richer type support and stronger standards compliance, while MySQL is often '
       'simpler to operate for read-heavy workloads.',
       ARRAY['postgresql', 'mysql', 'comparison']);
text
INSERT 0 4

目标一:构建带权重的搜索列 ​

标题权重 A、标签 B、摘要 C、正文 D:

sql
ALTER TABLE blog_posts
    ADD COLUMN search_vec tsvector
    GENERATED ALWAYS AS (
        setweight(to_tsvector('english', coalesce(title,   '')), 'A') ||
        setweight(to_tsvector('english', coalesce(array_to_string(tags, ' '), '')), 'B') ||
        setweight(to_tsvector('english', coalesce(summary, '')), 'C') ||
        setweight(to_tsvector('english', coalesce(body,    '')), 'D')
    ) STORED;
text
ALTER TABLE

看一眼产出:

sql
SELECT id, search_vec
FROM blog_posts
WHERE id = 1;
text
 id |                                                        search_vec
----+----------------------------------------------------------------------------------------------------------------------
  1 | '35':19A 'advanc':15A 'architectur':22A 'databas':16A,26A 'develop':18 'integr':32A 'introduc':4A 'object-relation':12
    | 'open':6A,11A 'postgresql':1A,5A 'power':7A 'proven':21A 'reliabl':30A 'reput':28A 'sourc':10A 'start':2A,13A ...
(1 row)

正文与摘要的词位带 C / D 权重标签,标题与标签带 A / B。注意同一词位可能出现在多个权重档('databas':16A,26A 表示在位置 16 和 26 出现且权重为 A)。

建索引:

sql
CREATE INDEX idx_blog_search ON blog_posts USING gin (search_vec);
text
CREATE INDEX

目标二:测出权重是否真的生效 ​

搜 postgresql——它同时出现在标题、标签、正文里:

sql
SELECT id,
       title,
       round(ts_rank(search_vec, websearch_to_tsquery('english', 'postgresql'))::numeric, 4) AS rank
FROM blog_posts
WHERE search_vec @@ websearch_to_tsquery('english', 'postgresql')
ORDER BY rank DESC;
text
 id |                 title                  |  rank
----+----------------------------------------+---------
  4 | PostgreSQL versus MySQL                |  0.8029
  1 | Getting Started with PostgreSQL        |  0.8029
  2 | Indexing Strategies for Full-Text Search|  0.0608
(3 rows)

title 里出现 postgresql 的两篇排在最前(A 权重加分最多),第三篇只在正文提及,分数低一个数量级。

用 ts_rank_cd 做归一化排序(对长文档更公平):

sql
SELECT id, title,
       round(ts_rank_cd(search_vec, websearch_to_tsquery('english', 'database'), 32)::numeric, 5) AS rank_cd
FROM blog_posts
WHERE search_vec @@ websearch_to_tsquery('english', 'database')
ORDER BY rank_cd DESC;
text
 id |                 title                  |  rank_cd
----+----------------------------------------+-----------
  3 | Understanding Database Normalization   |   0.00571
  4 | PostgreSQL versus MySQL                |   0.00413
  1 | Getting Started with PostgreSQL        |   0.00345
(3 rows)

第三篇标题里就有 Database(A 权重),排名第一。

目标三:自定义配置(保留停用词 + 同义词) ​

默认 english 会滤掉 versus、with 这类词。假设运营希望"搜 vs 也能命中 PostgreSQL versus MySQL",用 synonym 词典解决:

准备同义词文件 $SHAREDIR/tsearch_data/blog_synonym.syn:

text
vs versus
db database
pg postgresql

# 注意:文件必须按字典序排列
bash
# 把文件放到正确位置
cp ./blog_synonym.syn $(pg_config --sharedir)/tsearch_data/blog_synonym.syn

创建词典和配置:

sql
CREATE TEXT SEARCH DICTIONARY public.blog_synonym (
    TEMPLATE = synonym,
    Synonyms = blog_synonym
);

CREATE TEXT SEARCH CONFIGURATION public.blog_en ( COPY = pg_catalog.english );

-- 关键:把同义词词典插到词干词典之前
ALTER TEXT SEARCH CONFIGURATION public.blog_en
    ALTER MAPPING FOR asciiword, asciihword, word
    WITH public.blog_synonym, english_stem;

验证同义词生效:

sql
SELECT to_tsvector('public.blog_en', 'pg vs db');
text
                  to_tsvector
---------------------------------------------
 'database':3 'postgresql':1 'versus':2
(1 row)

pg → postgresql、db → database、vs → versus,三个词全部被改写。(versus 本身不是 english_stem 的停用词,所以保留了下来。)

现在"搜 pg"能命中标题含 PostgreSQL 的行——因为索引里的词位就是 postgresql。

目标四:诊断"为什么搜不到" ​

这是配置知识最实用的用途。 用户反馈搜 MySQL 搜不到结果?按下面流程走。

第 1 步:确认索引里到底有什么词位。

sql
SELECT id, search_vec
FROM blog_posts
WHERE id = 4;
text
 id |                                     search_vec
----+-------------------------------------------------------------------------------------
  4 | 'backend':... 'comparison':... 'excel':... 'mysql':9A 'oper':... 'postgresql':...
(1 row)

mysql 确实在索引里,说明不是索引问题。

第 2 步:确认查询侧生成了什么 tsquery。

sql
SELECT websearch_to_tsquery('english', 'MySQL') AS q,
       plainto_tsquery('english', 'MySQL')     AS q2;
text
     q     |    q2
-----------+----------
 'mysql'   | 'mysql'
(1 row)

tsquery 也正确。

第 3 步:手工验证 @@。

sql
SELECT id, title FROM blog_posts WHERE search_vec @@ to_tsquery('english', 'mysql');
text
 id |            title
----+-----------------------------
  4 | PostgreSQL versus MySQL
(1 row)

能查到。那"搜不到"就是应用层问题——很可能是应用把用户输入传给了 to_tsquery 而不是 websearch_to_tsquery,用户输入带特殊字符时直接抛错了。

第 4 步:如果第 3 步也查不到,跑 ts_debug:

sql
SELECT token, dictionaries, dictionary, lexemes
FROM ts_debug('english', 'MySQL');
text
  token  | dictionaries  |  dictionary  | lexemes
---------+---------------+--------------+----------
 MySQL   | {english_stem}| english_stem | {mysql}
(1 row)

如果这里 lexemes 是 {},那就是词位在索引侧就被丢弃了——需要检查建索引时的配置与查询配置是否一致,或者该词命中了停用词表。

排查结论速查表:

现象可能原因处理
ts_debug 的 lexemes 为空停用词过滤 / 词典未命中换配置或自建停用词表
索引里有词位但查不到查询配置与索引配置不一致统一配置名,重建索引
EXPLAIN 显示 Seq Scan表达式与索引不匹配复制粘贴表达式
to_tsquery 抛语法错误用户输入含特殊字符改用 websearch_to_tsquery
中文搜不到无中文解析器见下一节 zhparser 方案
邮箱/URL 搜不到token 类型无映射ADD MAPPING FOR email, url WITH simple

注意事项 ​

  1. 配置名是索引的一部分,不是"查询参数"。 to_tsvector('english', body) 里的 'english' 会随表达式一起固化进索引定义。改配置 = 重建索引。

  2. 内置配置和词典是只读的。 ALTER TEXT SEARCH CONFIGURATION pg_catalog.english 会报错。要改就 COPY 一份到自己的 schema。

  3. ADD MAPPING 不能覆盖已有映射。 对已有映射的 token 类型用 ADD MAPPING 会直接报 mapping for token type ... already exists,此时要用 ALTER MAPPING。

  4. 词典链的顺序决定结果。 排在前的词典优先命中并短路,后面的词典根本不会被调用。同义词词典一定要放在词干词典之前,否则 pgsql 会先被词干化成 pgsql 而错过映射。

  5. 停用词文件必须按字典序排序。 PostgreSQL 用二分查找,乱序文件中排序位置之后的词会被静默忽略——不报错,只是不生效,极难排查。

  6. 自定义停用词/同义词文件放在 $SHAREDIR/tsearch_data/ 下,不带扩展名的写法要注意。 Synonyms = blog_synonym 会去找 blog_synonym.syn;StopWords = my_stop 会去找 my_stop.stop。

  7. simple 词典没有停用词能力(但模板支持)。 内置的 pg_catalog.simple 词典未配 StopWords,所以不过滤;但你可以用 simple 模板自建带停用词表的词典。

  8. ts_debug 只反映当前配置,不反映索引历史。 如果配置改过而索引没重建,ts_debug 的结果会与索引内容不一致——这正是排查时最容易被误导的地方。

  9. default_text_search_config 是会话级的。 连接池复用连接时,前一个请求 SET 过的配置会泄漏到下一个请求。要么别用它,要么在请求开始时重置。

  10. 生成列的表达式必须 IMMUTABLE。 to_tsvector('english', body) 是 IMMUTABLE;to_tsvector(body)(依赖会话变量)不是,建列时会报错。

  11. array_to_string 是 STABLE 而非 IMMUTABLE。 在生成列里对数组字段做拼接时要注意:PostgreSQL 实际允许部分场景,但跨版本行为不保证,稳妥做法是把数组在写入前就拼成 text 列。

  12. pg_trgm 的索引比 GIN 全文索引大。 三元组数量与文本长度成正比且基数更高,大字段上索引膨胀明显,别在长正文上乱建。

  13. % 操作符依赖阈值而非相似度排序。 WHERE name % 'x' 只是过滤,想要"最相似的在前面"必须额外 ORDER BY similarity(...) DESC。

  14. ts_rank 的分数不能跨查询比较。 它受文档长度、词频、权重共同影响,只适合同一查询内部排序。

  15. 改了配置后记得 ANALYZE。 统计信息不更新,优化器可能仍选错计划(尤其是在 GIN 索引与顺序扫描之间摇摆的表上)。

最佳实践 ​

  1. 先定配置,再建索引。 配置决定了 tsvector 的内容,索引一旦建好,改配置就得重建。顺序不能反。

  2. 需要微调就 COPY 内置配置。 CREATE TEXT SEARCH CONFIGURATION my_en (COPY = pg_catalog.english) 是标准姿势,别从 PARSER = default 从零搭。

  3. 同义词/词典一律放在自己的 schema。 前缀写 public. 或业务 schema 名,避免与内置对象混淆,也便于迁移时导出。

  4. 生产环境显式写配置名。 每个 to_tsvector / to_tsquery 都带上 'english' 这样的字面量,永不依赖 default_text_search_config。

  5. 多字段用生成列,单字段用表达式索引。 生成列让查询只写列名,可读性和一致性都远胜重复的长表达式。

  6. 权重分配遵循 A 标题 / B 标签 / C 摘要 / D 正文。 与 ts_rank 默认系数一致,最符合用户直觉。

  7. coalesce(字段, '') 包住每个拼接项。 任何一列为 NULL,整个 search_vec 就是 NULL,该行静默失去搜索能力。

  8. 词典链按"越具体越靠前"排序。 同义词词典 → 无停用词词干词典 → 通用词干词典,逐级兜底。

  9. 中英文混排时分别建配置与索引。 一列存一种语言的 tsvector,用 lang 字段路由查询,别指望一个配置通吃。

  10. 用 ts_debug 做单元测试。 为关键配置写几条断言式查询,纳入 CI——它能在配置漂移时第一时间报警。

  11. 批量导入后 VACUUM 或 gin_clean_pending_list()。 GIN 的 fastupdate 会让新数据先进 pending list,不清的话新文档搜不到。

  12. pg_trgm 只用于容错与短文本。 它的定位是"拼写容错和 LIKE 加速",不是替代全文搜索。两者并用、OR 合并是最佳组合。

下一节

继续学习:中文全文搜索方案

参考链接 ​