nestonexStart a project
NESTONEX PLANNING NOTES

RAG without a vector database: when a simpler store is enough

A vector database is the default first purchase on most RAG projects and frequently the least necessary one. Retrieval quality is governed by chunking and ranking, both independent of where the vectors live. This is how to tell when the store is genuinely the constraint.

At small to moderate scale you do not need one. With L2-normalised embeddings, similarity is a dot product and retrieval is one matrix operation over an in-process array or a Postgres column. A dedicated vector database earns its place when the corpus outgrows memory, needs concurrent writers, or needs filtered search at speed.

Why a plain array is often enough

Cosine similarity divides the dot product of two vectors by the product of their magnitudes. Normalise every vector to unit length when you store it and both magnitudes are one, so the division does nothing and similarity collapses to a dot product.

A dot product against every stored vector is a single matrix multiplication. For tens of thousands of chunks that runs in milliseconds in-process, with no service to deploy, no index to tune, and no network hop between your code and your data.

That is not a compromise for a small corpus. It is exact rather than approximate, one dependency lighter, and considerably easier to reason about when retrieval starts returning something odd.

  • Normalise at write time so similarity is a dot product
  • An exact scan of tens of thousands of chunks is milliseconds
  • No index tuning, no extra service, no network hop

Where Postgres fits

If you already run Postgres, storing embeddings in it is usually the right middle step. You get persistence, transactions, backups and the ability to filter on ordinary columns in the same query — the last of which is the thing hand-rolled stores handle badly.

Filtered search is the requirement that catches teams out. Restricting retrieval to one customer's documents, one date range or one document type is trivial in SQL and awkward when your store is an array you scan linearly. If your product is multi-tenant, you need this on day one rather than at scale.

Extensions add approximate indexing when exact scans stop being fast enough, which means the migration from exact to approximate happens inside a database you already operate rather than as a new service with its own failure modes.

The thresholds that actually force the decision

Memory comes first. Vectors are floats, and a few hundred thousand chunks at typical dimensions is real memory you probably do not want resident in an application process.

Concurrency comes second. An in-process store belongs to one process. The moment you need several writers, or several machines reading a shared index, the store has to move outside the process.

Latency under filtered load comes third, and it usually arrives before raw size does. An exact scan is linear, so it degrades predictably — which is fine until a filtered query has to scan everything before filtering.

Notice that none of these thresholds is about answer quality. They are about scale and operations, which is exactly why reaching for a vector database to fix bad answers does not work.

What actually decides retrieval quality

Chunking. How the source is split decides what can be retrieved at all. Splitting on word and section boundaries with overlap keeps a fact that straddles a boundary retrievable; splitting on a character count guarantees that some of them are not.

Ranking. Semantic similarity finds passages that mean something similar; keyword matching finds passages containing the exact term. Identifiers, negation and rare domain terms defeat the first and are trivial for the second, which is why hybrid ranking usually wins.

And the baseline nobody builds. Implement TF-IDF keyword retrieval alongside the embeddings and compare on your own corpus. If keyword matching is within a few points, the embeddings are buying complexity rather than relevance — and finding that out costs an afternoon.

What we did, and what we would do again

We built a RAG pipeline from first principles as an R&D project, deliberately without a managed service, so every decision stayed visible: word-boundary chunking, one embedder interface hiding both local sentence-transformers and a TF-IDF baseline, normalised vectors, and a hand-rolled in-process store. It runs entirely offline.

The TF-IDF backend is the part we would insist on again. It answers the question most retrieval projects skip, which is whether semantic embeddings are actually beating keyword matching here.

For a client product we would usually start with embeddings in Postgres rather than in memory, because filtered search and persistence arrive early in any real product. The reasoning is the same: pick the simplest store that meets the constraints you can name, and move when a constraint actually binds.

COMMON QUESTIONS /

Questions people also ask

How many documents before you need a vector database?

There is no fixed number. The practical triggers are vectors no longer fitting comfortably in memory, needing concurrent writers, or needing fast filtered search. Tens of thousands of chunks are comfortable in process; millions are not, and filtered search usually forces the decision before raw size does.

Will a vector database improve my answers?

Rarely on its own. It improves speed, scale and operational robustness, not relevance. Answer quality comes from chunking, what you embed and how you rank. If retrieval returns the wrong passages, moving them to a different store returns the same wrong passages faster.

Is pgvector good enough for production?

For a great many products, yes, and it has the advantage of living in a database you already back up, monitor and know how to operate. The case for a dedicated service is scale, specialised index types, or a retrieval workload heavy enough to warrant its own capacity planning.

Can you change embedding models later?

Yes, but it is a migration rather than a setting. Vectors from two different models occupy different spaces and are not comparable, so changing the model means re-embedding the whole corpus. Budget for it, and keep the source text where you can re-run it.

THE PROOF /

Where we have actually done this.