Your vector database has 10 million documents. How can it find the 5 most similar documents without comparing the query against all 10 million ?
Imagine you have 10 million photographs in a giant warehouse. Someone hands you one photo and asks: "Find me the 5 photos in this warehouse that look most like this one."
If you check every single photo one by one, comparing it to the one in your hand, you would be there for days. That is exactly the problem a vector database faces every single time someone runs a search.
In this article, we are going to open up that warehouse, turn on the lights, and understand — step by step, like you are 10 years old — exactly how modern AI systems find the "5 most similar documents" out of 10 million, in a fraction of a second, without checking every single one.
1. First, What Even IS a "Document" to a Computer?
A computer cannot "read" text the way you and I do. It cannot feel that "puppy" and "dog" are similar words. So before anything else happens, every document (a sentence, a paragraph, a PDF, an image, anything) gets converted into a list of numbers called a vector or embedding.
Think of it like giving every document a secret "address" in a giant invisible city. Documents that mean similar things get addresses that are close to each other. Documents that mean very different things get addresses that are far apart.
So "10 million documents" really means "10 million points floating in a giant multi-dimensional space." Usually not 2 dimensions like the picture above — real embeddings often have 384, 768, or even 1536+ dimensions. Your brain can't picture that, and honestly, neither can mine. But the math works the same way regardless of dimension count.
2. The Naive Way: Brute Force Search (And Why It's Too Slow)
The simplest idea is: take your query vector, and compare it against every single one of the 10 million vectors, calculate how "close" each one is, then sort and pick the top 5. This is called brute-force / exhaustive / linear search.
- 10 million comparisons per single search query
- If 1,000 people search at the same time, that's 10 billion comparisons
- Latency grows linearly — double the documents, double the search time
Here is the "why it's slow" idea explained like you're 10: imagine you lost your favorite toy in a warehouse with 10 million boxes. Brute force means opening every single box, one after another, until you've checked them all. Even if opening one box takes just 1 millisecond, checking 10 million boxes takes almost 3 hours. Nobody wants to wait 3 hours for a Google search.
import numpy as np
def brute_force_search(query_vector, all_vectors, top_k=5):
# Step 1: Compute similarity between query and EVERY vector
similarities = []
for doc_id, vector in enumerate(all_vectors):
# cosine similarity = how "aligned" two vectors are (1 = identical direction)
score = np.dot(query_vector, vector) / (
np.linalg.norm(query_vector) * np.linalg.norm(vector)
)
similarities.append((doc_id, score))
# Step 2: Sort ALL 10 million results by score (expensive!)
similarities.sort(key=lambda x: x[1], reverse=True)
# Step 3: Return the top 5
return similarities[:top_k]
This works perfectly for accuracy — it's mathematically guaranteed to find the true top 5. The problem is purely speed. So the entire field of "vector search" is really about one question:
3. The Big Idea Behind ANN: "Don't Search Everywhere, Search Smart"
Here's the 10-year-old version. Imagine a library with 10 million books, but instead of throwing them in one giant pile, the librarian organizes them into sections: Science, Sports, Cooking, Fantasy, History.
If you ask for a book about dragons, the librarian doesn't check the Cooking section at all. She walks straight to Fantasy. That's the entire secret of ANN search: organize the data cleverly ahead of time, so at search time you only check a small, relevant chunk of it.
There are three big families of "smart organizing" tricks used in 2026. Let's go through each one, slowly.
4. Technique #1 — Clustering / IVF (Inverted File Index)
IVF stands for "Inverted File Index." Don't worry about the scary name. The idea is simple: group similar vectors into buckets (clusters) ahead of time, kind of like sorting 10 million toys into labeled bins: cars, dolls, blocks, animals.
Here's how it works step by step, like a kid organizing toys:
- Step 1 (Offline, done once): Look at all 10 million vectors and find, say, 1,000 "center points" (like the middle of each toy bin). This uses an algorithm called k-means clustering.
- Step 2 (Offline): Assign every one of the 10 million vectors to its nearest center point / bin.
- Step 3 (At search time): When a query comes in, first figure out which few bins (maybe 10 out of 1,000) are closest to the query.
- Step 4: Only brute-force search inside those 10 bins, not all 1,000.
If each bin has roughly 10,000 vectors (10 million ÷ 1,000 bins), and you only check 10 bins, you just went from comparing 10 million vectors down to comparing about 100,000 — a 100x speedup, with a small trade-off in accuracy because the true best match could theoretically sit in a bin you skipped.
nprobe) instead of the whole dataset.
import faiss import numpy as np dimension = 768 # size of each embedding vector num_clusters = 1000 # how many "bins" to sort vectors into # Step 1: Build the index structure quantizer = faiss.IndexFlatL2(dimension) index = faiss.IndexIVFFlat(quantizer, dimension, num_clusters) # Step 2: Train the index — this is where clusters get computed index.train(all_vectors) # all_vectors shape: (10_000_000, 768) # Step 3: Add all your real vectors into their assigned clusters index.add(all_vectors) # Step 4: At search time, only check the 10 nearest clusters index.nprobe = 10 distances, doc_ids = index.search(query_vector.reshape(1, -1), k=5)
nprobe), the more accurate the result — but the slower the
search. This is the core tension in ALL ANN algorithms:
speed vs. accuracy (recall). There is no free lunch.
5. Technique #2 — HNSW (The 2026 Industry Favorite)
HNSW stands for "Hierarchical Navigable Small World." It sounds complicated, but the analogy is one you already know: flying on an airplane.
When you fly from a small town to another small town far away, you rarely take a direct flight. Instead, you fly small-town → big city airport (hub) → another big city airport (hub) → your small destination town. Long trips use a small number of big "highway" jumps, then zoom into local detail at the end.
HNSW organizes your 10 million vectors into several "layers," like floors in a building:
- Top floor: Very few points, but they are spread far apart — these are the "hub airports." Great for big, fast jumps.
- Middle floors: More points, medium-range connections.
- Ground floor: ALL 10 million points, densely connected to their close neighbors — this is where fine-grained precision happens.
Search always starts at the top floor (the fastest, roughest jump) and "greedily" walks toward the query, always moving to whichever neighbor is closer. Once it can't get any closer on that floor, it drops down one floor and repeats, with more and more precision, until it reaches the ground floor and finds the true nearest neighbors.
This is why HNSW is the default choice in most 2026 vector databases (Qdrant, Weaviate, pgvector's HNSW index, Pinecone's pod-based indexes): it gives you very high accuracy (95-99% recall) at logarithmic search time — meaning even if your dataset grows from 10 million to 100 million, search time barely increases.
hnswlib Python library, adds 10 million
768-dimensional vectors to it, and then searches for the 5 closest matches
to a query. The parameters M and ef_construction
control how "well-connected" the graph is — more connections means better
accuracy but more memory usage.
import hnswlib import numpy as np dim = 768 num_elements = 10_000_000 # Step 1: Create the HNSW index index = hnswlib.Index(space='cosine', dim=dim) # M = how many connections each point keeps to its neighbors # ef_construction = how thoroughly we search while BUILDING the graph index.init_index(max_elements=num_elements, M=16, ef_construction=200) # Step 2: Insert all your vectors (this builds the "floors") index.add_items(all_vectors, ids=np.arange(num_elements)) # Step 3: Set how thoroughly we search at QUERY time # Higher ef = slower but more accurate index.set_ef(50) # Step 4: Search — this walks down the floors described above labels, distances = index.knn_query(query_vector, k=5)
ef_construction and M
based on your recall requirements before going to production — test with a
labeled validation set to measure recall@5 vs. real brute-force results.
6. Technique #3 — Product Quantization (Making Vectors Smaller and Faster)
Here's a problem nobody tells beginners about: 10 million vectors × 768 numbers × 4 bytes each = about 30 GB just to store the raw vectors in memory. That's a LOT of RAM, and RAM is expensive.
Product Quantization (PQ) is like compressing a huge, detailed painting into a much smaller thumbnail image, while still keeping enough detail to tell which painting it is at a glance.
Here's the kid-friendly version: instead of storing every crayon color (millions of possible RGB values) for every pixel, you make a "crayon box" of just 256 approved colors, and every pixel gets assigned to its closest crayon in the box. Now you only need to store which of the 256 crayons was used (1 byte) instead of the full color (3-4 bytes) — and you do this trick for small "chunks" of the vector at a time.
import faiss
dimension = 768
num_clusters = 1000
num_subquantizers = 8 # split each vector into 8 chunks
bits_per_chunk = 8 # 2^8 = 256 possible "crayon colors" per chunk
quantizer = faiss.IndexFlatL2(dimension)
index = faiss.IndexIVFPQ(
quantizer, dimension, num_clusters,
num_subquantizers, bits_per_chunk
)
index.train(all_vectors) # learns the best 256 "crayons" per chunk
index.add(all_vectors)
index.nprobe = 10
distances, doc_ids = index.search(query_vector.reshape(1, -1), k=5)
Real numbers matter here: PQ can shrink a 30 GB index down to 2-3 GB while still keeping 90%+ recall — meaning it fits comfortably on a single machine instead of needing an expensive multi-node cluster.
7. The 2026 Trend: Combining ALL Three (IVF + HNSW + PQ)
In 2026, production vector databases rarely use just one trick. They stack them, similar to how a good student combines multiple study strategies instead of relying on just one.
8. Bonus 2026 Trend: Hybrid Search (Dense + Sparse)
Here's something that tripped up a LOT of teams building "pure vector" search in 2023-2024: embeddings are great at understanding meaning, but bad at exact keyword matches — like product codes, names, or acronyms.
The 2026 best practice is Hybrid Search: run TWO searches at once and blend the results.
- Dense search (the vector/embedding search we've been discussing) — great for "meaning" and synonyms.
- Sparse search (traditional keyword search, like BM25) — great for exact terms, product IDs, rare words.
def hybrid_search(query_text, query_vector, alpha=0.5, top_k=5):
# alpha controls the blend: 1.0 = pure vector, 0.0 = pure keyword
dense_results = hnsw_index.search(query_vector, k=20)
sparse_results = bm25_index.search(query_text, k=20)
combined_scores = {}
for doc_id, score in dense_results:
combined_scores[doc_id] = alpha * score
for doc_id, score in sparse_results:
combined_scores[doc_id] = combined_scores.get(doc_id, 0) + (1 - alpha) * score
ranked = sorted(combined_scores.items(), key=lambda x: x[1], reverse=True)
return ranked[:top_k]
9. Bonus 2026 Trend: Reranking (The Final Quality Check)
Even after ANN search narrows 10 million down to, say, 50 candidates, the final step in 2026 pipelines is usually a reranker — a smaller, slower, but much more accurate model that carefully re-scores just those 50 candidates using a "cross-encoder" that looks at the query and document together, instead of comparing pre-computed vectors.
10. Interview Deep-Dive: What FAANG Interviewers Actually Probe
Everything above will get you 90% of the way to a great understanding. But if this question ever comes up in a Google, Meta, or similar system design interview, there are a handful of sharper follow-ups interviewers love to ask. Let's cover every single one, still in plain language first, then with the technical precision an interviewer expects.
10.1 — Technique #4: LSH (Locality Sensitive Hashing)
Imagine sorting mail into bins, but instead of reading every letter's full address, you use a special stamp that "smudges" nearby addresses into the same bin on purpose. Two houses right next to each other almost always get the same smudge. Two houses far apart almost always get different smudges.
That "smudge stamp" is a hash function — but a special kind, designed so that similar vectors are likely to collide into the same bucket, unlike a normal hash function (like the ones used in hash maps) which is designed to scatter everything randomly and avoid collisions.
- Step 1 (Offline): Apply several random hash functions to every vector, producing a short "signature" per vector.
- Step 2 (Offline): Vectors with the same signature get placed in the same hash bucket.
- Step 3 (Search time): Hash the query the same way, jump directly to its bucket, and only compare against the (small) handful of vectors already there.
import numpy as np
def build_lsh_signature(vector, random_planes):
# For each random plane, check which side the vector falls on
# A positive dot product = one side (bit 1), negative = other side (bit 0)
bits = (np.dot(random_planes, vector) > 0).astype(int)
return ''.join(map(str, bits)) # e.g. "1011010"
dim = 768
num_planes = 12 # more planes = more precise buckets, but more buckets total
random_planes = np.random.randn(num_planes, dim)
buckets = {}
for doc_id, vector in enumerate(all_vectors):
signature = build_lsh_signature(vector, random_planes)
buckets.setdefault(signature, []).append(doc_id)
# Search: hash the query, only check vectors sharing its bucket
query_signature = build_lsh_signature(query_vector, random_planes)
candidates = buckets.get(query_signature, [])
10.2 — Technique #5: Tree-Based Search (KD-Trees and Annoy)
Picture playing "20 questions" with the vectors instead of checking each one individually: "Is the point's first number greater than 5? Yes. Is its second number greater than 2? No." Each question cuts the remaining candidates roughly in half. That's a KD-tree.
KD-trees work great in low dimensions (like 2D or 3D, think GPS coordinates), but fall apart badly above roughly 20-30 dimensions — the "halving" trick stops working because in high dimensions almost every point ends up looking equally close to the splitting boundary. This is directly connected to the curse of dimensionality below.
Annoy (Approximate Nearest Neighbors Oh Yeah, built by Spotify for music recommendations) fixes this by building many random trees instead of one, each splitting the space with a random hyperplane instead of a single-axis cut, then combining the results from all trees at search time. It's simple, memory-mappable (great for sharing an index across many processes), and still used today for read-heavy, rarely-updated datasets like song catalogs.
10.3 — Big-O Complexity: The Numbers Interviewers Want
Let n = number of vectors (10,000,000), d =
number of dimensions per vector (e.g. 768), and k = number of
clusters in IVF.
| Method | Build Time | Query Time | Memory |
|---|---|---|---|
| Brute Force | O(1) | O(n·d) | O(n·d) |
| IVF | O(n·d) (k-means) | O((n/k)·d) approx | O(n·d) |
| HNSW | O(n·log n) | O(log n) approx | O(n·M) (M = connections/node) |
| LSH | O(n·d·L) (L = hash tables) | O(d·L + bucket size) | O(n·L) |
| Product Quantization | O(n·d) | O(n·d/8) approx (compressed distance calc) | O(n) (bytes, not floats) |
10.4 — The Curse of Dimensionality (Why This Problem Is Actually Hard)
Here's the mind-bending part almost nobody explains simply: as you add more and more dimensions to your vectors, something strange happens — the distance between the closest point and the farthest point starts to shrink toward each other. Everything starts looking almost equally far away.
Kid-friendly version: imagine you're standing in a small room (2 dimensions, like a floor plan) — it's easy to tell which wall is closest to you. Now imagine a "room" with 768 doors, walls, and directions all at once. In that crowded, weird space, almost every wall ends up roughly the same distance from you. Your sense of "near" and "far" stops being useful.
This is exactly why simple tricks like KD-trees (which rely on "near" and "far" being meaningfully different) break down in high dimensions, and why smarter graph-based and cluster-based approaches (HNSW, IVF) had to be invented — they don't rely purely on raw distance comparisons the same way.
10.5 — Distance Metrics: Cosine vs. Euclidean (L2) vs. Dot Product
"Similar" needs a precise mathematical definition, and picking the wrong one is a common real-world bug. Here's the plain-language difference:
- Cosine similarity — measures the angle between two vectors, ignoring their length/magnitude. Best when you only care about "direction" (meaning), like comparing sentence embeddings of different lengths.
- Euclidean distance (L2) — measures the actual straight-line distance between two points. Best when magnitude genuinely matters, like comparing raw pixel or sensor data.
- Dot product — like cosine similarity, but does NOT normalize for length, so longer vectors can score higher even if not more "similar" in direction. Fast to compute, and correct to use when your embedding model was specifically trained with dot-product similarity in mind (many modern embedding models are).
10.6 — Measuring Quality: What "Recall@k" Actually Means
We used the term "recall@k" earlier — here's the precise, formal definition an interviewer expects:
In plain terms: if the true, brute-force-computed top 5 documents are {A, B, C, D, E}, and your fast ANN search returns {A, B, C, X, Y}, then your recall@5 is 3/5 = 0.6, or 60%. You correctly found 3 out of the 5 real answers.
def measure_recall_at_k(test_queries, ann_index, ground_truth_index, k=5):
total_recall = 0
for query in test_queries:
true_neighbors = set(ground_truth_index.search(query, k)) # brute force
ann_neighbors = set(ann_index.search(query, k)) # HNSW/IVF/etc.
overlap = len(true_neighbors & ann_neighbors)
total_recall += overlap / k
return total_recall / len(test_queries) # average recall across all test queries
Production teams typically target recall@5 or recall@10 above 0.9 (90%) before shipping an ANN index — below that, users start noticing genuinely relevant results going missing.
10.7 — DiskANN: Searching Vectors That Don't Fit in RAM
Everything above assumes your vectors fit comfortably in memory. But what happens when you have a billion vectors and even compressed, they're too big for RAM? This is where DiskANN (developed by Microsoft Research) comes in — it's the graph-based approach HNSW uses, but redesigned to live mostly on fast SSD storage instead of RAM.
Kid-friendly version: HNSW is like keeping every single toy out on your bedroom floor so you can grab any of them instantly — great, but you run out of floor space. DiskANN is like keeping most toys in a very well organized closet (the SSD) with just a small "cheat sheet" (a compressed index) on your desk (RAM) telling you exactly where to find the specific toy you want, so you barely need to open more than one or two closet drawers per search.
10.8 — Scaling Past 10 Million: Sharding and Distributed Search
The final piece an interviewer will often push on: what if it's not 10 million documents, but 1 billion, and it doesn't fit on one machine at all — RAM, disk, or otherwise? The answer is the same idea used in distributed databases everywhere: sharding.
- Partition your 1 billion vectors across, say, 20 machines (shards) — often by hashing document IDs, or by clustering similar vectors onto the same shard for efficiency.
- Fan-out: when a query comes in, send it to all 20 shards in parallel, and each shard runs its own local HNSW/IVF search for its top candidates.
- Merge: a coordinator node collects the top candidates from all 20 shards (e.g., top 20 from each = 400 total) and does one final, cheap comparison to pick the true global top 5.
This "fan-out and merge" (also called scatter-gather) pattern is exactly how Milvus, Elasticsearch, and Vespa scale vector search to billions of documents across a cluster, and it's the standard follow-up answer when an interviewer asks "what if it doesn't fit on one machine anymore?"
11. Which Vector Database Should You Actually Use in 2026?
| Database | Default Algorithm | Best For |
|---|---|---|
| Qdrant | HNSW + quantization | Self-hosted, high performance, filtering |
| Pinecone | Proprietary (HNSW-based) | Fully managed, minimal ops overhead |
| Weaviate | HNSW | Built-in hybrid search, GraphQL API |
| Milvus | IVF / HNSW / DiskANN | Massive scale (billions of vectors) |
| pgvector | HNSW / IVFFlat | Teams already using PostgreSQL |
12. Common Mistakes (Learn From Others' Pain)
- Use brute-force search once you cross a few hundred thousand vectors
- Assume higher
ef/nprobealways helps — it costs latency for diminishing accuracy gains - Skip evaluating recall@k against a ground-truth brute-force baseline before shipping
- Forget metadata filtering — searching "closest coffee shops" should still respect a city filter
- Use the wrong distance metric for your embedding model (check the model docs — cosine, dot product, and L2 are NOT interchangeable)
- Assume a single-tree or single-hash-table method will scale — always benchmark against the curse of dimensionality at your real dimension count
- Start with HNSW as your default — it's the best general-purpose choice in 2026
- Add Product Quantization once memory becomes a real cost concern
- Combine dense + sparse (hybrid search) for anything involving exact terms, IDs, or names
- Always rerank your final shortlist if answer quality really matters
- Measure recall@k against brute force on a held-out query set before every major index config change
- Move to DiskANN or a similar SSD-backed index once your data no longer comfortably fits in RAM
- Shard (fan-out and merge) once a single machine can't hold the full dataset, even compressed
13. The Whole Journey, In One Sentence
A vector database finds the top 5 out of 10 million documents not by checking all 10 million, but by organizing them cleverly ahead of time (into clusters, graphs, or compressed codes) so that at search time, it only has to look at a tiny, highly relevant slice of the data — trading a small, controllable amount of accuracy for a massive amount of speed.
Comments
Post a Comment