Just brute force your embeddings

Author: Doug Turnbull

Summary

Most teams reaching for a vector database don’t have the corpus size to justify one. On 384-dimensional float32 embeddings on an M4 MacBook Pro, a single NumPy dot product against the whole matrix serves ~1M documents at 79.7 QPS single-threaded with 12ms average latency. For low enough n, brute force the embeddings “until you can’t bear to.”

The framing borrows from Raymond Chen, quoted in the post: “My O(n) algorithm can run circles around your O(log n) algorithm; why much of what you learned in school simply doesn’t matter.” Doug’s addition: true of sorting, true of vector search.

The Measurement

Index SizeClient ThreadsQPSAvg Latency
1,000,000179.70.012s
1,000,00010170.50.058s
8,841,82319.340.106s
8,841,8231018.340.106s

384-dim embeddings, float32, M4 MacBook Pro. One oddity, reproduced here as published: at 8,841,823 vectors the table reports the same 0.106s average latency for one and ten client threads, although QPS roughly doubles between them. The other three rows are consistent with QPS ≈ threads ÷ latency; this one is not.

The entire search is one line:

# Dot product against all
scores = self.doc_vectors @ query_vector.astype(np.float32, copy=False)

When Brute Force Is Enough

The profile of teams Doug describes as not needing vector database complexity:

  • ~1m documents to search
  • Low query traffic
  • Embeddings written up front — no live index updates

The cost avoided is not just licensing but operations: “They don’t need to buy a multi-million dollar vector database, or spend 6 months learning to operate it.”

Past that point: consider a database, or load everything into memory with FAISS and call it done. The post cites Jo Kristian Bergum“an exhaustive search may be all you need” — from Three mistakes when introducing embeddings and vector search.

Headroom Left on the Table

The numbers are naive NumPy and could go faster. Two openings noted:

  • Give each thread more than one query per scan (credited in the post to Andreas Erickson, post) — amortising the memory pass over a batch of queries instead of re-scanning per query.
  • Collecting into a top-n heap during the scan rather than having NumPy score everything first.

Both are throughput optimisations, not asymptotic ones — the scan stays O(n × d).

People

External References