pghybrid
Hybrid search on the Postgres you already have.
Vector similarity + full-text search, combined by Reciprocal Rank Fusion, on plain
pgvector. No pg_search, no VectorChord, no Elasticsearch, no vector database, no
extension you need superuser to install.
pip install pghybrid
Why
Hybrid search on Postgres is a solved problem if you can install a C extension.
pg_search, VectorChord and pg_textsearch are all excellent, and all of them are
extensions — and on managed Postgres you usually cannot install extensions at all.
pgvector is available almost everywhere. BM25 extensions are available almost
nowhere: not on RDS, Aurora, Cloud SQL, Azure Database, Supabase or Heroku, and
removed from Neon for new projects in March 2026.
This is not a claim to search better than ParadeDB — pg_search's BM25 genuinely beats
ts_rank_cd, and if you can install it you probably should. It is search you can
actually install.
The case it exists for
A contract. Someone asks "renewal notice period". The clause that answers it says "sixty days written notice prior to the anniversary date" — it never uses the word renewal. Vector search puts a plausible-but-wrong clause first. Keyword search puts the clause that uses all three words and answers none of them first. The right answer is second on both signals and first on neither, which is exactly what rank fusion is for.
Use it
from pghybrid import Config, HybridSearch
search = HybridSearch(
Config(table="chunks", text_column="content", vector_column="embedding",
tsvector_column="fts", extra_columns=["title"], paramstyle="pyformat"),
execute=lambda sql, params: conn.execute(sql, params).fetchall(),
)
for row in search.search("renewal notice period", embedding=query_vector, limit=10):
print(row.score, row.matched_by, row.get("title"))
You pass the embedding in. pghybrid never calls a model, so it works with OpenAI,
Cohere, Voyage, a local sentence-transformer or anything else, and needs no API key. It
never opens a connection either — it generates SQL and hands it to the driver you already
use. for_psycopg, for_sqlalchemy, for_asyncpg and for_django set the placeholder
style for you.
row.matched_by tells you which signal found each row — both, vector or text —
which is usually the first thing you want when a search returns the wrong thing.
Read the statement instead of running it
sql, params = search.build_sql("renewal notice period", query_vector, limit=10)
It is one query with a candidate CTE per signal, fused by RRF. Nothing is hidden, and
pghybrid sql prints it without a database at all.
Command line
pghybrid init --dsn "$DATABASE_URL" --table chunks # inspects, writes the migration
pghybrid doctor --dsn "$DATABASE_URL" --table chunks # measured recall, read-only
doctor measures recall@k against exact search, sweeps ef_search/probes, catches
queries that silently fall back to a sequential scan, and reports a tsvector that has
stopped matching its text — a failure that returns wrong answers without erroring.
explain decomposes a single result set: both ranks, both raw scores, each signal's
contribution, and the near-miss band just below your cut-off.
Also in TypeScript
npm install pghybrid generates byte-identical
SQL, checked on every commit rather than assumed.
Full documentation, the reasoning, and copy-and-paste SQL: github.com/pavangupta352/pghybrid
MIT © Pavan Gupta
Release files for pghybrid 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pghybrid-0.1.0.tar.gz | 355.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pghybrid-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 442.8 kB
Release files / pghybrid-0.1.0.tar.gz
| Download URL | pghybrid-0.1.0.tar.gz |
|---|---|
| Size | 355.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1033d132983af18895033d406fb02f4b4da6e5464cb8c84ad5f8b674aab1138e
|
|
BLAKE2b-256 checksum How to use checksums |
744af5d0534531e7ea94671f64f26c182abc97dabb71bbe0781dd2aacc52e9a3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 31, 2026.
Transparency logRelease files / pghybrid-0.1.0-py3-none-any.whl
| Download URL | pghybrid-0.1.0-py3-none-any.whl |
|---|---|
| Size | 86.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d501cf0eaa0d7965b6c7a6f8bdadcd314a45870765102318d53f3dbd44700cf4
|
|
BLAKE2b-256 checksum How to use checksums |
c3176faafa4b4b3637640d02e18589906606eda574f8699e330a5e3f5795c09f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 31, 2026.
Transparency log