Skip to content
~/benhattab
Back to notes
·3 min read

Why I keep my embeddings inside Postgres

On Synka Sphere I chose pgvector over a dedicated vector database. It is not a performance argument — it is about consistency and how many systems you have to back up.

RAGPostgreSQLpgvectorArchitecture

When you build RAG, the reflex is to add a vector database: Qdrant, Pinecone, Weaviate. They are excellent, and I used Qdrant on an OCR pipeline with very good results.

On Synka Sphere I nevertheless kept the embeddings in PostgreSQL with pgvector. Here is the reasoning, because the answer is not "it is faster" — it is not always faster.

What a second system actually costs

A separate vector database is not just one more service in docker-compose. It is:

  • a second backup to schedule, encrypt and — above all — test restoring;
  • a second system to monitor, patch and secure;
  • a consistency problem to solve by hand between two stores that share no transaction.

That last point is the real issue. When a document is deleted in Postgres, its vectors must disappear from the index. With no shared transaction there is a window — short, but real — in which the two systems disagree.

On a product that ingests its customers' internal documents, that window has a name: a deleted document still feeding an agent's answers.

Isolation matters more than latency

Synka Sphere is multi-tenant. One workspace's documents must never be reachable from another — including through semantic search.

With pgvector, that constraint is expressed in the same language as the rest of the domain:

SELECT id, content, embedding <=> :query AS distance
FROM document_chunks
WHERE workspace_id = :workspace   -- the same guarantee as everywhere else
ORDER BY distance
LIMIT 8;

It is an ordinary WHERE clause, subject to the same scopes, the same policies and the same tests as any other query. With an external system, isolation becomes a collection-naming convention — one more layer to get wrong, on exactly the subject where getting it wrong is unacceptable.

One transaction, not two writes

Ingesting a document becomes atomic:

DB::transaction(function () use ($document, $chunks) {
    $document->save();
    $document->chunks()->createMany($chunks);   // vectors included
});

If anything fails, everything rolls back. There is no state where the document is stored but not indexed, or the reverse. With two systems you only get that guarantee by writing compensation logic yourself — which is to say, by reimplementing transactions, worse.

When I will change my mind

This choice has a limit, and I would rather name it:

  • Past a few million chunks, pgvector's HNSW index needs serious tuning, and a dedicated store genuinely does perform better.
  • If vector load and relational load diverge, sharing one instance becomes a constraint rather than a simplification.
  • If metadata filtering gets very complex, specialised engines have better primitives for it.

None of those thresholds are anywhere close. And migrating to Qdrant later is bounded, predictable work: the business logic does not move, only the retrieval layer changes.

The general principle

Young architecture is not judged by the peak performance it reaches, but by how many things can break while you are still looking for customers.

Add a system when the pain of not having it becomes real — not when a blog post tells you it is best practice.

One database to back up, one to restore, one to secure. At this stage, that is what matters most.