Everything for Vector Databases in one place — pick a section below. 15 reviewed items across 4 content types.
Quick Answer
A vector database stores embeddings — dense numeric vectors capturing semantic meaning — and answers nearest-neighbor queries. RAG flow: the question is embedded by the same model that embedded the documents, the DB finds the k closest vectors via an ANN index, applies any metadata filters, and returns the associated chunks for the LLM's context.
Detailed Answer
Documents are chunked and each chunk run through an embedding model producing a vector (typically 384-3072 dimensions); vectors, chunk text, and metadata (source, tenant, timestamp, ACL) are stored together. At query time the question is embedded into the same vector space, and the database performs approximate nearest-neighbor (ANN) search — exact search is O(n) per query, so indexes like HNSW trade a little recall for orders-of-magnitude speed. Similarity is cosine/dot-product/L2 (must match how the embedding model was trained). Results usually flow through metadata filtering (tenant isolation, recency) and often a reranking stage (cross-encoder) before the top chunks enter the LLM prompt. The two invariants that break in production when violated: the same embedding model (and version) must be used at index and query time, and the distance metric must match the index configuration. Also know: 'vector database' spans dedicated engines (Qdrant, Weaviate, Milvus, Pinecone) and extensions to existing stores (pgvector, OpenSearch/Elasticsearch kNN, Redis) — the capability, not the product category, is the point.
Code Example
# same model both sides — non-negotiable
doc_vec = embed_model.encode(chunk) # at ingest
db.upsert(id, doc_vec, metadata={'tenant': t, 'src': url})
q_vec = embed_model.encode(question) # at query
hits = db.search(q_vec, top_k=8, filter={'tenant': t})Interview Tip
Say 'approximate' out loud and why (exact kNN is linear scan) — the recall/latency tradeoff is the core concept everything else in this domain hangs off.
💬 Comments
Quick Answer
HNSW is a multi-layer navigable graph: sparse upper layers for coarse routing, dense bottom layer for precision; queries greedily descend toward the target region. Tuning: M (edges per node), ef_construction (build quality), ef_search (query-time candidate list — the recall/latency dial). Costs: the whole graph wants to live in RAM, builds are expensive, and deletes/updates degrade the graph over time.
Detailed Answer
Hierarchical Navigable Small World builds a layered proximity graph — each node appears in the bottom layer and, with exponentially decreasing probability, in higher layers. Search enters at the top, greedily walks toward the query, and drops down a layer each time it reaches a local minimum, finally doing a beam search (width ef_search) at layer 0. Parameters: M controls graph connectivity (higher = better recall, more memory — typically 16-64); ef_construction controls build-time effort (higher = better graph, slower build); ef_search is the runtime knob you actually operate — raise it for recall, lower it for latency, per-query if needed. Operational realities: memory — the graph plus vectors must effectively be RAM-resident for advertised latencies (a common sizing surprise; quantization/PQ variants exist to compress); build cost — indexing millions of vectors is hours of CPU, and in pgvector an HNSW build/rebuild has historically contended with writes, so plan maintenance windows or blue/green indexes; churn — deletes are usually tombstones and heavy update traffic degrades graph quality until compaction/rebuild. Contrast with IVF (cluster-then-probe: cheaper memory, faster builds, generally lower recall at same latency) and disk-based indexes (DiskANN-style) for corpora too big for RAM.
Code Example
-- pgvector
CREATE INDEX ON chunks USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);
SET hnsw.ef_search = 100; -- session-level recall/latency dial
# qdrant: hnsw_config {m: 32, ef_construct: 256}, search params {hnsw_ef: 128}Interview Tip
Name ef_search as 'the knob you operate in production' and mention delete-induced graph degradation — those two operational details separate builders from readers.
💬 Comments
Quick Answer
Default to pgvector when vectors live next to relational data you already run: one system, real transactions, joins with business tables, and Postgres ops you know. Move to a dedicated engine when scale (tens of millions of actively-updated vectors), heavy filtered search, or recall/latency SLOs outgrow it. It's a boring-technology decision, not a benchmark contest.
Detailed Answer
The case for pgvector: your chunks' metadata is probably already relational (users, tenants, documents); keeping vectors in Postgres gives ACID upserts with the source row, SQL joins instead of application-side stitching, one backup/HA/monitoring story, and zero new infrastructure. Well-tuned pgvector HNSW serves single-digit-millisecond queries at low-millions scale — for most RAG products the embedding call dominates end-to-end latency anyway. The case for dedicated engines: native filtered ANN (filter-aware index traversal rather than pre/post-filtering), horizontal sharding/replication designed for vectors, quantization options, and better behavior under heavy vector churn — pgvector HNSW rebuilds hurt once you're rewriting millions of embeddings regularly (rule-of-thumb pain threshold around 3-4M actively-rewritten vectors). Decision inputs to name: corpus size and growth, update/delete rate, filter selectivity and cardinality, recall SLO (measure on your data — vendor benchmarks don't transfer), multi-tenancy isolation needs, and team ops maturity. Managed-vs-self-hosted is orthogonal: Pinecone-style serverless buys zero-ops at the cost of tunability (it exposes few index knobs) and per-query economics; Qdrant/Weaviate/Milvus self-hosted buy control at the cost of running them.
Code Example
-- pgvector: vectors joined with business data in one query SELECT c.text, 1 - (c.embedding <=> :qvec) AS score FROM chunks c JOIN docs d ON d.id = c.doc_id WHERE d.tenant_id = :tenant AND d.deleted_at IS NULL ORDER BY c.embedding <=> :qvec LIMIT 8;
Interview Tip
Anchor on 'vectors want to live near the data they describe' and give a concrete graduation trigger (heavy filtered search, millions of actively-rewritten vectors) — decision frameworks beat product loyalty in interviews.
💬 Comments
Quick Answer
ANN indexes are built over the whole vector space, not per filter value. Post-filtering (search then filter) returns too few results when the filter is selective; pre-filtering (filter then exact search) is correct but slow on large candidate sets; filter-aware traversal (Qdrant-style, filterable HNSW) applies conditions during graph walk — the reason filtered-search performance varies so wildly across engines.
Detailed Answer
The failure mode: you ask for top-10 with tenant_id=X. Post-filtering fetches top-k globally then drops non-matching — if tenant X owns 1% of vectors, top-100 global may contain zero of theirs, returning empty results despite relevant data existing ('k starvation'; oversampling k×100 is the crude fix and it wrecks latency). Pre-filtering selects matching IDs first then searches only those — exact and correct, but at large match counts it degenerates to brute force. Filter-aware indexes integrate predicates into traversal: the graph walk skips non-matching nodes while maintaining connectivity guarantees (Qdrant builds additional links for this; pgvector 0.8+ improved iterative scanning for filtered HNSW; engines differ enormously here — it was a documented reason teams migrated off Pinecone for filter-heavy workloads). Design responses worth naming: partition/collection-per-tenant when isolation is the dominant filter (turns filtering into routing — also better for compliance); composite strategies (coarse partition + fine filter); and testing recall under your real filters, because unfiltered benchmark recall says nothing about filtered behavior. Multi-tenancy adds the security angle: tenant filters must be enforced server-side (or by physical isolation), never assembled client-side.
Code Example
# k-starvation reproduction: selective filter + post-filtering
hits = db.search(qvec, top_k=10) # global top-10
mine = [h for h in hits if h.meta['tenant'] == 'X'] # often []
# qdrant: filter participates in traversal
client.search(collection='chunks', query_vector=qvec, limit=10,
query_filter=Filter(must=[FieldCondition(key='tenant', match=MatchValue(value='X'))]))Interview Tip
Demonstrate k-starvation with the 1%-tenant example — it makes the abstract pre/post distinction concrete and shows you've hit it. Then mention partition-per-tenant as the isolation-first alternative.
💬 Comments
Quick Answer
Work the ingestion-to-query pipeline: is the document actually indexed (ingestion gaps), does its chunking preserve the answer intact, does the embedding model match between index and query time, is ef_search/recall tuned too low, and are filters (tenant/ACL/recency) silently excluding it? Reproduce with the specific failing pair: fetch the doc's chunks directly, then test where similarity breaks.
Detailed Answer
This is a recall investigation, so make it concrete with one failing (question, known-answer-doc) pair: (1) Existence — query the DB by document ID: was it ever ingested? Ingestion pipelines fail partially all the time (parser errors on PDFs, size limits, sync lag from the source system). (2) Chunking — pull the doc's chunks and read them: does any chunk actually contain the answer coherently, or did a bad splitter shear it across boundaries / bury it in boilerplate? (3) Embedding-space sanity — embed the question and the best chunk directly and compute similarity; if it's low, the issue is semantic (chunk too long/noisy, or query vocabulary far from document vocabulary — consider query rewriting or hybrid search) rather than the index. (4) Index recall — compare ANN results against exact brute-force search on the same query; a gap means index tuning (raise ef_search, check index build params, look for degradation from heavy deletes). (5) Filters — rerun without metadata filters; ACL/tenant/date filters silently excluding the doc is extremely common. (6) Systemic checks — embedding model/version consistency between ingest and query paths, and distance-metric mismatch (cosine model queried with L2). Then institutionalize: a golden retrieval set with recall@k tracked over time, so this class of bug pages you instead of surprising users.
Code Example
# 1. is it there?
chunks = db.get(filter={'doc_id': doc})
# 3. direct similarity of question vs best chunk
print(cos(embed(q), embed(best_chunk_text)))
# 4. ANN vs exact
ann = db.search(embed(q), k=20)
exact = brute_force(embed(q), k=20) # small sample offline
print(recall(ann, exact))
# 5. filters off
db.search(embed(q), k=20, filter=None)Interview Tip
The ANN-vs-brute-force comparison is the professional's move — it cleanly splits 'index problem' from 'embedding/chunking problem' and most candidates never mention it.
💬 Comments
Quick Answer
Hybrid search runs lexical retrieval (BM25) and vector retrieval in parallel and fuses results (typically reciprocal rank fusion), often followed by a cross-encoder reranker. Pure vector search misses exact identifiers, product codes, names, and rare terms — exactly what users search for — while BM25 misses paraphrases; each covers the other's blind spots.
Detailed Answer
Embeddings compress meaning, and that compression loses lexical precision: 'error QX-4471' and 'error QX-4472' embed nearly identically, version numbers and SKUs are noise to most embedding models, and out-of-vocabulary jargon lands in unhelpful regions of the space. BM25 nails those exact matches but fails on 'how do I make the pod stop restarting' vs a doc that says 'CrashLoopBackOff remediation'. Production pattern: run both retrievers, fuse with reciprocal rank fusion (score = sum of 1/(k + rank) across lists — rank-based, so no fragile score normalization across incomparable scoring scales), take a generous candidate pool (50-100), then apply a cross-encoder reranker that scores each (query, chunk) pair jointly for a final ordering — rerankers routinely add more quality than any index tuning. Many engines ship this natively (Weaviate/OpenSearch hybrid queries, Qdrant with sparse vectors, pgvector combined with Postgres full-text search in one SQL query). Cost note: rerankers add latency/compute per query, so budget them (rerank 50, not 1000). Evaluation note: hybrid's win shows up on identifier-heavy and long-tail queries — build your eval set from real production queries or you'll under-measure the benefit.
Code Example
-- Postgres: BM25-ish (ts_rank) + vector in one shot, RRF fusion
WITH lex AS (SELECT id, row_number() OVER (ORDER BY ts_rank(tsv, q) DESC) r
FROM chunks, websearch_to_tsquery(:query) q WHERE tsv @@ q LIMIT 50),
vec AS (SELECT id, row_number() OVER (ORDER BY embedding <=> :qvec) r
FROM chunks LIMIT 50)
SELECT id, sum(1.0/(60+r)) score FROM (SELECT * FROM lex UNION ALL SELECT * FROM vec) u
GROUP BY id ORDER BY score DESC LIMIT 10;Interview Tip
Give the concrete failure ('error code QX-4471 vs QX-4472 embed identically') and name RRF's virtue — rank fusion needs no score normalization. Mentioning the reranker as the biggest quality lever is a bonus point.
💬 Comments
Context
A B2B SaaS adding an AI assistant over customer knowledge bases (~2M chunks projected). The platform team already ran HA Postgres with mature backups, monitoring, and on-call; the AI team proposed adding a managed vector DB.
Problem
A new stateful service meant new failure modes, another bill, cross-store consistency (chunks in Postgres, vectors elsewhere), and duplicated tenant-ACL logic in two systems — heavy costs for a corpus Postgres could plausibly handle.
Solution
Added pgvector to the existing cluster: chunks table with embedding vector(1536) column, HNSW index (m=16, ef_construction=200), tenant_id in every query's WHERE clause enforced by row-level security, and hybrid retrieval combining ts_rank full-text with cosine similarity via RRF in one SQL query. Ingestion upserts chunk text + vector in one transaction with the source document row, eliminating cross-store drift by construction. A nightly recall probe compares HNSW results against exact scan on a 500-query sample.
Commands
CREATE EXTENSION vector; CREATE INDEX ON chunks USING hnsw (embedding vector_cosine_ops) WITH (m=16, ef_construction=200);
ALTER TABLE chunks ENABLE ROW LEVEL SECURITY;
SET hnsw.ef_search = 80; -- tuned from recall probe
Outcome
Launched with zero new infrastructure; p95 retrieval 9ms at 2.1M vectors (embedding calls dominate end-to-end latency); tenant isolation inherited Postgres RLS and passed the security review unchanged; nightly recall probe holds at 0.97 vs exact.
Lessons Learned
Transactional co-location of chunk + vector killed a whole class of consistency bugs the team had budgeted time for. The graduation criteria are written down (sustained vector churn >1M rewrites/week or filtered-recall degradation), so a future migration is a plan, not a panic.
💬 Comments
Context
A marketplace running semantic product search (30M vectors) on a managed vector DB, with every query carrying 3-6 metadata filters (category, price band, region, availability).
Problem
Filtered queries showed degraded results and rising latency: the service's filtering strategy required heavy oversampling for selective filters, costs scaled with that oversampling, and the platform exposed no index parameters to tune recall — a documented pain point that pushes filter-heavy workloads off such services.
Solution
Migrated to self-hosted Qdrant on Kubernetes: filter-aware HNSW (payload indexes on the six filter fields), scalar quantization to fit 30M vectors in RAM budget, 3-node cluster with replication factor 2, and shard keys aligned to region for the dominant filter. Dual-write during migration with a shadow-read comparison harness (top-10 overlap + human-labeled relevance on 1K queries) gating cutover per traffic segment.
Commands
qdrant: create_collection(vectors=VectorParams(size=768, distance=COSINE), quantization_config=ScalarQuantization(...), shard_number=6, replication_factor=2)
create_payload_index(collection, field_name='category', field_schema='keyword')
python shadow_compare.py --queries prod-sample.jsonl --metric overlap@10,ndcg
Outcome
Filtered-query p95 dropped from 210ms to 28ms; relevance on filter-heavy queries improved measurably (the old oversampling path had been silently truncating results); infra cost came in ~60% below the managed bill at this scale — offset partly by real on-call ownership.
Lessons Learned
Filtered-search behavior is the least portable thing across vector engines — benchmark with your real filter distributions, never vendor demos. Self-hosting made recall tunable but bought an operational pager; the team considers that trade explicitly correct only above ~10M vectors.
💬 Comments
Context
A knowledge-assistant product with 8M indexed chunks needed to move from an older embedding model to a substantially better one (higher retrieval quality, longer context, cheaper per token).
Problem
Vectors from different embedding models live in incompatible spaces — mixing them in one index poisons retrieval. Naive in-place re-embedding would serve degraded results for the days the backfill takes, and any failure mid-way would leave an unusable hybrid index.
Solution
Blue/green index strategy: created a parallel collection stamped with the new model's identifier, backfilled by replaying the chunk store through the new embedder (rate-limited, checkpointed, resumable), and ran the golden retrieval suite plus shadow reads (same queries against both indexes, compare answer quality downstream) before atomically switching the query path's alias to the new collection. The old index stayed warm for one week as instant rollback. Embedding-model version is asserted at query time against index metadata, making cross-model queries a hard error forever after.
Commands
python backfill.py --src chunks --dst kb_v2 --model text-embed-v4 --checkpoint s3://migration/ckpt --qps 200
python eval/retrieval_golden.py --index kb_v2 --baseline kb_v1 --metric recall@8,ndcg
db.update_alias('kb_serving', point_to='kb_v2') # atomic cutoverOutcome
Cutover completed with zero user-visible downtime; golden-set recall@8 improved from 0.89 to 0.95 and downstream answer-quality scores rose accordingly; a tokenizer edge case found during shadow reads was fixed before customers ever saw it.
Lessons Learned
Treat embedding model version as index schema — the query-time assertion has since blocked two accidental mismatches from config drift. Checkpointed, resumable backfill is essential: the job died twice on rate limits and resumed without restarting 8M embeddings.
💬 Comments
Symptom
Small tenants intermittently get zero search results for queries that should obviously match their documents; large tenants are unaffected. The assistant answers 'I couldn't find anything about that' while the documents sit in the index. No errors anywhere.
Error Message
No error. Search returns HTTP 200 with an empty (or near-empty) result set for affected tenants; the same query with the filter removed returns strong matches belonging to that tenant.
Root Cause
The engine applied post-filtering: fetch global top-k by similarity, then drop rows failing tenant_id. For a tenant owning 0.5% of 20M vectors, the global top-50 rarely contained any of their vectors, so filtering yielded nothing — the relevant chunks were rank 300+ globally while being rank 1-10 within the tenant. Classic k-starvation, invisible for big tenants and in single-tenant staging.
Diagnosis Steps
Solution
Short term: oversample (top_k=2000 before filtering) — correct results at 8x the latency. Real fix: moved tenant filtering into the engine's filter-aware search path (payload-indexed filter participating in traversal); evaluated partition-per-tenant and adopted it for the largest isolation-sensitive customers. Empty-result rate for small tenants dropped to baseline.
Commands
db.search(qvec, top_k=50) # inspect global ranks
db.search(qvec, top_k=50, filter={'tenant': t}) # starved pathdb.search(qvec, top_k=5000, filter={'tenant': t}) # oversampling proofPrevention
Test retrieval with production-shaped tenant skew — staging with one tenant can't exhibit the bug. Track empty-result rate segmented by tenant size as a standing metric with alerts. When evaluating any vector engine, benchmark filtered recall at your real filter selectivities, not the unfiltered ANN numbers vendors publish.
💬 Comments
Symptom
During a scheduled re-index to apply new HNSW parameters, the document-ingestion pipeline backs up: upserts to the chunks table block, queue depth alarms fire, and users stop seeing newly uploaded documents in search for the duration of the build (~3 hours at 12M vectors).
Error Message
PostgreSQL: 'canceling statement due to lock timeout' on chunk upserts; pg_stat_activity shows writers waiting on the CREATE INDEX transaction's locks
Root Cause
CREATE INDEX (non-concurrent) on the chunks table takes locks that block writes for the whole multi-hour HNSW build — expensive by nature (graph construction over 12M vectors). The team had treated an HNSW rebuild like a routine small-table index change; write-blocking behavior during long vector index builds is a known pgvector operational hazard, and CREATE INDEX CONCURRENTLY, while avoiding the hard block, runs even longer and can still fail partway needing cleanup.
Diagnosis Steps
Solution
Aborted the blocking build; re-ran as CREATE INDEX CONCURRENTLY overnight with maintenance_work_max and parallel workers tuned, plus a queue-drain plan for the slower-but-nonblocking window. Adopted blue/green for future parameter changes: build the new index alongside (different name), validate recall, then atomically swap usage and drop the old — ingestion never blocks.
Commands
SELECT pid, wait_event_type, query FROM pg_stat_activity WHERE wait_event_type='Lock';
SELECT * FROM pg_stat_progress_create_index;
CREATE INDEX CONCURRENTLY chunks_emb_v2 ON chunks USING hnsw (embedding vector_cosine_ops) WITH (m=24, ef_construction=300);
Prevention
Classify vector index rebuilds as maintenance events with change-management windows, not routine DDL. Prefer additive blue/green index swaps over in-place rebuilds. Monitor bloat/recall drift to schedule rebuilds proactively (heavy update/delete churn degrades HNSW quality), and rehearse the rebuild on a restored snapshot to know its true duration before scheduling.
💬 Comments
Symptom
RAG answer quality drifts down gradually — more 'not found' complaints, eval scores off ~6 points from launch — with no deploys to blame. Latency is normal; the index 'works'. The corpus has high churn: nightly syncs delete and re-add ~5% of chunks.
Error Message
No error. Golden-set recall@8 trended from 0.96 to 0.88 over six weeks; ANN-vs-exact comparison shows the gap concentrated in heavily-churned document segments.
Root Cause
HNSW handles deletes as tombstones/unlinking rather than restructuring the graph; sustained delete/re-insert churn accumulates dead nodes and suboptimal links, degrading navigability and recall over time. Nothing surfaces this by default — the index answers queries confidently from a slowly rotting graph, and vacuum/optimizer processes weren't configured for vector-index hygiene.
Diagnosis Steps
Solution
Immediate: raised ef_search to buy back recall at some latency cost. Structural: scheduled periodic index maintenance — engine-native optimization/compaction where supported, blue/green rebuilds where not (build fresh index from live vectors, validate recall on the golden set, swap atomically). Recall probes (nightly ANN-vs-exact on a sampled query set) became a first-class SLO metric with alerting at 2% degradation.
Commands
python probe_recall.py --sample 500 --compare exact --alert-below 0.94
qdrant: POST /collections/kb/index # or engine-native optimize/compact
SET hnsw.ef_search = 160; -- stopgap while rebuilding
Prevention
Any high-churn corpus needs (1) a standing recall probe against exact search — recall is invisible without measurement; (2) scheduled index hygiene proportional to churn rate; (3) vendor/engine due diligence on delete handling (tombstone accumulation, when compaction triggers, whether rebuilds block writes). Treat 'index freshness' like you treat database bloat.
💬 Comments