Every recommendation feed, duplicate detector, and retrieval-augmented answer starts with the same question: of these billion vectors, which few are closest to mine? Faiss, started by Herve Jegou at Meta’s Fundamental AI Research group and maintained by an author list that reads like a history of the field, has been the standard answer since 2017. The MIT-licensed C++ library, now at version 1.15.1 with 207 cpp files and 286 headers under faiss/ alone, implements exact search, inverted files, graph indexes, and a shelf of quantizer codecs, wraps all of it through SWIG for Python with typed stubs, exposes a stable C API, and ports the hot paths to CUDA with 75.cu files plus an Apple Metal variant. Its research lineage is documented in three citable papers, from the 2024 overview The Faiss Library to the GPU work in Billion-scale similarity search with GPUs. As always in this series, what follows is an educational tour of published source code.
The design is a study in one abstraction done thoroughly. Everything is an Index: a base struct that promises train, add, and search, with distances defined as either L2 or inner product, cosine included by normalizing first. Around that contract grows a family tree, some members exact baselines, some compressed-domain codecs that never store the original vectors, some graph structures that trade memory for hops, and wrappers like IndexShards, IndexReplicas, and IndexRefine that compose any index into a distributed or re-ranking layout. A string parser named index_factory turns recipes like IVF4096,PQ32 into configured objects, which is why every tutorial you have ever read looks the same. Underneath, kernels do the arithmetic, with a BLAS threshold deciding when matrix multiplication beats loops, and SIMD dispatch choosing instruction sets at runtime. Reading the overview from left to right follows one recipe string to computed neighbors.
Overview of Faiss: the factory parses recipe strings into the Index contract, flat, IVF, and HNSW families provide search structures, quantizers and inverted lists handle storage, exact kernels and k-means do the math, and GPU plus Python layers carry it to hardware and users.
Reading the overview from left to right:
- The Index contract is line 101 of a 430-line header: train at 132, add at 164, search, and search parameters.
- index_factory at line 1258 of 1330 lines parses recipe strings, with coarse quantizer parsing at 285.
- IndexFlat is the exact baseline at line 21, with IP at 87, L2 at 92, and a Panorama layout at 114.
- IndexIVF spans 1784 lines: search_preassigned at 495, add_core at 306.
- IndexHNSW is 1469 lines of graph search, with the graph itself in impl/HNSW.cpp at 2056 lines.
- The quantizers run 941 lines for PQ, 714 for scalar, 795 for the 1-bit RaBitQ.
- Inverted lists hold the data in 1003 lines, with on-disk and blocked variants beside them.
- The GPU layer clones any CPU index to CUDA in 617 lines of to_gpu and to_cpu logic.
Why You Need This
The first reason is that the index zoo is not a pile of classes, it is a composition system, and reading it teaches you how search systems are actually layered. An IndexIVF needs a coarse quantizer, which is itself an Index; an IndexHNSW can store raw floats, PQ codes, or RaBitQ codes through constructor choices; IndexPreTransform wraps any index with a PCA or OPQ rotation; IndexRefine re-ranks another index’s output with full-precision rescoring; IndexShards and IndexReplicas spread one dataset across machines or GPUs. The factory string grammar mirrors this: IVF4096,PQ32 means cluster into 4096 cells, encode members with 32 sub-quantizers, and the 1330-line parser maps every token to a constructor call. Once you see the pattern, the 40-plus index types stop being trivia and become a menu of trade-offs along search time, quality, memory, and build cost that the README tabulates explicitly.
The second reason is the performance engineering, which is meticulous at every scale. The exact kernels in a 1405-line distances.cpp switch between plain loops and BLAS matrix multiplication based on a documented threshold of 128000 floating operations, tile the computation to cache, and specialize fp16 and Top1 variants; the SIMD layer dispatches between instruction sets at runtime, with the changelog recording the addition of dynamic AVX512 popcount levels. The IVF scanners and quantizer decoders are hand-tuned, the HNSW implementation tracks per-visit tables to avoid re-scanning, and the GPU port covers flat, IVF, IVFPQ, IVF scalar quantizer, binary flat, and Cagra graph search, with cuVS available as an optional backend. Even serialization has zerocopy and memory-mapped readers, because at billion scale the cost of loading an index rivals the cost of searching it.
The third reason is trust: this is one of the few numerical libraries where the reference implementations ship next to the fast ones on purpose. Every quantizer has a slow, obvious decoder you can diff against; distances.cpp keeps exhaustive sequence kernels beside the tiled BLAS ones; the clustering module’s kmeans_clustering function at line 526 is a forty-year-old algorithm in modern clothing with subsampling, GPU offload, and a SuperKMeans extension visible in the Python wrappers. The tutorial directory carries parallel cpp and python versions of the same examples, the C API gives non-C++ languages a stable 49-file surface, and the doxygen site documents every class. When your production search starts returning strange results, this is the codebase you can actually debug down to the kernel.
The detailed map: the API and wrapper layer, flat and IVF structures, graph indexes, the quantizer shelf, compute and storage layers, and the GPU plus binding stacks.
Follow one recipe through the factory first. The Index struct at line 101 declares the contract: train on a sample, add vectors one batch at a time, search with k results, and optionally respect SearchParameters structs that tune recall per call, defined at line 88. The factory splits the string on commas and dispatches to parse functions, from parse_coarse_quantizer at 285 to the newer parse_svs_datatype at 692 that wires in the third-party SVS backend, whose Flat, IVF, and Vamana variants with LeanVec and LVQ compression live under faiss/svs. Composition wrappers keep the grammar open-ended: VectorTransform.cpp at 1643 lines supplies the PCA and OPQ matrices that IndexPreTransform applies transparently, IndexRefine at 397 lines holds a base index plus full vectors for re-ranking, and IndexShards at 271 lines fans a query out to sub-indexes.
The two workhorse families deserve separate reads. IndexIVF clusters the dataset with k-means from Clustering.cpp, whose train function at line 48 and train_impl at 92 handle subsampling and numeric types, then stores each vector’s code in the inverted lists of 1003 lines, an abstract array interface whose on-disk variant spills to files and whose blocked variant batches allocation; a DirectMap, enabled at line 383, turns remove calls from scans into lookups. At query time, search_preassigned at line 495 probes nprobe lists, and a per-list InvertedListScanner, declared at line 543 of IndexIVF.h, accumulates distances, which is how IVFPQ, IVFSQ, and IVFRaBitQ all reuse one search driver. IndexHNSW takes the graph route: search at 509 walks the layered graph, add at 559 inserts with the greedy descent in impl/HNSW.cpp, 2056 lines that include add_links_starting_from at 635 and the candidate search at 1117, while NSG and NNDescent in the same impl directory provide alternative graph builds and IndexHNSWCagra at line 261 imports GPU-built Cagra graphs for CPU search.
The quantizer shelf is where memory per vector drops from 1024 bytes to a handful. ProductQuantizer at 941 lines splits each vector into sub-vectors, codes each against a learned codebook, and provides generic, 8-bit, and 16-bit encoder and decoder structs; ScalarQuantizer at 714 lines quantizes per-dimension ranges into 8 or fewer bits; RaBitQuantizer at 795 lines encodes each vector as a single bit per dimension plus error bounds, with a multi-bit variant and a dedicated code packer in impl. The additive family, residual and local search quantizers with their training utilities, composes multiple codebooks, and the polysemous training plus lattice modules refine the codes for hamming-distance search. All of them feed the SIMD dispatch for fast decode, and the Panorama layout, exposed as IndexFlatPanorama at line 114 of IndexFlat.h, shows the newest idea in the tree, pruning distance computations using sorted attribute order.
The compute and porting layers complete the story. distances.cpp implements every exact kernel, from exhaustive_inner_product_seq at 286 to the BLAS tiled path at 545, gated by the threshold constant at line 705 and pushing results into the ResultHandler sinks from impl. Serialization lives in index_read.cpp, 3728 lines that deserialize every index type ever shipped, with a 1452-line writer and four IO flavors including memory mapping. The GPU side starts from GpuCloner.cpp, whose to_gpu function turns any CPU index into a GpuIndex, backed by GpuIndexFlat, GpuIndexIVFFlat at 393 lines, GpuIndexIVFPQ at 622, and the CUDA brute-force kernels of GpuDistance, all prefixed by a GpuClonerOptions struct that tunes temporary memory and allocation, with GpuAutoTune searching those options. Bindings close the loop: the SWIG interface at 1460 lines generates the Python module that init.py assembles with 605 lines of helpers, class_wrappers.py adds 1680 lines of argument validation and clustering wrappers, py.typed and the stub file give IDEs real signatures, and the 49-file C API at the top level mirrors every index for languages without SWIG.
From Install to a Search
Conda packages are the supported path, with pip wheels and source builds documented in INSTALL.md:
conda install -c pytorch faiss-cpu
# GPU build:
conda install -c pytorch faiss-gpu
Then the canonical four-line tour, straight from the library’s own conventions:
import numpy as np
import faiss
d = 64
nb, nq = 100000, 10000
xb = np.random.random((nb, d)).astype("float32")
xq = np.random.random((nq, d)).astype("float32")
index = faiss.IndexFlatL2(d)
index.add(xb)
D, I = index.search(xq, 5)
The factory is the real entry point: faiss.index_factory(64, “IVF4096,PQ32”) builds the two-level index, but it needs training first, so call index.train on a representative sample of at least about 39 times 4096 vectors per the library’s own rule of thumb, then add, then search. Swap the recipe for “IVF4096,HNSW32” or “OPQ32_64,IVF4096,PQ32” and the same four calls run a different point on the speed-quality-memory curve, and faiss.extract_index_ivf or the GpuCloner functions let you reach in and reconfigure or move it to the GPU without changing the search code.
Honest limits: the GPU build is Linux and Windows only through conda, not macOS, where the Metal port covers a subset of index types; IVF indexes with many vectors per list can bottleneck on single-threaded list scanning unless the NUMA and threading guides in the wiki are followed; the factory grammar is powerful but unforgiving, and an invalid recipe errors out only at parse time; and the C API, while broad, lags new index types by design so its surface stays stable. Start with the Index header, then the factory, then one index family end to end; that order turns a dense catalog into a system you can extend. Enjoyed this post? Never miss out on future posts by following us