Vector Benchmark

Nowadays, vector search is becoming a common component in many products: site search, recommendations, semantic autocomplete, support tooling, and AI agents that retrieve the right chunks before calling an LLM. The implementation choices change a lot as the data size grows. With a few thousand vectors, an exact k-NN scan can be perfectly fine. Once you move into larger collections, approximate nearest neighbor (ANN) indexing becomes the practical approach. You build an index, persist it, and query it efficiently.

At that point, many teams reach for a full-featured vector database because it bundles ANN with a service layer. That bundle is valuable when you need it. It also comes with a baseline overhead: extra moving parts, background processes, configuration surface area, and memory overhead that is often “always on” even for small-to-mid sized datasets. If you are deploying in tight containers, edge machines, or low-cost instances, the baseline matters as much as raw search speed.

So the first question is “what do I actually need to run in production” instead of “which system is fastest”. If you need pre-filtering, rich metadata, payload indexing, authentication, replication, multi-tenancy, and operational tooling, and you are operating at tens of millions of vectors, then a full vector database is usually the right choice.

A lot of real systems sit in a different zone. They need fast ANN search, plus the core lifecycle operations: insert, upsert, delete, local sharding, and periodic rebuild/compaction. They already have a metadata store, so duplicating that layer inside a vector DB is redundant. In that setup, a full DB can feel like paying in RAM and operational complexity for features that aren’t used.

Building an index engine from scratch is also a rarely worth it for most teams. It’s time-consuming, and it pulls attention away from the core product. The usual alternative is in-process libraries such as FAISS and hnswlib. They are quite fast, with great accuracy, yet they often push you toward a RAM-first model where large portions of the index and vectors live in memory. In some cases, they consume more RAM than a full vector database. Production details like persistence workflow, safe mutation, concurrency, predictable memory growth should also be written on top of them.

brinicle Vector Engine targets this gap: a production-oriented ANN index engine designed to stay usable under strict resource budgets. It focuses on disk-first operation and low memory overhead, while still supporting the operations you typically need in a real service: build/load, search, insert/upsert/delete, local sharding, and rebuild.

brinicle: disk-first ANN indexing for low-RAM vector search

brinicle is an open source C++ vector index engine for approximate nearest neighbor search. It is built for disk-first operation and low-RAM environments. The goal is simple: keep RAM usage predictable, keep tail latency stable, and still hit high recall.

brinicle supports:

  • building and loading indexes
  • parallel insert, upsert, delete, and rebuild
  • safe search

It also ships with a Python wrapper (pybind), so you can use it directly from Python.


What brinicle is, and what it is not

brinicle is an index engine. You embed it in a service or pair it with your own metadata store.

brinicle is not a vector database. It does not aim to provide database features like filtering, payload indexing, distributed replication, auth, or multi-tenancy. If you need those features, use a vector database.

This separation is intentional. The benchmarks in this post show why: a full DB stack often has a baseline memory footprint that is not compatible with extreme RAM caps, even before you start tuning.

When brinicle is a fit

  • You’re under 10M vectors and already have a metadata store
  • You must run in tight RAM (≤1–2GB) or pack many tenants per node
  • You want ANN + CRUD + rebuild/compaction, not DB features

When a vector DB is the right tool

  • You need filtering/payload indexing as part of retrieval
  • You need replication, auth, multi-tenancy, operational UI/tooling
  • You're operating at large scale (tens/hundreds of millions) and want a managed service

What you trade

  • Lower baseline RAM / simpler stack in exchange for bringing your own metadata + service layer.

Benchmark setup

We cover two kinds of comparisons.

1) Vector databases tested as services over HTTP:

  • Qdrant, Weaviate, Milvus, Chroma

2) In-process ANN libraries imported directly:

  • FAISS, hnswlib

These are different deployment models. The DB results include server overhead. The in-process results do not. We first build the index, and then run the search for 10 times, and take the average of search latency and recall@10.

Environment

  • Host OS: Ubuntu 25.10
  • CPU: Intel Core i7-13650HX (20 cores)
  • RAM: 32 GiB
  • Storage: NVMe SSD
  • Docker: 29.1.3
  • Storage driver: overlay2

Datasets and distance

  • Datasets are downloaded directly from ann-benchmarks.com with no preprocessing.
  • Distance metric is L2 across all systems.
  • Parameters are fixed (M=16, ef_construction=200, ef_search varies where explicitly swept).

Recall@K

Recall is computed as average overlap between predicted top K and ground truth top K:

def compute_recalls(pred_ids: np.ndarray, gt_top: np.ndarray, K: int):
    nq = gt_top.shape[0]
    out = {}
    hits = 0
    for i in range(nq):
        a = pred_ids[i, :K]
        b = gt_top[i, :K]
        hits += len(set(a.tolist()) & set(b.tolist()))
    out[f"recall@{K}"] = hits / (nq * K)
    return out

Important detail about configuration
We did not tune database configs or do a parameter search. We kept parameters fixed to reduce degrees of freedom and to keep the comparison reproducible.


Result 1: extreme RAM caps (256MB) are a hard boundary for many DBs

This is the core motivation for brinicle. In a constrained container (MNIST, 256MB RAM, 1 CPU), the following happened. All failures were verified as OOMKilled by Docker.

MNIST (60K, 784 dim), 256MB RAM, 1 CPU

SystemOutcome
briniclePASS
chromaPASS
qdrantOOMKilled
weaviateOOMKilled
milvusOOMKilled

This table answers a practical question: if you want vector search in a very small container, which systems can actually complete a build and serve queries without being killed by the memory limit.


Result 2: latency and memory profiles under constrained DB deployments

Below are snapshots from the constrained HTTP service benchmark runs.

Fashion-MNIST (60K, 784 dim), 512MB RAM, 2 CPU

SystemBuild (s)Recall@10Avg (ms)P50 (ms)P95 (ms)P99 (ms)QPSBuild peak (MB)Search peak (MB)
qdrant14.8270.99791.5791.1733.4416.947690.38512.0282.8
chroma29.2990.99783.0853.0804.6465.205328.20512.00512.01
weaviate45.3870.997863.5593.3145.10410.330281.49512.10512.03
brinicle144.2230.997820.9270.7971.7052.2661086.64469.85285.20
milvus18.6170.998862.6722.6653.6364.513376.091024.00887.67

Note: Milvus required 1024MB in this setup because it was OOMKilled at 512MB.

MNIST (60K, 784 dim), 256MB RAM, 1 CPU

SystemBuild (s)Recall@10Avg (ms)P50 (ms)P95 (ms)P99 (ms)QPSBuild peak (MB)Search peak (MB)
brinicle147.4350.998181.0180.8651.9432.452991.01256.00224.95
chroma49.9280.998072.0091.7413.6674.539505.67256.20255.89

Note: Only brinicle, and chroma survived.

SIFT (1M, 128 dim), 4096MB RAM, 2 CPU

SystemBuild (s)Recall@10Avg (ms)P50 (ms)P95 (ms)P99 (ms)QPSBuild peak (MB)Search peak (MB)
weaviate937.5920.962762.4202.3902.9663.207413.204096.003594.80
qdrant14.1150.994504.5703.04610.29424.532599.221986.831480.99
milvus204.4100.984322.4632.4493.1425.681406.542732.632445.63
chroma228.9880.963522.9423.0004.2224.670341.231705.381705.62
brinicle387.0650.969930.8380.7461.4772.0361204.121552.76982.94

Result 3: recall versus latency tradeoff (ef_search sweep)

Higher recall usually costs more latency. To make that tradeoff explicit, we ran a sweep:

  • Dataset: SIFT (1M, 128 dim)
  • Resources: 4GB RAM, 2 CPU
  • Distance: L2
  • Fixed: M=16, ef_construction=200
  • Sweep: ef_search [16, 32, 64, 128, 256]
latency_recall_curve_p95latency_recall_curve_p99memory_bars

Result 4: in-process libraries (FAISS, hnswlib, brinicle)

How does brinicle compare when used the same way you would use FAISS or hnswlib, inside one process, with no network overhead.

GIST (1M, 960 dim)

SystemBuild (s)Recall@10Avg (ms)P50 (ms)P95 (ms)P99 (ms)QPS
faiss872.2730.772700.3350.3430.4080.4452981.32
hnswlib1032.7070.756200.4080.3970.4700.4992450.41
brinicle1138.4790.770200.4940.4500.8481.5492023.60

SIFT (1M, 128 dim)

SystemBuild (s)Recall@10Avg (ms)P50 (ms)P95 (ms)P99 (ms)QPS
faiss237.2820.969990.0920.0950.1150.12710857.43
hnswlib241.3010.963640.0930.0920.1100.12010711.86
brinicle234.5720.970040.0950.0950.1200.13310563.05

MNIST (60K, 784 dim)

SystemBuild (s)Recall@10Avg (ms)P50 (ms)P95 (ms)P99 (ms)QPS
brinicle20.7540.998180.1610.1590.2210.2556208.06
faiss19.7980.998060.1420.1390.1920.2217062.90
hnswlib21.4740.998080.1770.1760.2390.2735663.67

Fashion-MNIST (60K, 784 dim)

SystemBuild (s)Recall@10Avg (ms)P50 (ms)P95 (ms)P99 (ms)QPS
hnswlib17.8000.997780.1570.1510.2020.2346362.37
brinicle17.0640.997820.1470.1440.2010.2376817.81
faiss16.7870.997700.1250.1250.1670.1947976.62

What to take away from these results

1) Survivability under hard RAM caps matters. In the 256MB MNIST run, multiple database containers were OOMKilled. brinicle completed build and search.

2) Tail latency is a primary metric for search systems. Average latency is useful, but p95 and p99 are where disk-first and constrained environments tend to show problems. That is why the tables and plots emphasize percentiles.

3) The recall-latency curve is the most informative comparison. The ef_search sweep shows how each system behaves as you push toward higher recall.

4) brinicle is positioned as an engine. If you want a full vector database, you should use one. If you want the index layer with a small memory footprint, brinicle is designed for that.


Future work: more realistic workload benchmarks

This blog focuses on a clean workflow, build the index, then run read-only search, to make comparisons reproducible. Real production systems are more complicated, and the benchmark suite should expand to reflect that.

  • Mixed read/write workloads: run sustained search traffic while one or more writer processes perform insert/upsert in parallel, and report the impact on p95/p99 latency and recall.
  • Delete-heavy workloads and long-running degradation: repeatedly delete a fraction of vectors (and optionally reinsert) to measure how tombstones/fragmentation affect recall and tail latency over time, and how often optimize graph is needed to recover performance.
  • Update patterns: build once with 90% of the dataset and insert the rest of the data to see how recall/latency will be affected by insertion.
  • Bigger and more diverse datasets: include higher-scale datasets (multi-million to 10M) across a wider range of dimensionalities, plus different query batch sizes and multi-threaded query loads.

Reproducing the benchmarks