A PostgreSQL extension that provides Elasticsearch-quality full-text search using Tantivy (BM25) directly in your database.
@@@ operator - Natural SQL syntax for full-text search in WHERE clauses-- Create extension
CREATE EXTENSION pdb;
-- Create a table with text you want to search
CREATE TABLE articles (
id BIGSERIAL PRIMARY KEY,
title TEXT,
body TEXT
);
-- Create a BM25 index on the body column
SELECT create_bm25_index('articles', 'body');
-- Insert some data (index updates automatically)
INSERT INTO articles (title, body) VALUES
('Rust Guide', 'Rust is a systems programming language'),
('Python Tutorial', 'Python is great for data science');
-- Search using @@@ operator
SELECT pdb_search_init('articles', 'body', 'rust programming');
SELECT id, title, pdb_operator_score(id) as score
FROM articles
WHERE body @@@ 'rust'
AND pdb_operator_score(id) > 0 -- filter to actual matches
ORDER BY score DESC;
# Install Rust + pgrx
cargo install cargo-pgrx --version 0.16.1
cargo pgrx init --pg17 $(which pg_config)
# Build and run
cargo pgrx run pg17
# In psql
CREATE EXTENSION pdb;
# Build image
docker build -t pdb-postgres .
# Run container
docker compose up -d
# Connect
psql -h localhost -p 5432 -U pdb -d pdb_dev
# Setup local cluster (kind + kubectl)
./scripts/k8s-local-setup.sh
# Build and deploy
./scripts/k8s-deploy.sh --build
# Connect
kubectl port-forward -n pdb svc/pdb-postgres 5432:5432
psql -h localhost -U pdb_admin -d pdb
| Function | Description |
|---|---|
create_bm25_index(table, column) | Create index with auto-sync trigger |
drop_bm25_index(table, column) | Remove index and trigger |
bm25_search(table, column, query) | Search, returns (pk, score) |
bm25_search_limit(table, column, query, n) | Search with result limit |
bm25_index_info(table, column) | Index stats (doc count, size) |
@@@ OperatorThe @@@ operator enables natural SQL search syntax in WHERE clauses:
-- Step 1: Initialize search context
SELECT pdb_search_init('articles', 'body', 'rust programming');
-- Step 2: Use @@@ in WHERE clause with scoring
SELECT id, title, pdb_operator_score(id) as score
FROM articles
WHERE body @@@ 'rust'
AND pdb_operator_score(id) > 0
ORDER BY score DESC;
How it works:
pdb_search_init() executes the search and caches matching PKs@@@ filters rows with non-null contentpdb_operator_score(pk) returns the BM25 score (0 = no match)Operator Functions:
| Function | Description |
|---|---|
pdb_search_init(table, column, query) | Initialize search context |
pdb_operator_score(pk) | Get score for row (0 = no match) |
body @@@ 'query' | Filter in WHERE clause |
Standard Tantivy query syntax:
SELECT pdb_search_init('articles', 'body', 'rust AND fast'); -- Boolean
SELECT pdb_search_init('articles', 'body', '"rust programming"'); -- Phrase
SELECT pdb_search_init('articles', 'body', 'rust -python'); -- Exclusion
SELECT pdb_search_init('articles', 'body', 'rust^2 python'); -- Boost
If you prefer JOINs over the operator pattern:
SELECT a.id, a.title, s.score
FROM articles a
JOIN bm25_search('articles', 'body', 'rust') s ON a.id = s.pk
ORDER BY s.score DESC;
For simpler inline filtering without the operator:
SELECT id, title, pdb_score('articles', 'body', 'rust', id) as score
FROM articles
WHERE pdb_match('articles', 'body', 'rust', id)
AND author = 'John'
ORDER BY score DESC;
Architecture:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ PostgreSQL │◄─────►│ pgrx │◄─────►│ Tantivy │
│ Table │ │ Extension │ │ Indexes │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
│ │ │
▼ ▼ ▼
CRUD ops Triggers BM25 Search
(buffered) (on commit)
Key Implementation Details:
$PGDATA/pdb_indexes/<table>_<column>/pk (i64) and content (text)writer.rs, flushed on PostgreSQL COMMIT via register_xact_callbackpdb_sync_trigger() fires on INSERT/UPDATE/DELETE, extracts PK from tupleTEXT typepdb/
├── src/
│ ├── index.rs # create/drop/refresh indexes
│ ├── search.rs # bm25_search functions
│ ├── trigger.rs # auto-sync trigger
│ ├── writer.rs # transaction-safe buffered writes
│ ├── operator.rs # inline search (pdb_match, pdb_score)
│ └── utils.rs # index info
├── k8s/ # Kubernetes manifests (Kustomize)
│ ├── base/ # Common resources
│ └── overlays/ # dev, aws configs
├── terraform/ # AWS EC2 + k3s deployment
├── tests/ # SQL test scripts
├── Dockerfile # Multi-stage build
└── docker-compose.yml
Kubernetes (k8s/):
Terraform (terraform/):
# Run E2E tests in psql
\i tests/test_trigger_e2e.sql # Transaction safety
\i tests/test_inline_search.sql # pdb_match/pdb_score
\i tests/test_operator.sql # @@@ operator
\i tests/test_complete_flow.sql # Full workflow
MIT
11 commits
Rust
53.8%
Shell
18.1%
HCL
15.9%
PLpgSQL
9.1%
Dockerfile
3.1%
A PostgreSQL extension that provides Elasticsearch-quality full-text search using Tantivy (BM25) directly in your database.
@@@ operator - Natural SQL syntax for full-text search in WHERE clauses-- Create extension
CREATE EXTENSION pdb;
-- Create a table with text you want to search
CREATE TABLE articles (
id BIGSERIAL PRIMARY KEY,
title TEXT,
body TEXT
);
-- Create a BM25 index on the body column
SELECT create_bm25_index('articles', 'body');
-- Insert some data (index updates automatically)
INSERT INTO articles (title, body) VALUES
('Rust Guide', 'Rust is a systems programming language'),
('Python Tutorial', 'Python is great for data science');
-- Search using @@@ operator
SELECT pdb_search_init('articles', 'body', 'rust programming');
SELECT id, title, pdb_operator_score(id) as score
FROM articles
WHERE body @@@ 'rust'
AND pdb_operator_score(id) > 0 -- filter to actual matches
ORDER BY score DESC;
# Install Rust + pgrx
cargo install cargo-pgrx --version 0.16.1
cargo pgrx init --pg17 $(which pg_config)
# Build and run
cargo pgrx run pg17
# In psql
CREATE EXTENSION pdb;
# Build image
docker build -t pdb-postgres .
# Run container
docker compose up -d
# Connect
psql -h localhost -p 5432 -U pdb -d pdb_dev
# Setup local cluster (kind + kubectl)
./scripts/k8s-local-setup.sh
# Build and deploy
./scripts/k8s-deploy.sh --build
# Connect
kubectl port-forward -n pdb svc/pdb-postgres 5432:5432
psql -h localhost -U pdb_admin -d pdb
| Function | Description |
|---|---|
create_bm25_index(table, column) | Create index with auto-sync trigger |
drop_bm25_index(table, column) | Remove index and trigger |
bm25_search(table, column, query) | Search, returns (pk, score) |
bm25_search_limit(table, column, query, n) | Search with result limit |
bm25_index_info(table, column) | Index stats (doc count, size) |
@@@ OperatorThe @@@ operator enables natural SQL search syntax in WHERE clauses:
-- Step 1: Initialize search context
SELECT pdb_search_init('articles', 'body', 'rust programming');
-- Step 2: Use @@@ in WHERE clause with scoring
SELECT id, title, pdb_operator_score(id) as score
FROM articles
WHERE body @@@ 'rust'
AND pdb_operator_score(id) > 0
ORDER BY score DESC;
How it works:
pdb_search_init() executes the search and caches matching PKs@@@ filters rows with non-null contentpdb_operator_score(pk) returns the BM25 score (0 = no match)Operator Functions:
| Function | Description |
|---|---|
pdb_search_init(table, column, query) | Initialize search context |
pdb_operator_score(pk) | Get score for row (0 = no match) |
body @@@ 'query' | Filter in WHERE clause |
Standard Tantivy query syntax:
SELECT pdb_search_init('articles', 'body', 'rust AND fast'); -- Boolean
SELECT pdb_search_init('articles', 'body', '"rust programming"'); -- Phrase
SELECT pdb_search_init('articles', 'body', 'rust -python'); -- Exclusion
SELECT pdb_search_init('articles', 'body', 'rust^2 python'); -- Boost
If you prefer JOINs over the operator pattern:
SELECT a.id, a.title, s.score
FROM articles a
JOIN bm25_search('articles', 'body', 'rust') s ON a.id = s.pk
ORDER BY s.score DESC;
For simpler inline filtering without the operator:
SELECT id, title, pdb_score('articles', 'body', 'rust', id) as score
FROM articles
WHERE pdb_match('articles', 'body', 'rust', id)
AND author = 'John'
ORDER BY score DESC;
Architecture:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ PostgreSQL │◄─────►│ pgrx │◄─────►│ Tantivy │
│ Table │ │ Extension │ │ Indexes │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
│ │ │
▼ ▼ ▼
CRUD ops Triggers BM25 Search
(buffered) (on commit)
Key Implementation Details:
$PGDATA/pdb_indexes/<table>_<column>/pk (i64) and content (text)writer.rs, flushed on PostgreSQL COMMIT via register_xact_callbackpdb_sync_trigger() fires on INSERT/UPDATE/DELETE, extracts PK from tupleTEXT typepdb/
├── src/
│ ├── index.rs # create/drop/refresh indexes
│ ├── search.rs # bm25_search functions
│ ├── trigger.rs # auto-sync trigger
│ ├── writer.rs # transaction-safe buffered writes
│ ├── operator.rs # inline search (pdb_match, pdb_score)
│ └── utils.rs # index info
├── k8s/ # Kubernetes manifests (Kustomize)
│ ├── base/ # Common resources
│ └── overlays/ # dev, aws configs
├── terraform/ # AWS EC2 + k3s deployment
├── tests/ # SQL test scripts
├── Dockerfile # Multi-stage build
└── docker-compose.yml
Kubernetes (k8s/):
Terraform (terraform/):
# Run E2E tests in psql
\i tests/test_trigger_e2e.sql # Transaction safety
\i tests/test_inline_search.sql # pdb_match/pdb_score
\i tests/test_operator.sql # @@@ operator
\i tests/test_complete_flow.sql # Full workflow
MIT
11 commits
Rust
53.8%
Shell
18.1%
HCL
15.9%
PLpgSQL
9.1%
Dockerfile
3.1%