Skip to content
Home
Approximate Nearest Neighbor Search

Approximate Nearest Neighbor Search

Data Structures Data Structures 7 min read 1410 words Beginner ExcellentWiki Editorial Team

Nearest neighbor search is everywhere: recommendation systems, image search, document similarity. Exact search is too slow at scale — approximate is the practical solution.

The Problem

Given a query point, find the closest points in a dataset of N dimensions.

Why Exact Search Fails

Dataset SizeExact (Brute Force)What Takes Time
10K points1 msFine for small datasets
1M points100 msSlow for real-time
1B points100 secondsUnusable

Exact k-NN: O(N×D) per query (N = points, D = dimensions).

Approximate Nearest Neighbor (ANN)

Trade a small accuracy loss for massive speedup.

MetricExactANNRatio
Recall100%90-99%Acceptable
Queries/sec1010,0001000× faster
MemoryFull datasetIndex2-5× dataset size

LSH (Locality-Sensitive Hashing)

Hash similar points to the same bucket with high probability.

Random Projection LSH

import numpy as np

class RandomProjectionLSH:
    def __init__(self, dim, num_tables=10, hash_len=8):
        self.num_tables = num_tables
        self.hash_len = hash_len
        # Random projection vectors
        self.tables = []
        for _ in range(num_tables):
            r = np.random.randn(dim, hash_len)
            self.tables.append(r)
        self.buckets = [{} for _ in range(num_tables)]
    
    def _hash(self, table_idx, vector):
        r = self.tables[table_idx]
        projection = vector @ r  # Dot product
        return tuple((projection > 0).astype(int))  # Binary hash
    
    def insert(self, item_id, vector):
        for t in range(self.num_tables):
            h = self._hash(t, vector)
            if h not in self.buckets[t]:
                self.buckets[t][h] = []
            self.buckets[t][h].append(item_id)
    
    def query(self, vector, k=10):
        candidates = set()
        for t in range(self.num_tables):
            h = self._hash(t, vector)
            if h in self.buckets[t]:
                candidates.update(self.buckets[t][h])
        # Now compute exact distances only among candidates
        return candidates
PropertyValue
Query timeSub-linear (depends on bucket size)
Accuracy70-95% (adjustable via num_tables)
DimensionsGood for 50-500
MemoryO(N) + table overhead

Product Quantization (PQ)

Compresses vectors by splitting them into sub-vectors and quantizing each.

How It Works

Original vector (128 dims):
  [x1, x2, ..., x128]

Split into 8 sub-vectors of 16 dims each:
  [x1...x16], [x17...x32], ..., [x113...x128]

For each of the 8 sub-spaces, learn a codebook of 256 centroids:
  Sub-space 0: [c0_0, c0_1, ..., c0_255]
  Sub-space 1: [c1_0, c1_1, ..., c1_255]
  ...
  Sub-space 7: [c7_0, c7_1, ..., c7_255]

Encode each vector as 8 bytes (one centroid index per sub-space):
  Original (128 × 4 bytes = 512 bytes)
  → Encoded (8 × 1 byte = 8 bytes)
  → 64× compression!
class ProductQuantizer:
    def __init__(self, dim, num_subvectors=8, num_centroids=256):
        self.M = num_subvectors  # Number of sub-vectors (m)
        self.K = num_centroids  # Number of centroids per sub-vector (k*)
        self.Ds = dim // self.M  # Dimension per sub-vector (d*)
        self.codebooks = None
    
    def fit(self, vectors):
        """Learn centroids via k-means per sub-space."""
        n = vectors.shape[0]
        subspaces = vectors.reshape(n, self.M, self.Ds)
        
        self.codebooks = []
        for m in range(self.M):
            sub_vectors = subspaces[:, m, :]
            # K-means with K centroids
            centroids = self._kmeans(sub_vectors, self.K)
            self.codebooks.append(centroids)
    
    def encode(self, vector):
        """Encode as M centroid indices."""
        sub_vectors = vector.reshape(self.M, self.Ds)
        codes = []
        for m in range(self.M):
            # Nearest centroid
            dists = np.linalg.norm(self.codebooks[m] - sub_vectors[m], axis=1)
            codes.append(np.argmin(dists))
        return codes  # M bytes
PropertyValue
Compression10-100×
Query speed100-1000× faster than brute force
Accuracy80-95% (with ADC distance)
Works withIVF, HNSW

HNSW (Hierarchical Navigable Small World)

The most popular ANN algorithm. Builds a multi-layer graph.

Layer 3 (top):    ●──●──● (few nodes, long connections)
                     │
Layer 2:          ●──●──●──●──● (more nodes)
                  │  │  │  │  │
Layer 1 (bottom): ●──●──●──●──●──●──● (all nodes, short connections)

Search Algorithm

  1. Start at top layer (few nodes)
  2. Greedily navigate to nearest node
  3. Descend to next layer
  4. Repeat until bottom layer
  5. At bottom layer, expand search
def search(hnsw_graph, query, ef=100):
    """HNSW search with ef = number of candidates to explore."""
    current = hnsw_graph.entry_point
    
    # Phase 1: Coarse search through layers (top → bottom)
    for layer in reversed(range(1, len(hnsw_graph.layers))):
        current = greedy_search(hnsw_graph, query, current, layer)
    
    # Phase 2: Fine search at bottom layer
    candidates = set()
    candidates.add(current)
    visited = set()
    results = MinHeap()
    results.push(current, distance(query, current.vector))
    
    while candidates and len(visited) < ef:
        node = candidates.pop()
        visited.add(node)
        for neighbor in hnsw_graph.bottom_layer[node]:
            if neighbor not in visited:
                d = distance(query, neighbor.vector)
                results.push(neighbor, d)
                candidates.add(neighbor)
    
    return results.k_smallest(k)

| Property | HNSW | PQ | LSH | |

IVF (Inverted File Index)

Like LSH but uses k-means clustering.

How It Works

  1. Cluster all vectors into K clusters (k-means)
  2. For each cluster, store a list of vectors assigned to it
  3. Query: find nearest cluster centroids, search only those clusters
class IVF:
    def __init__(self, n_clusters=100):
        self.n_clusters = n_clusters
        self.centroids = None
        self.lists = None
    
    def fit(self, vectors, ids):
        from sklearn.cluster import KMeans
        kmeans = KMeans(n_clusters=self.n_clusters)
        labels = kmeans.fit_predict(vectors)
        self.centroids = kmeans.cluster_centers_
        
        self.lists = [[] for _ in range(self.n_clusters)]
        for idx, label in enumerate(labels):
            self.lists[label].append((vectors[idx], ids[idx]))
    
    def query(self, vector, k=10, nprobe=10):
        # Find nprobe nearest clusters
        dists = np.linalg.norm(self.centroids - vector, axis=1)
        nearest = np.argsort(dists)[:nprobe]
        
        # Search those clusters exhaustively
        candidates = []
        for cluster_id in nearest:
            candidates.extend(self.lists[cluster_id])
        
        # Exact search among candidates
        candidates.sort(key=lambda x: np.linalg.norm(x[0] - vector))
        return candidates[:k]

faiss (Facebook AI Similarity Search)

The standard library for ANN.

import faiss
import numpy as np

# Generate data
d = 128  # Dimension
n = 1000000  # 1M vectors
xb = np.random.random((n, d)).astype('float32')

# Build index
index = faiss.IndexFlatL2(d)  # Exact search (baseline)
# index = faiss.IndexIVFFlat(index, d, 100)  # IVF (faster)
# index = faiss.IndexHNSWFlat(d, 32)  # HNSW (fast, accurate)
# index = faiss.IndexPQ(d, 8, 8)  # Product Quantization (compressed)

index.train(xb)
index.add(xb)

# Search
xq = np.random.random((1, d)).astype('float32')
D, I = index.search(xq, k=10)  # 10 nearest neighbors

Index Types in faiss

IndexTypeSpeed vs Exact
IndexFlatL2Exact (brute force)
IndexIVFFlatIVF + exact10-100×
IndexIVFPQIVF + PQ100-1000×
IndexHNSWFlatHNSW100-1000×
IndexBinaryFlatBinary vectors40× compression

Evaluation

MetricWhat It Measures
Recall@kFraction of true nearest neighbors found
Queries per second (QPS)Throughput
Index build timeTraining cost
Memory usageRAM required

Typical Parameters

AlgorithmParameterEffect
HNSWefSearch (100-500)Higher = more accurate, slower
HNSWM (16-48)Higher = more accurate, more memory
IVFnprobe (10-100)Higher = more accurate, slower
PQM (8-32)Higher = more accurate, larger index
LSHnum_tables (5-20)Higher = more accurate, larger
RecallSpeed vs Exact
90%1000× faster
95%100× faster
99%10× faster

Approximate search is the difference between “impossible at scale” and “instant.”

Evaluation Metrics for ANN

Quality metrics for approximate nearest neighbor search balance accuracy against speed and memory. Recall@k measures the fraction of true nearest neighbors (from brute-force k-NN) that appear in the approximate result set — a standard target is 95%+ recall at 10-100 neighbors. Queries per second (QPS) measures throughput under concurrent load. Index build time and memory footprint determine whether an algorithm scales to billion-scale datasets. The trade-off is intuitive: higher recall requires more memory and slower queries. The winning approach in the ANN Benchmarks consistently combines multiple algorithms: IVF for coarse partitioning followed by HNSW for refinement within each partition.

Practical Applications

ANN powers recommendation systems at major technology companies. When a user views a product, the system encodes it into an embedding vector and queries the ANN index for the nearest neighbors — products that are most similar in embedding space. Spotify uses ANN for music recommendation, Pinterest for visual similarity search, and Uber for ride-matching optimization. In biology, ANN accelerates genomic sequence alignment by finding similar DNA sequences in large databases. In computer vision, ANN enables real-time image retrieval across billions of photographs, where the query is an image embedding from a convolutional neural network. In each case, the ability to find approximate nearest neighbors in milliseconds rather than seconds or minutes makes the application feasible at scale.

Frequently Asked Questions

What is the difference between exact and approximate nearest neighbor search? Exact nearest neighbor search guarantees finding the true closest points by exhaustive comparison, which is O(n) per query and impractical for large datasets. Approximate nearest neighbor search returns the closest points with high probability, sacrificing perfect accuracy for orders-of-magnitude speedup — typically 10-1000x faster than exact search.

Which ANN algorithm is best? No single algorithm dominates across all metrics. HNSW offers the best speed-recall tradeoff for in-memory datasets up to millions of vectors. IVF provides better memory efficiency. Milvus and other vector databases combine multiple algorithms and let users choose based on their workload characteristics. Benchmark on your specific data distribution and query pattern.

How do I choose the number of nearest neighbors? The optimal k depends on the application. For recommendation, k=10-100 is typical. For classification, use cross-validation. For similarity search, the number of results users want to see determines k. Higher k values require more computation and memory but provide more comprehensive results.

Graph Algorithms GuideHash Tables GuideSearching Algorithms Guide

Section: Data Structures 1410 words 7 min read Beginner 756 articles in section Report inaccuracy Back to top