How pg_bestmatch converts BM25 scoring into sparse vector inner products, installation from packages or source, and quickstart setup.
This is the multi-page printable view of this section. .
pg_bestmatch Documentation
pg_bestmatch (pg_bestmatch.rs) is a PostgreSQL extension for servicing Okapi BM25 text queries inside PostgreSQL by converting documents and queries into BM25-weighted sparse vectors compatible with pgvector (sparsevec) and pgvecto.rs (svector).
vchord_bm25 + pg_tokenizerpg_bestmatch.rs was TensorChord’s first-generation BM25 extension built on sparse vector dot products. For new production deployments, we recommend vchord_bm25 paired with pg_tokenizer, which replaces HNSW sparse-vector approximation with a native Block-WeakAnd (WAND) inverted index for 100% exact Top-K BM25 scoring and up to 3x higher throughput.
Key Capabilities
- In-Database BM25 Sparse Vector Generation: Maintains document frequency, term frequency, and document length statistics inside
bm25_catalogand emits BM25-weighted sparse vectors for both documents and queries. - Native
pgvector&pgvecto.rsIntegration: Outputs standardsparsevec(pgvector 0.7.0+) orsvector(pgvecto.rs) values so you can score documents with<#>(negative inner product) and accelerate search with HNSW indexes. - Built-in Tokenization: Ships with a built-in HuggingFace
bert-base-uncasedWordPiece vocabulary (30,522dimensions) and a standalonetokenize(text)SQL function.
Documentation Sections
End-to-end BM25 retrieval workflow on the Stanford LoCo benchmark using pgvector sparsevec or pgvecto.rs svector.
Complete reference for bm25_create, bm25_refresh, bm25_drop, bm25_document_to_svector, bm25_query_to_svector, and tokenize.
1 - Getting Started
How Does pg_bestmatch Work?
Given a document and a query tokenized into vocabulary terms , the Okapi BM25 relevance score is:
Notice that this sum is the inner product (dot product) of two sparse vectors over the vocabulary space (dimension for bert-base-uncased):
- Document Sparse Vector : For each unique token , store the normalized term-frequency weight:
- Query Sparse Vector : For each token , store its inverse document frequency weight .
When you compute the inner product using pgvector or pgvecto.rs’s <#> operator, PostgreSQL returns so that ORDER BY embedding <#> query_vec ASC returns the highest-scoring documents first!
Installation
Prerequisites
Install either pgvector (v0.7.0+ for sparsevec support) or pgvecto.rs (v0.3.0+ for svector support) first.
Install from Release Packages (Debian / Ubuntu)
Download the prebuilt .deb package matching your PostgreSQL major version from GitHub Releases:
Build from Source (cargo pgrx)
Ensure Rust and cargo-pgrx (0.12.0-alpha.1 or compatible) are installed:
Enable the Extension
Connect to PostgreSQL, create the extension, and add bm25_catalog to your search_path:
Test tokenization with a single SQL query:
Continue to the Usage Guide for a complete end-to-end indexing and query workflow.
2 - Usage Guide
This guide walks through a complete BM25 text retrieval pipeline using pg_bestmatch and pgvector (sparsevec), followed by the equivalent configuration for pgvecto.rs (svector).
1. Enable Extensions & Set Search Path
2. Create the Documents Table & Load Data
3. Create BM25 Corpus Statistics
Call bm25_create(table_name, column_name, statistic_name, b, k) to register corpus statistics (b = 0.75 and k = 1.2 are standard defaults):
What bm25_create does under the hood:
- Creates materialized views and helper tables inside
bm25_catalogto track term frequencies, document frequencies, and total document count. - Installs a trigger on
documentsso newly inserted or deleted rows are tracked automatically.
When you load a large batch of new documents and want to recompute global IDF and average document length (avgdl), run:
4. Materialize Document Sparse Vectors (pgvector)
Add a sparsevec column to documents and populate it using bm25_document_to_svector:
5. Create an HNSW Sparse Vector Index
Use pgvector’s HNSW index with inner-product operator class sparsevec_ip_ops:
6. Run BM25 Queries
Convert the search query into a sparse vector with bm25_query_to_svector and order by <#> (negative inner product):
Using pg_bestmatch with pgvecto.rs (svector)
If you are using pgvecto.rs (v0.3.0+) instead of pgvector, omit the 'pgvector' style argument (or pass 'pgvecto.rs') and cast to svector:
3 - SQL Reference & Comparison
SQL Function Reference (bm25_catalog)
All pg_bestmatch functions reside in the bm25_catalog schema.
bm25_create
Creates BM25 corpus statistics for a specified table and text column.
table_name: Target table containing the text documents.column_name: Name of thetextcolumn to analyze.statistic_name: Unique identifier for this BM25 statistic set (must be a valid SQL identifier).b: BM25 document-length normalization factor (default0.75).k: BM25 term-frequency saturation parameter (default1.2).
bm25_refresh
Refreshes the materialized corpus statistics for statistic_name after document insertions, updates, or deletions.
bm25_drop
Drops all internal tables, materialized views, and triggers associated with statistic_name.
bm25_document_to_svector
Converts a document string into a BM25 term-frequency sparse vector representation (text) formatted for either pgvecto.rs (svector) or pgvector (sparsevec).
statistic_name: Name of the registered statistic set.passage: Input document text.style: Output format —'pgvector'(1-based indices) or'pgvecto.rs'(0-based indices).
bm25_query_to_svector
Converts a search query string into an IDF-weighted sparse vector representation (text).
tokenize
Tokenizes an input string into an array of WordPiece subword tokens using the built-in bert-base-uncased vocabulary.
Comparison: pg_bestmatch vs vchord_bm25
Both pg_bestmatch and vchord_bm25 were created by the TensorChord team to bring Okapi BM25 ranking into PostgreSQL, but they use fundamentally different architectures:
| Dimension | pg_bestmatch (pg_bestmatch.rs) |
vchord_bm25 + pg_tokenizer |
|---|---|---|
| Core Mechanism | Projects BM25 TF/IDF weights into sparse vectors (sparsevec / svector) |
Native inverted index with Block-WeakAnd (WAND) Top-K pruning |
| Index Type | Relies on pgvector or pgvecto.rs HNSW sparse index |
Dedicated USING bm25 (vec bm25_ops) index access method |
| Search Accuracy | Approximate (HNSW graph traversal over sparse vectors) | 100% Exact Top-K BM25 scoring |
| Tokenization | Fixed bert-base-uncased WordPiece (30,522 dims) |
Pluggable pg_tokenizer (Unicode, Jieba, Lindera, HuggingFace, Snowball, custom corpus-trained models) |
| Recommended Use | Lightweight experiments when you already only have pgvector HNSW |
Production full-text and hybrid search |