Skip to content

This is the multi-page printable view of this section. .

Return to the regular view of this page.

pg_bestmatch Documentation

Generate Okapi BM25 sparse vectors inside PostgreSQL and run lexical similarity search over pgvector (sparsevec) or pgvecto.rs (svector).

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).

Successor Recommendation: vchord_bm25 + pg_tokenizer

pg_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_catalog and emits BM25-weighted sparse vectors for both documents and queries.
  • Native pgvector & pgvecto.rs Integration: Outputs standard sparsevec (pgvector 0.7.0+) or svector (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-uncased WordPiece vocabulary (30,522 dimensions) and a standalone tokenize(text) SQL function.

Documentation Sections

How pg_bestmatch converts BM25 scoring into sparse vector inner products, installation from packages or source, and quickstart setup.

Complete reference for bm25_create, bm25_refresh, bm25_drop, bm25_document_to_svector, bm25_query_to_svector, and tokenize.

1 - Getting Started

How pg_bestmatch converts BM25 scoring into sparse vector inner products, and how to install and enable the extension.

How Does pg_bestmatch Work?

Given a document DD and a query QQ tokenized into vocabulary terms q1,q2,…,qnq_1, q_2, \dots, q_n, the Okapi BM25 relevance score is:

BM25(D,Q)=∑i=1nIDF(qi)⋅f(qi,D)⋅(k1+1)f(qi,D)+k1⋅(1−b+b⋅∣D∣avgdl) \text{BM25}(D, Q) = \sum_{i=1}^{n} \text{IDF}(q_i) \cdot \frac{f(q_i, D) \cdot (k_1 + 1)}{f(q_i, D) + k_1 \cdot \left(1 - b + b \cdot \frac{|D|}{\text{avgdl}}\right)}

Notice that this sum is the inner product (dot product) of two sparse vectors over the vocabulary space VV (dimension ∣V∣=30,522|V| = 30{,}522 for bert-base-uncased):

  1. Document Sparse Vector v⃗D\vec{v}_D: For each unique token t∈Dt \in D, store the normalized term-frequency weight: vD[t]=f(t,D)⋅(k1+1)f(t,D)+k1⋅(1−b+b⋅∣D∣avgdl) v_D[t] = \frac{f(t, D) \cdot (k_1 + 1)}{f(t, D) + k_1 \cdot \left(1 - b + b \cdot \frac{|D|}{\text{avgdl}}\right)}
  2. Query Sparse Vector v⃗Q\vec{v}_Q: For each token qi∈Qq_i \in Q, store its inverse document frequency weight IDF(qi)\text{IDF}(q_i).

When you compute the inner product v⃗D⋅v⃗Q\vec{v}_D \cdot \vec{v}_Q using pgvector or pgvecto.rs’s <#> operator, PostgreSQL returns −BM25(D,Q)-\text{BM25}(D, Q) 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:

# Example for PostgreSQL 16 on x86_64
wget https://github.com/tensorchord/pg_bestmatch.rs/releases/download/v0.0.1/pg-bestmatch-pg16_0.0.1_amd64.deb
sudo dpkg -i pg-bestmatch-pg16_0.0.1_amd64.deb

Build from Source (cargo pgrx)

Ensure Rust and cargo-pgrx (0.12.0-alpha.1 or compatible) are installed:

git clone https://github.com/tensorchord/pg_bestmatch.rs.git
cd pg_bestmatch.rs
cargo pgrx install --release

Enable the Extension

Connect to PostgreSQL, create the extension, and add bm25_catalog to your search_path:

CREATE EXTENSION IF NOT EXISTS pg_bestmatch;
CREATE EXTENSION IF NOT EXISTS vector; -- or vectors for pgvecto.rs

SET search_path TO "$user", public, bm25_catalog;

Test tokenization with a single SQL query:

SELECT tokenize('PostgreSQL BM25 full-text search with pg_bestmatch');
--                 tokenize
-- -----------------------------------------
--  {postgresql,bm,##25,full,-,text,search,with,pg,_,best,##mat,##ch}

Continue to the Usage Guide for a complete end-to-end indexing and query workflow.

2 - Usage Guide

Step-by-step workflow for BM25 sparse vector indexing and retrieval with pg_bestmatch and pgvector or pgvecto.rs.

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

CREATE EXTENSION IF NOT EXISTS pg_bestmatch;
CREATE EXTENSION IF NOT EXISTS vector;

SET search_path TO "$user", public, bm25_catalog;

2. Create the Documents Table & Load Data

CREATE TABLE documents (
    id bigint PRIMARY KEY,
    passage text
);

INSERT INTO documents (id, passage) VALUES
  (1, 'PostgreSQL is an advanced open-source relational database system.'),
  (2, 'VectorChord brings disk-friendly RaBitQ vector search to PostgreSQL.'),
  (3, 'BM25 is a bag-of-words ranking function used in information retrieval.');

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):

SELECT bm25_create('documents', 'passage', 'documents_passage_bm25', 0.75, 1.2);

What bm25_create does under the hood:

  1. Creates materialized views and helper tables inside bm25_catalog to track term frequencies, document frequencies, and total document count.
  2. Installs a trigger on documents so newly inserted or deleted rows are tracked automatically.
Refreshing Corpus Statistics After Bulk Updates

When you load a large batch of new documents and want to recompute global IDF and average document length (avgdl), run:

SELECT bm25_refresh('documents_passage_bm25');

4. Materialize Document Sparse Vectors (pgvector)

Add a sparsevec column to documents and populate it using bm25_document_to_svector:

ALTER TABLE documents ADD COLUMN embedding sparsevec(30522);

UPDATE documents
SET embedding = bm25_document_to_svector(
    'documents_passage_bm25',
    passage,
    'pgvector'
)::sparsevec;

5. Create an HNSW Sparse Vector Index

Use pgvector’s HNSW index with inner-product operator class sparsevec_ip_ops:

CREATE INDEX documents_embedding_bm25_idx
ON documents
USING hnsw (embedding 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):

SELECT
    id,
    passage,
    (embedding <#> bm25_query_to_svector('documents_passage_bm25', 'PostgreSQL vector search', 'pgvector')::sparsevec) * -1 AS bm25_score
FROM documents
ORDER BY embedding <#> bm25_query_to_svector('documents_passage_bm25', 'PostgreSQL vector search', 'pgvector')::sparsevec
LIMIT 5;

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:

CREATE EXTENSION IF NOT EXISTS vectors;

ALTER TABLE documents ADD COLUMN embedding svector(30522);

UPDATE documents
SET embedding = bm25_document_to_svector('documents_passage_bm25', passage)::svector;

CREATE INDEX documents_embedding_svector_idx
ON documents
USING vectors (embedding svector_dot_ops);

SELECT id, passage
FROM documents
ORDER BY embedding <#> bm25_query_to_svector('documents_passage_bm25', 'PostgreSQL vector search')::svector
LIMIT 5;

3 - SQL Reference & Comparison

Complete SQL function reference for pg_bestmatch (bm25_catalog) and architectural comparison with vchord_bm25.

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.

bm25_create(
    table_name     regclass,
    column_name    text,
    statistic_name text,
    b              real DEFAULT 0.75,
    k              real DEFAULT 1.2
) RETURNS void
  • table_name: Target table containing the text documents.
  • column_name: Name of the text column to analyze.
  • statistic_name: Unique identifier for this BM25 statistic set (must be a valid SQL identifier).
  • b: BM25 document-length normalization factor (default 0.75).
  • k: BM25 term-frequency saturation parameter k1k_1 (default 1.2).

bm25_refresh

Refreshes the materialized corpus statistics for statistic_name after document insertions, updates, or deletions.

bm25_refresh(
    statistic_name text
) RETURNS void

bm25_drop

Drops all internal tables, materialized views, and triggers associated with statistic_name.

bm25_drop(
    statistic_name text
) RETURNS void

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).

bm25_document_to_svector(
    statistic_name text,
    passage        text,
    style          text DEFAULT 'pgvecto.rs'
) RETURNS text
  • 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).

bm25_query_to_svector(
    statistic_name text,
    query          text,
    style          text DEFAULT 'pgvecto.rs'
) RETURNS text

tokenize

Tokenizes an input string into an array of WordPiece subword tokens using the built-in bert-base-uncased vocabulary.

tokenize(
    input text
) RETURNS text[]

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