AI SWE Prep
0/10in AI/ML Engineering
AI/ML Engineering

Vector Databases & ANN Indexes

Exact search does not scale. HNSW, IVF, and the recall-vs-latency knobs behind every production vector store.

~15 minLesson 38 of 60
Your progressNot started

Embeddings turn text into vectors. A vector database makes those vectors usable at product scale: search, filter, update, and retrieve the nearest neighbors fast enough to sit inside a user request.

The hard part is that exact nearest-neighbor search is linear. Comparing one query against ten thousand vectors is fine. Comparing it against a hundred million vectors, at interactive latency, is a different system.

The mental model

Exact k-nearest-neighbor search is conceptually simple: score every vector, sort, return the top k.

import math

def cosine(a, b):
    dot = sum(x * y for x, y in zip(a, b))
    na = math.sqrt(sum(x * x for x in a))
    nb = math.sqrt(sum(y * y for y in b))
    return dot / (na * nb)

def exact_search(query, rows, k):
    scored = [(cosine(query, row["vector"]), row) for row in rows]
    scored.sort(key=lambda item: item[0], reverse=True)
    return scored[:k]

That is O(N · d) work per query for N vectors of dimension d, plus sorting. Approximate nearest neighbor (ANN) indexes avoid scoring everything. They trade a little recall for much lower latency.

Why exact kNN stops scaling

ANN index familiestap a column to focus
Flat (exact)IVFHNSWIVF+PQ
Recall100%tunablevery highlower
Query speedslowfastfastestfast
Memoryfull vectorsfull vectorshigh (graph)compressed
Best forsmall / ground truthmedium, balancedlow-latency servingbillions, memory-bound
Approximate indexes trade a little recall for large speed and memory wins.

The brute-force scan is attractive because it is correct and operationally simple. For a small corpus, it is often the right answer. It breaks when any of these grow:

  • N — more vectors means more dot products per query.
  • d — larger embeddings make every comparison more expensive.
  • QPS — even a tolerable single-query scan becomes expensive under traffic.
  • Filters — tenant, language, ACL, and freshness constraints change which vectors are eligible.

A flat index over 50k vectors may be simpler and more reliable than a tuned ANN index. A flat scan over 500M vectors will dominate your latency budget.

ANN: search less, miss a little

ANN indexes precompute structure so the query visits a promising subset of the space. The result is not guaranteed to be the exact top k; you measure it with recall@k: of the true top k, how many did the approximate search return?

For RAG, perfect recall is not always necessary. If the answer is supported by several similar chunks, returning one good chunk may be enough. For legal or medical retrieval, missing the one decisive passage may be unacceptable.

HNSW: a navigable graph

HNSW (Hierarchical Navigable Small World) builds a graph where each vector connects to nearby vectors. Search starts from an entry point, greedily walks to closer neighbors, and refines at lower layers.

The intuition: instead of scanning a city block by block, you use highways to get near the neighborhood, then local roads to find the address.

Important knobs:

  • Graph degree — more edges improve recall but use more memory.
  • Build effort — more careful insertion improves graph quality but slows indexing.
  • Search effort — visiting more candidates improves recall but raises latency.

HNSW is a common default for low-latency, high-recall search when memory is available.

IVF: cluster first, search nearby cells

IVF (Inverted File Index) clusters the vector space into coarse partitions. Each vector belongs to a cluster. At query time, you find the nearest clusters and scan vectors inside only those clusters.

The intuition: sort books by shelf first, then search the few shelves closest to the topic instead of the whole library.

Important knobs:

  • Number of clusters — too few means each cluster is large; too many means the right vectors may be split across many cells.
  • Probes — searching more clusters improves recall and latency gets worse.
  • Training sample — bad clustering produces bad partitions.

IVF is useful when memory is tighter or when the vector store can pair it with compression.

Metadata filtering

Real retrieval almost never asks for “nearest vectors globally.” It asks for nearest vectors the user is allowed to see, in the right product area, language, time range, or document type.

There are two broad strategies:

  • Pre-filter — apply metadata constraints before vector search. Correct and safe, but may leave too few vectors for the ANN index to work well.
  • Post-filter — search broadly, then discard ineligible results. Fast and easy, but can return fewer than k results or miss eligible neighbors.

Security filters, especially tenant and ACL filters, should be enforced before the model sees text. Do not rely on the generator to ignore unauthorized chunks.

Pattern recognition

Variations

Worked problems

DesignEasy
  • ANN
  • Latency

Choose an index

You need semantic search over 80,000 help-center chunks at 5 QPS with a 150 ms retrieval budget. Later, the corpus may grow to 30 million chunks. Choose an index for the first launch and the growth path.

Approach. Start from the simplest index that meets the budget. Do not pay ANN complexity until the flat scan is near the limit, but choose an abstraction that lets you swap later.

Show solution

For 80,000 vectors, a flat index may be fine, especially with normalized vectors and a fast native library. It is exact, easy to validate, and has fewer tuning knobs. Benchmark it with realistic filters before introducing HNSW.

For 30 million chunks, use ANN. HNSW is a strong default if memory is acceptable and queries need high recall. IVF becomes attractive if memory pressure is high or you can tune cluster probes per latency tier.

def choose_index(num_vectors: int, qps: int, strict_latency_ms: int) -> str:
    if num_vectors <= 100_000 and qps <= 10 and strict_latency_ms >= 100:
        return "flat"
    if num_vectors <= 50_000_000:
        return "hnsw"
    return "ivf_or_sharded_hnsw"

Complexity. The decision function is O(1); the real validation is a benchmark. Flat search is O(N · d), while ANN is sublinear in practice but must be measured with recall.

DrillMedium
  • Recall
  • Evaluation

Reason about recall@k

An offline eval has 100 queries. For each query, you know the exact top 5 chunks from a flat scan. Your HNSW index returns 5 chunks per query. Across all queries, 390 of the 500 exact neighbors appear in the HNSW results. Compute recall@5 and say what to tune if the target is 90%.

Approach. Recall@k is recovered relevant neighbors divided by total relevant neighbors. If it is low, increase search effort or candidate count before blaming the generator.

Show solution
def recall_at_k(exact_sets, approx_sets):
    found = 0
    total = 0
    for exact, approx in zip(exact_sets, approx_sets):
        exact = set(exact)
        approx = set(approx)
        found += len(exact & approx)
        total += len(exact)
    return found / total if total else 0.0

print(390 / 500)  # 0.78

Recall@5 is 78%, below the 90% target. Tune the ANN search effort upward, retrieve more than 5 candidates and re-rank, or adjust graph/build parameters. If filters are involved, check whether pre-filtering leaves sparse partitions.

Complexity. O(q · k) for q queries and k labels per query. The metric is cheap; collecting trustworthy exact labels is the expensive part.

DesignMedium
  • Metadata
  • Security

Design filtered search

A SaaS app stores support docs for many customers. A query must retrieve only chunks from the caller’s tenant and only documents the caller’s role can access. Design the retrieval flow.

Approach. Treat authorization as part of retrieval, not generation. Push hard filters into the vector store when possible, over-retrieve if post-filtering is required, and verify every returned chunk before prompt assembly.

Show solution
def retrieve_for_user(vector_db, embed, question, user, k=5):
    query_vec = embed(question)
    filters = {
        "tenant_id": user.tenant_id,
        "allowed_roles": {"$contains": user.role},
        "status": "published",
    }

    candidates = vector_db.search(
        vector=query_vec,
        top_k=k,
        filter=filters,
        include_metadata=True,
    )

    safe = []
    for item in candidates:
        meta = item["metadata"]
        if meta["tenant_id"] == user.tenant_id and user.role in meta["allowed_roles"]:
            safe.append(item)
    return safe[:k]

Prefer pre-filtering for tenant and ACL constraints. Keep the explicit verification because bugs in query construction should fail closed. If pre-filtering hurts recall, shard by tenant or retrieve a larger candidate set within the authorized partition.

Complexity. O(log N) to sublinear in the index in practice, plus O(k) metadata checks. The important property is not asymptotic speed; it is that unauthorized text never enters the prompt.

The classic pitfall

Your progressNot started