跳转到主要内容

这是本节的多页打印视图。 .

返回本页常规视图.

分词器 (pg_tokenizer)

pg_tokenizer 完整指南:文本分析器、字符/词元过滤器、预训练与自定义词表模型,以及多语言分词。

pg_tokenizer(由 Pigsty 在 pgsty/pg_tokenizer 维护)是 vchord_bm25 (pgsty/vchord_bm25) 的配套 PostgreSQL 分词扩展,提供全文分析器与词表模型,可将原始文本转换为整数词元 ID 数组(INT[])及稀疏 bm25vector 向量。

了解文本分析器(Text Analyzer)与词表模型(Model)如何协同工作,将原始文本转化为词元 ID。

配置字符过滤器、预分词器(Unicode、正则、结巴 Jieba)、词干提取器(支持 30 种语言)、停用词、同义词与 N-gram。

使用内置预训练模型(llmlingua2、bert_base_uncased、wiki_tocken、gemma2b)、训练自定义语料模型,或导入 HuggingFace 与 Lindera 模型。

面向英文、中文(Jieba)、日文(Lindera)及特定领域自定义语料库的开箱即用配置方案。

1 - 分词器架构与基本用法

理解 pg_tokenizer 的两阶段流水线:文本分析器(Text Analyzer)与分词模型(Tokenization Model)。

两阶段分词流水线

在 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)"]
  1. 文本分析器(text_analyzer):按顺序应用以下三级流水线,将原始文本切分为归一化后的字符串词元数组 TEXT[]:
    • 字符过滤器(character_filters):在切分前对原始字符串进行整体转换(例如 to_lowercase 小写转换、unicode_normalization Unicode 规范化)。
    • 预分词器(pre_tokenizer):将字符串初步切分为子串(支持 unicode_segmentation、regex 正则或中文 jieba 分词)。
    • 词元过滤器(token_filters):对单个词元进行过滤或转换(支持 skip_non_alphanumeric、stemmer 词干提取、stopwords 停用词、synonym 同义词、pg_dict、以及 ngram)。
  2. 分词模型(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 格式,并通过美元符号引用的字符串($$ ... $$)传入。

方式一:单阶段分词器(使用内置模型)

SELECT create_tokenizer('llm_tok', $$
model = "llmlingua2"
$$);

SELECT tokenize('PostgreSQL is an object-relational database system.', 'llm_tok');

方式二:内联文本分析器 + 模型

你可以直接在 create_tokenizer 内部定义 text_analyzer 流水线:

SELECT create_tokenizer('wiki_eng', $$
model = "wiki_tocken"

[text_analyzer.character_filters.to_lowercase]
[text_analyzer.pre_tokenizer.unicode_segmentation]
[[text_analyzer.token_filters.stopwords]]
words = "nltk_english"
[[text_analyzer.token_filters.stemmer]]
kind = "english_porter2"
$$);

SELECT tokenize('PostgreSQL is an object-relational database system.', 'wiki_eng');

方式三:可复用的命名文本分析器 + 自定义模型

你可以使用 create_text_analyzer 先创建一个独立的命名 text_analyzer,随后在多个自定义模型和分词器之间复用它:

-- 1. 创建可复用的文本分析器
SELECT create_text_analyzer('english_analyzer', $$
[character_filters.to_lowercase]
[pre_tokenizer.unicode_segmentation]
[[token_filters.skip_non_alphanumeric]]
[[token_filters.stopwords]]
words = "nltk_english"
[[token_filters.stemmer]]
kind = "english_porter2"
$$);

-- 预览文本分析器如何将文本切分为 TEXT[] 词元数组:
SELECT apply_text_analyzer('PostgreSQL is a powerful database system!', 'english_analyzer');
-- 输出:{postgresql,power,databas,system}

管理分词器

-- 查看已配置的分词器列表
SELECT name FROM tokenizer_catalog.tokenizer;

-- 删除指定分词器
SELECT drop_tokenizer('wiki_eng');

2 - 文本分析器与过滤器

pg_tokenizer 中字符过滤器、预分词器与词元过滤器的完整配置参考手册。

文本分析器(Text Analyzer) 通过三个顺序阶段处理原始文本:

  1. 字符过滤器(character_filters) —— 对完整输入字符串执行零个或多个字符级转换。
  2. 预分词器(pre_tokenizer) —— 使用唯一的一个切分器将字符串切分为初始词元。
  3. 词元过滤器(token_filters) —— 对切分出的词元执行零个或多个转换、扩展或过滤操作。
SELECT create_text_analyzer('analyzer1', $$
character_filters = [
    "to_lowercase",
    { unicode_normalization = "nfkd" },
]
pre_tokenizer = "unicode_segmentation"
token_filters = [
    "skip_non_alphanumeric",
    { stopwords = "nltk_english" },
    { stemmer = "english_porter2" },
]
$$);
提示

在将文本分析器绑定到模型之前,你可以直接运行 SELECT apply_text_analyzer('Sample text here', 'analyzer1'); 来测试并查看其输出的 TEXT[] 词元数组。


1. 字符过滤器(character_filters)

to_lowercase

将输入文本中的所有字符转换为小写。

character_filters = ["to_lowercase"]
# 或使用表语法:
[character_filters.to_lowercase]

unicode_normalization

应用 Unicode 规范化形式(UAX #15),将组合/分解字符、重音符号以及全角/半角变体标准化。

支持的模式包括:

  • "nfc" —— 规范化形式 C(标准分解后再执行标准组合)
  • "nfd" —— 规范化形式 D(标准分解)
  • "nfkc" —— 规范化形式 KC(兼容性分解后再执行标准组合)
  • "nfkd" —— 规范化形式 KD(兼容性分解)
[[character_filters]]
unicode_normalization = "nfkd"

2. 预分词器(pre_tokenizer)

每个文本分析器最多只能指定一个 pre_tokenizer。

unicode_segmentation

按照 Unicode 标准附件 #29 (UAX #29) 规范(底层基于 Rust unicode-segmentation 库)在 Unicode 词边界处切分文本。非常适合以空格分隔的西方语言。

pre_tokenizer = "unicode_segmentation"
# 或:
[pre_tokenizer.unicode_segmentation]

regex

提取匹配自定义正则表达式的词元(底层基于 Rust regex 库):

[pre_tokenizer]
regex = '''\w+'''

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" 模式下生效)。
[pre_tokenizer.jieba]
mode = "search"
enable_hmm = true

3. 词元过滤器(token_filters)

词元过滤器会严格按照 TOML 数组中声明的顺序依次执行。

skip_non_alphanumeric

丢弃不包含任何字母或数字字符的词元(例如分词后残留的独立标点符号 ,、.、! 等)。

[[token_filters]]
skip_non_alphanumeric = {}
# 或使用数组简写形式:
token_filters = ["skip_non_alphanumeric"]

stemmer(Snowball 词干提取 —— 支持 30 种语言)

使用 Snowball 词干提取算法将屈折变化的单词还原为其词干形式。

[[token_filters]]
stemmer = "english_porter2"

支持的 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) 创建(每行一个词),随后按名称引用。
-- 创建自定义停用词表(每行一个词)
SELECT create_stopwords('my_stopwords', $$
it
is
a
an
the
$$);

SELECT create_text_analyzer('text_analyzer1', $$
pre_tokenizer = "unicode_segmentation"
[[token_filters]]
stopwords = "my_stopwords"
$$);

SELECT apply_text_analyzer('it is a cat', 'text_analyzer1');
-- 输出:{cat}

synonym(同义词归一)

将同义词组归一化为同一个标准词元。使用 create_synonym(name, text) 创建同义词典,其中每行包含以空格分隔的一组同义词(同一行中的所有词都会被替换为该行的第一个词):

SELECT create_synonym('my_synonym', $$
postgres pgsql postgresql
index indices
$$);

SELECT create_text_analyzer('text_analyzer2', $$
pre_tokenizer = "unicode_segmentation"
[[token_filters]]
synonym = "my_synonym"
$$);

SELECT apply_text_analyzer('pgsql and postgresql have many indices', 'text_analyzer2');
-- 输出:{postgres,and,postgres,have,many,index}

pg_dict(PostgreSQL 全文检索词典)

将词元过滤委托给任意已有的 PostgreSQL 全文检索词典(pg_ts_dict,支持 Simple、Ispell、Snowball 和 Synonym 词典;注意不支持 Thesaurus 分类词典,因为后者作用于多词短语而非单个词元):

[[token_filters]]
pg_dict = "english_stem"

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] 范围内时,是否同时保留未切分的原始词元。
[[token_filters.ngram]]
min_gram = 2
max_gram = 3
preserve_original = true

3 - 分词词表模型

预训练模型(llmlingua2、bert_base_uncased、wiki_tocken、gemma2b)、自定义语料训练模型、HuggingFace 分词器与 Lindera 模型。

在 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

SELECT create_tokenizer('bert_tok', $$
model = "bert_base_uncased"
$$);

SELECT tokenize('VectorChord-BM25 brings Block-WeakAnd to PostgreSQL.', 'bert_tok');

2. 自定义语料库模型(create_custom_model)

为了获得最高的 BM25 相关性精度与最小的内存占用,你可以直接基于自己的业务表列构建自定义词表模型。不同于大语言模型子词分词器(会将罕见的领域专有名词切碎为多个子词元),自定义模型会为 text_analyzer 产出的每个唯一归一化词元分配独立的 1:1 整数 ID,且在内存中仅占用几兆字节。

方式 A:一步到位辅助函数(create_custom_model_tokenizer_and_trigger)

该便捷函数会一步完成创建自定义模型、创建分词器、为所有存量行回填目标 bm25vector 列,并安装一个 BEFORE INSERT OR UPDATE 触发器——当数据行发生变化时,自动学习新词元并更新 bm25vector 列:

-- 1. 定义文本分析流水线
SELECT create_text_analyzer('my_analyzer', $$
character_filters = ["to_lowercase"]
pre_tokenizer = "unicode_segmentation"
token_filters = [
    "skip_non_alphanumeric",
    { stopwords = "nltk_english" },
    { stemmer = "english_porter2" },
]
$$);

-- 2. 创建模型与分词器、回填向量并挂载触发器
SELECT create_custom_model_tokenizer_and_trigger(
    tokenizer_name     => 'my_tokenizer',
    model_name         => 'my_model',
    text_analyzer_name => 'my_analyzer',
    table_name         => 'documents',
    source_column      => 'passage',
    target_column      => 'embedding'
);

方式 B:分步创建(不使用触发器的 create_custom_model)

如果你不希望挂载自动触发器(或者希望自行控制何时计算并回填 embedding 列):

-- 1. 基于现有表列构建自定义词表模型
SELECT create_custom_model('my_model', $$
table = "documents"
column = "passage"
text_analyzer = "my_analyzer"
$$);

-- 2. 创建引用该分析器与自定义模型的分词器
SELECT create_tokenizer('my_tokenizer', $$
text_analyzer = "my_analyzer"
model = "my_model"
$$);

-- 3. 手动回填或刷新词频向量列
UPDATE documents SET embedding = tokenize(passage, 'my_tokenizer');

3. 导入 HuggingFace 分词器(create_huggingface_model)

你可以使用 create_huggingface_model 将任意 HuggingFace tokenizers 的 JSON 配置文件(tokenizer.json)导入 PostgreSQL。

在 psql 中,可以使用 \set 直接从 HuggingFace Hub 下载 tokenizer.json 到变量中并传给 create_huggingface_model:

\set content `wget -q -O - https://huggingface.co/google-bert/bert-base-uncased/resolve/main/tokenizer.json`
SELECT create_huggingface_model('bert_import', :'content');

SELECT create_tokenizer('bert_import_tok', $$
model = "bert_import"
$$);

也可以直接将 tokenizer.json 文件的完整内容作为美元符号引用的字符串传入:

SELECT create_huggingface_model('bge_m3_model', $$
{
  "version": "1.0",
  ...
}
$$);

SELECT create_tokenizer('bge_m3_tok', $$
model = "bge_m3_model"
$$);

4. Lindera 形态学分词模型(create_lindera_model)

对于日文、韩文、中文等需要形态学词典分析的东亚语言,pg_tokenizer 集成了 lindera。

说明

如果你从源码编译 pg_tokenizer,请在编译时通过 --features 启用所需的 Lindera 词典特性:

cargo pgrx install --release --features "lindera-ipadic lindera-ipadic-neologd lindera-unidic lindera-ko-dic lindera-cc-cedict"

以下完整示例展示了基于 ipadic 词典配置日文叠字符(Iteration Mark)规范化、NFKC Unicode 规范化、字符映射、复合词合并、数词归一化、日文停用词性(Stop-tags)过滤、片假名词干提取以及变音符号移除:

SELECT create_lindera_model('japanese_ipadic', $$
[segmenter]
mode = "normal"
  [segmenter.dictionary]
  kind = "ipadic"

[[character_filters]]
kind = "japanese_iteration_mark"
  [character_filters.args]
  normalize_kanji = true
  normalize_kana = true

[[character_filters]]
kind = "unicode_normalize"
  [character_filters.args]
  kind = "nfkc"

[[character_filters]]
kind = "mapping"
  [character_filters.args.mapping]
  "リンデラ" = "Lindera"

[[token_filters]]
kind = "japanese_compound_word"
  [token_filters.args]
  kind = "ipadic"
  tags = [
    "名詞,数",
    "名詞,接尾,助数詞",
  ]
  new_tag = "名詞,数"

[[token_filters]]
kind = "japanese_number"
  [token_filters.args]
  tags = ["名詞,数"]

[[token_filters]]
kind = "japanese_stop_tags"
  [token_filters.args]
  tags = [
    "接続詞",
    "助詞",
    "助詞,格助詞",
    "助詞,格助詞,一般",
    "助詞,格助詞,引用",
    "助詞,格助詞,連語",
    "助詞,係助詞",
    "助詞,副助詞",
    "助詞,間投助詞",
    "助詞,並立助詞",
    "助詞,終助詞",
    "助詞,副助詞/並立助詞/終助詞",
    "助詞,連体化",
    "助詞,副詞化",
    "助詞,特殊",
    "助動詞",
    "記号",
    "記号,一般",
    "記号,読点",
    "記号,句点",
    "記号,空白",
    "記号,括弧閉",
    "その他,間投",
    "フィラー",
    "非言語音",
  ]

[[token_filters]]
kind = "japanese_katakana_stem"
  [token_filters.args]
  min = 3

[[token_filters]]
kind = "remove_diacritical_mark"
  [token_filters.args]
  japanese = false
$$);

SELECT create_tokenizer('jp_tok', $$
model = "japanese_ipadic"
$$);

5. 模型预加载(add_preload_model)

大型模型(如约 200 MB 的 llmlingua2、约 100 MB 的 gemma2b、japanese_ipadic)在每个新建的 PostgreSQL 后端进程首次查询时,需要数秒钟时间和可观的内存来加载。通过在 shared_preload_libraries 启动阶段预加载模型,PostgreSQL 只需在 postmaster 主进程中加载一次模型,即可通过写时复制(Copy-on-Write)内存在所有后端连接之间共享(llmlingua2 默认已加入预加载列表):

-- 将模型加入预加载列表(持久化保存在 $PGDATA/pg_tokenizer/preload_models)
SELECT add_preload_model('my_model');

-- 查看当前已配置的预加载模型列表(默认为 {llmlingua2})
SELECT list_preload_models();

-- 从预加载列表中移除模型(例如不使用 llmlingua2 时可释放约 200 MB 内存)
SELECT remove_preload_model('llmlingua2');

调用 add_preload_model(...) 后(请确保已在 postgresql.conf 中设置 shared_preload_libraries = 'pg_tokenizer.so'),重启 PostgreSQL 即可使预加载对所有后端连接生效。

4 - 多语言分词配方与限制

面向英文、中文(结巴 Jieba)、日文(Lindera)的完整生产级分词配置方案及已知使用限制。

本页提供可直接用于生产环境的英文、中文和日文全文 BM25 检索分词配置配方,并列出在实际运维中需要注意的关键限制事项。

配方一:带词干提取与停用词过滤的英文检索(wiki_tocken)

将 Unicode 词边界切分、NLTK 停用词过滤、Porter2 词干提取与内置维基百科词表(wiki_tocken)结合使用:

SELECT create_tokenizer('eng_wiki', $$
model = "wiki_tocken"

[text_analyzer.character_filters.to_lowercase]
[text_analyzer.pre_tokenizer.unicode_segmentation]
[[text_analyzer.token_filters.skip_non_alphanumeric]]
[[text_analyzer.token_filters.stopwords]]
words = "nltk_english"
[[text_analyzer.token_filters.stemmer]]
kind = "english_porter2"
$$);

SELECT tokenize('PostgreSQL databases are running analytical and vector workloads.', 'eng_wiki');

配方二:带自动触发器的英文自定义语料库模型

源自上游 pg_tokenizer 示例文档,该工作流直接基于 documents 表构建特定领域的英文词表,并通过触发器自动同步更新词频向量:

CREATE TABLE documents (
    id SERIAL PRIMARY KEY,
    passage TEXT,
    embedding bm25vector
);

SELECT create_text_analyzer('text_analyzer1', $$
pre_tokenizer = "unicode_segmentation"
[[character_filters]]
to_lowercase = {}
[[character_filters]]
unicode_normalization = "nfkd"
[[token_filters]]
skip_non_alphanumeric = {}
[[token_filters]]
stopwords = "nltk_english"
[[token_filters]]
stemmer = "english_porter2"
$$);

SELECT create_custom_model_tokenizer_and_trigger(
    tokenizer_name => 'tokenizer1',
    model_name => 'model1',
    text_analyzer_name => 'text_analyzer1',
    table_name => 'documents',
    source_column => 'passage',
    target_column => 'embedding'
);

INSERT INTO documents (passage) VALUES
('PostgreSQL is a powerful, open-source object-relational database system. It has over 15 years of active development and a proven architecture that has earned it a strong reputation for reliability, feature robustness, and performance.'),
('Full-text search is a technique for searching a computer-stored document or a collection in a full-text database. Full-text search is distinguished from searches based on metadata or on parts of the original texts represented in databases (such as titles, abstracts, selected sections, or bibliographical references).'),
('BM25 is a ranking function used by search engines to estimate the relevance of documents to a given search query. It is based on the probabilistic retrieval framework developed in the 1970s and 1980s by Stephen E. Robertson, Karen Spärck Jones, and others.'),
('PostgreSQL supports advanced data types and performance optimization features, only available in the expensive commercial databases, such as Oracle and SQL Server. It is known for its resilience, integrity, and correctness.'),
('The BM25 algorithm computes a score for each document in a collection based on the query terms appearing in the document, regardless of their proximity within the document. It is a bag-of-words retrieval function that ranks a set of documents based on the query terms appearing in each document.'),
('PostgreSQL has a wide range of extensions and tools that make it suitable for various use cases, from small single-machine applications to large internet-facing applications with many concurrent users.'),
('Full-text search in PostgreSQL allows users to perform complex queries on text data, including phrase searching, ranking of results, and support for multiple languages through configurations and dictionaries.'),
('VectorChord-BM25 is a postgresql extension designed for bm25 ranking algorithm. Block-WeakAnd algorithm is implemented in it to accelerate the ranking process.'),
('pg_tokenizer is a postgresql extension designed for full-text search. It provides a set of functions to preprocess text data and generate tokens for indexing and searching.'),
('Look for organizations which use PostgreSQL for mission-critical or scaled applications. Look for organizations that have used PostgreSQL for a significant length of time.');

CREATE INDEX documents_passage_bm25 ON documents USING bm25 (embedding bm25_ops);

SELECT id, passage, embedding <&> to_bm25query('documents_passage_bm25', tokenize('PostgreSQL BM25', 'tokenizer1')) AS rank
FROM documents
ORDER BY rank
LIMIT 10;

配方三:中文全文检索(jieba + 自定义模型)

处理中文文本时,推荐将 jieba 预分词器(mode = "search",enable_hmm = true)与基于业务表训练的自定义语料词表模型结合使用(以下示例采用钱钟书《围城》选段作为测试语料):

CREATE TABLE zh_docs (
    id SERIAL PRIMARY KEY,
    passage TEXT,
    embedding bm25vector
);

-- 1. 创建启用 NFKD 规范化与结巴搜索引擎模式的中文文本分析器
SELECT create_text_analyzer('text_analyzer1', $$
[character_filters.to_lowercase]
[character_filters.unicode_normalization]
kind = "nfkd"
[pre_tokenizer.jieba]
mode = "search"
enable_hmm = true
[[token_filters.skip_non_alphanumeric]]
$$);

-- 2. 创建自定义词表模型、分词器,并绑定自动更新触发器
SELECT create_custom_model_tokenizer_and_trigger(
    tokenizer_name     => 'tokenizer1',
    model_name         => 'model1',
    text_analyzer_name => 'text_analyzer1',
    table_name         => 'zh_docs',
    source_column      => 'passage',
    target_column      => 'embedding'
);

-- 3. 写入中文语料(触发器会自动分词并填充 embedding 列)
INSERT INTO zh_docs (passage) VALUES
('红海早过了,船在印度洋面上开驶着,但是太阳依然不饶人地迟落早起,侵占去大部分的夜。'),
('夜仿佛纸浸了油,变成半透明体;它给太阳拥抱住了,分不出身来,也许是给太阳陶醉了,所以夕照霞隐褪后的夜色也带着酡红。'),
('到红消醉醒,船舱里的睡人也一身腻汗地醒来,洗了澡赶到甲板上吹海风,又是一天开始。'),
('这是七月下旬,合中国旧历的三伏,一年最热的时候。在中国热得更比常年利害,事后大家都说是兵戈之象,因为这就是民国二十六年【一九三七年】。'),
('这条法国邮船白康蒂号(VicomtedeBragelonne)正开向中国来。早晨八点多钟,冲洗过的三等舱甲板湿意未干,但已坐立满了人,法国人、德国流亡出来的犹太人、印度人、安南人,不用说还有中国人。'),
('海风里早含着燥热,胖人身体给风吹干了,蒙上一层汗结的盐霜,仿佛刚在巴勒斯坦的死海里洗过澡。'),
('毕竟是清晨,人的兴致还不没给太阳晒萎,烘懒,说话做事都很起劲。那几个新派到安南或中国租界当警察的法国人,正围了那年轻善撒娇的犹太女人在调情。'),
('俾斯麦曾说过,法国公使大使的特点,就是一句外国话不会讲;这几位警察并不懂德文,居然传情达意,引得犹太女人格格地笑,比他们的外交官强多了。'),
('这女人的漂亮丈夫,在旁顾而乐之,因为几天来,香烟、啤酒、柠檬水全靠这几个警察孝敬。船上买东西用法国钱,这犹太人只有一点儿沿着那位丹麦王子汉姆烈德(Hamlet)路线逃出来的德国马克。'),
('当时德国跟日本虽在谈恋爱,尚未结婚,到中国来的犹太人还是德国公民,有他们本国的保护。这船上的犹太人有过路到香港的,有到上海居留的,聚在一起在讨论生计,声音活像小洋磁盆里托的石子。'),
('几个年轻的安南人比手画脚,像猴子跳电线一样迅捷地交换着法语,华侨在旁羡慕地听着。这船上都是回家乡的中国人,像报纸上的标题,电影的字幕,标语,籍贯姓名年纪一张表上那几格,他们老早填满了,此番回来,不过把这些公式平滑无波地重演一次。'),
('只有鼻梁上架着一副黑眼镜的苏小姐,心算着她离国这几年,家里有什么变化,她父亲是否老了,她母亲新添了什么病痛,她弟弟结婚了没有。'),
('苏小姐在里昂研究法国文学,做了一篇《中国十八世纪的法国文学里的中国》的论文,刚得了博士。'),
('方鸿渐也到甲板上来了,在苏小姐身旁走过,向她脱帽招呼。苏小姐一壁应着,一壁想他真算厚脸皮,昨天向自己借了照相机,今天见了面,提都不提。');

-- 4. 创建 BM25 索引并执行检索
CREATE INDEX zh_docs_passage_bm25 ON zh_docs USING bm25 (embedding bm25_ops);

SELECT id, passage,
       embedding <&> to_bm25query('zh_docs_passage_bm25', tokenize('苏小姐 法国', 'tokenizer1')) AS rank
FROM zh_docs
ORDER BY rank
LIMIT 10;

配方四:日文全文检索(lindera IPADIC)

处理日文文本时,可创建基于 Lindera IPADIC 的形态学分词模型,并配置叠字符规范化、NFKC 规范化、日文停用词性(Stop-tags)过滤以及片假名词干提取(以下示例采用夏目漱石《吾輩は猫である》选段作为测试语料):

CREATE TABLE jp_docs (
    id SERIAL PRIMARY KEY,
    passage TEXT,
    embedding bm25vector
);

INSERT INTO jp_docs (passage) VALUES
('吾輩は猫である。名前はまだ無い。'),
('どこで生れたかとんと見当がつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。'),
('吾輩はここで始めて人間というものを見た。しかもあとで聞くとそれは書生という人間中で一番獰悪な種族であったそうだ。'),
('この書生というのは時々我々を捕えて煮て食うという話である。しかしその当時は何という考もなかったから別段恐しいとも思わなかった。'),
('ただ彼の掌に載せられてスーと持ち上げられた時何だかフワフワした感じがあったばかりである。'),
('掌の上で少し落ちついて書生の顔を見たのがいわゆる人間というものの見始であろう。この時妙なものだと思った感じが今でも残っている。'),
('第一毛をもって装飾されべきはずの顔がつるつるしてまるで薬缶だ。その後猫にもだいぶ逢ったがこんな片輪には一度も出会わした事がない。'),
('のみならず顔の真中があまりに突起している。そうしてその穴の中から時々ぷうぷうと煙を吹く。どうも咽せぽくて実に弱った。'),
('これが人間の飲む煙草というものである事はようやくこの頃知った。'),
('この書生の掌の裏でしばらくはよい心持に坐っておったが、しばらくすると非常な速力で運転し始めた。'),
('書生が動くのか自分だけが動くのか分らないが無暗に眼が廻る。胸が悪くなる。到底助からないと思っていると、どさりと音がして眼から火が出た。'),
('それまでは記憶しているがあとは何の事やらいくら考え出そうとしても分らない。'),
('ふと気が付いて見ると書生はいない。たくさんおった兄弟が一疋も見えぬ。肝心の母親さえ姿を隠してしまった。'),
('その上今までの所とは違って無暗に明るい。眼を明いていられぬくらいだ。果てな何でも容子がおかしいと、のそのそ這い出して見ると非常に痛い。'),
('吾輩は藁の上から急に笹原の中へ棄てられたのである。');

SELECT create_lindera_model('lindera_ipadic', $$
[segmenter]
mode = "normal"
  [segmenter.dictionary]
  kind = "ipadic"

[[character_filters]]
kind = "japanese_iteration_mark"
  [character_filters.args]
  normalize_kanji = true
  normalize_kana = true

[[character_filters]]
kind = "unicode_normalize"
  [character_filters.args]
  kind = "nfkc"

[[character_filters]]
kind = "mapping"
  [character_filters.args.mapping]
  "リンデラ" = "Lindera"

[[token_filters]]
kind = "japanese_compound_word"
  [token_filters.args]
  kind = "ipadic"
  tags = [
    "名詞,数",
    "名詞,接尾,助数詞",
  ]
  new_tag = "名詞,数"

[[token_filters]]
kind = "japanese_number"
  [token_filters.args]
  tags = ["名詞,数"]

[[token_filters]]
kind = "japanese_stop_tags"
  [token_filters.args]
  tags = [
    "接続詞", "助詞", "助詞,格助詞", "助詞,格助詞,一般", "助詞,格助詞,引用",
    "助詞,格助詞,連語", "助詞,係助詞", "助詞,副助詞", "助詞,間投助詞",
    "助詞,並立助詞", "助詞,終助詞", "助詞,副助詞/並立助詞/終助詞",
    "助詞,連体化", "助詞,副詞化", "助詞,特殊", "助動詞", "記号",
    "記号,一般", "記号,読点", "記号,句点", "記号,空白", "記号,括弧閉",
    "その他,間投", "フィラー", "非言語音"
  ]

[[token_filters]]
kind = "japanese_katakana_stem"
  [token_filters.args]
  min = 3

[[token_filters]]
kind = "remove_diacritical_mark"
  [token_filters.args]
  japanese = false
$$);

SELECT create_tokenizer('tokenizer1', $$
model = "lindera_ipadic"
$$);

UPDATE jp_docs SET embedding = tokenize(passage, 'tokenizer1');

CREATE INDEX jp_docs_passage_bm25 ON jp_docs USING bm25 (embedding bm25_ops);

SELECT id, passage,
       embedding <&> to_bm25query('jp_docs_passage_bm25', tokenize('人間 吾輩', 'tokenizer1')) AS rank
FROM jp_docs
ORDER BY rank
LIMIT 10;

运维注意事项与已知限制

  1. 对象名称命名规范:所有分词器、文本分析器、模型、停用词典和同义词典的名称只能包含字母、数字和下划线([A-Za-z0-9_]),且不能以数字开头。
  2. 事务回滚与会话级内存对象缓存:每个 PostgreSQL 后端连接都会在进程内存(DashMap)中缓存 tokenizer、text_analyzer、model、stopwords 和 synonym 对象。由于该内存对象池未挂载到 PostgreSQL 事务回滚回调机制上,当事务执行 ROLLBACK 时,磁盘上的 tokenizer_catalog 元数据表会回滚,但当前连接的内存缓存中仍会残留刚才创建的对象。在 ROLLBACK 之后,请显式调用对应的 drop_* 函数(或重新建立数据库连接)以清理内存缓存:
    BEGIN;
    SELECT create_text_analyzer('text_analyzer1', $$
    pre_tokenizer = "unicode_segmentation"
    $$);
    ROLLBACK;
    
    -- 清理当前会话内存中残留的缓存项(或者重新建立数据库连接)
    SELECT drop_text_analyzer('text_analyzer1');
    
    -- 现在可以正常重新创建同名对象
    SELECT create_text_analyzer('text_analyzer1', $$
    pre_tokenizer = "unicode_segmentation"
    $$);
  3. 只读从库上的模型预加载:add_preload_model 会将预加载模型列表写入主节点本地的 $PGDATA/pg_tokenizer 目录。由于 PostgreSQL 关系表以外的文件系统文件不会通过 WAL 日志复制到从库,如果你在只读从库上也需要预加载模型,请在从库上独立执行一次 add_preload_model。
  4. 内置大语言模型分词器的内存开销:llmlingua2(~200 MB)、gemma2b(~100 MB)和 lindera 等模型会占用较多内存。在生产环境中,请务必使用 add_preload_model 让模型在 postmaster 主进程中仅加载一次,避免在每个连接中重复占用内存;或者优先选用仅占用几兆字节内存的自定义语料模型(create_custom_model)。