Files
tasq/node_modules/agentdb/src/tests/ruvector-integration.test.ts
T
2026-04-09 19:01:53 +08:00

1590 lines
48 KiB
TypeScript

/**
* RuVector Integration Tests
*
* Comprehensive test suite for AgentDB's RuVector-powered features:
* - SIMD Vector Operations
* - Vector Quantization (8-bit, 4-bit, Product Quantization)
* - RuVectorBackend Enhancements
* - Enhanced Embedding Service
* - Attention Optimized Modules
*
* @module ruvector-integration.test
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
// SIMD Vector Operations
import {
cosineSimilaritySIMD,
euclideanDistanceSIMD,
euclideanDistanceSquaredSIMD,
dotProductSIMD,
l2NormSIMD,
batchCosineSimilarity,
batchEuclideanDistance,
normalizeVector,
normalizeVectorInPlace,
detectSIMDSupport,
SIMDVectorOps,
randomUnitVector,
vectorAdd,
vectorSub,
vectorScale,
} from '../simd/simd-vector-ops.js';
// Vector Quantization
import {
quantize8bit,
quantize4bit,
dequantize8bit,
dequantize4bit,
calculateQuantizationError,
getQuantizationStats,
ProductQuantizer,
QuantizedVectorStore,
createScalar8BitStore,
createScalar4BitStore,
createProductQuantizedStore,
} from '../quantization/vector-quantization.js';
// Attention Modules
import {
scaledDotProductAttention,
scaledDotProductAttentionOptimized,
batchScaledDotProductAttention,
batchSequenceAttention,
MultiHeadAttention,
MultiHeadAttentionOptimized,
FlashAttention,
FlashAttentionOptimized,
LinearAttention,
HyperbolicAttention,
createAttention,
createAttentionOptimized,
toFloat32Array,
flatten2D,
getBufferPool,
benchmarkAttention,
} from '../wrappers/attention-fallbacks.js';
// RuVector Backend
import { RuVectorBackend, Semaphore, BufferPool } from '../backends/ruvector/RuVectorBackend.js';
// Enhanced Embedding Service
import { EnhancedEmbeddingService } from '../controllers/EnhancedEmbeddingService.js';
// WASM Vector Search
import { WASMVectorSearch } from '../controllers/WASMVectorSearch.js';
// ============================================================================
// Test Utilities
// ============================================================================
/**
* Generate a random Float32Array vector
*/
function randomVector(dimension: number): Float32Array {
const vec = new Float32Array(dimension);
for (let i = 0; i < dimension; i++) {
vec[i] = Math.random() * 2 - 1;
}
return vec;
}
/**
* Generate a normalized random vector
*/
function normalizedRandomVector(dimension: number): Float32Array {
const vec = randomVector(dimension);
return normalizeVector(vec);
}
/**
* Compare two Float32Arrays for approximate equality
*/
function vectorsApproxEqual(
a: Float32Array,
b: Float32Array,
tolerance = 1e-5
): boolean {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (Math.abs(a[i] - b[i]) > tolerance) return false;
}
return true;
}
/**
* Reference cosine similarity implementation for validation
*/
function referenceCosine(a: Float32Array, b: Float32Array): number {
let dot = 0, normA = 0, normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
const denom = Math.sqrt(normA) * Math.sqrt(normB);
return denom === 0 ? 0 : dot / denom;
}
/**
* Reference Euclidean distance implementation
*/
function referenceEuclidean(a: Float32Array, b: Float32Array): number {
let sum = 0;
for (let i = 0; i < a.length; i++) {
const diff = a[i] - b[i];
sum += diff * diff;
}
return Math.sqrt(sum);
}
// ============================================================================
// 1. SIMD Vector Operations Tests
// ============================================================================
describe('SIMD Vector Operations', () => {
describe('cosineSimilaritySIMD', () => {
it('should return 1.0 for identical vectors', () => {
const v = normalizedRandomVector(384);
const similarity = cosineSimilaritySIMD(v, v);
expect(similarity).toBeCloseTo(1.0, 5);
});
it('should return -1.0 for opposite vectors', () => {
const v1 = new Float32Array([1, 0, 0]);
const v2 = new Float32Array([-1, 0, 0]);
const similarity = cosineSimilaritySIMD(v1, v2);
expect(similarity).toBeCloseTo(-1.0, 5);
});
it('should return 0.0 for orthogonal vectors', () => {
const v1 = new Float32Array([1, 0, 0]);
const v2 = new Float32Array([0, 1, 0]);
const similarity = cosineSimilaritySIMD(v1, v2);
expect(similarity).toBeCloseTo(0.0, 5);
});
it('should handle zero vectors', () => {
const zero = new Float32Array([0, 0, 0]);
const v = new Float32Array([1, 2, 3]);
const similarity = cosineSimilaritySIMD(zero, v);
expect(similarity).toBe(0);
});
it('should handle empty vectors', () => {
const empty = new Float32Array(0);
const similarity = cosineSimilaritySIMD(empty, empty);
expect(similarity).toBe(0);
});
it('should throw error for mismatched dimensions', () => {
const v1 = new Float32Array([1, 2, 3]);
const v2 = new Float32Array([1, 2]);
expect(() => cosineSimilaritySIMD(v1, v2)).toThrow();
});
it('should match reference implementation for random vectors', () => {
for (let i = 0; i < 10; i++) {
const v1 = randomVector(384);
const v2 = randomVector(384);
const simd = cosineSimilaritySIMD(v1, v2);
const ref = referenceCosine(v1, v2);
expect(simd).toBeCloseTo(ref, 5);
}
});
it('should handle vectors not divisible by 8', () => {
const v1 = randomVector(13);
const v2 = randomVector(13);
const simd = cosineSimilaritySIMD(v1, v2);
const ref = referenceCosine(v1, v2);
expect(simd).toBeCloseTo(ref, 5);
});
it('should be bounded between -1 and 1', () => {
for (let i = 0; i < 100; i++) {
const v1 = randomVector(384);
const v2 = randomVector(384);
const similarity = cosineSimilaritySIMD(v1, v2);
expect(similarity).toBeGreaterThanOrEqual(-1);
expect(similarity).toBeLessThanOrEqual(1);
}
});
});
describe('euclideanDistanceSIMD', () => {
it('should return 0 for identical vectors', () => {
const v = randomVector(384);
const distance = euclideanDistanceSIMD(v, v);
expect(distance).toBeCloseTo(0, 5);
});
it('should compute correct distance for unit vectors', () => {
const v1 = new Float32Array([1, 0, 0]);
const v2 = new Float32Array([0, 1, 0]);
const distance = euclideanDistanceSIMD(v1, v2);
expect(distance).toBeCloseTo(Math.sqrt(2), 5);
});
it('should match reference implementation', () => {
for (let i = 0; i < 10; i++) {
const v1 = randomVector(384);
const v2 = randomVector(384);
const simd = euclideanDistanceSIMD(v1, v2);
const ref = referenceEuclidean(v1, v2);
expect(simd).toBeCloseTo(ref, 5);
}
});
it('should handle empty vectors', () => {
const empty = new Float32Array(0);
const distance = euclideanDistanceSIMD(empty, empty);
expect(distance).toBe(0);
});
it('should always be non-negative', () => {
for (let i = 0; i < 100; i++) {
const v1 = randomVector(384);
const v2 = randomVector(384);
const distance = euclideanDistanceSIMD(v1, v2);
expect(distance).toBeGreaterThanOrEqual(0);
}
});
});
describe('batchCosineSimilarity', () => {
it('should process batch correctly', () => {
const query = normalizedRandomVector(64);
const vectors = Array.from({ length: 100 }, () => normalizedRandomVector(64));
const results = batchCosineSimilarity(query, vectors);
expect(results).toHaveLength(100);
expect(results[0].similarity).toBeGreaterThanOrEqual(results[results.length - 1].similarity);
});
it('should return top-k results when specified', () => {
const query = normalizedRandomVector(64);
const vectors = Array.from({ length: 100 }, () => normalizedRandomVector(64));
const results = batchCosineSimilarity(query, vectors, { topK: 10 });
expect(results).toHaveLength(10);
});
it('should filter by threshold', () => {
const query = new Float32Array([1, 0, 0]);
const vectors = [
new Float32Array([1, 0, 0]), // similarity = 1.0
new Float32Array([0.9, 0.1, 0]), // similarity ~ 0.99
new Float32Array([0, 1, 0]), // similarity = 0.0
new Float32Array([-1, 0, 0]), // similarity = -1.0
];
const results = batchCosineSimilarity(query, vectors, { threshold: 0.5 });
expect(results.length).toBe(2);
results.forEach(r => {
expect(r.similarity).toBeGreaterThanOrEqual(0.5);
});
});
it('should preserve index information', () => {
const query = new Float32Array([1, 0, 0]);
const vectors = [
new Float32Array([0, 1, 0]), // index 0
new Float32Array([1, 0, 0]), // index 1, most similar
new Float32Array([0, 0, 1]), // index 2
];
const results = batchCosineSimilarity(query, vectors, { topK: 1 });
expect(results[0].index).toBe(1);
});
});
describe('SIMDVectorOps class', () => {
let ops: SIMDVectorOps;
beforeEach(() => {
ops = new SIMDVectorOps({
bufferPoolSize: 16,
defaultDimension: 384,
enableLogging: false,
});
});
afterEach(() => {
ops.clearBufferPool();
});
describe('buffer pool acquire/release', () => {
it('should acquire buffer of specified size', () => {
const buffer = ops.acquireBuffer(256);
expect(buffer).toBeInstanceOf(Float32Array);
expect(buffer.length).toBe(256);
});
it('should reuse released buffers', () => {
const buffer1 = ops.acquireBuffer(128);
ops.releaseBuffer(buffer1);
const buffer2 = ops.acquireBuffer(128);
// Buffer should be reused (same or similar object)
expect(buffer2.length).toBe(128);
});
it('should track statistics', () => {
ops.cosineSimilarity(randomVector(64), randomVector(64));
ops.euclideanDistance(randomVector(64), randomVector(64));
const stats = ops.getStats();
expect(stats.operationsCount).toBeGreaterThanOrEqual(2);
expect(stats.vectorsProcessed).toBeGreaterThanOrEqual(4);
});
it('should reset statistics', () => {
ops.cosineSimilarity(randomVector(64), randomVector(64));
ops.resetStats();
const stats = ops.getStats();
expect(stats.operationsCount).toBe(0);
expect(stats.vectorsProcessed).toBe(0);
});
});
it('should report SIMD detection status', () => {
const stats = ops.getStats();
expect(typeof stats.simdEnabled).toBe('boolean');
});
});
describe('SIMD detection', () => {
it('should return boolean for SIMD support', () => {
const supported = detectSIMDSupport();
expect(typeof supported).toBe('boolean');
});
it('should cache detection result', () => {
const result1 = detectSIMDSupport();
const result2 = detectSIMDSupport();
expect(result1).toBe(result2);
});
});
});
// ============================================================================
// 2. Vector Quantization Tests
// ============================================================================
describe('Vector Quantization', () => {
describe('8-bit quantization', () => {
it('should quantize and dequantize with minimal error', () => {
const original = new Float32Array([0.1, 0.5, 0.9, -0.3, 0.0]);
const quantized = quantize8bit(original);
expect(quantized.type).toBe('8bit');
expect(quantized.data.length).toBe(original.length);
expect(quantized.dimension).toBe(original.length);
const reconstructed = dequantize8bit(
quantized.data,
quantized.min,
quantized.max
);
expect(reconstructed.length).toBe(original.length);
const error = calculateQuantizationError(original, reconstructed);
expect(error.maxError).toBeLessThan(0.01);
});
it('should achieve approximately 4x compression', () => {
const vector = randomVector(384);
const stats = getQuantizationStats(vector, '8bit');
expect(stats.compressionRatio).toBeCloseTo(4, 0.5);
});
it('should handle uniform vectors', () => {
const vector = new Float32Array([0.5, 0.5, 0.5, 0.5]);
const quantized = quantize8bit(vector);
const reconstructed = dequantize8bit(
quantized.data,
quantized.min,
quantized.max
);
for (let i = 0; i < vector.length; i++) {
expect(reconstructed[i]).toBeCloseTo(0.5, 5);
}
});
it('should handle empty vectors', () => {
const empty = new Float32Array(0);
const quantized = quantize8bit(empty);
expect(quantized.data.length).toBe(0);
const reconstructed = dequantize8bit(quantized.data, 0, 0);
expect(reconstructed.length).toBe(0);
});
it('should preserve relative ordering of values', () => {
const vector = new Float32Array([0.1, 0.3, 0.5, 0.7, 0.9]);
const quantized = quantize8bit(vector);
const reconstructed = dequantize8bit(
quantized.data,
quantized.min,
quantized.max
);
for (let i = 1; i < reconstructed.length; i++) {
expect(reconstructed[i]).toBeGreaterThan(reconstructed[i - 1]);
}
});
});
describe('4-bit quantization', () => {
it('should quantize and dequantize with acceptable error', () => {
const original = new Float32Array([0.1, 0.5, 0.9, -0.3, 0.0, 0.7]);
const quantized = quantize4bit(original);
expect(quantized.type).toBe('4bit');
expect(quantized.data.length).toBe(Math.ceil(original.length / 2));
expect(quantized.dimension).toBe(original.length);
const reconstructed = dequantize4bit(
quantized.data,
quantized.min,
quantized.max,
quantized.dimension
);
expect(reconstructed.length).toBe(original.length);
const error = calculateQuantizationError(original, reconstructed);
expect(error.maxError).toBeLessThan(0.15);
});
it('should achieve approximately 8x compression', () => {
const vector = randomVector(384);
const stats = getQuantizationStats(vector, '4bit');
expect(stats.compressionRatio).toBeCloseTo(8, 1);
});
it('should handle odd-length vectors', () => {
const vector = new Float32Array([0.1, 0.5, 0.9]);
const quantized = quantize4bit(vector);
expect(quantized.data.length).toBe(2);
const reconstructed = dequantize4bit(
quantized.data,
quantized.min,
quantized.max,
quantized.dimension
);
expect(reconstructed.length).toBe(3);
});
});
describe('ProductQuantizer', () => {
const dimension = 64;
const numSubspaces = 8;
const numCentroids = 16;
let pq: ProductQuantizer;
let trainingVectors: Float32Array[];
beforeEach(async () => {
trainingVectors = Array.from({ length: 100 }, () => randomVector(dimension));
pq = new ProductQuantizer({
dimension,
numSubspaces,
numCentroids,
maxIterations: 10,
seed: 42,
});
await pq.train(trainingVectors);
});
it('should train successfully', () => {
expect(pq.isTrained()).toBe(true);
});
it('should encode and decode vectors', () => {
const vector = trainingVectors[0];
const encoded = pq.encode(vector);
expect(encoded.codes.length).toBe(numSubspaces);
expect(encoded.norm).toBeGreaterThan(0);
const decoded = pq.decode(encoded);
expect(decoded.length).toBe(dimension);
});
it('should compute asymmetric distance', () => {
const query = trainingVectors[0];
const encoded = pq.encode(trainingVectors[1]);
const distance = pq.asymmetricDistance(query, encoded);
expect(distance).toBeGreaterThanOrEqual(0);
});
it('should compute distance using precomputed tables', () => {
const query = trainingVectors[0];
const encoded = pq.encode(trainingVectors[1]);
const tables = pq.precomputeDistanceTables(query);
expect(tables.length).toBe(numSubspaces);
const tableDistance = pq.distanceFromTables(tables, encoded);
const directDistance = pq.asymmetricDistance(query, encoded);
expect(tableDistance).toBeCloseTo(directDistance, 5);
});
it('should export and import codebooks', () => {
const exported = pq.exportCodebooks();
expect(typeof exported).toBe('string');
const pq2 = new ProductQuantizer({
dimension,
numSubspaces,
numCentroids,
});
pq2.importCodebooks(exported);
expect(pq2.isTrained()).toBe(true);
const vector = trainingVectors[0];
const encoded1 = pq.encode(vector);
const encoded2 = pq2.encode(vector);
expect(Array.from(encoded1.codes)).toEqual(Array.from(encoded2.codes));
});
it('should report correct stats', () => {
const stats = pq.getStats();
expect(stats.trained).toBe(true);
expect(stats.dimension).toBe(dimension);
expect(stats.numSubspaces).toBe(numSubspaces);
expect(stats.subspaceDim).toBe(dimension / numSubspaces);
expect(stats.numCentroids).toBe(numCentroids);
expect(stats.compressionRatio).toBeGreaterThan(1);
expect(stats.codebookSizeBytes).toBeGreaterThan(0);
});
it('should throw on invalid dimension', () => {
expect(() => {
new ProductQuantizer({
dimension: 65,
numSubspaces: 8,
numCentroids: 256,
});
}).toThrow();
});
it('should throw on too many centroids', () => {
expect(() => {
new ProductQuantizer({
dimension: 64,
numSubspaces: 8,
numCentroids: 257,
});
}).toThrow();
});
});
describe('QuantizedVectorStore', () => {
const dimension = 64;
describe('Scalar 8-bit store', () => {
let store: QuantizedVectorStore;
beforeEach(() => {
store = createScalar8BitStore(dimension);
});
it('should insert and search vectors', () => {
const v1 = normalizedRandomVector(dimension);
const v2 = normalizedRandomVector(dimension);
const v3 = normalizedRandomVector(dimension);
store.insert('v1', v1, { label: 'first' });
store.insert('v2', v2, { label: 'second' });
store.insert('v3', v3, { label: 'third' });
const results = store.search(v1, 3);
expect(results.length).toBe(3);
expect(results[0].id).toBe('v1');
expect(results[0].similarity).toBeGreaterThan(0.9);
expect(results[0].metadata?.label).toBe('first');
});
it('should remove vectors', () => {
store.insert('v1', randomVector(dimension));
store.insert('v2', randomVector(dimension));
expect(store.remove('v1')).toBe(true);
expect(store.remove('v1')).toBe(false);
const stats = store.getStats();
expect(stats.count).toBe(1);
});
it('should retrieve dequantized vectors', () => {
const original = randomVector(dimension);
store.insert('v1', original);
const retrieved = store.getVector('v1');
expect(retrieved).not.toBeNull();
expect(retrieved!.length).toBe(dimension);
const error = calculateQuantizationError(original, retrieved!);
expect(error.meanError).toBeLessThan(0.05);
});
it('should export and import store', () => {
store.insert('v1', randomVector(dimension), { tag: 'a' });
store.insert('v2', randomVector(dimension), { tag: 'b' });
const exported = store.export();
const store2 = createScalar8BitStore(dimension);
store2.import(exported);
const stats = store2.getStats();
expect(stats.count).toBe(2);
const v1 = store2.getVector('v1');
expect(v1).not.toBeNull();
});
it('should batch insert vectors', () => {
const items = Array.from({ length: 50 }, (_, i) => ({
id: `v${i}`,
vector: randomVector(dimension),
metadata: { index: i },
}));
store.insertBatch(items);
const stats = store.getStats();
expect(stats.count).toBe(50);
});
it('should report memory usage', () => {
for (let i = 0; i < 100; i++) {
store.insert(`v${i}`, randomVector(dimension));
}
const stats = store.getStats();
expect(stats.memoryUsageBytes).toBeGreaterThan(0);
expect(stats.memoryUsageBytes).toBeLessThan(dimension * 4 * 100);
});
});
describe('Scalar 4-bit store', () => {
it('should achieve higher compression than 8-bit', () => {
const store4bit = createScalar4BitStore(dimension);
const store8bit = createScalar8BitStore(dimension);
for (let i = 0; i < 100; i++) {
const vec = randomVector(dimension);
store4bit.insert(`v${i}`, vec);
store8bit.insert(`v${i}`, vec);
}
const stats4bit = store4bit.getStats();
const stats8bit = store8bit.getStats();
expect(stats4bit.compressionRatio).toBeGreaterThan(stats8bit.compressionRatio);
});
it('should still find similar vectors', () => {
const store = createScalar4BitStore(dimension);
const baseVector = normalizedRandomVector(dimension);
const similarVector = new Float32Array(baseVector);
for (let i = 0; i < dimension; i++) {
similarVector[i] += (Math.random() - 0.5) * 0.1;
}
store.insert('base', baseVector);
store.insert('similar', normalizeVector(similarVector));
store.insert('random', normalizedRandomVector(dimension));
const results = store.search(baseVector, 2);
expect(results.length).toBe(2);
expect(results[0].id).toBe('base');
expect(results[1].id).toBe('similar');
});
});
describe('Product Quantized store', () => {
it('should require training before insertion', () => {
const store = createProductQuantizedStore(dimension, 8, 16);
expect(store.isReady()).toBe(false);
expect(() => {
store.insert('v1', randomVector(dimension));
}).toThrow();
});
it('should work after training', async () => {
const store = createProductQuantizedStore(dimension, 8, 16);
const trainingData = Array.from({ length: 50 }, () =>
normalizedRandomVector(dimension)
);
await store.train(trainingData);
expect(store.isReady()).toBe(true);
store.insert('v1', trainingData[0], { index: 0 });
store.insert('v2', trainingData[1], { index: 1 });
const results = store.search(trainingData[0], 2);
expect(results.length).toBe(2);
expect(results[0].id).toBe('v1');
});
it('should achieve high compression', async () => {
const store = createProductQuantizedStore(dimension, 8, 16);
const trainingData = Array.from({ length: 50 }, () => randomVector(dimension));
await store.train(trainingData);
for (let i = 0; i < 50; i++) {
store.insert(`v${i}`, trainingData[i]);
}
const stats = store.getStats();
expect(stats.compressionRatio).toBeGreaterThan(4);
});
});
});
describe('Quantization accuracy measurement', () => {
it('should track mean and max error', () => {
const original = randomVector(384);
const quantized = quantize8bit(original);
const reconstructed = dequantize8bit(
quantized.data,
quantized.min,
quantized.max
);
const error = calculateQuantizationError(original, reconstructed);
expect(error.meanError).toBeGreaterThanOrEqual(0);
expect(error.maxError).toBeGreaterThanOrEqual(error.meanError);
expect(error.mse).toBeGreaterThanOrEqual(0);
});
it('should throw for mismatched lengths', () => {
const v1 = new Float32Array([1, 2, 3]);
const v2 = new Float32Array([1, 2]);
expect(() => calculateQuantizationError(v1, v2)).toThrow();
});
});
});
// ============================================================================
// 3. RuVectorBackend Enhancement Tests
// ============================================================================
describe('RuVectorBackend Enhancements', () => {
describe('Semaphore', () => {
it('should limit concurrent operations', async () => {
const semaphore = new Semaphore(2);
let concurrent = 0;
let maxConcurrent = 0;
const tasks = Array.from({ length: 10 }, async () => {
await semaphore.acquire();
concurrent++;
maxConcurrent = Math.max(maxConcurrent, concurrent);
await new Promise(resolve => setTimeout(resolve, 10));
concurrent--;
semaphore.release();
});
await Promise.all(tasks);
expect(maxConcurrent).toBeLessThanOrEqual(2);
});
it('should track available permits', () => {
const semaphore = new Semaphore(3);
expect(semaphore.available).toBe(3);
semaphore.acquire();
expect(semaphore.available).toBe(2);
semaphore.release();
expect(semaphore.available).toBe(3);
});
});
describe('BufferPool', () => {
let pool: BufferPool;
beforeEach(() => {
pool = new BufferPool(10);
});
afterEach(() => {
pool.clear();
});
it('should acquire new buffers', () => {
const buffer = pool.acquire(256);
expect(buffer).toBeInstanceOf(Float32Array);
expect(buffer.length).toBe(256);
});
it('should reuse released buffers', () => {
const buffer1 = pool.acquire(128);
buffer1[0] = 42;
pool.release(buffer1);
const buffer2 = pool.acquire(128);
expect(buffer2[0]).toBe(0); // Buffer should be cleared
});
it('should maintain separate pools for different sizes', () => {
const buf128 = pool.acquire(128);
const buf256 = pool.acquire(256);
pool.release(buf128);
pool.release(buf256);
const stats = pool.getStats();
expect(stats.totalBuffers).toBe(2);
});
it('should report statistics', () => {
pool.acquire(64);
pool.acquire(64);
const buf = pool.acquire(128);
pool.release(buf);
const stats = pool.getStats();
expect(stats.totalBuffers).toBeGreaterThanOrEqual(1);
expect(stats.totalMemory).toBeGreaterThanOrEqual(128 * 4);
});
});
describe('Adaptive parameters', () => {
it('should return small dataset params for < 1000 vectors', () => {
const params = RuVectorBackend.getRecommendedParams(500);
expect(params.M).toBe(8);
expect(params.efConstruction).toBe(100);
expect(params.efSearch).toBe(50);
});
it('should return medium dataset params for 1000-100000 vectors', () => {
const params = RuVectorBackend.getRecommendedParams(50000);
expect(params.M).toBe(16);
expect(params.efConstruction).toBe(200);
expect(params.efSearch).toBe(100);
});
it('should return large dataset params for > 100000 vectors', () => {
const params = RuVectorBackend.getRecommendedParams(500000);
expect(params.M).toBe(32);
expect(params.efConstruction).toBe(400);
expect(params.efSearch).toBe(200);
});
});
});
// ============================================================================
// 4. Enhanced Embedding Service Tests
// ============================================================================
describe('EnhancedEmbeddingService', () => {
let service: EnhancedEmbeddingService;
beforeEach(async () => {
service = new EnhancedEmbeddingService({
model: 'mock-model',
dimension: 384,
provider: 'local',
enableWASM: true,
enableBatchProcessing: true,
batchSize: 50,
});
await service.initialize();
});
describe('Batch embedding', () => {
it('should embed batch of texts', async () => {
const texts = ['hello', 'world', 'test', 'embedding', 'service'];
const embeddings = await service.embedBatch(texts);
expect(embeddings).toHaveLength(5);
embeddings.forEach(emb => {
expect(emb).toBeInstanceOf(Float32Array);
expect(emb.length).toBe(384);
});
});
it('should handle large batches', async () => {
const texts = Array.from({ length: 200 }, (_, i) => `text ${i}`);
const embeddings = await service.embedBatch(texts);
expect(embeddings).toHaveLength(200);
});
it('should deduplicate identical texts', async () => {
const texts = ['same', 'same', 'same', 'different'];
const embeddings = await service.embedBatch(texts);
expect(embeddings).toHaveLength(4);
expect(vectorsApproxEqual(embeddings[0], embeddings[1])).toBe(true);
expect(vectorsApproxEqual(embeddings[0], embeddings[2])).toBe(true);
});
});
describe('Similarity calculation', () => {
it('should calculate text similarity', async () => {
const similarity = await service.similarity('hello world', 'hello world');
expect(similarity).toBeCloseTo(1.0, 5);
});
it('should find most similar texts', async () => {
const corpus = [
'machine learning',
'artificial intelligence',
'deep learning',
'cooking recipes',
'neural networks',
];
const results = await service.findMostSimilar('AI and ML', corpus, 3);
expect(results).toHaveLength(3);
expect(results[0]).toHaveProperty('text');
expect(results[0]).toHaveProperty('similarity');
expect(results[0]).toHaveProperty('index');
results.forEach(result => {
expect(result.similarity).toBeGreaterThanOrEqual(-1);
expect(result.similarity).toBeLessThanOrEqual(1);
expect(corpus).toContain(result.text);
});
});
});
describe('Statistics', () => {
it('should provide service statistics', () => {
const stats = service.getStats();
expect(stats).toHaveProperty('cacheSize');
expect(stats).toHaveProperty('wasmEnabled');
expect(stats).toHaveProperty('simdEnabled');
expect(typeof stats.cacheSize).toBe('number');
});
});
describe('LRU cache behavior', () => {
it('should cache embeddings', async () => {
await service.embed('cached text');
await service.embed('cached text');
const stats = service.getStats();
expect(stats.cacheSize).toBeGreaterThanOrEqual(1);
});
});
});
// ============================================================================
// 5. Attention Optimized Tests
// ============================================================================
describe('Attention Optimized', () => {
describe('scaledDotProductAttention', () => {
it('should compute attention correctly', () => {
const query = [1, 0, 0, 0];
const key = [1, 0, 0, 0];
const value = [1, 2, 3, 4];
const { output, weights } = scaledDotProductAttention(query, key, value);
expect(output).toHaveLength(4);
expect(weights).toHaveLength(1);
});
it('should apply mask', () => {
const query = [1, 0, 0, 0];
const key = [1, 0, 0, 0];
const value = [1, 2, 3, 4];
const mask = [0];
const { weights } = scaledDotProductAttention(query, key, value, mask);
expect(weights[0]).toBe(0);
});
});
describe('scaledDotProductAttentionOptimized', () => {
it('should compute attention with TypedArrays', () => {
const query = new Float32Array([1, 0, 0, 0]);
const key = new Float32Array([1, 0, 0, 0]);
const value = new Float32Array([1, 2, 3, 4]);
const { output, weights } = scaledDotProductAttentionOptimized(query, key, value);
expect(output).toBeInstanceOf(Float32Array);
expect(output.length).toBe(4);
expect(weights).toBeInstanceOf(Float32Array);
});
it('should match original implementation', () => {
const query = new Float32Array([0.5, 0.3, 0.2, 0.1]);
const key = new Float32Array([0.4, 0.3, 0.2, 0.1]);
const value = new Float32Array([1, 2, 3, 4]);
const optimized = scaledDotProductAttentionOptimized(query, key, value);
const original = scaledDotProductAttention(
Array.from(query),
Array.from(key),
Array.from(value)
);
for (let i = 0; i < value.length; i++) {
expect(optimized.output[i]).toBeCloseTo(original.output[i], 4);
}
});
});
describe('MultiHeadAttention vs MultiHeadAttentionOptimized', () => {
const config = { hiddenDim: 64, numHeads: 8 };
it('should produce outputs of same dimensions', () => {
const mha = new MultiHeadAttention(config);
const mhaOpt = new MultiHeadAttentionOptimized(config);
const query = Array.from({ length: 64 }, () => Math.random());
const key = Array.from({ length: 64 }, () => Math.random());
const value = Array.from({ length: 64 }, () => Math.random());
const queryF32 = new Float32Array(query);
const keyF32 = new Float32Array(key);
const valueF32 = new Float32Array(value);
const original = mha.forward(query, key, value);
const optimized = mhaOpt.forward(queryF32, keyF32, valueF32);
expect(original.output.length).toBe(optimized.output.length);
expect(original.attentionWeights.length).toBe(optimized.attentionWeights.length);
});
it('should support batch processing', () => {
const mhaOpt = new MultiHeadAttentionOptimized(config);
const batchSize = 4;
const queries = Array.from({ length: batchSize }, () => randomVector(64));
const keys = Array.from({ length: batchSize }, () => randomVector(64));
const values = Array.from({ length: batchSize }, () => randomVector(64));
const { outputs, attentionWeights } = mhaOpt.batchForward(queries, keys, values);
expect(outputs).toHaveLength(batchSize);
expect(attentionWeights).toHaveLength(batchSize);
});
it('should allow weight get/set', () => {
const mhaOpt = new MultiHeadAttentionOptimized(config);
const weights = mhaOpt.getWeights();
expect(weights.query).toBeInstanceOf(Float32Array);
expect(weights.key).toBeInstanceOf(Float32Array);
expect(weights.value).toBeInstanceOf(Float32Array);
expect(weights.output).toBeInstanceOf(Float32Array);
// Modify and set back
weights.query[0] = 999;
mhaOpt.setWeights(weights);
const retrieved = mhaOpt.getWeights();
expect(retrieved.query[0]).toBe(999);
});
});
describe('FlashAttention vs FlashAttentionOptimized', () => {
const config = { hiddenDim: 64 };
it('should compute attention over sequences', () => {
const flash = new FlashAttention(config);
const seqLen = 4;
const query = Array.from({ length: seqLen }, () =>
Array.from({ length: 64 }, () => Math.random())
);
const key = Array.from({ length: seqLen }, () =>
Array.from({ length: 64 }, () => Math.random())
);
const value = Array.from({ length: seqLen }, () =>
Array.from({ length: 64 }, () => Math.random())
);
const { output, attentionScores } = flash.forward(query, key, value, 8);
expect(output).toHaveLength(seqLen);
expect(attentionScores).toHaveLength(seqLen);
});
it('should match dimensions in optimized version', () => {
const flashOpt = new FlashAttentionOptimized({
hiddenDim: 64,
blockSizeQ: 2,
blockSizeKV: 4,
});
const seqLen = 8;
const dim = 64;
const query = randomVector(seqLen * dim);
const key = randomVector(seqLen * dim);
const value = randomVector(seqLen * dim);
const { output, attentionScores } = flashOpt.forward(
query,
key,
value,
seqLen,
dim,
8
);
expect(output.length).toBe(seqLen * dim);
expect(attentionScores.length).toBe(seqLen * seqLen);
flashOpt.releaseBuffer(output);
flashOpt.releaseBuffer(attentionScores);
});
it('should support causal masking', () => {
const flashOpt = new FlashAttentionOptimized({
hiddenDim: 64,
causal: true,
});
const seqLen = 4;
const dim = 64;
const query = randomVector(seqLen * dim);
const key = randomVector(seqLen * dim);
const value = randomVector(seqLen * dim);
const { attentionScores } = flashOpt.forward(
query,
key,
value,
seqLen,
dim,
8
);
// The implementation stores raw scores (pre-softmax), so masked positions
// should have very large negative values (approximately -1e9)
// Check upper triangle has been masked (large negative values)
for (let i = 0; i < seqLen; i++) {
for (let j = i + 1; j < seqLen; j++) {
// Masked positions should be -1e9 (or very close to it)
expect(attentionScores[i * seqLen + j]).toBeLessThan(-1e8);
}
}
// Also verify lower triangle (including diagonal) is NOT masked
// These should be normal attention scores, not large negative values
for (let i = 0; i < seqLen; i++) {
for (let j = 0; j <= i; j++) {
expect(attentionScores[i * seqLen + j]).toBeGreaterThan(-100);
}
}
});
it('should support batch processing', () => {
const flashOpt = new FlashAttentionOptimized({ hiddenDim: 32 });
const batchSize = 3;
const seqLens = [4, 4, 4];
const dim = 32;
const queries = seqLens.map(len => randomVector(len * dim));
const keys = seqLens.map(len => randomVector(len * dim));
const values = seqLens.map(len => randomVector(len * dim));
const { outputs, attentionScores } = flashOpt.batchForward(
queries,
keys,
values,
seqLens,
dim,
4
);
expect(outputs).toHaveLength(batchSize);
expect(attentionScores).toHaveLength(batchSize);
});
});
describe('batchSequenceAttention', () => {
it('should process sequence attention', () => {
const seqLen = 4;
const dim = 8;
const queries = randomVector(seqLen * dim);
const keys = randomVector(seqLen * dim);
const values = randomVector(seqLen * dim);
const { output, weights } = batchSequenceAttention(
queries,
keys,
values,
seqLen,
dim
);
expect(output.length).toBe(seqLen * dim);
expect(weights.length).toBe(seqLen * seqLen);
});
it('should apply mask correctly', () => {
const seqLen = 2;
const dim = 4;
const queries = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0]);
const keys = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0]);
const values = new Float32Array([1, 2, 3, 4, 5, 6, 7, 8]);
const mask = new Float32Array([1, 0, 1, 1]); // Block attention from pos 0 to pos 1
const { weights } = batchSequenceAttention(
queries,
keys,
values,
seqLen,
dim,
mask
);
// Weight from position 0 to position 1 should be very small
expect(weights[1]).toBeLessThan(0.01);
});
});
describe('LinearAttention', () => {
it('should compute linear attention', () => {
const linear = new LinearAttention({ hiddenDim: 8 });
const query = [[1, 2, 3, 4, 5, 6, 7, 8]];
const key = [[1, 2, 3, 4, 5, 6, 7, 8]];
const value = [[1, 2, 3, 4, 5, 6, 7, 8]];
const { output } = linear.forward(query, key, value);
expect(output).toHaveLength(1);
expect(output[0]).toHaveLength(8);
});
});
describe('HyperbolicAttention', () => {
it('should compute hyperbolic attention', () => {
const hyper = new HyperbolicAttention({ hiddenDim: 4 });
const query = [0.1, 0.2, 0.3, 0.4];
const key = [0.15, 0.25, 0.35, 0.45];
const value = [1, 2, 3, 4];
const { output, distance } = hyper.forward(query, key, value);
expect(output).toHaveLength(4);
expect(distance).toBeGreaterThanOrEqual(0);
});
});
describe('Factory functions', () => {
it('should create attention modules', () => {
const config = { hiddenDim: 64 };
const mha = createAttention('multi-head', config);
expect(mha).toBeInstanceOf(MultiHeadAttention);
const flash = createAttention('flash', config);
expect(flash).toBeInstanceOf(FlashAttention);
const linear = createAttention('linear', config);
expect(linear).toBeInstanceOf(LinearAttention);
const hyper = createAttention('hyperbolic', config);
expect(hyper).toBeInstanceOf(HyperbolicAttention);
});
it('should create optimized attention modules', () => {
const config = { hiddenDim: 64 };
const mhaOpt = createAttentionOptimized('multi-head', config);
expect(mhaOpt).toBeInstanceOf(MultiHeadAttentionOptimized);
const flashOpt = createAttentionOptimized('flash', config);
expect(flashOpt).toBeInstanceOf(FlashAttentionOptimized);
});
});
describe('Buffer pool', () => {
it('should provide global buffer pool access', () => {
const pool = getBufferPool();
const buffer = pool.acquire(64);
expect(buffer).toBeInstanceOf(Float32Array);
expect(buffer.length).toBe(64);
pool.release(buffer);
});
});
describe('Utility functions', () => {
it('should convert arrays to Float32Array', () => {
const arr = [1, 2, 3, 4];
const f32 = toFloat32Array(arr);
expect(f32).toBeInstanceOf(Float32Array);
expect(Array.from(f32)).toEqual(arr);
});
it('should flatten 2D arrays', () => {
const arr2d = [[1, 2], [3, 4], [5, 6]];
const flat = flatten2D(arr2d);
expect(flat).toBeInstanceOf(Float32Array);
expect(Array.from(flat)).toEqual([1, 2, 3, 4, 5, 6]);
});
});
});
// ============================================================================
// 6. WASM Vector Search Tests
// ============================================================================
describe('WASMVectorSearch', () => {
let mockDb: any;
let wasmSearch: WASMVectorSearch;
beforeEach(() => {
mockDb = {
prepare: () => ({
all: () => [],
get: () => null,
run: () => ({ lastInsertRowid: 1, changes: 1 }),
}),
exec: () => {},
};
wasmSearch = new WASMVectorSearch(mockDb);
});
describe('Cosine Similarity', () => {
it('should calculate correctly for identical vectors', () => {
const v = new Float32Array([1, 0, 0]);
const similarity = wasmSearch.cosineSimilarity(v, v);
expect(similarity).toBeCloseTo(1.0, 5);
});
it('should handle orthogonal vectors', () => {
const v1 = new Float32Array([1, 0, 0]);
const v2 = new Float32Array([0, 1, 0]);
const similarity = wasmSearch.cosineSimilarity(v1, v2);
expect(similarity).toBeCloseTo(0.0, 5);
});
it('should throw on mismatched dimensions', () => {
const v1 = new Float32Array([1, 0, 0]);
const v2 = new Float32Array([1, 0]);
expect(() => wasmSearch.cosineSimilarity(v1, v2)).toThrow();
});
});
describe('Batch Operations', () => {
it('should calculate batch similarities', () => {
const query = new Float32Array([1, 0, 0]);
const vectors = [
new Float32Array([1, 0, 0]),
new Float32Array([0, 1, 0]),
new Float32Array([0, 0, 1]),
];
const similarities = wasmSearch.batchSimilarity(query, vectors);
expect(similarities).toHaveLength(3);
expect(similarities[0]).toBeCloseTo(1.0, 5);
expect(similarities[1]).toBeCloseTo(0.0, 5);
expect(similarities[2]).toBeCloseTo(0.0, 5);
});
it('should handle large batches', () => {
const query = new Float32Array(384).fill(0.5);
const vectors: Float32Array[] = [];
for (let i = 0; i < 1000; i++) {
vectors.push(new Float32Array(384).fill(Math.random()));
}
const startTime = performance.now();
const similarities = wasmSearch.batchSimilarity(query, vectors);
const duration = performance.now() - startTime;
expect(similarities).toHaveLength(1000);
expect(duration).toBeLessThan(1000);
});
});
describe('Vector Index', () => {
it('should build index for large datasets', () => {
const vectors: Float32Array[] = [];
const ids: number[] = [];
for (let i = 0; i < 1500; i++) {
vectors.push(new Float32Array(128).fill(Math.random()));
ids.push(i);
}
wasmSearch.buildIndex(vectors, ids);
const stats = wasmSearch.getStats();
expect(stats.indexBuilt).toBe(true);
expect(stats.indexSize).toBe(1500);
});
it('should skip index for small datasets', () => {
const vectors = [new Float32Array([1, 0, 0])];
const ids = [1];
wasmSearch.buildIndex(vectors, ids);
const stats = wasmSearch.getStats();
expect(stats.indexBuilt).toBe(false);
});
it('should search index correctly', () => {
const vectors: Float32Array[] = [
new Float32Array([1, 0, 0]),
new Float32Array([0, 1, 0]),
new Float32Array([0, 0, 1]),
new Float32Array([0.7, 0.7, 0]),
];
const ids = [1, 2, 3, 4];
wasmSearch = new WASMVectorSearch(mockDb, { indexThreshold: 3 });
wasmSearch.buildIndex(vectors, ids);
const query = new Float32Array([1, 0, 0]);
const results = wasmSearch.searchIndex(query, 2);
expect(results).toHaveLength(2);
expect(results[0].id).toBe(1);
expect(results[0].similarity).toBeCloseTo(1.0, 5);
});
it('should clear index', () => {
wasmSearch = new WASMVectorSearch(mockDb, { indexThreshold: 0 });
wasmSearch.buildIndex([new Float32Array([1, 0, 0])], [1]);
let stats = wasmSearch.getStats();
expect(stats.indexBuilt).toBe(true);
wasmSearch.clearIndex();
stats = wasmSearch.getStats();
expect(stats.indexBuilt).toBe(false);
});
});
describe('Statistics', () => {
it('should report correct stats', () => {
const stats = wasmSearch.getStats();
expect(stats).toHaveProperty('wasmAvailable');
expect(stats).toHaveProperty('simdAvailable');
expect(stats).toHaveProperty('indexBuilt');
expect(stats).toHaveProperty('indexSize');
expect(typeof stats.wasmAvailable).toBe('boolean');
expect(typeof stats.simdAvailable).toBe('boolean');
});
});
});
// ============================================================================
// Performance Tests
// ============================================================================
describe('Performance', () => {
describe('SIMD operations performance', () => {
it('should process 1000 similarity calculations efficiently', () => {
const query = randomVector(384);
const vectors = Array.from({ length: 1000 }, () => randomVector(384));
const startTime = performance.now();
vectors.forEach(v => cosineSimilaritySIMD(query, v));
const duration = performance.now() - startTime;
expect(duration).toBeLessThan(500);
});
it('should batch process efficiently', () => {
const query = randomVector(384);
const vectors = Array.from({ length: 10000 }, () => randomVector(384));
const startTime = performance.now();
batchCosineSimilarity(query, vectors, { topK: 100 });
const duration = performance.now() - startTime;
expect(duration).toBeLessThan(5000);
});
});
describe('Quantization performance', () => {
it('should quantize 1000 vectors efficiently', () => {
const vectors = Array.from({ length: 1000 }, () => randomVector(384));
const startTime = performance.now();
vectors.forEach(v => quantize8bit(v));
const duration = performance.now() - startTime;
expect(duration).toBeLessThan(1000);
});
});
describe('Attention performance', () => {
it('should benchmark attention implementation', () => {
const attention = new FlashAttentionOptimized({ hiddenDim: 64 });
const result = benchmarkAttention(attention, {
seqLen: 32,
dim: 64,
iterations: 10,
numHeads: 8,
});
expect(result.avgTimeMs).toBeGreaterThan(0);
expect(result.opsPerSecond).toBeGreaterThan(0);
expect(result.memoryUsed).toBeGreaterThan(0);
});
});
});