See It Flow

Retrieval-Augmented Generation · Module 5

Vector Databases

Vector-search infrastructure choices for storing, filtering, and retrieving embeddings in RAG systems.

Mangesh MandlikLast updated:

Visual Experience

Where Embeddings Live In Practice

The animation shows the scale problem. This page focuses on the operational question: what vector-search capability you need, and where it can live.

Retrieval Augmented Generation · Module 5

Vector Databases

Vector-search infrastructure choices for storing, filtering, and retrieving embeddings in RAG systems.

Mute narration
1 / 20

01 ·Document Chunks

Thousands of document chunks from the knowledge base, ready to be embedded.

Knowledge Base

"We start with thousands of document chunks from our knowledge base."

Previous scene
Play
Next scene
Mute narration
Replay from Scene 1

What Is a Vector Database? How Vector Databases Work for RAG

A vector database stores embedding vectors together with their IDs and metadata, and answers the question "which stored vectors are nearest to this query vector, among the records this user may see?" RAG uses one because retrieval has to find semantically related chunks quickly and only among permitted records. The database avoids comparing the query with every vector by building an approximate nearest-neighbor (ANN) index that trades a little recall for large speed gains; the toy graph walk in the HNSW section below shows how that index can miss. Around the index it provides what a RAG system needs in production: upserts and deletes, metadata and ACL filtering, and operational features such as replication and backups. Specifics vary by product, and for a small corpus you may not need a dedicated database at all. Relevance ranking of the returned chunks is covered in Similarity Search, and the index algorithms in Vectors & Similarity Search.

Vector Database for LLM Applications

A vector database for an LLM application holds embeddings of your own documents so that, at question time, the system can retrieve the few chunks most related to the question and place them in the prompt. The model then answers from that supplied context instead of relying only on what its weights encode. The database does the retrieval step only: it does not generate text, and ranking and prompt assembly happen around it.

When Brute Force Stops Being Enough

A prototype indexes 1,000 chunks of 768 dimensions. To answer a query it computes a similarity with each stored vector: 1,000 × 768 = 768,000 multiply-adds, which a modern CPU finishes in a fraction of a millisecond. Nobody needs an index for that. Then the knowledge base grows. At 1,000,000 chunks the same query costs 1,000,000 × 768 = 768,000,000 multiply-adds, plus the memory traffic of reading 3 GB of vectors. The cost per query grows linearly with the corpus, so it is slower and more expensive, it competes with other work for memory bandwidth, and it multiplies by every concurrent user. Specialized indexes exist to bend that line.

Two caveats keep this in proportion. Brute-force search is vectorizable and hardware-friendly, and for hundreds of thousands of vectors it can still be the right choice, especially behind restrictive filters that shrink the candidate set. And speed is not the only reason to use a database: lifecycle, filtering, durability, and sharing across services matter just as much.

What Gets Stored

A vector alone is useless to a RAG system. The stored unit is a record containing the vector, a unique ID, the chunk's text or a reference to where the text lives, the source document and version, filterable metadata, authorization fields, and timestamps:

A stored record (illustrative)
{
  "id": "POL-REF-004#v4#c0007",
  "vector": [0.0132, -0.0481, "... 768 floats ..."],
  "payload": {
    "source_id": "POL-REF-004", "version": 4, "status": "current",
    "effective_date": "2026-10-01",
    "tenant_id": "acme-eu", "acl": ["support-agent", "support-lead"],
    "heading_path": ["Refunds", "Duplicate charges"],
    "text_ref": "s3://policies/POL-REF-004/v4.json#5120-5894",
    "embedding_model": "example-embed-v2",
    "ingested_at": "2026-10-01T09:04:12Z"
  }
}

It helps to separate three things that are often blurred. A vector index is a data structure that answers nearest-neighbor queries. A vector database wraps one or more such indexes with storage, metadata handling, a query API, durability, and operations. A document store holds full original documents and is usually a different system. Many deployments keep chunk text in the vector database's payload for convenience; others keep only an ID and fetch text from a primary store so that there is one source of truth. Either can be right, but decide deliberately, because it determines where updates and permission changes must be applied.

The Exact Baseline

Exact nearest-neighbor search computes the similarity between the query and every stored vector, then keeps the top k, typically with a heap rather than sorting the whole list. It is simple, deterministic, and returns the true nearest neighbors for the chosen metric. Even when you use approximate search in production, you should keep an exact implementation around: it is the ground truth that lets you measure how much the approximate index loses. The ANN question is never "is it fast?" but "how much recall do I give up for this speed, on my data?"

Approximate Nearest-Neighbor Indexes

ANN indexes reorganize vectors so that a query examines only a small, promising fraction of them. They return likely nearest neighbors, so the result can differ from the exact answer, and the fraction of true neighbors recovered is the index's recall. Parameters let you buy recall with more work per query. Two families are common, and products implement different ones with different defaults.

Graph-based indexes such as HNSW (hierarchical navigable small world; Malkov & Yashunin) connect each vector to a handful of near neighbors. A query enters at some node and repeatedly hops to whichever linked neighbor is closer to the query, until no neighbor improves. Layers of coarser graphs let the search take long strides first and refine locally later. Cluster-based indexes such as IVF (inverted file) partition the space into regions around a set of centroids; a query finds the few nearest centroids and scans only the vectors in those regions. Compression techniques such as product quantization (Jégou, Douze & Schmid, IEEE TPAMI 2011) are often combined with either to reduce memory. Reference implementations: FAISS and pgvector.

Do not read general performance claims into any of this. How fast an index is at a given recall depends on the data distribution, dimension, parameters, hardware, and whether filters are applied. Measure on your own data.

Why Graph Search Can Miss: A Toy Walk

The following graph is a simplified illustration, not an implementation of HNSW. Each node is a stored vector. Links can be traversed in either direction (the arrowheads show the direction of the walk below). We hold a query and know each node's distance to it, which a real search would compute as it visits nodes:

Greedy graph search that stops earlyNodes A, B, C, D, E and G are linked as A to B, A to C, B to D, C to G, and D to E. The query's distances are: A 5.0, B 3.2, C 4.1, D 1.8, E 2.5, G 1.1. Greedy search starting at A moves to its closest neighbor B, then to D, where no neighbor is closer, and stops with D at distance 1.8. The true nearest vector, G at 1.1, is only reachable through C, which looked worse than B at the first step.Adist 5.0 (entry)Bdist 3.2Cdist 4.1Ddist 1.8Gdist 1.1 (true NN)Edist 2.5
Greedy graph search that stops early. Solid arrows are the path greedy search takes; dashed links were not followed. The search ends at D (1.8) although G (1.1) is closer.
  1. 1

    Start at A (5.0)

    Neighbors are B (3.2) and C (4.1). B is closer to the query than A, so move to B. C is set aside.
  2. 2

    At B (3.2)

    Its neighbor D has distance 1.8, which improves, so move to D.
  3. 3

    At D (1.8)

    Neighbors are B (3.2) and E (2.5); neither beats 1.8. The search stops and returns D.
  4. 4

    The miss

    G (1.1) was never visited because it hangs off C. A search that keeps a wider candidate list, so that C remains a live option, would reach G. Real indexes expose that knob (often called a search breadth or ef parameter): larger values visit more nodes, raise recall, and cost time.

This is why ANN is a speed–recall trade-off and not an exact algorithm. For RAG, a miss on the true nearest neighbor matters most when the missed chunk was the only one containing the answer, which is why recall should be measured against the exact baseline rather than assumed.

Building the Index and Keeping It Current

Indexes come into existence by bulk loading (build once from a large set, often the most efficient path) and then change by incremental upserts. Graph indexes support insertion reasonably well; cluster indexes may degrade as new data drifts from the centroids chosen at build time. Deletes are the awkward part: removing a node from a graph can break paths, so many systems mark records as tombstones that are filtered out of results and physically reclaimed later during compaction or rebuild. Until then, deleted data still occupies memory and can influence traversal. Switching embedding models requires an entirely new index, as the Embeddings lesson describes.

Updating a policy: v4 to v5 (illustrative)
t0  v4 records live, status=current
t1  upsert v5 chunks (ids ...#v5#...), status=current       -> both versions searchable
t2  set status=superseded on all v4 chunks                  -> query filter hides v4
t3  background compaction removes superseded records

Stale-result risk: between t1 and t2, or if t2 is lost on a replica, a query can
retrieve v4. Mitigations: filter on effective_date / current-version pointer,
write order that is safe under retries, and a post-retrieval check against the
document registry.

The timeline below shows when each version can be retrieved during the swap.

Visibility of v4 and v5 while a policy is replacedAbstract time with t0 at 0, t1 at 10, t2 at 20 and t3 at 35. v4 chunks are searchable from the start until the superseded status filter takes effect at t2, and are physically removed by compaction at t3. v5 chunks are upserted at t1 and searchable afterwards. Between t1 and t2 both versions are searchable, and a lost status update on a replica extends that window.0102035t0 · t1 (upsert v5) · t2 (v4 superseded) · t3 (compaction), illustrative spacingv4 searchablehidden at t2v4 stored, filtered outremoved at t3v5 searchableBoth versions visiblestale-result risk
Visibility of v4 and v5 while a policy is replaced. The rose window is where a query can retrieve v4. Writing v5 before retiring v4 avoids an empty gap; the registry check and effective_date filter cover the window and replicas that missed the status update.

Freshness has two parts here as well. Your ingestion pipeline determines when new data is submitted; the database determines when it becomes visible to searches. Many systems are eventually consistent or batch index updates, so a record written a second ago may not be searchable yet. Check your product's guarantees rather than assuming read-your-writes behavior.

Filtering and Access Control

RAG queries are almost never "nearest in the whole corpus". They are "nearest among chunks this tenant may read, that are current, in this language". How the database combines a filter with an ANN search has large consequences. In post-filtering, the index returns its top candidates and the filter removes those that do not match. If the filter is selective, most candidates may be removed and you can be left with too few results, or none, even though matching records exist. In pre-filtering, the filter limits the search domain first, which is correct but harder to do efficiently inside a graph. Products offer different strategies, such as filtered graph traversal, partitioning by tenant, or switching to exact scan when the filter leaves few vectors. Test how yours behaves with a highly selective filter and check the number of results returned.

Move the slider to see how many of 30 candidates survive each strategy.

10%
Post-filtering shrinks the result list as the filter gets selectiveWith top_k = 30 and 10% of records matching the filter, post-filtering keeps about 3 of the 30 candidates (expected value if matches are spread evenly among the nearest candidates). Pre-filtering searches only matching records and still returns 30, provided at least 30 matching records exist.Post-filter: ANN returns the top 30, then the filter removes non-matches3 of 30 survivePre-filter: restrict the search to matching records first30 of 30 returned (needs at least 30 matching records)
Post-filtering shrinks the result list as the filter gets selective. Illustrative expected counts, not a measurement: real data can be better or worse, since matching records may cluster near or far from the query.
Filtered query (illustrative request, not a specific product's API)
{
  "vector": [ ...768 floats from embed_query(...) ],
  "top_k": 30,
  "filter": {
    "must": [
      { "field": "tenant_id", "equals": "acme-eu" },
      { "field": "status", "equals": "current" },
      { "field": "acl", "contains_any": ["support-agent"] }
    ]
  },
  "return": ["id", "score", "payload"]
}

The filter values come from the authenticated user's identity in trusted server code. They are never derived from text the user or the model produced.

Authorization must not be delegated to the language model. Once an unauthorized chunk is in the prompt, it has been disclosed to a component whose behavior you cannot fully control. Enforce tenant and ACL filters in the database query (and, where required, again against the system of record) so that forbidden chunks are never candidates.

The Query Path Through the Database

Query execution inside a vector databaseA query vector arrives with a filter. The ANN index generates candidate neighbors, the metadata and ACL filter is applied during or after candidate generation, the top k results are selected with scores, the chunk text is fetched from payload or a document store, and the results go to an optional reranker. A retrieval trace records scores and filters.Query vector+ filterANN candidatesMetadata / ACLfilterTop-kwith scoresFetch textRerank
Query execution inside a vector database. A query vector arrives with a filter. The ANN index generates candidate neighbors, the metadata and ACL filter is applied during or after candidate generation, the top k results are selected with scores, the chunk text is fetched from payload or a document store, and the results go to an optional reranker. A retrieval trace records scores and filters.

What comes back is a list of IDs with scores and payloads. Keep the trace as well: which filter was applied, how many candidates the index examined, and the scores. When an answer is wrong, the trace tells you whether the right chunk was never a candidate, was filtered out, or ranked too low. Scores depend on the metric and model and are not probabilities; see Similarity Search.

Vector Database Comparison: Where to Put the Vectors

OptionMain advantageMain trade-off
Vector extension in a relational databaseVectors sit next to your other data; joins and transactions; no new systemIndex options, scale ceiling, and filtering depend on the extension and the database load
Dedicated vector search engineBuilt for ANN at scale; richer index choices and tuningAnother system to operate and keep consistent with primary data
Hybrid search platformSimplifies hybrid queries: lexical and vector retrieval in one placeCapabilities vary by platform
Local library in your processSimplest for a few hundred thousand vectors or lessDurability, sharing, and updates are left to you

There are several architectural options and no universal winner. A vector extension in a relational database keeps vectors next to your other data, lets you join and transact across them, and avoids a new system to run; the trade-off is that its index options, scale ceiling, and filtering behavior depend on the extension and on how much search load the database should carry. A dedicated vector search engine is built for ANN at scale, with richer index choices and tuning, at the cost of another system to operate and keep consistent with your primary data. A hybrid search platformcombining lexical and vector retrieval in one place simplifies hybrid queries. A local libraryrunning in your process is the simplest for a few hundred thousand vectors or less, but leaves durability, sharing, and updates to you.

Choose by what you must do: whether you need transactions across metadata and vectors, how large the corpus is, how selective your filters are, how fast documents change, what your team can operate, and how much operational risk you will accept. A vector database is a means, not a requirement; plenty of effective RAG systems run on modest indexes.

Storage Arithmetic

Vector memory is easy to estimate and often the first surprise. For float32 vectors (4 bytes per number):

Raw vector size (illustrative scale, decimal units)
1,000,000 vectors x 768 dimensions x 4 bytes
  = 3,072,000,000 bytes  ~= 3.07 GB          (before anything else)

scalar quantization to 1 byte per number:
  1,000,000 x 768 x 1 = 768,000,000 bytes    ~= 0.77 GB   (4x smaller)

This counts only raw vectors. Index structures, metadata, chunk text, and replicas come on top. Each of three replicas multiplies the vector storage by three.

Vary the corpus size, quantization and replica count to recompute both the per-query cost and the memory.

1,000,000
1×
Cost per query and memory as the corpus grows1,000,000 vectors of 768 dimensions: a brute-force query costs 768,000,000 multiply-adds. Raw vectors take 3.07 GB at 4 bytes per number, 3.07 GB across 1 replica(s). Index structures, metadata and text are extra. At 1,000,000 vectors the raw float32 size is 3.07 GB and a query costs 768,000,000 multiply-adds.Query multiply-adds768,000,000Raw vector memory3.07 GB768 dimensions · 4 byte(s) per number · 1 replica(s)
Cost per query and memory as the corpus grows. Both bars use a fixed scale (10,000,000 chunks; 3 float32 replicas for memory), so growth is visible when you move the sliders. Raw vectors only.
Multiply-adds per query
768,000,000
1,000,000 × 768
Raw vectors, one copy
3.07 GB
1,000,000 × 768 × 4 B
Across 1 replica(s)
3.07 GB
Not counted
index, payload, text
come on top

Quantization stores each number with fewer bits, and product quantization compresses groups of dimensions into short codes. The trade-off is that distances computed from compressed vectors are approximations, so recall can fall; many systems fetch a short list using compressed vectors and then rescore it with full-precision vectors. Whether the loss is acceptable is an empirical question for your data. Lower-dimension embeddings, when your model offers them, reduce size the same way and may also reduce quality.

Running It in Production

Production concerns resemble those of any database. Replication provides availability and read throughput; check whether replicas update synchronously, since asynchronous ones can serve stale or not-yet-deleted data. Sharding or partitioning splits the data across nodes when it does not fit on one machine or when you want tenant isolation; it adds a merge step to query results and complicates rebalancing. Backup and restore should be rehearsed, including how long an index takes to rebuild from source if a snapshot is lost; because the index is derived data, the source documents and the ingestion pipeline are your real backup. Corrupt or partially built indexes need detection and a rebuild path, ideally partial and per-tenant. Watch p50, p95, and p99 query latency, recall on a canary query set, index memory, ingestion lag, and error rates. All of this varies by product, so read the operational documentation of the one you choose.

Implementation Sketch

Pseudocode: upsert, filtered query, delete, version-aware retrieval
def upsert_chunks(chunks):
    # Idempotent: deterministic IDs mean a retry overwrites instead of duplicating.
    db.upsert([{"id": c.chunk_id, "vector": c.vector, "payload": c.payload} for c in chunks])

def search(question, user, k=30):
    qvec = embed_query(question)
    flt = {"tenant_id": user.tenant,               # from the authenticated session
           "acl_any": user.groups,
           "status": "current"}
    hits = db.query(vector=qvec, top_k=k, filter=flt)
    # Defense in depth: confirm against the registry of current versions.
    return [h for h in hits if registry.current_version(h.payload["source_id"]) == h.payload["version"]]

def delete_document(source_id):
    ids = db.ids_where({"source_id": source_id})
    db.delete(ids)                                  # tombstone now; compaction later

def replace_version(source_id, new_chunks, new_version):
    upsert_chunks(new_chunks)                       # 1. add new
    registry.set_current(source_id, new_version)    # 2. flip the pointer
    db.set_payload({"source_id": source_id, "version_lt": new_version},
                   {"status": "superseded"})        # 3. retire old

Writing the new version first and retiring the old second avoids a window with no content; the registry check closes the window where both are visible.

Choosing and Benchmarking With Your Data

Benchmark with a representative sample of your vectors and queries, including your real filters. Compute the exact top-k for a set of queries and measure the index's Recall@K against it; a result is only interesting alongside its recall. Record latency at p50, p95 and p99 under realistic concurrency, since tail latency is what users experience. Measure memory, ingestion throughput, and how long a full build takes. Test filter behavior at several selectivities, from "everyone" to "one tenant with 200 records". Test update freshness: how long until a new or deleted record is visible. And estimate operational cost, including replicas and the engineering time to run it. Treat vendor benchmarks as hypotheses to confirm, never as results.

What's Next

The database can retrieve nearest vectors; the next question is what "nearest" means and why it is not enough.

Up Next · Retrieval Augmented Generation (RAG)

Similarity Search

Next, focus on the retrieval operation itself: how a query becomes top-K candidates, why nearest is not the same as correct, and when hybrid search wins.