Self-hosted service
ChuckHend/pg_vectorize avatar
ChuckHend/pg_vectorize

pg_vectorize: Embedding Pipelines and Hybrid Search Inside Postgres

Full-text and semantic search on any Postgres

832 stars41 forksRustLicense varies

At a glance

What is it?
pg_vectorize wraps pgvector, pgmq and SentenceTransformers into two delivery modes: a standalone HTTP server for managed databases and a SQL extension for self-hosted Postgres. The interesting part is not the search itself but the job bookkeeping that keeps embeddings current.
Who is it for?
Adopt pg_vectorize if you already run Postgres with pgvector and want embedding generation, refresh-on-update and hybrid ranking handled by a job rather than by application code. Do not adopt it if your database is fully managed and you cannot run a separate service next to it, or if you need to audit the licence before shipping.
Can I use it commercially?
Not without permission. GitHub finds no licence file in the repository, and without a licence all rights are reserved by default: you may read the code but not reuse it. Check the README, or ask the authors, before using it.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The problem is not similarity search, it is keeping embeddings current

pgvector already answers the nearest-neighbour question. What it does not do is generate the vectors, notice when a source row changes, or re-embed the row after the change. In most RAG prototypes that work lands in application code: a script that walks a table, calls an embedding endpoint, writes to a vector column, and then a second script or a cron entry that tries to keep up with updates. pg_vectorize takes that second half and moves it into the database's neighbourhood. The README describes the project as automating "the transformation and orchestration of text to embeddings" and providing "hooks into the most popular LLMs". The unit of work is a job. You name a source table, the columns to embed, a primary key, a timestamp column, and a model. From that point the project generates embeddings for existing rows and, per the README, "continuously watches for updates or new data". That watching is the actual product. Search is the payoff, but the job is what you are adopting.

Two delivery modes and the constraint that decides between them

The repository ships the same capability in two shapes. The HTTP server mode runs as a standalone service that connects to Postgres and exposes a REST API, with POST /api/v1/table to create a job and GET /api/v1/search to query one. The extension mode installs into Postgres itself and exposes SQL functions such as vectorize.table() and vectorize.search(). The README's own selection rule is blunt: use the HTTP server when Postgres is managed, such as RDS or Cloud SQL, or when you cannot install extensions; use the extension when you self-host and can install them. The deciding factor is filesystem access to the Postgres installation, which the extension requires and which managed providers do not grant. The HTTP server path has its own requirement, stated plainly: pgvector must be available in the database. So pgvector is not optional in either mode. pg_vectorize orchestrates around it rather than replacing it. If pgvector cannot be installed where your data lives, neither mode applies.

What the job actually touches: pgvector, pgmq and a model server

The README names three dependencies and what each contributes. pgvector handles vector similarity search. pgmq handles "orchestration in background workers". SentenceTransformers supplies the embedding models, and the quick-start example uses sentence-transformers/all-MiniLM-L6-v2. That split tells you where the moving parts are. The queue is the mechanism that turns a table change into a re-embedding, which is why the project can claim continuous watching without asking you to write a trigger. The consequence is a second piece of infrastructure: pgmq stores state in Postgres, and a worker process has to be running to drain it. The docker compose quick start brings up Postgres, an embeddings server and the management API, so three processes in the local case. In production the same three exist, just distributed. This is not a library you import. It is a small system with a database, a queue and a model endpoint, and the failure modes follow from that shape rather than from the SQL.

Hybrid ranking is visible in the response, not just the docs

The search response in the README is more informative than most. A single query for "camping backpack" with limit=1 returns product_id 6, product_name "Backpack", a similarity_score of 0.6296013593673706, an fts_rank of 1, a semantic_rank of 1, and an rrf_score of 0.03278688524590164. Reciprocal rank fusion is therefore the combination method, and both a full-text rank and a semantic rank are computed before fusion. That matters for evaluation. Because the individual ranks are returned alongside the fused score, you can see which retriever placed a result where, instead of treating the ranking as a black box. It also means the full-text path is not a fallback for when embeddings fail; it runs as a first-class retriever on every query. If your corpus is dominated by exact identifiers, SKUs or error codes, the fts_rank column is the one to inspect before you assume semantic search is doing the work.

Getting it running: compose, example data, then a job

The README's quick start is three steps. First, docker compose up -d, which the README says runs Postgres, the embeddings server and the management API. Second, optionally load the sample dataset with psql postgres://postgres:postgres@localhost:5432/postgres -f server/sql/example.sql, which the README shows producing CREATE TABLE and INSERT 0 40. Third, POST a job to http://localhost:8080/api/v1/table with a JSON body containing job_name, src_table, src_schema, src_columns, primary_key, update_time_col and model. The example returns an id, for instance 16b80184-2e8e-4ee6-b7e2-1a068ff4b314. Search is a GET to /api/v1/search with job_name, query and limit as query parameters. Two config details are easy to miss. update_time_col is what the watcher uses to find changed rows, so a table without a maintained timestamp column is a poor fit. src_columns is a list, and the example passes two columns, which implies the text sent to the model is composed from more than one field. The README does not show how that composition works, so check the API documentation before assuming a separator or weighting.

Where it is the wrong tool

The clearest boundary is the one the README draws itself. If your Postgres is managed and you cannot run a service alongside it, the extension mode is unavailable, and if pgvector is not present in that managed database, the HTTP server mode is unavailable too. Both conditions are common on locked-down platforms, and neither is something the project can work around. The second boundary is operational. A job that watches for updates depends on the worker staying alive; a stopped worker means embeddings silently fall behind the source table while the API keeps answering queries from stale vectors. Nothing in the supplied material describes alerting on queue depth or lag, so that monitoring is yours to build. Third, the model choice is a first-class parameter, and changing it is not a no-op: a new model produces vectors in a different space, so existing embeddings for that job become incomparable. The README does not document a supported migration path for that case. Fourth, and simplest, if your corpus is a few thousand rows and changes rarely, a batch script that embeds everything on a schedule is less machinery than a queue, a worker and a model server.

The alternative worth comparing: pgvector plus your own pipeline

The honest comparison is not another vector database. It is pgvector on its own with an application-side embedding pipeline, which is the arrangement pg_vectorize is built to replace. pgvector gives you the vector column, the index and the distance operators, and it is already a dependency here. The difference in approach is where the orchestration lives. With pgvector alone, you own the loop: detect changed rows, batch them, call the model, write vectors back, handle retries, and decide what to do when the model endpoint is down. With pg_vectorize, that loop is a job registered through vectorize.table() or POST /api/v1/table, and pgmq holds the pending work. The trade is control for convention. A hand-rolled pipeline can batch however you like, target any embedding provider, and log whatever your observability stack expects. pg_vectorize gives you a fixed job shape with a defined set of fields and, per the README, hooks into popular LLMs. If your embedding provider is unusual or your batching requirements are specific, the convention may cost more than it saves.

Version cadence, licence and what to verify before adopting

The release history shows v0.27.0, v0.26.2 and v0.26.1 across roughly four months, with the 0.x major version signalling that the API is still moving. Anyone pinning pg_vectorize should pin the server and extension versions together, since the two modes expose the same concepts through different surfaces and drift between them would be easy to miss. The repository metadata supplied here does not state a licence, and the README does not mention one either. That is not a detail to defer: the project depends on pgvector, pgmq and SentenceTransformers, each with its own terms, and the absence of a stated licence in the metadata means you cannot treat the terms as known. Read the LICENSE file in the repository root and confirm it before the code reaches a production database. On maintenance cost, the recurring work is the worker process, the model server, and the pgmq tables growing in the same database you are querying. None of that is unusual for this class of tool, but all of it is infrastructure you now operate.

Editorial conclusion

Adopt pg_vectorize if you already run Postgres with pgvector and want embedding generation, refresh-on-update and hybrid ranking handled by a job rather than by application code. Do not adopt it if your database is fully managed and you cannot run a separate service next to it, or if you need to audit the licence before shipping. Verify first that pgvector is installable in your target database, that a background worker can reach both Postgres and the embedding model, and that the repository's licence file matches what your legal review requires, since the metadata supplied here does not state one.

Official sources

  1. ChuckHend/pg_vectorize on GitHub
  2. Issues
  3. Project website
  4. README
  5. Releases
Community notes

Community notes