Files
ruvnet__ruflo/v3/@claude-flow/memory/benchmarks/hnsw-search.bench.ts
ruv ed95d67822 feat(memory): #2061 ADR-125 Phase 6 — runnable benchmarks + baseline results
Previously `npm run bench` matched no files because `vitest.config.ts`
restricts include to `src/**/*.test.ts`. ADR-125 Phase 6 lands a dedicated
`vitest.bench.config.ts` that discovers `benchmarks/**/*.bench.ts`, and
the `bench` script now points at it.

Additions:
- `vitest.bench.config.ts` — bench discovery config; legacy custom-framework
  bench files (cache-hit-rate, hnsw-indexing, memory-write, vector-search)
  are excluded for now and will be ported in a follow-up alongside Phase 3.
- `benchmarks/hnsw-search.bench.ts` — first real vitest `bench()` suite.
  Measures `HNSWIndex.search` (k=10, k=50) and `addPoint` against 1k random
  128-dim vectors.
- `benchmarks/results/baseline-20260519T212453Z.md` — first measured baseline,
  committed so the README perf table has a real referent.
- `benchmarks/results/scripts/run-baseline.mjs` — companion script that
  produces the JSON blob the baseline.md references.

Measured baseline on Darwin / M-series:
  build_time_ms: 533.42      (1k vectors, 128-dim)
  search_k10_avg_ms: 0.5294  (~1.9k ops/s)
  search_k50_avg_ms: 0.5235
  add_avg_ms: 0.8656

378 tests still passing — no regression.

Refs: ADR-125 Phase 6
Closes: part of #2061

Co-Authored-By: RuFlo <ruv@ruv.net>
2026-05-19 17:26:26 -04:00

103 lines
2.7 KiB
TypeScript

/**
* HNSW Search Benchmark (ADR-125 Phase 6).
*
* Vitest `bench()` suite that measures the canonical `HNSWIndex.search()`
* against a 1k-entry index. The bench exists so `npm run bench` produces
* non-empty output and gives the README perf table a real referent.
*
* Result interpretation:
* - `hnsw.search k=10` is the headline number — single-query latency against
* 1,000 random 128-dim vectors.
* - `hnsw.add (single)` measures incremental insert cost; useful for tracking
* regression as ADR-125 Phase 3 adds persistence.
*
* @see {@link ../docs/adr/ADR-125-memory-consolidation.md}
*/
import { describe, bench, beforeAll } from 'vitest';
import { HNSWIndex } from '../src/hnsw-index.js';
// Bench scale chosen to fit Phase 6's "ONE simple benchmark against 1k entries"
// scope. Dimensions kept small (128) so the bench completes in seconds — the
// goal is a runnable referent, not a full perf evaluation (that's Phase 3+).
const N = 1_000;
const DIM = 128;
const M = 16;
const EF_CONSTRUCTION = 200;
function randomVector(dim: number): Float32Array {
const v = new Float32Array(dim);
let norm = 0;
for (let i = 0; i < dim; i++) {
v[i] = Math.random() * 2 - 1;
norm += v[i] * v[i];
}
norm = Math.sqrt(norm) || 1;
for (let i = 0; i < dim; i++) v[i] /= norm;
return v;
}
describe('HNSW search — 1k entries, 128-dim', () => {
let index: HNSWIndex;
let queries: Float32Array[];
beforeAll(async () => {
index = new HNSWIndex({
dimensions: DIM,
M,
efConstruction: EF_CONSTRUCTION,
maxElements: N + 100,
metric: 'cosine',
});
// Pre-populate
for (let i = 0; i < N; i++) {
await index.addPoint(`vec-${i}`, randomVector(DIM));
}
// Pre-generate query vectors so the bench measures search, not RNG.
queries = Array.from({ length: 50 }, () => randomVector(DIM));
});
bench(
'hnsw.search k=10',
async () => {
const q = queries[Math.floor(Math.random() * queries.length)]!;
await index.search(q, 10);
},
{ iterations: 100, warmupIterations: 10 }
);
bench(
'hnsw.search k=50',
async () => {
const q = queries[Math.floor(Math.random() * queries.length)]!;
await index.search(q, 50);
},
{ iterations: 100, warmupIterations: 10 }
);
});
describe('HNSW add — incremental insert cost', () => {
let index: HNSWIndex;
let counter = 0;
beforeAll(() => {
index = new HNSWIndex({
dimensions: DIM,
M,
efConstruction: EF_CONSTRUCTION,
maxElements: 10_000,
metric: 'cosine',
});
});
bench(
'hnsw.add (single)',
async () => {
await index.addPoint(`add-${counter++}`, randomVector(DIM));
},
{ iterations: 500, warmupIterations: 50 }
);
});