Qora API — AI API Gateway for Developers

AI API Gateway for Developers

One clear API workflow for your apps, scripts and automations.

How to Choose a Vector Database for RAG

Cover graphic reading Choosing a Vector Database — for RAG, search and recommendations, with pills for Vector DB, RAG and Selection

Choose a vector database by matching four things to your workload: how selective your metadata filters are, whether you need keyword-plus-vector hybrid search, how much recall you will trade for latency, and whether you want to operate the index yourself. Everything else — brand, benchmark charts, pricing pages — is downstream of those four.

What a vector database actually does

A vector database stores a high-dimensional float array per item, a small metadata payload, and an index that answers one query: give me the k items nearest to this query vector. If the concepts behind those vectors are new, start with our guide to embeddings and RAG.

Exact nearest-neighbour search is a full scan. At 10M vectors × 1536 dimensions one query is roughly 15 billion multiply-adds — hundreds of milliseconds on a CPU, scaling linearly with corpus size. So every production vector database implements approximate nearest-neighbour (ANN) search: it visits a fraction of the corpus and returns probably the true top-k. The discipline is how small that fraction can get before recall collapses.

  • HNSW (graph). A multi-layer navigable small-world graph. Queries descend greedily from a sparse top layer to a dense bottom layer while holding a candidate list of size ef_search. Query time is roughly logarithmic in corpus size, recall beats every other mainstream index at low latency, and inserts are incremental. Costs: graph and full-precision vectors live in RAM, deletes are tombstoned rather than reclaimed, and every hop is a random access, which makes sharding awkward.
  • IVF (clustering). k-means over a training sample yields nlist centroids; each vector joins its nearest cell, and a query scans only the nprobe nearest cells. It composes naturally with product quantization, making it the classic choice for very large, mostly static corpora. Costs: it needs a representative training sample, recall depends heavily on nprobe, and inserts drift the centroids, forcing periodic retraining.
  • Flat (no index). Brute force with SIMD. Exact, zero tuning, fast below a few hundred thousand vectors.

Recall, latency, and memory form a triangle: improve any two at the expense of the third. Raising ef_search buys recall with latency. Raising m buys recall with memory. Quantizing buys memory with recall. No configuration wins all three, and a vendor claiming otherwise is describing a benchmark dataset, not your data.

The selection axes

Seven axes decide almost every real decision. Score your workload on each before you look at a vendor.

AxisWhat to checkDecision signal
Hosting modelManaged, self-hosted, or embeddedManaged wins once ops hours exceed the price delta; self-host when data cannot leave your VPC
Hybrid searchNative sparse+dense fusion, or a BM25 sidecarRequired if users search by IDs, error codes, or function names
Metadata filteringPre-filter vs post-filter, field types, cardinalityPre-filter is mandatory when a filter keeps under ~10% of the corpus
Scale ceilingVectors per node, sharding, RAM per vectorPast ~50M vectors at high QPS you need sharded or disk-based indexes
Cost modelRAM-hour, per-vector-month, or per-queryPer-query suits spiky traffic; RAM-hour punishes a large idle index
Ops burdenBackups, re-index on upgrade, on-callEngineer-hours per month × loaded rate, versus the invoice
Multi-tenancyNamespaces, partitions, isolationShared collection plus tenant filter is cheapest until one tenant dominates

Metadata filtering and hybrid search

Filtering is where vector search quietly breaks. Post-filtering retrieves the top-k by distance, then discards rows that fail the filter. That is correct only when the filter keeps most of the corpus: to return k results under a filter with selectivity s you must over-fetch roughly k/s candidates, so at s = 0.01 and k = 10 that is 1,000 candidates per query — and recall is still poor, because the traversal never visited the region where the matching vectors live. You silently get fewer than k results. This is the most common cause of “our RAG got worse in production but the index metrics look fine”.

Pre-filtering restricts the candidate set first. Naive pre-filtering degenerates into a brute-force scan over the matching subset — fine when that subset is small, because scanning 1% of 10M vectors exactly beats searching all of them approximately. Production engines combine both, using the filter to seed graph entry points or to select a partition-scoped index. Two rules follow: keep filterable metadata in the vector store’s payload, not a side table you join afterwards, since a join after the ANN stage means you already paid the recall loss; and partition on the dimension you always filter on.

Hybrid search matters for a different reason: dense embeddings are bad at exact tokens. A part number, a function name, an error code like ERR_4021, or a negation lives in the sparse signal, and BM25 or a learned sparse representation catches it. Technical queries are full of literal identifiers that embeddings blur into their neighbours.

Fuse the two lists with Reciprocal Rank Fusion, not a weighted score sum. RRF operates on ranks, so BM25’s unbounded scores and cosine’s bounded similarities never have to be normalized against each other — a normalization bug that silently makes hybrid search perform worse than dense-only.

def hybrid_search(query, k=10, alpha=0.7, tenant=None):
    """Fuse dense and sparse retrieval by rank, not by score."""
    qvec = embed(query)
    dense = vec_index.search(qvec, k=k * 5, filter={"tenant": tenant})
    sparse = bm25.search(query, k=k * 5, filter={"tenant": tenant})

    K = 60                                    # RRF constant (Cormack et al.)
    fused = {}
    for rank, hit in enumerate(dense):
        fused[hit.id] = fused.get(hit.id, 0.0) + alpha / (K + rank)
    for rank, hit in enumerate(sparse):
        fused[hit.id] = fused.get(hit.id, 0.0) + (1 - alpha) / (K + rank)

    candidates = sorted(fused.items(), key=lambda kv: -kv[1])[:k * 5]
    return rerank(query, candidates, top_k=k)  # cross-encoder rescoring

Then rerank the fused candidates with a cross-encoder — the largest relevance gain in the stack, because it scores each (query, document) pair jointly instead of comparing two independently produced vectors. Embeddings and reranking can come from one OpenAI-compatible endpoint: qoraapi.com serves both from a single API key, which also keeps your index and reranker on the same embedding model version. For the pipeline around these pieces, see our guide to production RAG.

Index types and trade-offs

Sizes below are for 1M vectors at 1536 dimensions in fp32, excluding IDs and payload.

IndexRAM / 1M vectorsBuildQuery knobRecall@10Use when
Flat (exact)~6.1 GBNoneNone1.00Under a few hundred thousand vectors, or as ground truth for recall
HNSW, fp32~6.3 GBMinutes to hoursef_search0.95–0.99Best recall at low latency, incremental writes, up to tens of millions of vectors
HNSW + int8~1.7 GBMinutesef_search0.93–0.98RAM is the binding constraint and a small recall loss is acceptable
IVF-PQ (16 B/vec)~0.1 GBTraining + rebuildsnprobe0.70–0.92100M+ vectors, mostly static, disk-friendly
DiskANN~0.2 GB + SSDHoursSearch list size0.90–0.97Corpus far exceeds RAM, latency budget in tens of ms

HNSW’s three parameters: m is graph degree per node — 16 is a sane default, 32–64 for high-dimensional data or high recall targets; memory and build time grow linearly with it, but query latency barely moves, because each hop scans only a few more neighbours. Returns diminish above 64. ef_construction is the build-time candidate list (100–200 normal, 400+ for hard datasets). ef_search is the query-time candidate list — the knob you tune per query class.

The asymmetry matters: m and ef_construction are baked into the graph, so changing them means a full rebuild, while ef_search is dynamic. The recall curve is concave — ef_search 16 to 64 usually buys most of the available recall, while 256 to 1024 buys a fraction of a point and roughly doubles p99.

-- pgvector: build HNSW with explicit params instead of defaults.
-- m and ef_construction are baked into the graph: changing them = full rebuild.
CREATE INDEX CONCURRENTLY docs_emb_hnsw
  ON docs USING hnsw (embedding vector_cosine_ops)
  WITH (m = 32, ef_construction = 200);

SET hnsw.ef_search = 120;   -- query-time only; raise until recall@10 plateaus

-- Recent pgvector: keep traversing until k rows survive a selective filter,
-- instead of collecting k candidates and discarding most of them.
SET hnsw.iterative_scan = strict_order;

SELECT id, 1 - (embedding <=> :query_vec) AS cosine_score
FROM docs
WHERE tenant_id = :tenant AND status = 'published'
  AND embedding <=> :query_vec < 0.35   -- distance ceiling = relevance floor
ORDER BY embedding <=> :query_vec
LIMIT 10;

When recall drops it is almost always one of five things: quantization, which discards the low-order components separating near-duplicates (fix: oversample 4–10× from the compressed index, then rescore against full-precision vectors kept on disk); stale IVF centroids after bulk inserts; a selective filter the graph never reaches; distance-metric mismatch, such as indexing cosine but querying unnormalized vectors; and model skew, where query vectors come from a different embedding version than the documents.

Measure recall properly: take 1,000–5,000 real queries, compute exact top-k with a flat index, then compare. Report recall@k alongside p50 and p99 at that recall. A lone “95% recall” with no k, no dataset, and no latency is not information.

Ops concerns that decide the architecture

Re-embedding on model change. Swapping the embedding model invalidates every vector — vectors from two models occupy different spaces, so you cannot mix or reuse them. A model upgrade becomes a full-corpus batch job whose cost scales with corpus tokens, not query volume. Two habits make it survivable: keep the source text of every chunk, and store embedding_model, embedding_dim, and a chunk_hash in the payload so a re-embed skips unchanged chunks. For the cost side, see our guide to reduce AI API costs.

Versioned collections and alias swaps. Never mutate vectors in place during a migration. Create docs_v2, backfill, run your retrieval eval against both, then swap an alias so cutover is atomic and rollback is one line. The same pattern handles index parameter changes, since m is fixed at build time.

Backups. A vector index is usually rebuildable from source text plus model version — but only if you kept both. Snapshot the payload as the source of truth; snapshotting the graph is a restore-speed optimization, not a durability strategy. Test a restore, including rebuild time for a large HNSW graph.

Multi-tenancy comes in three patterns, in increasing isolation: a shared collection with a tenant_id filter (cheapest, but every query is a filtered ANN query and inherits the selectivity problem above); a namespace per tenant (the index is already scoped, so filtering is free, at the cost of per-tenant overhead and poor cache locality for tiny tenants); and a collection per tenant (strongest isolation, right for a few dozen large tenants, wrong for tens of thousands of small ones). Promote any tenant past ~10% of total vectors.

Capacity. RAM is the binding constraint, and a rebuild needs roughly double the steady-state memory. Per-vector-month pricing is comfortable for a demo and punishing for an index that grows while query volume stays flat; RAM-hour billing punishes idle capacity but rewards steady high QPS. Model both at your projected year-two size.

Build vs buy

A managed vector database pays for itself when the monthly price delta is smaller than your fully loaded engineer-hours for operating a stateful service — including upgrades that force re-indexing, on-call, failover, and capacity planning. It also wins when traffic is spiky, when you need multi-region replication you have no interest in building, or when nobody wants to own an index.

pgvector is enough when you already run Postgres and most of these hold: the corpus is under a few million vectors; peak query rate is moderate; you want transactional consistency, so deleting a document deletes its vectors in the same transaction; your filters are naturally SQL; and p99 in the tens of milliseconds is acceptable.

Break-even arithmetic, not a price list: 1M vectors at 1536 dimensions in fp32 is about 6 GB before graph overhead. If your database node already has that headroom, self-hosting is probably cheaper. Once you are sharding, running replicas for availability, and rebuilding indexes on every upgrade, the managed price starts to look like a discount. The crossover is not a fixed vector count — it is the point where your ops hours dominate.

When you do NOT need a dedicated vector database

Most teams asking this question have a corpus small enough that the answer is “you don’t”. Retrieval quality is usually bottlenecked by chunking and embedding choice, not by the index — moving from flat to HNSW changes latency, not relevance.

  • Under ~100k chunks. Keep the vectors in memory and brute-force them. 100k × 1536 fp32 is about 600 MB, and one matrix multiply per query is a few hundred MFLOPs — tens of milliseconds on one CPU core, with perfect recall and zero tuning. A large share of “we need a vector database” projects are 20k chunks.
  • Per-user corpora. If each user has a few thousand documents, an in-memory index per session or a WHERE user_id = ... scan in Postgres is simpler and exact.
  • Small corpus already in Postgres. pgvector with HNSW, or even a sequential scan, is fine. Do not add a second datastore to search 50,000 rows.
  • Batch or offline retrieval. If you retrieve once per document inside an offline enrichment job, latency is irrelevant and exact search wins outright.

You have outgrown the simple options when a few million vectors run under real concurrency and a flat scan blows the latency budget; when hybrid search, filtering, and reranking are first-class requirements rather than features you plan to bolt on; when you need per-tenant isolation and SLOs; or when vector QPS saturates your primary database. Then choose on the axes table above and measure recall on your own queries.

Frequently asked questions

Do I need a dedicated vector database for RAG?

For most teams below a few million chunks, no. pgvector or an in-memory flat index gives you exact search, transactional consistency with your app data, and one fewer system to operate. Move on when concurrency, hybrid search, multi-tenancy, or a hard recall/latency SLO outgrows what your existing database can serve.

HNSW or IVF?

HNSW when you need the best recall at low latency and your data changes continuously, because inserts are incremental and no retraining is required. IVF-PQ when memory is the binding constraint or the corpus is mostly static and you can retrain periodically. The two are not exclusive — several engines run an HNSW graph over quantized vectors.

What recall@k should I target?

Do not target an abstract number; measure against exact kNN on a sample of your own queries. Most RAG pipelines stop seeing end-to-end answer-quality gains somewhere between 0.90 and 0.97 recall@10, because the reranker and the model absorb the remainder. Tune ef_search until your own eval plateaus, not until a vendor benchmark says 99%.

Can I switch embedding models without downtime?

Yes, with versioned collections. Write new chunks to docs_v2 alongside the live index, backfill the existing corpus while skipping chunks whose hash and model id are unchanged, evaluate retrieval against both, then swap the alias the application reads from. Keep the old collection for one release so rollback is an alias change, not a re-embed.

Conclusion

Choosing a vector database is workload matching, not brand selection. Estimate vectors × dimensions to size memory, test recall under your most selective filter rather than your average one, decide whether hybrid search is a requirement or a nice-to-have, and compare the managed invoice against your own ops hours. Then measure recall@k and p99 on your own queries — every other input is someone else’s benchmark.

Build the retrieval layer on solid concepts first: embeddings and RAG for how vectors are produced and compared, then production RAG for the pipeline that wraps the index.

Related reading

Build AI features with one clear API

Qora API gives you a single, focused gateway to connect your apps, scripts and automations to AI. Start with one request.

qoraapi.com · AI API gateway for developers

Comments

3 responses to “How to Choose a Vector Database for RAG”

  1. […] How to Choose a Vector Database for RAG […]

  2. […] How to Choose a Vector Database for RAG […]

  3. […] How to Choose a Vector Database for RAG […]

Leave a Reply

Your email address will not be published. Required fields are marked *