Building and Searching 100 Million Vectors#

Authored by Lyon Lay

TL;DR: We built an open-source vector index that indexes 100 million vectors in 1 hour and searches them at 23 ms p99 latency with 96% recall@100. We found no published build on comparable data and hardware that is faster.1 It has many favorable scaling properties, suggesting it could scale to 100B vectors. The purpose of this post is to make this work available to the industry.2 The code, benchmarks, and results are all open source.

Vector search is a core component of many AI systems; it enables search for similar documents, images, or text over vast corpora of data. Brute force gives exact answers but is untenable at scale.3 An index makes search practical by reducing the work needed; it finds most of the nearest neighbors while only reading a small fraction of the data. Prequisite knowledge and terminology are in the appendix.

Hierarchical SPANN, our index architecture, is a tree that divides the collection into progressively smaller regions, allowing a query to traverse to the most promising regions. This structure scales well horizontally as subtrees can be divided across machines.

How the index works#

ANN (Approximate nearest neighbor search) relies on the fact that nearby vectors cluster together, so searching a few well-chosen groups can recover most neighbors. Each group forms a posting list, represented by a center vector, or centroid. Comparing a query with centers identifies promising lists without scanning their contents. SPANN 4 keeps the lists on disk and their centers in memory.

Finding those centers is itself a nearest-neighbor search. The index groups nearby centers, gives each group its own center, and repeats up to one root. Each level searches a smaller collection.

The data structure#

A leaf holds a posting list; an internal node holds its children's centers. With a target of about a hundred children per internal node, the measured 100M tree has four levels and the 100B-shaped tree has six. The tree stays shallow, though preserving recall may require exploring more branches.

Compression makes comparisons cheap. The query routes with 4-bit-per-dimension centroid codes, scans posting-list codes, 1-bit-per-dimension compressions of each stored vector (codes for short), and checks its shortlist at full precision. Both kinds of code use RaBitQ 5.

Subtrees also provide natural boundaries between machines. Splitting a proximity graph such as HNSW can leave search paths crossing machines and paying network round trips. A subtree owns a set of leaves that can be served together.

The query algorithm#

Choose the lists. Starting at the root, traverse down the tree: the query scores the children of retained nodes and keeps the most promising. The number retained is the beam width. At the bottom, it has selected a few hundred to a few thousand leaves. The completed traversal looks like a subtree

Scan the lists. The query scores every code in the selected lists and keeps an approximate shortlist larger than the requested result count, k.

Rerank the shortlist. It fetches the candidates' original vectors, computes exact distances, and returns the nearest k.

Searching more leaves or reranking more candidates can improve recall, the fraction of true nearest neighbors returned. Codes scanned measures posting-list work; latency includes routing, scanning, and reranking.

Building the index#

The writer alternates insertion with balancing: moving vectors, creating or removing nodes, and adding levels. It publishes snapshots for readers, allowing construction and querying to use different settings, representations, and machines.

The build time includes keeping the tree balanced and searchable after each batch. As the collection grows, insertion navigates a larger tree and balancing operates over more leaves.6

Storing each vector in one list simplifies maintenance. SPANN can replicate boundary vectors across neighboring lists to improve recall 4. In my experiments, the modest recall gain came with much longer builds: moving a vector requires removing its outdated copies. One copy also avoids cross-machine replica bookkeeping.

In Chroma's production architecture, writes enter our durable wal3 log, background workers publish index versions, and queries combine a committed index with recent writes. Integrating this prototype and measuring sustained-write performance are work for a later post.

Sharding the index#

When opening a snapshot, the reader orders leaves depth first to keep subtrees together, then divides them into equally sized parts. A coordinator routes the query; each machine scans its selected leaves and returns a shortlist. Full vectors come from a block store 7 on demand, and each machine indexes only its own leaves' blocks.

Each machine caches the full routing tree, about 0.75 bytes per stored vector at 100M, to avoiding distributed routing overhead: potentially a network round trip at every level.

Splitting preserves the selected lists and candidates. Each part reranks its own shortlist, however, so more parts check more candidates at full precision. Recall rises slightly, along with reranking work.

Scaling behavior#

As the collection grows so does the index. For an index to scale, we want to see that the work done for each query (# computations performed, the cost per computation, # of MB loaded) scales sub-linearly with collection size, or even converges on a cieling. This is indeed what we observe.

I built one FineWeb 8 index, saving checkpoints from 1M to 100M. The query set, dataset source, dimensions, construction settings, and hardware stay fixed; exact answers are computed for each size.

Maintaining accuracy at scale#

Moving from 10M to 100M vectors, recovering the same fraction of neighbors requires roughly two to three and a half times the scan, depending on the recall target. Brute force would require ten times the work.

Latency grows faster than codes scanned because navigating the larger tree adds work. To understand that cost, separate how well lists group neighbors from how well queries find those lists.

We lose the most accuracy when traversing the tree#

Grouping holds up over the directly measured range: from 1M to 10M, nearly all true neighbors remain concentrated in a few dozen lists.

Finding those lists gets harder as more centers compete for similar ranks. A center summarizes a list; it need not be among the closest centers for its list to contain a true neighbor. Relevant lists with far-away centers slip further down the ranking, requiring a wider search.

Internal routing faces the same choice. At small sizes, a query can score every internal node. Around 50M, retaining only approximately the best tenth of the second level saves enough work to search more leaves, reaching higher recall in less time.

The hierarchy still skips most vectors, but preserving recall requires more work to choose the right lists. That routing cost also limits the gains from distributing scans.

Scaling across machines#

Sharing leaves across machines increases capacity and throughput more than it improves single-query latency. I measured the difference by serving the same 100M index from one, two, four, and eight identical machines.

Once scans are distributed, routing dominates latency. Parallelizing it across the coordinator's cores helps, but selecting the next beam remains sequential. Queries also wait for the slowest scan worker, and selected lists rarely divide evenly.

At 100M, queries touch nearly every machine despite subtree placement. Communication would fall only if each machine owned a much larger region than a query typically explores. The deeper-tree experiment below tests that possibility.

The coordinator also caps throughput: it routes, scans its part, and merges every query. Total CPU work per query rises with machine count because scanning the same codes in smaller pieces adds overhead.

What would it take to search 100B vectors?#

Extending the 10M–100M trend three more factors of ten gives too wide a range for a useful prediction. Instead, I test a real routing tree at the depth of a 100B index and do some napkin math to derive the rest. A narrower 1B prediction is in the appendix.

A larger collection needs a wider search space#

Routing compares queries only with centers, so it can be tested without building every posting list. I use 70M real vectors as stand-in leaf centers in a tree with the depth and level widths expected at 100B. This measures routing work and retention of nearest centers, not end-to-end recall over 100B vectors.

A beam that retains nearly all nearest centers at the 100M shape loses recall as the tree deepens. Retaining 95% at the 100B shape requires an internal beam of about 8,192. Shallow navigation still takes substantial work.

The deeper tree offers more locality: selected lists concentrate in fewer subtrees, contacting a smaller fraction of machines. Distribution has room to grow if routing can keep up.

Routing accuracy is the bottleneck#

The budget separates measured results, estimates derived from current rates, and extrapolated trends. The scan estimate is an order of magnitude, not a promised operating point.

At the measured scoring rate, routing alone would take roughly 100–175 ms per query, before scanning any lists. This is derived, not directly timed on the 100B-shaped tree. Distributing more leaf data does not remove that bottleneck.

The hierarchy makes large collections practical by skipping most of their vectors. The next gains depend on choosing what to skip more efficiently.

Suggested reading#

These are the systems and papers closest to this work. Each takes a different position on where vectors live, how queries are routed and how vectors are compressed.

Vector search at production scale#

Partitioned and hierarchical indexes#

Compression#

Graph indexes on disk#

Appendix#

Next steps#

The prototype builds on one machine with bounded balancing work, publishes immutable snapshots, and serves them across machines with full-vector retrieval on demand. The next improvements target routing and storage costs:

  • Parallelize beam selection. Address the sequential step that limits routing once scans are distributed.
  • Distribute query coordination. Let each machine route and merge a share of queries. Alternatively, split routing itself across subtrees, trading local work for network round trips. Subtree placement and beam allocation determine whether that pays off.
  • Improve list selection. Better predictions of which lists contain neighbors could narrow the required beam. Full-precision centers are one option, but precision alone need not improve ranking. Their codes occupy eight times the space of 4-bit codes, increasing routing memory unless the tree is split.
  • Fetch full vectors individually. Reranking reads blocks containing whole lists, fetching far more than its shortlist uses on a cold cache. Finer-grained storage would reduce that amplification.

A prediction for 1B#

The figure extends the fitted 10M–100M trend one more factor of ten, recording a prediction before the 1B experiment runs.

Four assumptions define the prediction:

  1. The trend continues. A much larger scan or routing requirement would show it bending beyond the measured range.
  2. The search grid approximates target recall. Budgets double between settings. Even the 100M starting points are interpolated, contributing to wide ranges.
  3. Memory conditions hold. Latency assumes one machine holds the index in memory. At 1B, full vectors alone occupy about 3 TB; a practical deployment would split the index and incur the measured routing floor and coordination costs.
  4. Routing preserves enough candidates. The stand-in retains 98% of nearest lists at the 1B shape with a moderate beam. A real index requiring a much wider beam would challenge the prediction.

Background#

The post assumes some familiarity with the topics below. If one is new, the linked article is a good starting point.

Terminology#

  • Collection: the set of stored vectors being searched. Its size is the number of vectors, written N.
  • Dataset: where the vectors come from and how they are distributed, here FineWeb. The experiments vary the collection's size while keeping the dataset fixed.
  • Posting list (list): a group of nearby vectors that is searched as a unit.
  • Centroid (center): the vector that summarizes a list or a group of nodes; queries compare against it to decide where to search.
  • Leaf: a tree node that owns one posting list.
  • Internal node: a tree node that holds the centroids of its children and only routes queries.
  • Routing tree: all internal nodes and leaf centroids; it is small enough to keep in memory on every machine.
  • Routing: walking down the routing tree to choose which lists to search.
  • Beam (beam width): how many nodes a query keeps at each level while routing; a wider beam finds more of the right lists and costs more.
  • Posting-list code (code): the compressed, 1-bit-per-dimension form of a stored vector that the scan reads; each list holds one code per vector. Cost is measured in codes scanned per query.
  • Scan: scoring every code in the chosen lists.
  • Shortlist: the best candidates from the scan, passed on to rerank.
  • Rerank: computing exact distances for the shortlist with full-precision vectors and returning the best k.
  • Recall@k: the share of a query's true k nearest neighbors that appear among the k results.
  • Part: the share of the leaves one machine serves when the index is split across machines; each part is a run of whole subtrees.
  • Routing machine: the machine that routes a query, scans its own part and merges the other machines' shortlists.
  • Writer, reader and snapshot: the writer builds and balances the tree and publishes immutable snapshots; readers open a snapshot to serve queries.
  • Block store: Chroma's S3-backed storage for large immutable blocks, cached on local disk and in memory.

Code and data#

The implementation, full results, and benchmark code are in chroma-core/chroma#7960 (as of commit 4de458a), for reproducing the measurements, tuning the index, and testing the forecast on other datasets.

Footnotes#

  1. This is my assessment of the build result, not a matched benchmark against every index. Published results use different datasets, dimensions, hardware, and build procedures. Meta reports about 168 seconds to build IVF-PQ over 100M 96-dimensional vectors from Deep1B on one H100 GPU (Accelerating GPU indexes in Faiss with NVIDIA cuVS, 2025). InSituANN reports DiskANN builds of 26.6, 38.4, and 40.3 hours on SIFT-1B, DEEP-1B, and SPACEV-1B using 64 threads (InSituANN: Revisiting IVF for PCIe-Efficient Billion-Scale Vector Search, 2026). This build uses CPUs, handles vectors eight times wider than the GPU example, and maintains a balanced, searchable index after each batch of a million vectors rather than building only once over a fixed dataset. ↩

  2. This work builds on my personal vector database project and Chroma's next generation of SPANN. I am sharing it for others to extend; query-recall performance remains an area for improvement. ↩

  3. For example, 100M vectors would cost us about 300 GB of reads and 200 GFLOPs of arithmetic. ↩

  4. Chen et al., SPANN: Highly-efficient Billion-scale Approximate Nearest Neighbor Search, NeurIPS 2021. ↩ ↩2

  5. Gao and Long, RaBitQ: Quantizing High-Dimensional Vectors with a Theoretical Error Bound for Approximate Nearest Neighbor Search, SIGMOD 2024. ↩

  6. Insertion and balancing of 100M 768-dimensional FineWeb vectors on one r6i.32xlarge, with 16 insertion and 16 balancing workers. Excludes input loading and checkpoint saves. The tree is balanced and searchable after each batch. ↩

  7. Chroma's blockfile stores large immutable blocks in S3 and caches them on local SSD. See Chroma's distributed architecture. ↩

  8. Penedo et al., The FineWeb Datasets: Decanting the Web for the Finest Text Data at Scale, 2024. The vectors come from Qdrant-FineWeb-10B, which embeds FineWeb documents with Alibaba-NLP/gte-multilingual-base into 768-dimensional unit-norm vectors. ↩