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 --> Query
Two 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.