How Text Analyzers and Models combine to turn raw text into token IDs.
This is the multi-page printable view of this section. .
Tokenizers (pg_tokenizer)
- 1: Tokenizer Architecture & Usage
- 2: Text Analyzers & Filters
- 3: Tokenization Models
- 4: Multilingual Recipes & Limitations
pg_tokenizer (maintained by Pigsty at pgsty/pg_tokenizer) is the companion PostgreSQL extension for vchord_bm25 (pgsty/vchord_bm25) that provides full-text analyzers and vocabulary models to convert raw text into integer token ID arrays (INT[]) and sparse bm25vector embeddings.
Configure character filters, pre-tokenizers (Unicode, Regex, Jieba), stemmers (30 languages), stopwords, synonyms, and n-grams.
Use built-in models (llmlingua2, bert_base_uncased, wiki_tocken, gemma2b), train custom corpus models, or import HuggingFace & Lindera models.
Ready-to-use configurations for English, Chinese (Jieba), Japanese (Lindera), and custom domain corpora.
1 - Tokenizer Architecture & Usage
Two-Stage Tokenization Pipeline
A tokenizer in pg_tokenizer consists of up to two stages that transform a raw TEXT string into an INT[] array of integer token IDs:
flowchart LR
A["Raw Text\\n(TEXT)"] --> B["Stage 1: Text Analyzer\\n1. character_filters\\n2. pre_tokenizer\\n3. token_filters"]
B --> C["Normalized Tokens\\n(TEXT[])"]
C --> D["Stage 2: Model\\nVocabulary Mapping / Subword BPE"]
D --> E["Token ID Array\\n(INT[] → bm25vector)"]- Text Analyzer (
text_analyzer): Splits raw text into aTEXT[]array of normalized string tokens by applying a sequential pipeline of:- Character Filters (
character_filters): Transform the raw string before splitting (e.g.,to_lowercase,unicode_normalization). - Pre-tokenizer (
pre_tokenizer): Splits the string into initial substrings (unicode_segmentation,regex, or Chinesejieba). - Token Filters (
token_filters): Filter or transform individual tokens (skip_non_alphanumeric,stemmer,stopwords,synonym,pg_dict,ngram).
- Character Filters (
- Model (
model): Maps the string tokens (or raw text, for standalone subword models likellmlingua2,bert_base_uncased,gemma2b, HuggingFace, orlindera) into integer token IDs (INT[]).
- Pretrained subword models (
llmlingua2,bert_base_uncased,gemma2b), HuggingFace models, and Lindera models include their own built-in tokenization rules and do not require a separatetext_analyzerstage. - Dictionary/corpus models (
wiki_tockenand custom models created viacreate_custom_model) require atext_analyzerstage to split and normalize text before looking up token IDs in the model’s vocabulary table.
Creating and Using a Tokenizer
All configuration in pg_tokenizer uses TOML format passed as a dollar-quoted string ($$ ... $$).
Option 1: Single-Stage Tokenizer (Built-in Model)
Option 2: Inline Text Analyzer + Model
You can define the text_analyzer pipeline directly inside create_tokenizer:
Option 3: Reusable Named Text Analyzer + Custom Model
You can create a standalone text_analyzer once with create_text_analyzer and reference it across multiple custom models and tokenizers:
Managing Tokenizers
2 - Text Analyzers & Filters
A Text Analyzer processes raw text through three sequential stages:
- Character Filters (
character_filters) — Zero or more character-level transformations on the full text string. - Pre-tokenizer (
pre_tokenizer) — Exactly one splitter that breaks the string into initial tokens. - Token Filters (
token_filters) — Zero or more token-level transformations, expansions, or filters.
You can test any text analyzer directly using SELECT apply_text_analyzer('Sample text here', 'analyzer1'); to inspect the resulting TEXT[] array before wiring it to a model.
1. Character Filters (character_filters)
to_lowercase
Converts all characters in the input text to lowercase.
unicode_normalization
Applies Unicode Normalization Forms (UAX #15) to standardize composed/decomposed characters, accents, and full-width/half-width variants.
Supported modes:
"nfc"— Normalization Form C (Canonical Decomposition followed by Canonical Composition)"nfd"— Normalization Form D (Canonical Decomposition)"nfkc"— Normalization Form KC (Compatibility Decomposition followed by Canonical Composition)"nfkd"— Normalization Form KD (Compatibility Decomposition)
2. Pre-Tokenizers (pre_tokenizer)
A text analyzer may specify at most one pre_tokenizer.
unicode_segmentation
Splits text on Unicode word boundaries according to the Unicode Standard Annex #29 (UAX #29) specification (via the Rust unicode-segmentation crate). Ideal for space-delimited and Western languages.
regex
Extracts tokens matching a custom regular expression (powered by the Rust regex crate):
jieba (Chinese Word Segmentation)
Segments Chinese text into words using the jieba-rs dictionary and Hidden Markov Model (HMM):
| Parameter | Type | Default | Description |
|---|---|---|---|
mode |
string |
"search" |
"full" (standard jieba.cut), "precise" (jieba.cut_all scanning all dictionary matches), or "search" (jieba.cut_for_search finer-grained segmentation for search recall). |
enable_hmm |
bool |
true |
Enables HMM recognition for out-of-vocabulary words ("full" and "search" modes only). |
3. Token Filters (token_filters)
Token filters run in the exact order listed in your TOML array.
skip_non_alphanumeric
Drops any token that contains no alphanumeric characters (such as standalone punctuation marks ,, ., ! left behind by word segmentation).
stemmer (Snowball Stemming — 30 Languages)
Reduces inflected words to their root stem using the Snowball stemming algorithms.
Supported stemmer values:
| Language | stemmer Value |
Language | stemmer Value |
|---|---|---|---|
| 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
Removes common stopwords that carry little lexical signal.
- Built-in dictionaries:
"lucene_english"— Apache Lucene standard English stopword list"nltk_english"— NLTK standard English stopword list"iso_english"— Stopwords ISO English stopword list
- Custom stopword dictionary: Create your own with
create_stopwords(name, text)(one word per line) and reference it by name.
synonym
Normalizes synonymous terms to a single canonical token. Create a synonym dictionary with create_synonym(name, text) where each line contains space-separated synonyms (all words on a line are replaced by the first word on that line):
pg_dict (PostgreSQL Full-Text Dictionaries)
Delegates token filtering to any existing PostgreSQL Full-Text Search Dictionary (pg_ts_dict, including Simple, Ispell, Snowball, and Synonym dictionaries; note that Thesaurus dictionaries are not supported because they operate on multi-word phrases rather than single tokens):
ngram
Splits each token into character n-grams (useful for substring, prefix, CJK without dictionaries, or code/identifier search):
| Parameter | Type | Default | Range | Description |
|---|---|---|---|---|
min_gram |
int |
1 |
1..=255 (<= max_gram) |
Minimum character length of generated n-grams. |
max_gram |
int |
2 |
1..=255 (>= min_gram) |
Maximum character length of generated n-grams. |
preserve_original |
bool |
false |
true / false |
Whether to also emit the original unsplit token if its length falls outside [min_gram, max_gram]. |
3 - Tokenization Models
In pg_tokenizer, a Model maps string tokens (or raw text) to integer token IDs (INT[]). You can choose from four built-in pretrained models, train a custom vocabulary model directly on your PostgreSQL table, import any HuggingFace tokenizer.json, or use a Lindera morphological model.
1. Built-in Pretrained Models
| Model Name | Source / Architecture | Requires text_analyzer? |
Memory Footprint | Best For |
|---|---|---|---|---|
llmlingua2 |
microsoft/llmlingua-2-xlm-roberta-large-meetingbank (XLM-RoBERTa multilingual subword) |
No (forbidden) | ~200 MB (default preloaded) | Multilingual general text out of the box without training a custom vocabulary. |
bert_base_uncased |
google-bert/bert-base-uncased (WordPiece, 30,522 vocab) |
No (forbidden) | ~15 MB | General English text; fast startup and compact vocabulary. |
wiki_tocken |
Snowolf/wiki-tocken (Built-in English Wikipedia token dictionary) |
Yes (required) | ~30 MB | English full-text search combined with custom stemmers, stopwords, and synonyms. |
gemma2b |
google/gemma-2b (SentencePiece BPE, 256k vocab) |
No (forbidden) | ~100 MB | Code, technical identifiers, and multilingual LLM workloads. |
Example: Using bert_base_uncased or llmlingua2
2. Custom Corpus Models (create_custom_model)
For maximum BM25 accuracy and minimal memory footprint, you can build a custom vocabulary model directly from your own table column. Unlike subword LLM tokenizers (which split rare domain words into multiple sub-tokens), a custom model assigns a dedicated 1:1 integer ID to every unique normalized token produced by your text_analyzer—and weighs only a few megabytes in memory.
Option A: One-Step Helper (create_custom_model_tokenizer_and_trigger)
This convenience function creates the custom model, creates the tokenizer, populates the target bm25vector column for all existing rows, and installs a BEFORE INSERT OR UPDATE trigger that automatically learns new vocabulary tokens and updates the bm25vector column when rows change:
Option B: Step-by-Step (create_custom_model Without Trigger)
If you prefer not to attach an automatic trigger (or want to control when embedding is populated):
3. Importing HuggingFace Tokenizers (create_huggingface_model)
You can import any HuggingFace tokenizers JSON specification (tokenizer.json) into PostgreSQL using create_huggingface_model.
In psql, you can fetch tokenizer.json directly from HuggingFace Hub into a variable using \set and pass it to create_huggingface_model:
Or pass the contents of a tokenizer.json file directly as a dollar-quoted string:
4. Lindera Morphological Models (create_lindera_model)
For East Asian languages such as Japanese, Korean, and Chinese that benefit from morphological dictionary analysis, pg_tokenizer integrates lindera.
If you build pg_tokenizer from source, enable the desired Lindera dictionary features at compile time:
Example configuration using ipadic with iteration mark normalization, NFKC Unicode normalization, character mapping, compound word merging, number normalization, Japanese part-of-speech stop-tags, Katakana stemming, and diacritical mark removal:
5. Model Preloading (add_preload_model)
Large models (llmlingua2 at ~200 MB, gemma2b at ~100 MB, japanese_ipadic) take several seconds and significant RAM to load on first query in each new PostgreSQL backend process. By preloading models during shared_preload_libraries startup, PostgreSQL loads the model once in the postmaster process and shares it across all backend connections via copy-on-write memory (llmlingua2 is preloaded by default):
Restart PostgreSQL after calling add_preload_model(...) (with shared_preload_libraries = 'pg_tokenizer.so' set in postgresql.conf) for preloading to take effect across all backends.
4 - Multilingual Recipes & Limitations
This page provides production-ready tokenization recipes for English, Chinese, and Japanese full-text BM25 search, followed by important operational limitations to keep in mind.
Recipe 1: English with Stemming & Stopwords (wiki_tocken)
Combines Unicode word segmentation, NLTK stopwords removal, and Porter2 stemming with the built-in Wikipedia vocabulary (wiki_tocken):
Recipe 2: English Custom Corpus Model with Trigger
From the upstream pg_tokenizer examples, this workflow builds a domain-specific English vocabulary from your documents table and keeps embeddings updated automatically via trigger:
Recipe 3: Chinese Full-Text Search (jieba + Custom Model)
For Chinese text, combine the jieba pre-tokenizer (mode = "search", enable_hmm = true) with a custom corpus model trained on your table (demonstrated below using passages from Qian Zhongshu’s Fortress Besieged / 《围城》):
Recipe 4: Japanese Full-Text Search (lindera IPADIC)
For Japanese text, create a Lindera IPADIC morphological model with iteration mark normalization, NFKC normalization, Japanese stop-tag filtering, and Katakana stemming (demonstrated below using passages from Natsume Sōseki’s I Am a Cat / 《吾輩は猫である》):
Operational Notes & Known Limitations
- Object Name Validation: All names for tokenizers, text analyzers, models, stopword dictionaries, and synonym dictionaries must contain only alphanumeric characters and underscores (
[A-Za-z0-9_]), and must not start with a digit. - Transaction Rollback & Per-Connection Object Cache: Each PostgreSQL connection caches
tokenizer,text_analyzer,model,stopwords, andsynonymobjects in backend memory (DashMap). Because this in-memory object pool is not tied to PostgreSQL’s transaction rollback callbacks, rolling back a transaction (ROLLBACK) reverts thetokenizer_catalogtables on disk, but the current connection’s in-memory cache still holds the created object. After aROLLBACK, explicitly call the correspondingdrop_*function (or reconnect) to clear the in-memory cache: - Preload Models Across Replicas:
add_preload_modelwrites the preload model list to the local$PGDATA/pg_tokenizerdirectory on the primary node. Because filesystem files outside PostgreSQL relations are not replicated via WAL, you should runadd_preload_modelon read replicas independently if you preload models there. - Memory Consumption of Built-in LLM Models: Models like
llmlingua2(~200 MB),gemma2b(~100 MB), andlinderaconsume significant memory. Always useadd_preload_modelin production so the model is loaded once in the postmaster process rather than duplicated per connection, or prefer a custom corpus model (create_custom_model) which uses only a few megabytes.