了解文本分析器(Text Analyzer)与词表模型(Model)如何协同工作,将原始文本转化为词元 ID。
这是本节的多页打印视图。 .
分词器 (pg_tokenizer)
- 1: 分词器架构与基本用法
- 2: 文本分析器与过滤器
- 3: 分词词表模型
- 4: 多语言分词配方与限制
pg_tokenizer(由 Pigsty 在 pgsty/pg_tokenizer 维护)是 vchord_bm25 (pgsty/vchord_bm25) 的配套 PostgreSQL 分词扩展,提供全文分析器与词表模型,可将原始文本转换为整数词元 ID 数组(INT[])及稀疏 bm25vector 向量。
配置字符过滤器、预分词器(Unicode、正则、结巴 Jieba)、词干提取器(支持 30 种语言)、停用词、同义词与 N-gram。
使用内置预训练模型(llmlingua2、bert_base_uncased、wiki_tocken、gemma2b)、训练自定义语料模型,或导入 HuggingFace 与 Lindera 模型。
面向英文、中文(Jieba)、日文(Lindera)及特定领域自定义语料库的开箱即用配置方案。
1 - 分词器架构与基本用法
两阶段分词流水线
在 pg_tokenizer 中,一个分词器(tokenizer)最多由两个阶段组成,负责将原始 TEXT 字符串转换为由整数词元 ID 构成的 INT[] 数组:
flowchart LR
A["原始文本\n(TEXT)"] --> B["第一阶段:文本分析器 (Text Analyzer)\n1. character_filters(字符过滤器)\n2. pre_tokenizer(预分词器)\n3. token_filters(词元过滤器)"]
B --> C["归一化词元数组\n(TEXT[])"]
C --> D["第二阶段:模型 (Model)\n词表映射 / 子词 BPE"]
D --> E["词元 ID 数组\n(INT[] → bm25vector)"]- 文本分析器(
text_analyzer):按顺序应用以下三级流水线,将原始文本切分为归一化后的字符串词元数组TEXT[]:- 字符过滤器(
character_filters):在切分前对原始字符串进行整体转换(例如to_lowercase小写转换、unicode_normalizationUnicode 规范化)。 - 预分词器(
pre_tokenizer):将字符串初步切分为子串(支持unicode_segmentation、regex正则或中文jieba分词)。 - 词元过滤器(
token_filters):对单个词元进行过滤或转换(支持skip_non_alphanumeric、stemmer词干提取、stopwords停用词、synonym同义词、pg_dict、以及ngram)。
- 字符过滤器(
- 分词模型(
model):将归一化后的字符串词元(或对于llmlingua2、bert_base_uncased、gemma2b、HuggingFace 及lindera等独立子词模型,直接将原始文本)映射为整数词元 ID(INT[])。
- 预训练子词模型(
llmlingua2、bert_base_uncased、gemma2b)、HuggingFace 模型和 Lindera 模型已内置完整的分词规则,不需要(也不允许)配置单独的text_analyzer阶段。 - 词典/语料模型(
wiki_tocken以及通过create_custom_model创建的自定义模型)则必须搭配text_analyzer阶段,先完成文本切分与归一化,再去模型的词表(Vocabulary Table)中查找词元 ID。
创建与使用分词器
pg_tokenizer 中的所有配置均采用 TOML 格式,并通过美元符号引用的字符串($$ ... $$)传入。
方式一:单阶段分词器(使用内置模型)
方式二:内联文本分析器 + 模型
你可以直接在 create_tokenizer 内部定义 text_analyzer 流水线:
方式三:可复用的命名文本分析器 + 自定义模型
你可以使用 create_text_analyzer 先创建一个独立的命名 text_analyzer,随后在多个自定义模型和分词器之间复用它:
管理分词器
2 - 文本分析器与过滤器
文本分析器(Text Analyzer) 通过三个顺序阶段处理原始文本:
- 字符过滤器(
character_filters) —— 对完整输入字符串执行零个或多个字符级转换。 - 预分词器(
pre_tokenizer) —— 使用唯一的一个切分器将字符串切分为初始词元。 - 词元过滤器(
token_filters) —— 对切分出的词元执行零个或多个转换、扩展或过滤操作。
在将文本分析器绑定到模型之前,你可以直接运行 SELECT apply_text_analyzer('Sample text here', 'analyzer1'); 来测试并查看其输出的 TEXT[] 词元数组。
1. 字符过滤器(character_filters)
to_lowercase
将输入文本中的所有字符转换为小写。
unicode_normalization
应用 Unicode 规范化形式(UAX #15),将组合/分解字符、重音符号以及全角/半角变体标准化。
支持的模式包括:
"nfc"—— 规范化形式 C(标准分解后再执行标准组合)"nfd"—— 规范化形式 D(标准分解)"nfkc"—— 规范化形式 KC(兼容性分解后再执行标准组合)"nfkd"—— 规范化形式 KD(兼容性分解)
2. 预分词器(pre_tokenizer)
每个文本分析器最多只能指定一个 pre_tokenizer。
unicode_segmentation
按照 Unicode 标准附件 #29 (UAX #29) 规范(底层基于 Rust unicode-segmentation 库)在 Unicode 词边界处切分文本。非常适合以空格分隔的西方语言。
regex
提取匹配自定义正则表达式的词元(底层基于 Rust regex 库):
jieba(中文结巴分词)
使用 jieba-rs 词典与隐马尔可夫模型(HMM)将中文文本切分为词语:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
mode |
string |
"search" |
"full"(调用标准 jieba.cut 分词)、"precise"(调用 jieba.cut_all 扫描句中所有可成词的词语)或 "search"(搜索引擎模式,调用 jieba.cut_for_search 对长词再次切分以提高检索召回率)。 |
enable_hmm |
bool |
true |
是否启用 HMM 新词发现能力(仅在 "full" 和 "search" 模式下生效)。 |
3. 词元过滤器(token_filters)
词元过滤器会严格按照 TOML 数组中声明的顺序依次执行。
skip_non_alphanumeric
丢弃不包含任何字母或数字字符的词元(例如分词后残留的独立标点符号 ,、.、! 等)。
stemmer(Snowball 词干提取 —— 支持 30 种语言)
使用 Snowball 词干提取算法将屈折变化的单词还原为其词干形式。
支持的 stemmer 取值如下:
| 语言 | stemmer 取值 |
语言 | stemmer 取值 |
|---|---|---|---|
| 阿拉伯语 (Arabic) | arabic |
立陶宛语 (Lithuanian) | lithuanian |
| 亚美尼亚语 (Armenian) | armenian |
尼泊尔语 (Nepali) | nepali |
| 巴斯克语 (Basque) | basque |
挪威语 (Norwegian) | norwegian |
| 加泰罗尼亚语 (Catalan) | catalan |
葡萄牙语 (Portuguese) | portuguese |
| 丹麦语 (Danish) | danish |
罗马尼亚语 (Romanian) | romanian |
| 荷兰语 (Dutch) | dutch |
俄语 (Russian) | russian |
| 英语 (English Porter) | english_porter |
塞尔维亚语 (Serbian) | serbian |
| 英语 (English Porter2) | english_porter2 |
西班牙语 (Spanish) | spanish |
| 世界语 (Esperanto) | esperanto |
瑞典语 (Swedish) | swedish |
| 爱沙尼亚语 (Estonian) | estonian |
泰米尔语 (Tamil) | tamil |
| 芬兰语 (Finnish) | finnish |
土耳其语 (Turkish) | turkish |
| 法语 (French) | french |
意第绪语 (Yiddish) | yiddish |
| 德语 (German) | german |
||
| 希腊语 (Greek) | greek |
||
| 印地语 (Hindi) | hindi |
||
| 匈牙利语 (Hungarian) | hungarian |
||
| 印度尼西亚语 (Indonesian) | indonesian |
||
| 爱尔兰语 (Irish) | irish |
||
| 意大利语 (Italian) | italian |
stopwords(停用词过滤)
移除几乎不携带有效语义信号的常见停用词。
- 内置停用词典:
"lucene_english"—— Apache Lucene 标准英文停用词表"nltk_english"—— NLTK 标准英文停用词表"iso_english"—— Stopwords ISO 英文停用词表
- 自定义停用词典:通过
create_stopwords(name, text)创建(每行一个词),随后按名称引用。
synonym(同义词归一)
将同义词组归一化为同一个标准词元。使用 create_synonym(name, text) 创建同义词典,其中每行包含以空格分隔的一组同义词(同一行中的所有词都会被替换为该行的第一个词):
pg_dict(PostgreSQL 全文检索词典)
将词元过滤委托给任意已有的 PostgreSQL 全文检索词典(pg_ts_dict,支持 Simple、Ispell、Snowball 和 Synonym 词典;注意不支持 Thesaurus 分类词典,因为后者作用于多词短语而非单个词元):
ngram
将每个词元切分为字符级 N-gram(适用于子串匹配、前缀匹配、无词典 CJK 检索,或代码/标识符检索):
| 参数 | 类型 | 默认值 | 取值范围 | 说明 |
|---|---|---|---|---|
min_gram |
int |
1 |
1..=255 (<= max_gram) |
生成 N-gram 的最小字符长度。 |
max_gram |
int |
2 |
1..=255 (>= min_gram) |
生成 N-gram 的最大字符长度。 |
preserve_original |
bool |
false |
true / false |
当原始词元长度不在 [min_gram, max_gram] 范围内时,是否同时保留未切分的原始词元。 |
3 - 分词词表模型
在 pg_tokenizer 中,模型(Model) 负责将字符串词元(或原始文本)映射为整数词元 ID(INT[])。你可以直接选用四个内置预训练模型、在 PostgreSQL 表上训练自定义词表模型、导入任意 HuggingFace tokenizer.json,或者使用 Lindera 形态学分析模型。
1. 内置预训练模型
| 模型名称 | 来源 / 架构 | 是否需要 text_analyzer? |
内存占用 | 适用场景 |
|---|---|---|---|---|
llmlingua2 |
microsoft/llmlingua-2-xlm-roberta-large-meetingbank(XLM-RoBERTa 多语言子词模型) |
否(禁止) | ~200 MB(默认预加载) | 开箱即用的多语言通用文本分词,无需训练自定义词表。 |
bert_base_uncased |
google-bert/bert-base-uncased(WordPiece,30,522 词表) |
否(禁止) | ~15 MB | 通用英文文本;启动极快且词表紧凑。 |
wiki_tocken |
Snowolf/wiki-tocken(内置英文维基百科词元词典) |
是(必需) | ~30 MB | 配合自定义词干提取器、停用词与同义词使用的英文全文检索。 |
gemma2b |
google/gemma-2b(SentencePiece BPE,256k 词表) |
否(禁止) | ~100 MB | 代码、技术标识符以及多语言大语言模型工作负载。 |
示例:使用 bert_base_uncased 或 llmlingua2
2. 自定义语料库模型(create_custom_model)
为了获得最高的 BM25 相关性精度与最小的内存占用,你可以直接基于自己的业务表列构建自定义词表模型。不同于大语言模型子词分词器(会将罕见的领域专有名词切碎为多个子词元),自定义模型会为 text_analyzer 产出的每个唯一归一化词元分配独立的 1:1 整数 ID,且在内存中仅占用几兆字节。
方式 A:一步到位辅助函数(create_custom_model_tokenizer_and_trigger)
该便捷函数会一步完成创建自定义模型、创建分词器、为所有存量行回填目标 bm25vector 列,并安装一个 BEFORE INSERT OR UPDATE 触发器——当数据行发生变化时,自动学习新词元并更新 bm25vector 列:
方式 B:分步创建(不使用触发器的 create_custom_model)
如果你不希望挂载自动触发器(或者希望自行控制何时计算并回填 embedding 列):
3. 导入 HuggingFace 分词器(create_huggingface_model)
你可以使用 create_huggingface_model 将任意 HuggingFace tokenizers 的 JSON 配置文件(tokenizer.json)导入 PostgreSQL。
在 psql 中,可以使用 \set 直接从 HuggingFace Hub 下载 tokenizer.json 到变量中并传给 create_huggingface_model:
也可以直接将 tokenizer.json 文件的完整内容作为美元符号引用的字符串传入:
4. Lindera 形态学分词模型(create_lindera_model)
对于日文、韩文、中文等需要形态学词典分析的东亚语言,pg_tokenizer 集成了 lindera。
如果你从源码编译 pg_tokenizer,请在编译时通过 --features 启用所需的 Lindera 词典特性:
以下完整示例展示了基于 ipadic 词典配置日文叠字符(Iteration Mark)规范化、NFKC Unicode 规范化、字符映射、复合词合并、数词归一化、日文停用词性(Stop-tags)过滤、片假名词干提取以及变音符号移除:
5. 模型预加载(add_preload_model)
大型模型(如约 200 MB 的 llmlingua2、约 100 MB 的 gemma2b、japanese_ipadic)在每个新建的 PostgreSQL 后端进程首次查询时,需要数秒钟时间和可观的内存来加载。通过在 shared_preload_libraries 启动阶段预加载模型,PostgreSQL 只需在 postmaster 主进程中加载一次模型,即可通过写时复制(Copy-on-Write)内存在所有后端连接之间共享(llmlingua2 默认已加入预加载列表):
调用 add_preload_model(...) 后(请确保已在 postgresql.conf 中设置 shared_preload_libraries = 'pg_tokenizer.so'),重启 PostgreSQL 即可使预加载对所有后端连接生效。
4 - 多语言分词配方与限制
本页提供可直接用于生产环境的英文、中文和日文全文 BM25 检索分词配置配方,并列出在实际运维中需要注意的关键限制事项。
配方一:带词干提取与停用词过滤的英文检索(wiki_tocken)
将 Unicode 词边界切分、NLTK 停用词过滤、Porter2 词干提取与内置维基百科词表(wiki_tocken)结合使用:
配方二:带自动触发器的英文自定义语料库模型
源自上游 pg_tokenizer 示例文档,该工作流直接基于 documents 表构建特定领域的英文词表,并通过触发器自动同步更新词频向量:
配方三:中文全文检索(jieba + 自定义模型)
处理中文文本时,推荐将 jieba 预分词器(mode = "search",enable_hmm = true)与基于业务表训练的自定义语料词表模型结合使用(以下示例采用钱钟书《围城》选段作为测试语料):
配方四:日文全文检索(lindera IPADIC)
处理日文文本时,可创建基于 Lindera IPADIC 的形态学分词模型,并配置叠字符规范化、NFKC 规范化、日文停用词性(Stop-tags)过滤以及片假名词干提取(以下示例采用夏目漱石《吾輩は猫である》选段作为测试语料):
运维注意事项与已知限制
- 对象名称命名规范:所有分词器、文本分析器、模型、停用词典和同义词典的名称只能包含字母、数字和下划线(
[A-Za-z0-9_]),且不能以数字开头。 - 事务回滚与会话级内存对象缓存:每个 PostgreSQL 后端连接都会在进程内存(
DashMap)中缓存tokenizer、text_analyzer、model、stopwords和synonym对象。由于该内存对象池未挂载到 PostgreSQL 事务回滚回调机制上,当事务执行ROLLBACK时,磁盘上的tokenizer_catalog元数据表会回滚,但当前连接的内存缓存中仍会残留刚才创建的对象。在ROLLBACK之后,请显式调用对应的drop_*函数(或重新建立数据库连接)以清理内存缓存: - 只读从库上的模型预加载:
add_preload_model会将预加载模型列表写入主节点本地的$PGDATA/pg_tokenizer目录。由于 PostgreSQL 关系表以外的文件系统文件不会通过 WAL 日志复制到从库,如果你在只读从库上也需要预加载模型,请在从库上独立执行一次add_preload_model。 - 内置大语言模型分词器的内存开销:
llmlingua2(~200 MB)、gemma2b(~100 MB)和lindera等模型会占用较多内存。在生产环境中,请务必使用add_preload_model让模型在 postmaster 主进程中仅加载一次,避免在每个连接中重复占用内存;或者优先选用仅占用几兆字节内存的自定义语料模型(create_custom_model)。