Why BM25 + Block-WeakAnd outperforms native PostgreSQL ts_rank and GIN indexes.
This is the multi-page printable view of this section. .
Getting Started
- 1: Overview
- 2: Installation
- 3: Quick Start
Get started with vchord_bm25 in PostgreSQL:
Install vchord_bm25 and pg_tokenizer via Pigsty, Docker, Debian packages, or source.
Tokenize text, build a bm25 index, and run Top-K ranked full-text queries in minutes.
1 - Overview
PostgreSQL includes built-in full-text search via tsvector, tsquery, ts_rank, and GIN indexes. While effective for boolean keyword matching (@@), PostgreSQL’s built-in ranking has two structural limitations for modern search and Retrieval-Augmented Generation (RAG) workloads:
- Suboptimal Ranking Formula (
ts_rank): PostgreSQL’sts_rankandts_rank_cddo not track corpus-wide Inverse Document Frequency (IDF) or Average Document Length () across the index. Common words are weighted the same as rare, high-signal terms, and term frequency is not properly saturated. - Exhaustive Index Scans on
ORDER BY ... LIMIT K: A PostgreSQLGINindex only accelerates boolean matching (WHERE tsv @@ tsquery). To return the Top-10 highest-ranked documents, PostgreSQL must fetch every matching row from the heap, computets_rankfor all of them, and sort the entire result set.
How VectorChord-BM25 Solves Both Problems
VectorChord-BM25 (vchord_bm25) introduces a dedicated PostgreSQL index access method (USING bm25) and sparse BM25 scoring operator (<&>) specifically engineered for Top-K lexical retrieval:
| Capability | PostgreSQL Built-in (GIN + ts_rank) |
VectorChord-BM25 (vchord_bm25) |
|---|---|---|
| Ranking Model | Local term frequency / proximity (ts_rank) |
Corpus-wide Okapi BM25 (, , , ) |
| Top-K Execution | Fetch & score all matching tuples, then sort | Block-WeakAnd (Block-Max WAND) early pruning inside the index |
| Posting Compression | Uncompressed or basic varint TID lists | SIMD-friendly Delta-Bitpacking in 128-doc blocks |
| Document Length Norm | Optional crude bitmask flags | 1-byte Fieldnorm Quantization (256-entry lookup table) |
| Query Semantics | Boolean AND/OR match required |
Non-conjunctive Top-K ranking across all query terms |
| Tokenization | Built-in ts_debug / Snowball |
pg_tokenizer (LLM tokenizers, jieba, lindera, custom vocab) or native tsvector |
Architecture Overview
flowchart LR
subgraph Tokenization["1. Tokenization Layer"]
T1["Raw Text"] --> T2["pg_tokenizer (v0.1–v0.3)\\nor to_tsvector (main)"]
T2 --> T3["bm25vector / tsvector\\n(Token IDs + Term Frequencies)"]
end
subgraph Index["2. BM25 Index (USING bm25)"]
I1["Growing Segment\\n(Append-only pages)"] --> I2["Sealed Segment\\n(Delta-Bitpacked 128-doc blocks)"]
I2 --> I3["Term Statistics (DF)\\n+ 1-Byte Fieldnorm Array"]
end
subgraph Query["3. Top-K Query (ORDER BY vec <&> bm25query LIMIT k)"]
Q1["Block-Max Score Upper Bounds"] --> Q2["Block-WeakAnd Pivot Pruning"]
Q2 --> Q3["Top-K Heap (-BM25 Score)"]
end
T3 --> Index
Index --> QueryTwo API Generations
When working with vchord_bm25, be aware of the two API generations across releases:
-
Stable Releases (
v0.1.0–v0.3.0, schemabm25_catalog):- Uses custom data types
bm25vector(sparsetoken_id:frequencymap, e.g.,{1:1, 2:2}) andbm25query. - Pairs with the
pg_tokenizerextension (tokenizer_catalog), which converts text intoINT[]token ID arrays that cast implicitly tobm25vector. - Index creation:
CREATE INDEX idx ON documents USING bm25 (embedding bm25_ops); - Query construction:
to_bm25query('idx', tokenize('query text', 'tokenizer_name'))
- Uses custom data types
-
Next-Gen Native
tsvectorArchitecture (mainbranch):- Operates directly on PostgreSQL’s built-in
tsvectordata type (CREATE OPERATOR CLASS bm25_ops FOR TYPE tsvector USING bm25), removing the requirement for a separatebm25vectorcolumn. - Index creation:
CREATE INDEX idx ON documents USING bm25 (vector bm25_ops) WITH (options = $$ k1 = 1.2, b = 0.75 $$); - Query construction:
to_bm25query(to_tsvector('english', 'query text'), 'idx')
- Operates directly on PostgreSQL’s built-in
Both workflows are documented side-by-side throughout this guide so you can use whichever version matches your deployment.
2 - Installation
vchord_bm25 supports PostgreSQL 14, 15, 16, 17, and 18 on Linux (x86_64 and aarch64) and macOS.
Method 1: Pigsty / Native OS Packages (Recommended)
Pigsty provides pre-built native RPM and DEB packages for vchord, vchord_bm25, and pg_tokenizer across EL 8/9/10, Debian 12/13, and Ubuntu 22.04/24.04 for PostgreSQL 14–18.
Or install directly via apt / dnf once the Pigsty repository is configured:
Method 2: Docker (tensorchord/vchord-suite)
The official tensorchord/vchord-suite Docker image bundles PostgreSQL, VectorChord (vchord), VectorChord-BM25 (vchord_bm25), and pg_tokenizer out of the box:
You can also pull from GitHub Container Registry using ghcr.io/tensorchord/vchord-suite:pg18-latest (or pg17-latest, pg16-latest, pg15-latest, pg14-latest).
Connect with psql:
Method 3: GitHub Release Packages (.deb & RHEL .zip)
Download prebuilt .deb or RHEL 9+ .zip packages for your PostgreSQL major version (14–18) and CPU architecture (amd64 / x86_64 or arm64 / aarch64) from the GitHub Releases pages of pgsty/vchord_bm25 (upstream supervc-stack/VectorChord-bm25) and pgsty/pg_tokenizer (upstream supervc-stack/pg_tokenizer.rs):
Debian / Ubuntu (.deb)
RHEL 9+ / Enterprise Linux (.zip Archive)
For RHEL 9+ systems when installing manually from the release .zip archive (requires glibc >= 2.34):
Method 4: Build from Source (cargo-pgrx)
To compile pgsty/vchord_bm25 and pgsty/pg_tokenizer from source:
Optional Lindera Dictionaries at Compile Time: To embed Lindera CJK morphological dictionaries (ipadic, ipadic-neologd, unidic, ko-dic, cc-cedict) directly into pg_tokenizer.so, pass the --features flag when compiling pg_tokenizer:
Development & Debugging (cargo-pgrx)
When developing or debugging vchord_bm25 or pg_tokenizer locally:
Configure PostgreSQL & Enable Extensions
1. Shared Preload Libraries (Optional / Recommended for pg_tokenizer)
If you use pg_tokenizer and want preloaded vocabulary models initialized at server startup, add pg_tokenizer (and vchord if used) to shared_preload_libraries in postgresql.conf:
Restart PostgreSQL after modifying shared_preload_libraries. (vchord_bm25 itself does not require shared_preload_libraries and can be loaded dynamically per session via CREATE EXTENSION.)
2. Create Extensions and Set search_path
Connect to your target database and enable the extensions:
In v0.1.0–v0.3.0, vchord_bm25 installs its objects into the bm25_catalog schema and pg_tokenizer installs into tokenizer_catalog. Add both schemas to your search_path:
Verify the installed version:
3 - Quick Start
This guide walks through creating a table, tokenizing documents, building a bm25 index, and executing Top-K BM25 queries.
Workflow A: bm25vector + pg_tokenizer (v0.1 – v0.3)
In released versions v0.1.0 through v0.3.0, vchord_bm25 pairs with pg_tokenizer to tokenize text into a bm25vector column.
1. Enable Extensions and Configure search_path
2. Create a Tokenizer
Use create_tokenizer with the built-in llmlingua2 (or bert_base_uncased) model:
Need domain-specific vocabulary, Chinese (jieba), or Japanese (lindera)? See the Tokenizers (pg_tokenizer) section.
3. Create a Table and Populate Documents
4. Tokenize Documents into bm25vector
tokenize(text, tokenizer_name) returns an INT[] array of token IDs, which implicitly casts to bm25vector (aggregating duplicate token IDs into term frequencies):
5. Build the bm25 Index
6. Run Top-K BM25 Queries
Use the <&> operator with to_bm25query(index_name, query_vector) in an ascending ORDER BY clause:
Why is <&> negative?
PostgreSQL only supports index-driven ORDER BY scans in ascending (ASC) order. Therefore, <&> returns the negative BM25 score (e.g., -2.8415). More negative values represent higher relevance, allowing ORDER BY embedding <&> to_bm25query(...) ASC LIMIT K to use the Block-WeakAnd index scan directly. Negate the expression (-(embedding <&> ...)) in SELECT if you want positive scores displayed.
Workflow B: Native tsvector API (main Branch)
In the next-generation main branch rewrite, vchord_bm25 indexes PostgreSQL’s native tsvector type directly—no external tokenizer extension is required for standard languages.