PROMETHEUS OS
NATIVE AI OPERATING SYSTEM
[INIT] Prometheus OS loading...
[CORE] Neural interface initializing...
[AI] LLM router connecting...
[MEM] Memory graph loading...
[AGT] Agent chains on standby...
[SEC] Security protocols active...
[NET] Provider mesh established...
[UI] Workspace rendering...
[DONE] All systems nominal.
Welcome to PROMETHEUS OS.
READY · OFFLINE · SECURE · PRIVATE
🎨 THEME ENGINE
PROMETHEUS OS · 23 THEMES
Trial version: Dark + Light themes only.
Full theme access with PROMETHEUS OS license.
Dark theme active
Experience all 23 themes in PROMETHEUS OS

Vector Databases and Indexing

Advanced 22 min· 4 exercises

Choose a similarity metric, an index type (flat, IVF, HNSW, PQ) and a vector store for a given corpus, tune the recall-latency trade-off, and size the memory it needs.

By the end of this lesson you will be able to
  • Explain when cosine similarity, dot product and L2 distance produce identical rankings, and derive why from the expansion of the squared distance.
  • Compare exact search with IVF, HNSW and product-quantized indexes in terms of recall, latency, memory and update cost.
  • Describe how HNSW builds and searches its layered graph and what M, efConstruction and efSearch control.
  • Calculate the memory footprint of a vector index for given N, dimensions, precision and index overhead.
  • Implement exact search and recall@k measurement in numpy and an HNSW index in FAISS.
  • Choose between a library, a database extension and a dedicated vector database, and a metadata-filtering strategy, for a given workload.

Before this: Embeddings and Retrieval-Augmented Generation, Linear Algebra for AI

Every retrieval-augmented system eventually has to answer one question millions of times a day: "which of my stored vectors are closest to this one?" Answering it exactly is easy for ten thousand vectors and ruinously slow for a billion. Vector indexes are the data structures that make the question cheap, and vector databases wrap them with storage, filtering, updates and replication. This lesson takes you from the geometry of embeddings to the internals of HNSW and product quantization, and ends with how to size and choose a system.

The picture: a library with no catalogue

Imagine a library where every book has been placed in a vast room according to its meaning: cookbooks cluster in one corner, tax law in another, and books about Italian tax law on cooking equipment sit somewhere in between. A question is also placed in the room, and your job is to find the nearest books. The exact method is to walk to every book and measure the distance. An index is a shortcut: you might divide the room into neighbourhoods and only search the few nearest your question (IVF), or you might follow signposts from book to book, always stepping toward the question, starting with long-distance express routes and switching to local streets as you get close (HNSW). Both shortcuts occasionally miss a book that was in fact the closest. That miss rate, traded against speed and memory, is the central engineering trade-off of this lesson.

Embeddings: models and dimensions

An embedding model maps a piece of content (a sentence, a passage, an image) to a fixed-length vector x∈Rd\mathbf{x} \in \mathbb{R}^d, where dd is the dimension. Retrieval embedding models are usually transformer encoders trained with a contrastive objective so that a query and a passage that answers it end up close, and unrelated pairs end up far apart (see the prerequisite lesson on embeddings and RAG).

Practical facts that matter for indexing:

  • Dimensions for text embedding models commonly range from a few hundred to a few thousand. The widely used open model all-MiniLM-L6-v2 produces 384-dimensional vectors; many larger models use 768, 1024 or more. Memory and search cost grow linearly with dd.
  • Precision. Vectors are produced as 32-bit floats (4 bytes per dimension). Many systems store them as 16-bit floats, 8-bit integers, or even 1 bit per dimension (binary quantization), usually with a recall cost that should be measured, not assumed.
  • Truncatable embeddings. Models trained with Matryoshka Representation Learning (Kusupati et al., 2022) put the most important information in the leading dimensions, so you can keep, say, the first 256 of 1,024 dimensions and re-normalize, trading a little quality for a 4x smaller index.
  • Query and document must use the same model (and often a model-specific prefix or instruction for queries versus passages). Changing the model means re-embedding the entire corpus: vectors from different models live in unrelated spaces.

Similarity metrics and when they are equivalent

Three scores dominate. For vectors q\mathbf{q} (query) and x\mathbf{x} (stored item):

dot(q,x)=q⋅x=∑i=1dqixi\text{dot}(\mathbf{q},\mathbf{x}) = \mathbf{q}\cdot\mathbf{x} = \sum_{i=1}^{d} q_i x_i cos⁡(q,x)=q⋅x∥q∥ ∥x∥\cos(\mathbf{q},\mathbf{x}) = \frac{\mathbf{q}\cdot\mathbf{x}}{\lVert\mathbf{q}\rVert\,\lVert\mathbf{x}\rVert} L2(q,x)=∥q−x∥=∑i(qi−xi)2\text{L2}(\mathbf{q},\mathbf{x}) = \lVert\mathbf{q}-\mathbf{x}\rVert = \sqrt{\textstyle\sum_{i}(q_i-x_i)^2}

Higher dot and cosine mean more similar; lower L2 distance means more similar. Expanding the squared distance gives the key identity:

∥q−x∥2=∥q∥2+∥x∥2−2 q⋅x\lVert\mathbf{q}-\mathbf{x}\rVert^2 = \lVert\mathbf{q}\rVert^2 + \lVert\mathbf{x}\rVert^2 - 2\,\mathbf{q}\cdot\mathbf{x}

If every vector is unit-normalized (∥x∥=1\lVert\mathbf{x}\rVert = 1), this becomes ∥q−x∥2=2−2 q⋅x\lVert\mathbf{q}-\mathbf{x}\rVert^2 = 2 - 2\,\mathbf{q}\cdot\mathbf{x}, and the dot product equals the cosine. So for normalized vectors, all three metrics produce the same ranking. That is why the standard practice is: normalize once at ingestion and at query time, then use inner product (often the fastest kernel).

Check with numbers: a=(0.6,0.8)\mathbf{a} = (0.6, 0.8) and b=(0.8,0.6)\mathbf{b} = (0.8, 0.6) are both unit length. Their dot product is 0.48+0.48=0.960.48 + 0.48 = 0.96. Squared L2 distance is (−0.2)2+(0.2)2=0.08(-0.2)^2 + (0.2)^2 = 0.08, and indeed 2−2(0.96)=0.082 - 2(0.96) = 0.08.

When vectors are not normalized, the metrics disagree. Take q=(3,4)\mathbf{q} = (3,4), x1=(6,8)\mathbf{x}_1 = (6,8), x2=(4,3)\mathbf{x}_2 = (4,3):

Metricx1=(6,8)\mathbf{x}_1=(6,8)x2=(4,3)\mathbf{x}_2=(4,3)Winner
Dot18+32=5018+32 = 5012+12=2412+12 = 24x1\mathbf{x}_1
Cosine50/(5⋅10)=1.0050/(5 \cdot 10) = 1.0024/(5⋅5)=0.9624/(5 \cdot 5) = 0.96x1\mathbf{x}_1
L29+16=5.00\sqrt{9+16} = 5.001+1≈1.41\sqrt{1+1} \approx 1.41x2\mathbf{x}_2

x1\mathbf{x}_1 points in exactly the same direction as q\mathbf{q} but is twice as long; L2 penalizes that length difference, cosine ignores it, and the dot product rewards it. Use the metric the embedding model was trained with (its model card says so). Some models deliberately encode information in the norm, in which case unnormalized inner product is correct.

Exact versus approximate nearest neighbour

Exact k-nearest-neighbour (kNN) search computes the score against all NN vectors and keeps the top kk. Cost per query is O(Nd)O(Nd) multiply-adds. With N=106N = 10^6 and d=768d = 768 that is about 7.7×1087.7 \times 10^8 multiply-adds per query, which a modern CPU does in tens of milliseconds and a GPU much faster. Exact search is perfectly reasonable up to roughly a few million vectors, or for any corpus where queries are batched, and it is the ground truth you measure everything else against.

Approximate nearest neighbour (ANN) search uses an index to examine only a small fraction of vectors. Quality is measured by recall@k: the fraction of the true top-kk (from exact search) that the ANN search returns,

recall@k=∣ANNk(q)∩Exactk(q)∣k.\text{recall@}k = \frac{|\text{ANN}_k(\mathbf{q}) \cap \text{Exact}_k(\mathbf{q})|}{k}.

Note this is index recall (did the index find the true nearest vectors), not end-to-end retrieval recall (did we find the documents a human judges relevant). Both matter; they are covered in the advanced retrieval lesson.

Index types

Flat (brute force)

Store vectors in a contiguous array and scan them all. No training, no build time, 100% recall, trivially supports deletes. Memory is exactly N×d×N \times d \times bytes per value.

IVF: inverted file with coarse clustering

Run k-means on (a sample of) the data to get nlistn_\text{list} centroids. Each vector is assigned to its nearest centroid and stored in that centroid's inverted list. At query time, compare the query to all centroids, pick the nproben_\text{probe} closest, and scan only those lists.

  • Work per query is about nlist+nprobe⋅N/nlistn_\text{list} + n_\text{probe} \cdot N / n_\text{list} vector comparisons. With N=106N = 10^6, nlist=1024n_\text{list} = 1024 and nprobe=16n_\text{probe} = 16, that is roughly 1,024+16×977≈16,7001{,}024 + 16 \times 977 \approx 16{,}700 comparisons instead of 10610^6, about 60 times less work.
  • A common starting point is nlistn_\text{list} on the order of N\sqrt{N} (more for large corpora), then tune nproben_\text{probe} for the recall you need.
  • Failure mode: a true neighbour sitting just across a cluster boundary is missed unless its cluster is probed. Raising nproben_\text{probe} fixes this at the cost of speed.
  • IVF needs a training step, and if the data distribution drifts far from the training sample, the clusters become unbalanced and recall or latency degrade. Periodic retraining is part of operating it.

HNSW: hierarchical navigable small-world graphs

HNSW (Malkov and Yashunin, arXiv 2016) is the most widely deployed in-memory ANN index. It is a proximity graph: each vector is a node linked to some of its near neighbours. Search is greedy graph traversal: start at an entry node, look at its neighbours, move to whichever is closest to the query, and repeat until no neighbour is closer.

A single flat graph has two problems: greedy search can get stuck in a local minimum, and reaching a far-away region takes many hops. HNSW fixes both with a hierarchy, much like a skip list:

  1. Layers. Each inserted node draws a maximum layer ℓ\ell from an exponentially decaying distribution, so every node is in layer 0, a fraction is also in layer 1, a smaller fraction in layer 2, and so on. Upper layers are sparse "express routes"; layer 0 contains everything.
  2. Search. Start at the single entry point in the top layer. Greedily descend: in each upper layer, walk to the closest node you can reach, then drop to the next layer starting from there. In layer 0, run a beam search that keeps a dynamic candidate list of size efsearchef_\text{search}: repeatedly expand the closest unexpanded candidate, add its unseen neighbours, and keep the best efsearchef_\text{search} found so far. Stop when the closest unexpanded candidate is worse than the worst kept result. Return the top kk (so efsearch≥kef_\text{search} \ge k).
  3. Insertion. A new node runs the same search to find its neighbourhood (using a candidate list of size efconstructionef_\text{construction}) on each layer it belongs to, then links to up to MM neighbours, chosen with a heuristic that prefers diverse directions rather than MM near-duplicates. Neighbours whose link lists overflow are pruned.

The three parameters you will tune:

ParameterWhat it controlsRaising it
MMMax links per node per layer (implementations commonly allow 2M2M on layer 0)Higher recall, more memory, slower build; typical 16 to 64
efconstructionef_\text{construction}Candidate list size while buildingBetter graph quality and recall, slower build; typical 100 to 500
efsearchef_\text{search}Candidate list size while queryingHigher recall, higher latency; tune per workload, can change per query

HNSW gives excellent recall-latency curves and supports incremental inserts without retraining. Its costs are memory (the vectors stay uncompressed in RAM, plus the links), slow builds for large corpora, and awkward deletes (most implementations mark deleted nodes as tombstones and rebuild or repair periodically).

Product quantization (PQ)

PQ (Jégou, Douze and Schmid, 2011) compresses vectors so that far more fit in memory. Split each dd-dimensional vector into mm sub-vectors of d/md/m dimensions. For each sub-space, run k-means to learn a codebook of 2b2^{b} centroids (usually b=8b = 8, so 256 centroids). Each sub-vector is replaced by the index of its nearest centroid, so a vector becomes mm codes of bb bits: with d=768d = 768, m=96m = 96, b=8b = 8, a vector shrinks from 3,0723{,}072 bytes to 9696 bytes, a 32x reduction.

Distances are computed asymmetrically: the query stays at full precision. For each sub-space, precompute a lookup table of the distance from the query's sub-vector to all 256 centroids; the approximate distance to any stored vector is then the sum of mm table lookups. PQ is lossy, so it is usually combined with IVF (FAISS's IndexIVFPQ) and followed by re-ranking: fetch a few hundred candidates by PQ distance, then rescore them with the exact vectors (kept on disk or in cheaper memory).

Other families worth knowing

  • Scalar quantization (float32 to int8 per dimension): 4x compression, often small recall loss, simple.
  • Binary quantization: 1 bit per dimension, 32x compression, Hamming distance is extremely fast; typically used as a first stage with full-precision rescoring.
  • Disk-based graph indexes (for example the DiskANN family) keep compressed vectors in RAM and the graph plus full vectors on SSD, serving billion-scale corpora from a single machine.

The recall versus latency trade-off

Every ANN index exposes a knob (nproben_\text{probe}, efsearchef_\text{search}, number of rescored candidates) that moves you along a curve: more work per query buys higher recall. The professional workflow is:

  1. Build a held-out set of real queries and compute exact top-kk with a flat index.
  2. Sweep the knob and plot recall@kk against p50 and p99 latency (and throughput, queries per second, under realistic concurrency).
  3. Pick the cheapest setting that meets your recall target, often 0.95 to 0.99 for RAG, and re-check whenever data volume or distribution changes.

The code below makes this concrete with a toy IVF index written in numpy on clustered synthetic data.

import numpy as np

rng = np.random.default_rng(0)
N, d, k = 20_000, 64, 10
# Clustered synthetic data (real embeddings are clustered, not uniform noise)
centers = rng.standard_normal((200, d))
X = centers[rng.integers(0, 200, N)] + 1.5 * rng.standard_normal((N, d))
X = (X / np.linalg.norm(X, axis=1, keepdims=True)).astype(np.float32)
Q = centers[rng.integers(0, 200, 100)] + 1.5 * rng.standard_normal((100, d))
Q = (Q / np.linalg.norm(Q, axis=1, keepdims=True)).astype(np.float32)

def exact_search(Q, X, k):
    S = Q @ X.T                                    # (nq, N) cosine similarities
    idx = np.argpartition(-S, k, axis=1)[:, :k]    # unordered top-k per row
    order = np.argsort(-np.take_along_axis(S, idx, 1), axis=1)
    return np.take_along_axis(idx, order, 1)

truth = exact_search(Q, X, k)                      # ground truth for recall

Because all vectors are unit length, the matrix product Q @ X.T gives cosine similarities directly. argpartition finds the top kk in linear time, and only those kk are sorted. Now the IVF index:

def kmeans(X, nlist, iters=10):
    C = X[rng.choice(len(X), nlist, replace=False)].copy()
    for _ in range(iters):
        assign = np.argmax(X @ C.T, axis=1)        # nearest centroid by dot
        for c in range(nlist):
            m = X[assign == c]
            if len(m):
                C[c] = m.mean(0) / np.linalg.norm(m.mean(0))
    return C, np.argmax(X @ C.T, axis=1)

nlist = 128
C, assign = kmeans(X, nlist)
lists = [np.where(assign == c)[0] for c in range(nlist)]

def ivf_search(q, k, nprobe):
    probe = np.argsort(-(C @ q))[:nprobe]          # nprobe closest centroids
    cand = np.concatenate([lists[c] for c in probe])
    return cand[np.argsort(-(X[cand] @ q))[:k]], len(cand)

for nprobe in (1, 4, 16, 64):
    hits = scanned = 0
    for i, q in enumerate(Q):
        found, n = ivf_search(q, k, nprobe)
        hits += len(set(found) & set(truth[i])); scanned += n
    print(f"nprobe={nprobe:3d}  recall@10={hits / (k * len(Q)):.3f}  "
          f"scanned={scanned / (len(Q) * N):.1%}")

On one run this printed recall@10 of about 0.59 when scanning under 1% of the vectors, 0.86 at about 12.5%, and 0.985 at 50%: exactly the shape of the trade-off curve. Real embeddings are usually more clustered than this deliberately noisy data, so production indexes reach high recall while scanning far less. The same experiment with FAISS, which you would use in practice:

import faiss  # pip install faiss-cpu

d = X.shape[1]
flat = faiss.IndexFlatIP(d); flat.add(X)                  # exact, inner product
hnsw = faiss.IndexHNSWFlat(d, 32, faiss.METRIC_INNER_PRODUCT)  # M = 32
hnsw.hnsw.efConstruction = 200
hnsw.add(X)
hnsw.hnsw.efSearch = 64                                   # query-time knob
_, I_exact = flat.search(Q, 10)
_, I_hnsw = hnsw.search(Q, 10)
recall = np.mean([len(set(a) & set(b)) / 10 for a, b in zip(I_exact, I_hnsw)])
print(f"HNSW recall@10 = {recall:.3f}")

FAISS expects contiguous float32 arrays; call faiss.normalize_L2(X) in place if your vectors are not already normalized and you want cosine similarity.

Worked example: sizing memory

You must index N=10N = 10 million chunks with a 768-dimensional model.

Raw float32 vectors: N×d×4=107×768×4=3.072×1010N \times d \times 4 = 10^7 \times 768 \times 4 = 3.072 \times 10^{10} bytes =30.72= 30.72 GB (about 28.6 GiB).

HNSW overhead with M=32M = 32: layer 0 holds up to 2M=642M = 64 neighbour IDs per node at 4 bytes each, so 256256 bytes per vector, or 2.562.56 GB. Upper layers add only a few percent more because few nodes reach them. Total roughly 30.72+2.56≈33.330.72 + 2.56 \approx 33.3 GB, before IDs, metadata and the database's own overhead. Plan for a machine with comfortably more than that, since build time needs headroom too. (Exact overhead is implementation-dependent; measure it.)

Cheaper alternatives:

OptionBytes per vectorTotal for 10710^7Notes
float32 flat3,07230.7 GBExact
float161,53615.4 GBUsually negligible loss
Truncate to 256 dims (Matryoshka model), float321,02410.2 GBOnly if model supports it
int8 scalar quantization7687.7 GBRescore top candidates
IVF-PQ, m=96m = 96, 8 bits, plus 8-byte IDs1041.04 GBPlus a few MB of codebooks; rescore
Binary, 1 bit per dim960.96 GBFirst stage only

The PQ codebooks are tiny: 9696 sub-spaces ×\times 256256 centroids ×\times 88 dims ×\times 4 bytes =786,432= 786{,}432 bytes, under 1 MB. That table is the core of capacity planning: pick the cheapest row whose measured recall (after rescoring) meets your target.

Metadata filtering

Real queries come with constraints: "only documents this user may see", "only 2025 contracts", "language = de". Three strategies:

  • Post-filtering: run ANN for top kk, then drop items failing the filter. Simple, but if the filter is selective (say 1% of the corpus passes), most of your kk results are discarded and you may return nothing. Over-fetching (k′≫kk' \gg k) helps only partially.
  • Pre-filtering: compute the set of allowed IDs first, then search only among them. Exact for selective filters (brute force over the allowed subset is cheap), but naive graph search over a sparse allowed subset breaks connectivity.
  • Filter-aware search: the index checks the filter during traversal, skipping disallowed nodes while still using them for navigation, or maintains per-value partitions. Most mature vector databases implement some form of this; behaviour with highly selective filters is exactly what to benchmark.

For access control, never rely on post-filtering by the LLM: enforce permissions in the retrieval query itself, so unauthorized text never enters the prompt.

Hybrid indexes

Dense vectors capture meaning but are weak on exact tokens: part numbers, error codes, rare names. Sparse lexical indexes (BM25 over an inverted index, or learned sparse vectors) excel there. Hybrid search runs both and fuses the results; many systems now store dense and sparse representations side by side and fuse in one query. The fusion methods are covered in the advanced retrieval lesson.

Choosing a vector store

The options fall into three groups (feature sets change quickly, so verify against current documentation before committing):

  • Libraries such as FAISS (Meta) and hnswlib: in-process index implementations with no server, persistence layer or access control. Ideal for research, batch jobs and as the engine inside other systems.
  • Extensions to databases you already run, such as pgvector for PostgreSQL, which adds a vector column type, distance operators, and HNSW and IVFFlat index types. Elasticsearch, OpenSearch and several other databases offer comparable vector search. Big advantage: vectors live next to your relational data, with transactions, joins, backups and row-level security you already understand.
  • Dedicated vector databases (for example Milvus, Qdrant, Weaviate, Pinecone and Chroma): purpose-built for vector workloads, with features such as filtered ANN, multi-tenancy, quantization options, hybrid search and horizontal scaling.

A practical rule: if your corpus is in the low millions of vectors and you already operate PostgreSQL, start with pgvector; move to a dedicated system when measured scale, filtering performance or operational needs demand it.

Common pitfalls

  • Mixing metrics. Building an index with L2 on unnormalized vectors from a model trained for cosine silently degrades ranking.
  • Forgetting to normalize the query after normalizing the corpus.
  • Mixing embedding models or versions in one index. Re-embed everything when the model changes, and record the model ID with each vector.
  • Tuning on random queries instead of real ones. Recall on synthetic queries says little about production.
  • Measuring average latency only. Users feel p99, and p99 grows under concurrency and with filters.
  • Selective filters with post-filtering, returning empty or poor results.
  • Unbounded HNSW deletes. Tombstones accumulate, recall and memory degrade; schedule compaction or rebuilds.
  • Undersized RAM. An HNSW index that spills to swap is orders of magnitude slower.

In practice

  • Start with exact search as the baseline and ground truth; only add ANN when latency or cost demands it.
  • Normalize vectors and use inner product unless the model says otherwise.
  • For HNSW, a sensible start is M=16M = 16 to 3232, efconstruction=200ef_\text{construction} = 200, then sweep efsearchef_\text{search} against your recall target.
  • Store the full-precision vectors somewhere so you can rescore after quantization and re-index without re-embedding.
  • Version your index: embedding model, chunking settings and index parameters together, so you can roll back.
  • Benchmark with your real filters, real query mix and realistic concurrency before choosing a product.

Key takeaways

  • For unit-normalized vectors, cosine, dot product and L2 distance give the same ranking, so normalize once and use inner product unless the model says otherwise.
  • Exact search is the ground truth and is often fast enough up to a few million vectors; add ANN only when latency or cost requires it.
  • Every ANN index has a knob (nprobe, efSearch, rescoring depth) that trades recall for latency; tune it on real queries against exact results.
  • HNSW gives excellent recall and latency but keeps full vectors plus links in RAM; IVF-PQ and other quantization trade some recall for 10 to 30 times less memory.
  • Memory is roughly N times dimensions times bytes per value, plus index overhead; size it before choosing hardware or a service.
  • Enforce filters, especially permissions, inside the retrieval query, and benchmark filtered queries because selective filters break naive ANN.

Terms in this lesson

Embedding dimension
The length d of the vector an embedding model outputs, which sets per-vector memory and search cost.
Approximate nearest neighbour (ANN)
Search that returns most, but not necessarily all, of the true nearest vectors while examining only a fraction of the collection.
Recall@k (index)
The fraction of the exact top-k neighbours that an approximate search also returns in its top k.
IVF (inverted file index)
An index that clusters vectors with k-means and searches only the lists of the nprobe centroids closest to the query.
HNSW
Hierarchical Navigable Small World, a layered proximity graph searched greedily from sparse upper layers down to a dense bottom layer.
efSearch
The size of the candidate list HNSW keeps during a query; larger values raise recall and latency.
Product quantization (PQ)
A compression scheme that splits vectors into sub-vectors and replaces each with the ID of its nearest centroid in a learned per-sub-space codebook.
Pre-filtering
Restricting the search to items that satisfy metadata constraints before or during the similarity search, rather than discarding results afterwards.

Check your understanding

  1. 1.

    Two unit-length vectors have a dot product of 0.9. What is their squared Euclidean distance?

  2. 2.

    Your HNSW index meets latency targets but recall@10 is 0.91 and you need 0.97. Which change requires no rebuild?

  3. 3.

    Only 0.5% of documents pass a user's permission filter. The system takes the ANN top 100 and then post-filters to return 10. What is the most likely outcome?

  4. 4.

    1,024-dimensional float32 vectors are compressed with PQ using m = 64 sub-quantizers of 8 bits each. What is the compression ratio of the codes?

  5. 5.

    Which statement about an IVF index is correct?

Exercises

Work each problem before opening the solution.

  1. Exercise 1Core

    You will index 50 million chunks embedded with a 1,024-dimensional model. Compute the memory for: (a) float32 flat vectors; (b) float16 flat vectors; (c) the HNSW link overhead with M=16M = 16, assuming 2M2M 4-byte neighbour IDs per node on layer 0 and ignoring upper layers; (d) IVF-PQ codes with m=128m = 128 sub-quantizers at 8 bits plus an 8-byte ID per vector. Use 1 GB = 10910^9 bytes.

    Show solution

    (a) 5×107×1,024×4=2.048×10115\times10^7 \times 1{,}024 \times 4 = 2.048\times10^{11} bytes =204.8= 204.8 GB.

    (b) Half of that: 5×107×1,024×2=102.45\times10^7 \times 1{,}024 \times 2 = 102.4 GB.

    (c) 2M=322M = 32 links ×\times 4 bytes =128= 128 bytes per node; 5×107×128=6.45\times10^7 \times 128 = 6.4 GB on top of the vectors, so HNSW over float32 needs roughly 211211 GB before metadata and headroom.

    (d) 128+8=136128 + 8 = 136 bytes per vector; 5×107×136=6.85\times10^7 \times 136 = 6.8 GB, plus negligible codebooks (128×256×8×4=1,048,576128 \times 256 \times 8 \times 4 = 1{,}048{,}576 bytes, about 1 MB) and the coarse centroids. IVF-PQ fits on one ordinary server; float32 HNSW needs a very large-memory machine or sharding.

  2. Exercise 2Core

    Let q=(0,1)\mathbf{q} = (0, 1), x1=(0.6,0.8)\mathbf{x}_1 = (0.6, 0.8) and x2=(2,2)\mathbf{x}_2 = (2, 2). (a) Rank x1,x2\mathbf{x}_1, \mathbf{x}_2 by dot product, cosine and L2 distance. (b) Normalize x2\mathbf{x}_2 and rank again. (c) Explain the result using the identity ∥q−x∥2=∥q∥2+∥x∥2−2 q⋅x\lVert\mathbf{q}-\mathbf{x}\rVert^2 = \lVert\mathbf{q}\rVert^2 + \lVert\mathbf{x}\rVert^2 - 2\,\mathbf{q}\cdot\mathbf{x}.

    Show solution

    (a) Dot: q⋅x1=0.8\mathbf{q}\cdot\mathbf{x}_1 = 0.8, q⋅x2=2\mathbf{q}\cdot\mathbf{x}_2 = 2, so x2\mathbf{x}_2 wins. Cosine: x1\mathbf{x}_1 has norm 1, cosine 0.80.8; x2\mathbf{x}_2 has norm 22≈2.8282\sqrt2 \approx 2.828, cosine 2/2.828=0.7072/2.828 = 0.707, so x1\mathbf{x}_1 wins. L2: ∥(−0.6,0.2)∥=0.40=0.632\lVert(-0.6, 0.2)\rVert = \sqrt{0.40} = 0.632; ∥(−2,−1)∥=5=2.236\lVert(-2, -1)\rVert = \sqrt5 = 2.236, so x1\mathbf{x}_1 wins. Dot product disagrees with the other two because it rewards the large norm of x2\mathbf{x}_2.

    (b) x^2=(0.707,0.707)\hat{\mathbf{x}}_2 = (0.707, 0.707). Dot = cosine =0.707= 0.707 (below 0.80.8), L2 =0.7072+0.2932=0.586=0.765= \sqrt{0.707^2 + 0.293^2} = \sqrt{0.586} = 0.765 (above 0.6320.632). All three now rank x1\mathbf{x}_1 first.

    (c) With ∥q∥=∥x∥=1\lVert\mathbf{q}\rVert = \lVert\mathbf{x}\rVert = 1 the identity becomes 2−2 q⋅x2 - 2\,\mathbf{q}\cdot\mathbf{x}, a strictly decreasing function of the dot product, and the dot product equals the cosine. Hence the three orderings must agree. Without normalization the ∥x∥2\lVert\mathbf{x}\rVert^2 term differs between candidates and breaks the equivalence.

  3. Exercise 3Core

    Write a numpy function topk(X, q, k, metric) supporting 'dot', 'cosine' and 'l2'. Demonstrate on random vectors with varying norms that the three metrics can return different top-5 results, and that after normalizing all vectors they return identical results.

    Show solution
    import numpy as np
    
    def topk(X, q, k, metric):
        if metric == "dot":
            s = X @ q
        elif metric == "cosine":
            s = (X @ q) / (np.linalg.norm(X, axis=1) * np.linalg.norm(q))
        elif metric == "l2":
            s = -np.linalg.norm(X - q, axis=1)   # negate: smaller distance = better
        else:
            raise ValueError(metric)
        return np.argsort(-s)[:k]
    
    rng = np.random.default_rng(1)
    X = rng.standard_normal((1000, 32)) * rng.uniform(0.5, 3, (1000, 1))  # varied norms
    q = rng.standard_normal(32)
    print({m: topk(X, q, 5, m).tolist() for m in ("dot", "cosine", "l2")})
    
    Xn = X / np.linalg.norm(X, axis=1, keepdims=True)
    qn = q / np.linalg.norm(q)
    print({m: topk(Xn, qn, 5, m).tolist() for m in ("dot", "cosine", "l2")})
    

    The first print shows three different lists: dot favours long vectors, L2 favours vectors whose norm is close to the query's, and cosine ignores norms. The second print shows three identical lists, as the identity ∥q−x∥2=2−2 q⋅x\lVert\mathbf{q}-\mathbf{x}\rVert^2 = 2 - 2\,\mathbf{q}\cdot\mathbf{x} for unit vectors predicts. Multiplying each row by a random scale factor is what makes the norms vary; with plain Gaussian vectors in 32 dimensions the norms are similar and the lists often overlap heavily even before normalization.

  4. Exercise 4Challenge

    You must serve 100 million 768-dimensional vectors from one machine with a 16 GB RAM budget for the index, targeting recall@10 of at least 0.95. (a) Show that float32 HNSW cannot fit. (b) Propose an IVF-PQ configuration with nlist=65,536n_\text{list} = 65{,}536 and estimate RAM for the codes, IDs and centroids. (c) With nprobe=32n_\text{probe} = 32, estimate the number of vector comparisons per query and the fraction of the corpus scanned. (d) Explain how you would reach the recall target and verify it.

    Show solution

    (a) Vectors alone: 108×768×4=307.210^8 \times 768 \times 4 = 307.2 GB, about 19 times the budget, before graph links.

    (b) Choose m=64m = 64 sub-quantizers at 8 bits: 64 bytes of code + 8 bytes of ID = 72 bytes per vector, so 108×72=7.210^8 \times 72 = 7.2 GB. Coarse centroids: 65,536×768×4=201,326,59265{,}536 \times 768 \times 4 = 201{,}326{,}592 bytes ≈0.2\approx 0.2 GB. PQ codebooks: 64×256×12×4=786,43264 \times 256 \times 12 \times 4 = 786{,}432 bytes, under 1 MB. Total about 7.4 GB, leaving headroom within 16 GB. (Choosing m=96m = 96 would give 104 bytes per vector, 10.4 GB, still feasible and more accurate.)

    (c) Average list size 108/65,536≈1,52610^8 / 65{,}536 \approx 1{,}526. Comparisons ≈65,536\approx 65{,}536 (centroids) +32×1,526=65,536+48,828=114,364+ 32 \times 1{,}526 = 65{,}536 + 48{,}828 = 114{,}364, about 0.11%0.11\% of the corpus. The centroid scan is now the larger part; an HNSW index over the centroids (FAISS supports this as the coarse quantizer) reduces it.

    (d) PQ distances are approximate, so fetch a larger candidate set (say the top 200 to 500 by PQ distance) and rescore with full-precision vectors stored on SSD (307 GB on disk is cheap), returning the top 10. Verify by computing exact top-10 with a flat index (on a GPU or in batches) for about 1,000 real queries, then sweep nproben_\text{probe}, mm and the rescoring depth, plotting recall@10 against p99 latency, and pick the cheapest configuration meeting 0.95. Re-run the check when the data distribution changes.

Further reading

Go further

Sources

Fact-checked and last reviewed 2026-11-05

Stay in the loop
Prometheus OS™ · Native AI OS · Built for the future © 2025 - 2026