{"slug": "vector-search-how-hnsw-finds-nearest-neighbours", "title": "Vector Search — how HNSW finds nearest neighbours", "summary": "An engineer explains how Hierarchical Navigable Small World (HNSW) graphs enable approximate nearest-neighbor search in milliseconds for vector databases like FAISS, pgvector, Qdrant, Weaviate, and Milvus. The multi-layer graph structure reduces search complexity from linear to logarithmic, achieving 2 ms latency on 2 million documents versus 1.24 seconds with brute force. Key hyperparameters M and ef_search allow tuning recall and latency at query time.", "body_md": "📺 Prefer to watch?\n\n[90-second YouTube Short]· 💬[Telegram]\n\n*Originally published on software-engineer-blog.com.*\n\nYour documentation site has 2,000,000 help articles. A user types a question into the search box and gets the best match in 2 milliseconds—without the system ever comparing that question to almost any of them. This is not magic. It's HNSW: Hierarchical Navigable Small World, the graph structure that powers vector search in FAISS, pgvector, Qdrant, Weaviate, and Milvus.\n\n**Mental model:** HNSW is a multi-layer graph where every vector points to its nearest neighbors, and search is a greedy walk that descends from sparse long-range links at the top to dense fine-grained links at the bottom—like taking highways down to local streets to find an address.\n\nYou already have the pieces:\n\nThe obvious solution is also the correct one: compare the query point against all 2,000,000 document points, measure distance to each, return the closest. With 768 dimensions per vector, that's roughly 1,540,000 arithmetic operations per document, summing to about 1,240 milliseconds on a single core.\n\nWorse: it scales linearly. Double your documents, double your wait. Triple them, triple the wait. At 10 million documents, you are looking at 6+ seconds. At 100 million, a minute or more. And every new search pays the full price.\n\nYour first instinct: *Can't we just use a B-tree?* B-trees are brilliant for one-dimensional sorting. They let you find the number 42 in a billion-element list in log(n) comparisons.\n\nBut \"nearest neighbor\" in 768 dimensions is not sorting on one axis. Closeness is simultaneous proximity across all 768 axes. A B-tree sorts on one—your longitude, your latitude, your price. The second you introduce a second dimension, tree structures collapse because *there is no left-right order that preserves nearness in 2D, let alone 768D*.\n\nSome databases (PostgreSQL with pgvector) do attempt tree-based approaches like IVFFlat (Inverted File with flat clusters), but they still scan multiple clusters and fall back to brute force within each. They are faster than pure brute force, but still O(n) in the worst case.\n\nYou need a different shape: a graph.\n\nHNSW solves this by building a graph where:\n\nThis is *approximate* search: you may not find the true global nearest neighbor. Instead, you find a *local* nearest neighbor—the best one reachable by greedily following edges. But because every node is connected to its nearby neighbors, and those neighbors connect to their neighbors, you can usually reach the true global nearest in a few hops.\n\nOn the AskDocs example: instead of 2,000,000 comparisons, you make roughly 1,800 comparisons across maybe 15–20 hops. That's 0.09% of the work. Latency drops from 1,240 ms to 2 ms.\n\nOne flat graph with M neighbors per node has a problem: a greedy walk might need hundreds of hops to cross the graph. Start at one end, need to reach the other; your neighbors are all locally nearby, so you take tiny steps.\n\nHNSW solves this with *layers*—a skip-list-like idea:\n\nSearch descends:\n\nOn layer 0, you refine the answer among nearby vectors. The higher layers got you in the right neighborhood fast; the lower layers get you to the exact street.\n\nResult: 15–20 hops instead of hundreds. And the number of hops is *logarithmic* in the dataset size.\n\nHNSW has two main hyperparameters:\n\n**M:** Edges per node (default ~16). Higher M = faster search but more memory and slower insertions.\n\n**ef_search:** The size of a candidate list kept during search. The algorithm explores the graph, keeping the N best candidates seen so far. Higher ef_search = more of the graph explored = higher recall but slower search.\n\nYou set ef_search *at query time*, not build time. This lets you tune recall vs. latency per query. A strict recall requirement? Raise ef_search. A latency deadline? Lower it.\n\nHNSW is *approximate*, not exact. A greedy walk can settle into a local minimum—the best neighbor you can reach from your current position, but not the global best. This is especially likely in sparse regions of the embedding space or when M is small.\n\nAccuracy is measured as **recall:** the fraction of true nearest neighbors found. A recall of 0.99 means 99% of your top-10 results would have appeared in a brute-force top-10.\n\nRecall is tunable:\n\nYou cannot have perfect recall at perfect speed. You choose your point on the recall-latency curve. Most production systems run at 95–99% recall to stay sub-10ms; some (e.g., recommendation systems) drop to 90% recall and trade 0.5ms latency for a small recall penalty.\n\nBrute force on 2,000,000 vectors:\n\nHNSW with M=16, ef_search=200:\n\nYou trade 1% accuracy for a 600× speedup. In practice, that 1% is almost never noticed by users—the wrong answer is so close in meaning space that it is functionally equivalent.\n\n| Approach | Lookups (2M vectors) | Latency | Recall | Scaling | Memory Overhead |\n|---|---|---|---|---|---|\n| Brute Force | 2,000,000 | ~1,240 ms | 100% | O(n) | Minimal |\n| IVFFlat (clustering) | ~100,000 | ~60 ms | ~98% | O(n) worst-case | Low |\n| HNSW (hierarchical graph) | ~1,800 | ~2 ms | ~99% | O(log n) | Moderate (M × n edges) |\n\nIn Retrieval-Augmented Generation (RAG), you embed a user's prompt and search a knowledge base to fetch relevant context before passing it to an LLM. Latency here is **TTFT** (time-to-first-token): every millisecond spent searching is a millisecond the user waits before the model starts generating.\n\nHNSW is essential because:\n\nWhen building a RAG pipeline, choose HNSW (via FAISS, Qdrant, Weaviate, or pgvector) over brute-force search; your TTFT will stay latency-bound by the LLM, not the retriever.\n\n`CREATE INDEX ... USING hnsw`\n\n.**Reach for brute-force search when** you have <100k vectors and latency is not a constraint (offline analytics, one-time batch jobs).\n\n**Reach for HNSW when** you have millions of vectors, need sub-10ms latency, and can tolerate 1–5% recall loss (RAG, recommendation systems, real-time search).\n\nWatch the 90-second reel on YouTube to see this in motion: the walk down the layers, the greedy hops, the latency ticking from 1,240 ms down to 2 ms.", "url": "https://wpnews.pro/news/vector-search-how-hnsw-finds-nearest-neighbours", "canonical_source": "https://dev.to/vahid_aghajani_60ce9dbec9/vector-search-how-hnsw-finds-nearest-neighbours-368", "published_at": "2026-07-16 13:00:10+00:00", "updated_at": "2026-07-16 13:41:36.402243+00:00", "lang": "en", "topics": ["machine-learning", "artificial-intelligence", "large-language-models", "ai-infrastructure", "developer-tools"], "entities": ["FAISS", "pgvector", "Qdrant", "Weaviate", "Milvus", "PostgreSQL", "HNSW"], "alternates": {"html": "https://wpnews.pro/news/vector-search-how-hnsw-finds-nearest-neighbours", "markdown": "https://wpnews.pro/news/vector-search-how-hnsw-finds-nearest-neighbours.md", "text": "https://wpnews.pro/news/vector-search-how-hnsw-finds-nearest-neighbours.txt", "jsonld": "https://wpnews.pro/news/vector-search-how-hnsw-finds-nearest-neighbours.jsonld"}}