Measure the speed and recall of a vector index
Compare exact search with HNSW on 30,000 local vectors and measure how search effort changes recall, latency, and index size.
On this page
Make an exact baseline
The PDF search lab compares a query with every stored vector. Faiss can do that with a flat index. HNSW builds a graph and visits a subset of vectors when looking for the closest ones. Compare both on identical inputs so you can measure the neighbors the graph missed.
Use a Python 3 virtual environment and a platform with a wheel for the package below. Save and run the script. It creates 30,000 random unit vectors and 100 separate queries from a fixed seed. It uses inner product, which equals cosine similarity for unit vectors.
View compare_vector_indexes.py
import argparse
from statistics import median
from time import perf_counter
import faiss
import numpy as np
DIMENSIONS = 96
TOP_K = 10
def unit_vectors(generator, count):
vectors = generator.standard_normal((count, DIMENSIONS), dtype=np.float32)
faiss.normalize_L2(vectors)
return vectors
def build(index, vectors, label):
start = perf_counter()
index.add(vectors)
elapsed = perf_counter() - start
size = faiss.serialize_index(index).nbytes / 1024**2
print(f"{label}: {elapsed:.2f}s to build, {size:.1f} MiB serialized")
def measure(index, questions):
index.search(questions[:5], TOP_K)
results, times = [], []
for question in questions:
start = perf_counter()
_, ids = index.search(question.reshape(1, -1), TOP_K)
times.append((perf_counter() - start) * 1000)
results.append(ids[0])
return results, median(times)
def compare(count, queries, seed):
faiss.omp_set_num_threads(1)
generator = np.random.default_rng(seed)
vectors = unit_vectors(generator, count)
questions = unit_vectors(generator, queries)
exact = faiss.IndexFlatIP(DIMENSIONS)
graph = faiss.IndexHNSWFlat(DIMENSIONS, 16, faiss.METRIC_INNER_PRODUCT)
graph.hnsw.efConstruction = 80
build(exact, vectors, "Exact")
build(graph, vectors, "HNSW")
baseline, exact_time = measure(exact, questions)
print(f"Exact search: {exact_time:.3f} ms median per query")
for width in (10, 32, 96):
graph.hnsw.efSearch = width
results, elapsed = measure(graph, questions)
hits = sum(len(set(expected) & set(observed)) for expected, observed in zip(baseline, results))
print(f"HNSW efSearch={width:2}: recall@{TOP_K} {hits / (queries * TOP_K):.1%}, "
f"{elapsed:.3f} ms median per query")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Compare exact and graph-based vector search on the same inputs.")
parser.add_argument("--vectors", type=int, default=30_000)
parser.add_argument("--queries", type=int, default=100)
parser.add_argument("--seed", type=int, default=42)
args = parser.parse_args()
if args.vectors < TOP_K or args.queries < 1 or args.seed < 0:
parser.error(f"Use at least {TOP_K} vectors, one query, and a nonnegative seed.")
compare(args.vectors, args.queries, args.seed)python -m pip install "faiss-cpu==1.14.2"
python compare_vector_indexes.py
Read the recall and latency
For each query, count how many of the exact top ten neighbors also occur in the graph’s top ten, then divide by ten. The script averages that value over all queries. It uses one Faiss thread and times one query at a time after warming up each search setting.
You have build times and serialized sizes for both indexes, plus query times and recall for three efSearch settings. In one Apple silicon run, recall rose from 18% at 10 to 66% at 96, while median HNSW search time rose from about 0.01 to 0.05 ms. Those values describe the random data and machine used for this lab.
Compare a smaller index
python compare_vector_indexes.py --vectors 3000 --queries 100
Compare the cost of the graph and the query times with the flat index at both sizes. The serialized size measures the saved representation. Peak RAM and end-to-end latency also include other work and need separate measurement.
Before choosing an index for documents, run this comparison on embeddings from your actual corpus and queries. Exact neighbor recall measures agreement with the flat index. Use labeled source passages to measure whether retrieval found the answer.
Check your understanding
What does a recall of 80% against exact neighbors tell you?
On average the graph returned eight of the exact top ten neighbors. It still takes a labeled retrieval test to see whether those neighbors answer the questions.
Sources
Progress is saved in this browser.