Vector databases — what, why, and how
What a vector database is, why LLMs need one for RAG and memory, how ANN (HNSW/IVF/PQ) works, architecture, index selection, filtered search, ingest/ops, sizing, and when to use — end-to-end HI depth.
What a vector database is (plain English)
A vector database stores embeddings — lists of numbers that represent meaning — and answers one question fast:
Which stored items are most similar to this query embedding?
That is it. It does not “understand” your documents. It does not replace an LLM. It is a specialized index for similarity search over high-dimensional vectors, usually with metadata filters (tenant, ACL, doc type, time) so products can retrieve safe and relevant snippets.
Analogy: a relational DB answers “rows where status = 'paid'.” A vector DB answers “chunks whose meaning is near this question,” then your app packs those chunks into an LLM prompt.
One-sentence definition you can defend in an interview
A vector database is a persistence + ANN search system for embeddings and payloads, optimized for filtered nearest-neighbor queries that feed retrieval-augmented generation and agent memory.
If you cannot say ANN, payload/metadata, and filtered search, you are describing a numpy array, not a production vector store.
Why LLMs need this (the problem)
An LLM only “knows” what is in its weights and its context window. Your company wiki, tickets, contracts, and PDFs are in neither — unless you retrieve relevant snippets and put them in the prompt. That pattern is RAG (retrieval-augmented generation).
| Need | Without vectors | With a vector store |
|---|---|---|
| Fresh company knowledge | Retrain or paste everything | Update index; no retrain |
| Paraphrase match | Keyword miss (“refund” vs “return policy”) | Embedding neighborhood |
| Citations | Model invents sources | Return doc_id / spans |
| Agent long-term memory | Lost each session | Collection of notes + ANN |
| Scale | Scan all embeddings in RAM | Sub-linear ANN query |
Keyword search (BM25) finds exact tokens. Embeddings turn text into vectors so paraphrase still matches. A vector database (or vector-capable search engine) stores those vectors and runs: which chunks are nearest to this query embedding?
Without a vector store (or equivalent ANN index), you either scan every embedding in memory (fine for demos) or fall back to keyword search alone (misses paraphrase and multilingual near-matches).
flowchart TD
Docs[Documents] --> Chunk[Chunk + metadata]
Chunk --> Emb[Embedding model]
Emb --> VDB[(Vector DB / ANN index)]
Q[User question] --> QEmb[Embed query]
QEmb --> ANN[ANN search + filters]
VDB --> ANN
ANN --> Pack[Top-k chunks]
Pack --> LLM[LLM answers with citations]
Interview cue: “Vector DB” is the data plane for semantic retrieval. Wrong chunks → confident wrong answers. Evals must measure retrieval separately from generation.
Why not “just put all docs in the prompt”?
Context windows grew, but products still need retrieval because:
- Cost — stuffing 200 pages every turn is expensive
- Attention — models dilute on huge noisy contexts
- Freshness — index updates beat prompt paste jobs
- Access control — you cannot dump every tenant’s docs into one window
- Latency — retrieve top-k then generate beats shipping megabytes of tokens
Long context complements RAG; it does not delete the need for a retrieval layer in multi-tenant SaaS.
What a vector database actually does
Software that earns the name typically:
- Persists high-dimensional vectors + payload metadata (text, source ids, ACLs, timestamps)
- Builds an approximate nearest neighbor (ANN) index for fast similarity search
- Supports upsert / delete, concurrent queries, and usually filtered search
- Often exposes hybrid search (vectors + BM25/keywords) or plays nicely with one
- Offers backups, replicas, metrics so ops can treat it like a real datastore
It is not a replacement for the LLM. It retrieves candidates; the LLM still generates. It is also not magic memory — garbage chunks with a perfect index still poison the prompt.
| Capability | Why product engineers care |
|---|---|
| ANN search | Sub-linear latency at millions of vectors |
| Persistence | Survive restarts; back up / restore |
| Metadata filters | Tenant, ACL, freshness, doc type |
| Upsert / delete | Incremental corpus without full rebuild |
| Batch ingest | Amortize embedding API cost |
| Hybrid (often) | Exact IDs + paraphrase in one pipeline |
| Collections / namespaces | Version indexes; isolate tenants |
Vector DB vs “just Postgres” vs “numpy in RAM”
| Approach | Good for | Breaks when |
|---|---|---|
| In-memory arrays (FAISS/numpy) | Labs, <50k–200k vectors | Restarts, multi-process, rich filters, HA |
| Postgres + pgvector | Relational + vectors, mid scale | Pure ANN at huge scale / multi-region |
| Dedicated vector DB | High QPS ANN, rich filters, managed ops | You invent a second source of truth carelessly |
| Search engines with kNN (Elastic/OpenSearch) | Already run search cluster | JVM/ops weight; dense tuning still needed |
Ship rule: treat the vector index as a derived artifact. Canonical chunk text + metadata live in object storage / Postgres; you can always rebuild the ANN layer.
How it helps LLMs (concretely)
| Without vector retrieval | With vector DB + RAG |
|---|---|
| Hallucinates policy details | Grounds on retrieved clauses |
| Context window stuffed randomly | Packs top-k relevant chunks |
| No citations | Return source_id / spans |
| Stale knowledge in weights | Update index without retraining |
| Agent “memory” lost each session | Long-term notes in a collection |
Agents use the same primitive for long-term memory — different collection design, same ANN + metadata story. Caching layers (exact / semantic) often sit in front of both the LLM and the vector store; see Redis for AI caching.
Three product jobs that share one primitive
flowchart LR
subgraph SameANN[Same ANN + metadata machinery]
RAG[RAG over docs]
Mem[Agent long-term memory]
Rec[Similar-item / semantic recommend]
end
RAG --> Pack[Pack into LLM]
Mem --> Pack
Rec --> UI[UI / ranking]
| Job | Collection contents | Query |
|---|---|---|
| RAG | Doc chunks + ACL | User question embedding |
| Memory | Summaries / facts / episodes | “What do we know about X?” |
| Recommend | Item / profile vectors | Seed item or user vector |
Do not invent three unrelated systems. Share embedding versioning, id schemes, and eval habits.
Embeddings and distance — the math you actually need
An embedding model maps text → (\mathbb{R}^d) (common (d): 384, 768, 1024, 1536, 3072). Similar meanings land nearby under a chosen metric:
| Metric | Intuition | Watch-out |
|---|---|---|
| Cosine | Angle between vectors | Often pair with L2-normalized vectors |
| Inner product / dot | Same as cosine if normalized | Wrong if magnitudes vary |
| L2 (Euclidean) | Straight-line distance | Common in vision; check model docs |
Ship rule: the query and document embeddings must use the same model + version + metric. Changing any of those requires a full re-embed.
Some models use asymmetric prefixes (e.g. query: vs passage:). If the model card says so, apply prefixes consistently at ingest and query — that is part of the model contract, not optional flavor text.
# Shape only — pin model id in your index name
import numpy as np
def cosine(a: np.ndarray, b: np.ndarray) -> float:
a = a / (np.linalg.norm(a) + 1e-9)
b = b / (np.linalg.norm(b) + 1e-9)
return float(a @ b)
# Collection naming that survives model swaps
COLLECTION = "docs_v3_text-embedding-3-small_1536_cosine"Dimensionality and quality intuition
| Choice | Effect |
|---|---|
| Higher (d) | Often richer geometry; more RAM / bandwidth |
| Smaller / cheaper model | Faster ingest; may lose hard paraphrase |
| Matryoshka / truncated dims | Trade quality for RAM if model supports it |
| Domain fine-tuned embedder | Wins on jargon; ops cost of training + eval |
Start with a strong general embedding API; ablate on your eval set before fine-tuning an embedder.
Exact vs approximate nearest neighbor (ANN)
Exact nearest neighbor (brute force) is (O(n \cdot d)) per query. At millions of vectors that is too slow or too expensive for interactive RAG.
ANN algorithms trade a little recall for large speed / memory wins. You tune for recall@k on your eval queries — not vendor defaults.
flowchart LR
Q[Query vector] --> Exact[Exact: scan all]
Q --> ANN[ANN: search subset / graph]
Exact --> R1[100% recall · slow]
ANN --> R2[High recall · fast]
Recall@k = fraction of true top-k neighbors (from exact search) that ANN also returns. Product quality needs task recall (gold chunks in top-k), which correlates with but is not identical to ANN geometric recall.
Index families you will see in job interviews
| Idea | Intuition | Typical knobs |
|---|---|---|
| HNSW | Multi-layer proximity graph; greedy walk to neighbors | M, ef_construct, ef_search |
| IVF / IVFFlat | Cluster space; search only nearby coarse cells | nlist, nprobe |
| PQ / OPQ / SQ | Compress vectors into short codes → more in RAM | codebooks, bits |
| DiskANN / disk-backed | Keep graph/codes on SSD for huge corpora | I/O vs RAM trade |
| ScaNN / FAISS variants | Research/prod libraries behind many engines | engine-specific |
HNSW mental model: layers act like a skip list over a neighborhood graph. Search starts coarse (upper layers), descends, then expands neighbors at the bottom. Higher ef_search → better recall, higher latency. Higher M / ef_construct → better graph, more RAM and build time.
IVF mental model: train centroids on a sample; each vector belongs to a list. At query time, probe nprobe nearest lists and rank within them. Cheap to build relative to HNSW; recall depends heavily on training quality and nprobe. Under-trained centroids → silent recall cliffs.
Product quantization (PQ): split the vector into subspaces; replace each with a codebook id. Great for memory; combine with IVF (IVFPQ) or HNSW variants. Always re-check task quality — compression can blur near neighbors.
flowchart TD
subgraph HNSW
L2[Layer 2 long jumps] --> L1[Layer 1]
L1 --> L0[Layer 0 dense neighbors]
end
subgraph IVF
C[Coarse centroids] --> Lists[Inverted lists]
Lists --> Rank[Exact or PQ rank in nprobe lists]
end
HNSW search — step by step (whiteboard version)
- Pick an entry point on the top layer (often a fixed or random hub).
- Greedy walk: move to the neighbor closest to the query until no neighbor is closer.
- Drop one layer down; repeat with a larger candidate set.
- At the bottom layer, expand a candidate heap of size
ef_search; return the bestk.
Insert assigns a random max layer (geometric distribution), links the new node to M neighbors per layer, and may prune edges. Build quality is dominated by ef_construct (how hard you search while linking). Query quality is dominated by ef_search (how wide you explore at query time).
| Knob | Raise it when… | Cost |
|---|---|---|
M |
Graph feels disconnected; recall low at high QPS | RAM + build time |
ef_construct |
Build-time recall poor on sample | Build CPU / wall clock |
ef_search |
Online recall@k below gate | Query latency |
Ship rule: treat ef_search as a runtime dial you can A/B; treat M / ef_construct as a rebuild decision.
IVF + PQ — step by step (whiteboard version)
- Train: sample vectors → run k-means → get
nlistcoarse centroids. - Assign: each vector goes into the inverted list of its nearest centroid.
- Optional PQ: train codebooks per subspace; store codes instead of float32.
- Query: find
nprobenearest centroids → scan those lists (exact or asymmetric PQ distance) → return top-k.
| Knob | Raise it when… | Cost |
|---|---|---|
nlist |
Lists are huge / unbalanced | Train time; need enough sample |
nprobe |
Recall cliff on hard queries | Query latency ~linear in nprobe |
| PQ bits / subspaces | RAM ceiling | Quality loss — measure task recall |
Worked intuition: 10M vectors, nlist=4096 → ~2.4k vectors per list if balanced. nprobe=32 scans ~77k candidates — far cheaper than 10M, but only if centroids match the data distribution. Retrain after a major corpus shift.
Index selection decision tree
Need interactive RAG at <~1–5M vectors, heavy filters, simple ops?
└─ Start HNSW (or pgvector HNSW) in RAM
RAM / cost exploding above ~10–50M?
└─ IVF+PQ or disk-backed (DiskANN-class); accept latency
Already on Elastic/OpenSearch and hybrid is mandatory?
└─ Engine kNN + BM25 in one cluster; still measure recall@k
Lab / unit tests / <100k vectors?
└─ Exact or tiny HNSW; do not over-engineer
Multi-region write-heavy with managed SLA?
└─ Dedicated vector service — but keep Postgres as SoT| Index | Build | Query | RAM | Best fit |
|---|---|---|---|---|
| Exact / flat | Instant | O(n·d) | Vectors only | Eval gold; tiny corpora |
| HNSW | Slow / RAM-heavy | Fast | High (graph) | Default interactive RAG |
| IVFFlat | Train + assign | Depends on nprobe | Medium | Mid-scale, rebuild OK |
| IVFPQ | Train + codes | Fast-ish | Low | Huge corpora, cost-sensitive |
| DiskANN-like | Heavy | SSD-bound | Low RAM | 100M+ with disk budget |
There is no universally “best” index. There is only recall@k vs p95 vs $/month on your filters and QPS.
Filtered ANN (the product-critical part)
Real products rarely want “globally similar.” They want:
tenant_id = X AND doc_type = policy AND updated_at > … AND acl ∩ roles ≠ ∅Engines apply filters as pre-filter, post-filter, or constrained graph walk. Post-filter after global top-k can return empty results under strict ACLs. Prefer engines/docs that support filter-aware search, and load-test with realistic selectivity.
| Filter strategy | Behavior | Risk |
|---|---|---|
| Post-filter | ANN top-k then drop non-matches | Empty / biased under rare ACL |
| Pre-filter | Restrict candidates then ANN | Needs index support |
| Filter-aware graph | Walk only allowed nodes | Best when available; verify docs |
Ship rule: ACL bugs in retrieval are security bugs. Automate a test: tenant A query never returns tenant B payloads.
How it works end-to-end (ingest + query)
flowchart TB
subgraph Ingest
Src[Sources: Drive / S3 / Wiki] --> Parse[Parse + clean]
Parse --> Chunk[Chunker]
Chunk --> Meta[Attach metadata + stable ids]
Meta --> EJob[Embed batch job]
EJob --> Upsert[Upsert to vector store]
end
subgraph Online
User[User / agent] --> GW[API gateway]
GW --> EmbQ[Embed query]
EmbQ --> Search[Filtered ANN ± hybrid]
Search --> Rerank[Optional reranker]
Rerank --> Prompt[Pack context + citations]
Prompt --> LLM[LLM]
end
Upsert -.-> Search
Stable IDs and idempotent upserts
Stable chunk IDs are non-negotiable. Prefer something like hash(doc_id + chunk_index + embedding_model_id) or content-addressed ids so re-ingest is idempotent.
# Conceptual upsert — batch for throughput
points = []
for chunk in chunks:
points.append({
"id": chunk.stable_id,
"vector": embed(chunk.text),
"payload": {
"text": chunk.text,
"doc_id": chunk.doc_id,
"tenant_id": chunk.tenant_id,
"updated_at": chunk.updated_at.isoformat(),
"acl_roles": chunk.acl_roles,
"content_hash": chunk.content_hash,
},
})
client.upsert(collection="docs_v3_…", points=points) # batches of 100–500# Conceptual filtered search
hits = client.search(
collection="docs_v3_…",
query_vector=embed(user_query),
filter={"must": [
{"key": "tenant_id", "match": tenant_id},
{"key": "acl_roles", "any": user_roles},
]},
limit=40, # retrieve wide; rerank/pack later
)Retrieve wide, pack narrow
Interactive RAG rarely wants k=5 from ANN alone:
- Retrieve 20–50 candidates (vector ± BM25)
- Optional rerank to 5–10
- Pack under a token budget with citations
That pipeline is covered in hybrid search and rerankers and chunking and metadata.
sequenceDiagram
participant App
participant Embed as Embedding API
participant VDB as Vector DB
participant LLM
App->>Embed: embed(query)
Embed-->>App: qvec
App->>VDB: search(qvec, filters, k)
VDB-->>App: top-k payloads
App->>LLM: prompt + chunks
LLM-->>App: answer + cite ids
Memory, cost, and sizing intuition
Rough KV-free vector RAM (uncompressed float32):
[ \text{bytes} \approx n \times d \times 4 + \text{index overhead} + \text{payload} ]
Example: 10M vectors × 1536 dims × 4 bytes ≈ ~61 GB raw — before HNSW graph overhead and payloads. This is why PQ / disk indexes / tiering show up at scale, and why “just put it all in HNSW RAM” fails budget reviews.
| Lever | Effect |
|---|---|
Lower d / smaller embedding model |
Less RAM, often enough quality |
| PQ / scalar quant | More vectors per node |
| IVF + disk | Cost ↓, latency ↑ |
| Separate hot vs cold collections | Product freshness tiers |
| Drop unused payload fields from hot index | RAM ↓ |
Also budget embedding API cost for initial backfill and ongoing CDC. A 50M-chunk re-embed is a project with a calendar, not a Tuesday evening.
Ops concerns product engineers hit
- Dimension / metric mismatch — silent garbage neighbors
- Embedding train/serve skew — different models or prompts for query vs doc
- Idempotent upserts — unstable ids → duplicates forever
- Deletes / tombstones / GDPR — mark deleted + filter; compact later
- Reindex budget — model upgrade = full re-embed + dual-write cutover
- Hot vs cold — RAM indexes vs disk; cost curves
- Multi-tenancy — collection-per-tenant vs shared + mandatory filter
- Backup / restore — snapshot volumes before bulk reingest
- SLO — p95 query latency and recall@k on a frozen eval set
- Payload bloat — storing megabyte blobs in the vector node
Dual-write cutover (embedding model change)
- Create
docs_v4_newmodel_… - Backfill async; keep serving
v3 - Shadow-query both; compare recall@k / nDCG on eval set
- Flip traffic; keep
v3read-only until rollback window ends
flowchart LR
V3[docs_v3 serving] --> Shadow[Shadow compare]
V4[docs_v4 backfill] --> Shadow
Shadow -->|pass gates| Flip[Flip read traffic]
Flip --> Keep[Keep v3 for rollback]
Multi-tenancy patterns
| Pattern | Pros | Cons |
|---|---|---|
| Collection per tenant | Hard drop isolation | Index sprawl |
Shared + tenant_id filter |
Simple ops | Bug = leak; test hard |
| Namespace / partition features | Vendor help | Portability ↓ |
Start shared + mandatory filters for MVPs under ~50 tenants; graduate when compliance or noisy-neighbor requires harder walls. Decision detail: Choosing vector stores.
Architecture that survives production
flowchart TB
subgraph SoT[System of record]
PG[(Postgres: docs, ACL, chunk text)]
S3[(Object store: raw files)]
end
subgraph Derived[Derived indexes]
VDB[(Vector DB)]
BM25[(Optional search engine)]
end
subgraph Online
API[RAG API] --> Cache[Redis exact/semantic]
Cache --> Emb[Embed]
Emb --> VDB
Emb --> BM25
VDB --> Fuse[Hybrid + rerank]
BM25 --> Fuse
Fuse --> LLM[LLM]
end
PG --> VDB
S3 --> PG
Contract:
| Layer | Owns |
|---|---|
| Postgres / object store | Canonical text, ACL, versions |
| Vector DB | ANN over derived embeddings |
| Redis | Hot exact / semantic response cache |
| App | Filters, packing, citations, evals |
Sharding, replication, and HA (what “production” adds)
Most dedicated vector engines expose patterns you already know from search databases:
| Pattern | Purpose | Vector-specific gotcha |
|---|---|---|
| Replicas | Read QPS + failover | Rebuild lag → stale chunks for minutes |
| Sharding / partitions | Horizontal scale | Cross-shard top-k merge; filter skew |
| Leader / primary writes | Consistent upserts | Bulk backfill saturates write path |
| Snapshots / backups | RPO/RTO | Payload + graph overhead, not just vectors |
| Read-your-writes | Fresh ingest | Often eventual — document SLO explicitly |
Ship rule: the vector tier is a derived cache. If you lose it, rebuild from SoT + embed jobs — not from “hope the snapshot was recent.”
flowchart LR
subgraph Write
W1[Ingest workers] --> P[Primary shard]
end
subgraph Read
P --> R1[Replica A]
P --> R2[Replica B]
R1 --> API[RAG API]
R2 --> API
end
Change data capture (CDC) — keeping the index honest
Treat ingest as an event stream, not a one-time dump:
doc.updated → parse → chunk → content_hash changed?
yes → embed batch → upsert vectors + payload
no → skip embed (save $)
doc.deleted → tombstone SoT → delete vectors (or filter deleted_at)
acl.changed → update payload only (no re-embed if text unchanged)| Event | Vector action | Common bug |
|---|---|---|
| Text edit | Re-embed + upsert same stable id | New random id → duplicate hits |
| ACL only | Patch payload | Forgetting → leak |
| Soft delete | Filter + async compact | Zombie retrieval |
| Model upgrade | New collection + dual-write | In-place dim change |
Wire ingest lag metrics: now() - doc.updated_at at first successful search. Product teams notice “doc not found” before ops notice CPU.
How to evaluate retrieval (not vibes)
Hold a frozen set of queries with labeled relevant chunk ids (or answer spans).
| Metric | Asks |
|---|---|
| recall@k | Did any gold chunk appear in top-k? |
| MRR / nDCG@k | Ranking quality |
| Empty-rate under ACL | Filter too strict? |
| Faithfulness sample | Did the LLM use the right span? |
Ship rule: never ship a new embedder, chunker, or index knob without a before/after on the same eval set. Generation quality alone hides retrieval rot.
Three measurable metrics (staff bar)
- recall@10 on held-out labeled queries (target: set a floor, e.g. ≥ 0.85 for your corpus)
- p95 ANN latency under production-like filters
- Cross-tenant leak rate = 0 in automated isolation tests
Two degrade modes
- ANN / embedder outage → fall back to BM25-only or cached FAQ; tell the user answers may be weaker
- Empty filtered retrieval → refuse to invent; ask clarifying question or escalate — do not let the LLM freestyle policy
When you need more than vectors
Vectors alone miss exact IDs, error codes, SKUs, rare proper nouns. Production RAG usually adds:
- Hybrid BM25 + vector (hybrid search article)
- Rerankers on a shortlist
- Good chunking + metadata (chunking article)
Security and privacy threat note
| Threat | Layer that catches it |
|---|---|
| Cross-tenant chunk in prompt | Mandatory filters + isolation tests |
| Prompt injection via retrieved docs | Sanitize/pack; cite; tool policy |
| Deleted doc still retrievable | Tombstones + filter + compact job |
| Embedding API sees PII | Redact / minimize before embed; DPA |
| Stale cache of privileged answer | Cache keys include tenant + ACL version |
See also Privacy and data for AI.
Failure modes checklist
- Embedding train/serve skew (different models or prefixes)
- Chunks too large → diluted similarity; too small → no context
- Filter too strict → empty retrieval → model improvises
- Post-filter after ANN under ACL → empty / biased hits
- Evaluating only final answers → blind to retrieval rot
- No citation ids → un-debuggable hallucinations
- Unfiltered multi-tenant search → data leak into prompts
- Dimension mismatch after silent config change
- Re-embed without dual-write → partial corpus on new model
- Payload-only deletes without index compaction → zombie hits
Debugging playbook (first hour)
- Confirm model id + dim + metric match between ingest and query logs
- Run the failing query with filters disabled in a secure staging clone — is it filter or geometry?
- Compare exact top-10 vs ANN top-10 on a sample (recall cliff?)
- Inspect chunk text — parse/chrome garbage?
- Check content_hash / doc version — stale index?
- Verify citations map to real
doc_ids in SoT
Production readiness checklist
- Collection name encodes model + dim + metric
- Stable chunk ids + idempotent upsert
- Mandatory tenant/ACL filters in code review checklist
- Frozen eval set with recall@k gate in CI or weekly job
- Backup/restore drill for the vector volume
- Re-embed / dual-write runbook documented
- p95 latency + empty-retrieval alerts
- Export path for payloads (exit plan)
Tradeoffs and when to use a vector DB
| Prefer vectors + RAG when | Prefer something else when |
|---|---|
| Large / changing corpus | Tiny static FAQ (prompt or exact cache) |
| Paraphrase matters | Only exact SKU/ID lookup (BM25/SQL) |
| Multi-tenant ACL retrieval | Single public brochure site |
| Agents need durable semantic memory | Session-only chat (Redis list is enough) |
Decision matrix (staff bar)
| Signal | Lean vector DB + RAG | Lean simpler |
|---|---|---|
| Corpus | >500 docs or weekly updates | <50 static pages |
| Query style | Natural language, paraphrase | Exact codes / IDs |
| Tenancy | ACL per chunk | Public read-only |
| Latency budget | Need sub-200ms retrieval slice | Can paste context once |
| Team | Can operate an index + eval harness | No infra appetite |
When a dedicated vector DB beats pgvector
- ANN QPS >> what one Postgres primary can serve under mixed OLTP load
- Filter-aware ANN is first-class and you have sparse ACL tenants
- Managed snapshots / autoscaling beat babysitting HNSW bloat
- Multi-region read replicas without hand-rolling shard merge
When pgvector (or search engine kNN) beats a dedicated vector DB
- Postgres already owns docs, ACL, and chunk text
- Corpus and QPS fit one HA Postgres cluster
- You need transactional consistency between rows and vectors
- Hybrid can be
tsvector+ vector in one SQL query
Detail lives in Choosing vector stores and Postgres + pgvector — this page is the why and how, not a vendor pick list.
Interview prompts you should be able to answer
- Exact vs ANN — what do you trade, and how do you measure it?
- Why can post-filtering ANN results fail ACL-heavy products?
- Walk through a safe embedding-model upgrade.
- How do you size RAM for 5M × 768-dim float32 + HNSW?
- What belongs in the vector DB vs Postgres vs object store?
How embeddings are produced (pipeline detail)
sequenceDiagram
participant Src as Source system
participant Worker as Ingest worker
participant Emb as Embedding API
participant VDB as Vector DB
Src->>Worker: doc create/update/delete
Worker->>Worker: parse + chunk + hash
alt content_hash unchanged
Worker-->>Worker: skip embed
else changed
Worker->>Emb: batch embed texts
Emb-->>Worker: vectors
Worker->>VDB: upsert points
end
Batching and backpressure
- Batch 50–200 texts per embed call when the API allows — cuts HTTP overhead.
- Cap concurrent embed workers so you do not melt provider RPM or the vector store write path.
- On delete events: tombstone in SoT first, then delete vectors (or filter
deleted_at). - Dead-letter failed chunks; do not silently drop them or recall rot is invisible.
Query-time embedding tricks
| Trick | Use | Risk |
|---|---|---|
| Same model as docs | Always | N/A |
Query prefix (query:) |
If model card requires | Forgetting it → skew |
| Multi-query (3 paraphrases) | Hard recall | Latency ×3 |
| HyDE | Abstract questions | Extra LLM cost |
Collection design patterns
| Pattern | Example | When |
|---|---|---|
Monolithic docs_v3_… |
One big corpus | Simple products |
Per doc_type |
policies_…, tickets_… |
Different filters / SLAs |
| Hot / cold | Last 90d in RAM HNSW; archive on disk | Cost control |
| Memory vs RAG | Separate collections | Different id schemes / retention |
Never mix embedding models in one collection. Prefer a new collection name over an in-place dimension change.
Observability for the vector data plane
Log every retrieval:
query_id, tenant_id, model_id, collection, k, filter_hash,
hit_ids[], scores[], ann_ms, embed_ms, empty=true|falseAlert on:
- Empty retrieval rate spikes (filter or ingest break)
- p95
ann_msregressions after index rebuilds - Embedding error rate
- Collection count / disk unexpectedly flat during backfill (stuck job)
Trace ids should join retrieval spans to the LLM generation span so “wrong answer” tickets can show which chunks were packed.
Worked walkthrough: support policy RAG
- Ingest: Confluence export → HTML clean → heading chunks (~500 tokens) → embed
text-embedding-3-small→ upsert Qdrant/pgvector withtenant_id,acl_roles,heading,doc_id. - Query: User asks “How long do refunds take?” → embed → filter tenant+role → top 40 → optional rerank → pack 6 chunks with citations.
- Eval: 40 labeled questions; gate recall@10 ≥ 0.9 before launch.
- Ops: Weekly sample of empty retrievals; dual-write plan documented for next embed model.
Anti-patterns (vector DB edition)
- One vector per entire PDF “to keep it simple”
- Storing embeddings without chunk text (cannot cite or rebuild prompts)
- Sharing a collection across tenants without mandatory filters
- Tuning
ef_searchon vibes with no recall@k - Treating Pinecone/Qdrant as SoT for document bodies
- Re-embedding in place without versioned collections
How ANN search feels in practice (timing)
A typical interactive RAG query budget for the retrieval slice:
| Step | p50 sketch | Notes |
|---|---|---|
| Auth + bind filters | 1–5 ms | Must be server-side |
| Embed query | 20–80 ms | Dominated by network to embed API |
| ANN + filters | 5–50 ms | Depends on n, index, selectivity |
| Optional rerank | 50–200 ms | Cross-encoder pairs |
| Hydrate / pack | 5–20 ms | Joins to SoT |
If embed is remote and slow, cache query embeddings for identical strings (short TTL) — separate from answer caching.
Consistency models you will actually hit
| Situation | Behavior |
|---|---|
| Upsert then immediately query | May miss brand-new points until index catches up (engine-dependent) |
| Delete then query | Tombstones / eventual removal |
| Dual collections during cutover | App must pin version explicitly |
| Replica lag (pgvector) | Stale chunks for minutes |
Document freshness SLOs (“new doc searchable within N minutes”) and measure ingest lag — not just query latency.
Agent memory vs RAG collections
| RAG docs | Agent memory | |
|---|---|---|
| Writers | Ingest pipeline | Agent runtime |
| Retention | Long; governed | Often shorter / summarized |
| Trust | Curated corpus | Model-written — higher poison risk |
| Filters | ACL + doc_type | user_id + thread/run |
Ship rule: do not dump raw tool traces into the same collection as customer policy docs without type filters and separate evals.
Interview whiteboard: design a vector layer
Prompt: “Design retrieval for a 50-tenant SaaS with 5M chunks.”
A strong answer covers:
- SoT in Postgres; derived ANN (pgvector first or Qdrant)
- Mandatory tenant filters + isolation tests
- Chunking + metadata schema
- Hybrid for IDs; rerank optional
- recall@k eval harness
- Re-embed dual-write runbook
- Redis exact cache in front
- Graduation criteria to dedicated ANN
Weak answers only name a vendor.
Glossary
| Term | Meaning |
|---|---|
| Embedding | Dense vector representing meaning |
| ANN | Approximate nearest neighbor search |
| HNSW | Hierarchical graph ANN index |
| IVF | Inverted file / coarse quantizer lists |
| Payload | Metadata stored with a vector |
| recall@k | Fraction of relevant items in top-k |
| RAG | Retrieve chunks → pack into LLM prompt |
| Hybrid | Vector + keyword retrieval fused |
Hands-on next steps
- Choosing vector stores — Pinecone / Weaviate / Chroma / pgvector / Qdrant
- Postgres + pgvector — one-DB path
- Guided Vector DB — Docker Compose + ingest + query
- Hybrid search and Rerank / rewrite (vectors alone are not enough)
End-to-end lab checklist (do this once)
- Spin up Qdrant or Postgres+pgvector locally.
- Ingest 100 docs with stable ids + tenant metadata.
- Run 20 labeled queries; compute recall@5 exact vs ANN.
- Break it on purpose: wrong metric, missing tenant filter, huge chunks — observe symptoms.
- Fix with versioned collection name + mandatory filter helper + chunk ablation.
- Add Redis exact cache in front of one FAQ route.
- Write a one-page runbook: re-embed + dual-write.
That sequence teaches what / why / how better than reading vendor pages alone.
What “good” looks like in a design doc
A staff-level vector design doc usually includes:
- Corpus size, growth, and delete rate
- Embedding model + dim + metric + versioning scheme
- Chunking strategy + ablation results
- Index type + knobs + recall@k gate
- Tenancy / ACL approach + isolation tests
- Hybrid / rerank plan (or explicit deferral with date)
- SLOs: ingest lag, p95 query, empty-rate
- Cost model: embed + storage + QPS
- Exit plan: rebuild from SoT in another store
If any bullet is missing, the design is not ready for production review.
Common interview traps
| Trap | Better answer |
|---|---|
| “We’ll use Pinecone because it’s AI-native” | Ops/SLA/cost/exit rationale |
| “Vectors replace search” | Hybrid for IDs; vectors for paraphrase |
| “HNSW is always best” | Trade build RAM vs IVF; measure recall |
| “Just increase k” | Packing budget + rerank; noise rises |
| “Long context kills RAG” | Complements; ACL/cost still need retrieval |
Putting what / why / how together
| Lens | Answer |
|---|---|
| What | Persistence + ANN + payloads for embeddings |
| Why | LLMs need retrieved, ACL-safe, fresh context at scale |
| How | Embed → index (HNSW/IVF) → filtered search → pack → generate; operate with evals, versioning, dual-write |
If you can teach those three rows with a diagram and a failure story, you are past buzzword RAG.
FAQ (vector databases)
Do I need a vector DB if I use a 1M-token model?
Usually yes for multi-tenant ACL, cost control, and freshness. Long context reduces how often you retrieve, not whether you need a retrieval layer.
Is FAISS a vector database?
FAISS is a library for similarity search. You still need persistence, filters, ops, and an API around it for most products.
Can I store vectors in S3 and scan them?
Only for offline batch jobs. Interactive RAG needs an ANN index in memory or a specialized engine.
What recall@k should I target?
Task-dependent. Start by measuring; many teams gate launch on recall@10 ≥ 0.85–0.95 on a frozen set — then watch for regressions.
How often do I re-embed?
When the embedding model changes, or when chunk text changes (content_hash). Not on a calendar alone.
Deep dive: filtered search under sparse ACLs
Imagine 1M vectors, but a user may only access 0.5% of them. If the engine runs global ANN top-50 then post-filters, expected surviving hits ≈ 0.25 — often empty. The model then freestyles.
Mitigations:
- Prefer filter-aware ANN (constrained graph / prefilter) documented by the vendor.
- Retrieve larger
kwhen post-filter is unavoidable — still a blunt tool. - Maintain per-tenant collections for tiny tenants with ultra-strict ACLs.
- Measure empty-rate by tenant — averages hide the sparse tenants.
Load-test with production-like selectivity, not uniform random filters.
Deep dive: embedding model upgrades without downtime
Treat the collection as immutable versioned artifacts:
docs_v3_modelA_1536_cosineserves production.- Build
docs_v4_modelB_3072_cosinefrom SoT in a worker fleet. - Shadow 1–5% of queries to v4; log hit-id Jaccard vs v3 and eval recall.
- Flip reads via config flag; keep v3 warm for 48–72h.
- Only then delete v3 after backup.
Never “UPDATE embedding SET vector = …” in place across mixed dims.
Interface contract (staff bar)
Write this in the design doc before you pick a vendor:
| Surface | Contract |
|---|---|
| Inputs | Chunk id, text, embedding (dim D, metric M), payload (tenant, ACL, doc_id, hash, model_id) |
| Outputs | Ordered hits {id, score, payload} under filters; empty is a valid outcome |
| Invariants | Query and docs share model_id + metric; filters applied before user sees hits; deletes eventually remove vectors |
| Non-goals | Understanding text, generating answers, enforcing product auth by themselves |
If the store cannot state those four rows, you do not have a production data plane — you have a demo notebook.
Three metrics that prove the layer works
- recall@k on a frozen labeled set (gate releases)
- empty-hit rate by tenant / ACL selectivity (sparse tenants)
- p95 retrieval latency (embed + ANN + hydrate), separate from LLM TTFT
Two degrade modes when the happy path fails
- Keyword / hybrid fallback — serve BM25 hits when ANN is down or empty under filters; label the path in traces
- Stale-safe cache / last-good pack — Redis exact-answer or last successful chunk pack with TTL + “may be stale” UI, never silent invent
Threat note: a poisoned document or prompt-injection in retrieved text can steer the LLM. Catch it at ingest allowlists, payload ACL filters, output citations, and eval / HITL gates — not inside the ANN index.
What / why / how — worked mini-lab (30 minutes)
Do this once so the abstractions stick:
- What: embed three sentences with the same model; print cosine between (A, paraphrase-of-A) vs (A, unrelated). Confirm paraphrase is nearer.
- Why: ask an LLM the same question with and without those two nearest chunks in context — watch hallucination disappear when evidence is present.
- How: insert the three vectors into any store (Chroma local is fine); query with a paraphrase; assert top-1 id matches. Then add a
tenant_idfilter that excludes the true hit and confirm empty or wrong — that is your ACL lesson.
Ship the notebook + a one-page diagram. That portfolio artifact beats “I used Pinecone” on a resume.
Sizing worksheet (fill before buying hardware)
| Input | Your number | Implication |
|---|---|---|
| Chunks N | Index RAM ≈ N × D × bytes/dim × overhead | |
| Dim D | 768 vs 3072 is a 4× RAM tax | |
| QPS peak | Replica / shard count | |
| Filter selectivity | May force higher k or per-tenant collections |
|
| Re-embed window | Worker fleet + dual-write disk | |
| RPO / RTO | Snapshot cadence + restore drill |
Refuse “we will tune later” as a sizing strategy. Later is an incident.
Vendor landscape (names, not a pick list)
Every product below implements the same contract: persist vectors + payloads, ANN search, filters, upsert/delete. Differences are ops model, hybrid story, and filter semantics — not magic retrieval quality.
| Engine class | Examples | Differentiator |
|---|---|---|
| Dedicated vector DB | Qdrant, Pinecone, Weaviate, Milvus | ANN-first APIs, managed tiers |
| Postgres extension | pgvector | One database; SQL joins |
| Search + kNN | Elasticsearch, OpenSearch | BM25 + vector in one cluster |
| Embedded / dev | Chroma, LanceDB, FAISS+wrapper | Fast local start; you add ops |
| Cloud managed | Vertex Vector Search, Azure AI Search | IAM + billing integration |
Ship rule: pick on SoT fit, filter semantics, hybrid needs, exit plan — then run the same recall@k harness on two finalists. Marketing pages do not replace your eval set.
Micro-project
Draw (or Mermaid) the full path document → chunk → embedding → ANN → pack → LLM. Label at least five failure points (bad chunk, wrong model, filter miss, low recall, no citation). Bring that diagram to the RAG lab.