254 lines
8.3 KiB
TypeScript
254 lines
8.3 KiB
TypeScript
/**
|
||
* HNSW Exploration Simulation Tests
|
||
*
|
||
* Tests hierarchical navigable small world graph structure,
|
||
* small-world properties, and sub-millisecond search performance.
|
||
*
|
||
* Target Metrics:
|
||
* - M=32 configuration (optimal)
|
||
* - Small-world index σ=2.84 (range: 2.5-3.5)
|
||
* - Clustering coefficient: 0.39
|
||
* - Average path length: O(log N)
|
||
* - 8.2x speedup vs hnswlib
|
||
* - <100μs latency (target: 61μs p50)
|
||
*/
|
||
|
||
import { describe, it, expect, beforeAll } from 'vitest';
|
||
import { hnswExplorationScenario } from '../../scenarios/latent-space/hnsw-exploration';
|
||
import type { SimulationReport } from '../../types';
|
||
|
||
describe('HNSWExploration', () => {
|
||
let report: SimulationReport;
|
||
|
||
beforeAll(async () => {
|
||
report = await hnswExplorationScenario.run(hnswExplorationScenario.config);
|
||
}, 120000); // 120s timeout for large graphs
|
||
|
||
describe('Optimal M Configuration', () => {
|
||
it('should use M=32 for best performance', () => {
|
||
const best = report.summary.bestPerformance;
|
||
expect(best.M).toBe(32);
|
||
});
|
||
|
||
it('should test multiple M values', () => {
|
||
const mValues = hnswExplorationScenario.config.hnswParams.map(p => p.M);
|
||
expect(mValues).toContain(16);
|
||
expect(mValues).toContain(32);
|
||
expect(mValues).toContain(64);
|
||
});
|
||
|
||
it('should use appropriate efConstruction', () => {
|
||
const best = report.summary.bestPerformance;
|
||
expect(best.efConstruction).toBeGreaterThanOrEqual(200);
|
||
expect(best.efConstruction).toBeLessThanOrEqual(800);
|
||
});
|
||
});
|
||
|
||
describe('Small-World Properties', () => {
|
||
it('should achieve small-world index in range 2.5-3.5', () => {
|
||
const topology = report.metrics.graphTopology;
|
||
expect(topology.averageSmallWorldIndex).toBeGreaterThan(2.5);
|
||
expect(topology.averageSmallWorldIndex).toBeLessThan(3.5);
|
||
});
|
||
|
||
it('should target σ=2.84', () => {
|
||
const topology = report.metrics.graphTopology;
|
||
expect(topology.averageSmallWorldIndex).toBeCloseTo(2.84, 0.5);
|
||
});
|
||
|
||
it('should have clustering coefficient ~0.39', () => {
|
||
const topology = report.metrics.graphTopology;
|
||
expect(topology.averageClusteringCoeff).toBeCloseTo(0.39, 0.15);
|
||
});
|
||
|
||
it('should maintain clustering coefficient >0.3', () => {
|
||
const topology = report.metrics.graphTopology;
|
||
expect(topology.averageClusteringCoeff).toBeGreaterThan(0.3);
|
||
});
|
||
});
|
||
|
||
describe('Average Path Length', () => {
|
||
it('should scale as O(log N)', () => {
|
||
const results = report.detailedResults as any[];
|
||
const largeGraph = results.find(r => r.vectorCount === 100000);
|
||
|
||
if (largeGraph) {
|
||
const expectedPath = Math.log2(largeGraph.vectorCount) * 1.5;
|
||
expect(largeGraph.graphMetrics.averagePathLength).toBeLessThan(expectedPath);
|
||
}
|
||
});
|
||
|
||
it('should have efficient navigation paths', () => {
|
||
const results = report.detailedResults as any[];
|
||
results.forEach(r => {
|
||
const maxExpectedPath = Math.log2(r.vectorCount) * 2;
|
||
expect(r.graphMetrics.averagePathLength).toBeLessThan(maxExpectedPath);
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('Search Performance', () => {
|
||
it('should achieve 8.2x speedup vs hnswlib', () => {
|
||
const ruvector = (report.detailedResults as any[]).find(
|
||
r => r.backend === 'ruvector-gnn'
|
||
);
|
||
|
||
if (ruvector) {
|
||
expect(ruvector.speedupVsBaseline).toBeGreaterThan(2.0);
|
||
}
|
||
});
|
||
|
||
it('should target 8x+ speedup', () => {
|
||
const ruvector = (report.detailedResults as any[]).find(
|
||
r => r.backend === 'ruvector-gnn' && r.vectorCount === 100000
|
||
);
|
||
|
||
if (ruvector) {
|
||
expect(ruvector.speedupVsBaseline).toBeCloseTo(8.2, 3);
|
||
}
|
||
});
|
||
|
||
it('should achieve sub-millisecond latency', () => {
|
||
const results = report.detailedResults as any[];
|
||
const hasSubMs = results.some(r => {
|
||
const latencies = r.graphMetrics.searchLatencyUs || [];
|
||
return latencies.some((l: any) => l.p99 < 1000);
|
||
});
|
||
expect(hasSubMs).toBe(true);
|
||
});
|
||
|
||
it('should target 61μs p50 latency', () => {
|
||
const best = report.summary.bestPerformance;
|
||
const latencies = (best as any).graphMetrics?.searchLatencyUs || [];
|
||
|
||
if (latencies.length > 0) {
|
||
const p50 = latencies.find((l: any) => l.k === 10)?.p50 || 0;
|
||
expect(p50).toBeLessThan(100);
|
||
}
|
||
});
|
||
|
||
it('should maintain high QPS', () => {
|
||
const search = report.metrics.searchPerformance;
|
||
expect(search.bestQPS).toBeGreaterThan(1000);
|
||
});
|
||
});
|
||
|
||
describe('Recall Quality', () => {
|
||
it('should achieve >95% recall@10', () => {
|
||
const results = report.detailedResults as any[];
|
||
results.forEach(r => {
|
||
const recall10 = r.recallAtK.find((rec: any) => rec.k === 10);
|
||
if (recall10) {
|
||
expect(recall10.recall).toBeGreaterThan(0.95);
|
||
}
|
||
});
|
||
});
|
||
|
||
it('should test multiple k values', () => {
|
||
const kValues = hnswExplorationScenario.config.kValues;
|
||
expect(kValues).toContain(1);
|
||
expect(kValues).toContain(10);
|
||
expect(kValues).toContain(100);
|
||
});
|
||
});
|
||
|
||
describe('Graph Topology', () => {
|
||
it('should have hierarchical layer structure', () => {
|
||
const results = report.detailedResults as any[];
|
||
results.forEach(r => {
|
||
expect(r.graphMetrics.layers).toBeGreaterThan(1);
|
||
expect(r.graphMetrics.nodesPerLayer.length).toBe(r.graphMetrics.layers);
|
||
});
|
||
});
|
||
|
||
it('should have exponential layer decay', () => {
|
||
const results = report.detailedResults as any[];
|
||
const sample = results[0];
|
||
|
||
if (sample) {
|
||
const layers = sample.graphMetrics.nodesPerLayer;
|
||
for (let i = 1; i < layers.length; i++) {
|
||
expect(layers[i]).toBeLessThan(layers[i - 1]);
|
||
}
|
||
}
|
||
});
|
||
|
||
it('should track connectivity distribution', () => {
|
||
const results = report.detailedResults as any[];
|
||
results.forEach(r => {
|
||
expect(r.graphMetrics.connectivityDistribution).toBeDefined();
|
||
expect(r.graphMetrics.connectivityDistribution.length).toBe(r.graphMetrics.layers);
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('Backend Comparison', () => {
|
||
it('should test ruvector-gnn backend', () => {
|
||
const backends = hnswExplorationScenario.config.backends;
|
||
expect(backends).toContain('ruvector-gnn');
|
||
});
|
||
|
||
it('should test hnswlib baseline', () => {
|
||
const backends = hnswExplorationScenario.config.backends;
|
||
expect(backends).toContain('hnswlib');
|
||
});
|
||
|
||
it('should compare backend performance', () => {
|
||
const comparison = report.metrics.backendComparison;
|
||
expect(Array.isArray(comparison)).toBe(true);
|
||
expect(comparison.length).toBeGreaterThan(0);
|
||
});
|
||
});
|
||
|
||
describe('Parameter Sensitivity', () => {
|
||
it('should analyze M parameter impact', () => {
|
||
const sensitivity = report.metrics.parameterSensitivity;
|
||
expect(sensitivity.MImpact).toBeDefined();
|
||
});
|
||
|
||
it('should analyze efConstruction impact', () => {
|
||
const sensitivity = report.metrics.parameterSensitivity;
|
||
expect(sensitivity.efConstructionImpact).toBeDefined();
|
||
});
|
||
|
||
it('should analyze efSearch impact', () => {
|
||
const sensitivity = report.metrics.parameterSensitivity;
|
||
expect(sensitivity.efSearchImpact).toBeDefined();
|
||
});
|
||
});
|
||
|
||
describe('Target Validation', () => {
|
||
it('should meet 2-4x speedup target', () => {
|
||
const targetsMet = report.summary.targetsMet;
|
||
expect(targetsMet).toBe(true);
|
||
});
|
||
|
||
it('should validate small-world properties', () => {
|
||
const topology = report.metrics.graphTopology;
|
||
expect(topology.averageSmallWorldIndex).toBeGreaterThan(1.0);
|
||
});
|
||
});
|
||
|
||
describe('Report Generation', () => {
|
||
it('should generate complete analysis', () => {
|
||
expect(report.analysis).toBeDefined();
|
||
expect(report.analysis).toContain('HNSW');
|
||
});
|
||
|
||
it('should provide actionable recommendations', () => {
|
||
expect(report.recommendations).toBeDefined();
|
||
expect(report.recommendations.length).toBeGreaterThan(0);
|
||
expect(report.recommendations.some(r => r.includes('M='))).toBe(true);
|
||
});
|
||
|
||
it('should generate visualizations', () => {
|
||
expect(report.artifacts.graphVisualizations).toBeDefined();
|
||
expect(report.artifacts.performanceCharts).toBeDefined();
|
||
});
|
||
|
||
it('should complete within timeout', () => {
|
||
expect(report.executionTimeMs).toBeLessThan(120000);
|
||
});
|
||
});
|
||
});
|