Files
wehub-resource-sync 23f7624596
ADR-166 MCP Bridge Security Lock / Static-source security lock (push) Failing after 0s
ADR-166 MCP Bridge Security Lock / Compose default binds loopback + Mongo has auth (push) Failing after 2s
CodeQL Advanced / Analyze (rust) (push) Failing after 0s
ADR-166 MCP Bridge Security Lock / plugin-agent-federation bindHost default (push) Failing after 1s
ADR-166 MCP Bridge Security Lock / Runtime behavior — 401 + terminal gate + fail-closed (push) Failing after 4s
business-pods-smoke / smoke (push) Failing after 1s
all-plugins-smoke / smoke-all (push) Failing after 2s
CI/CD Pipeline / Security & Code Quality (push) Failing after 1s
CI/CD Pipeline / Test Suite (ubuntu-latest) (push) Failing after 1s
CI/CD Pipeline / Build & Package (macos-latest) (push) Has been skipped
CI/CD Pipeline / Build & Package (ubuntu-latest) (push) Has been skipped
CI/CD Pipeline / Build & Package (windows-latest) (push) Has been skipped
CI/CD Pipeline / Documentation & Examples (push) Failing after 1s
Clone Tracker (14-day rolling) / Snapshot clones for ruflo ecosystem (push) Failing after 1s
CodeQL Advanced / Analyze (actions) (push) Failing after 1s
CodeQL Advanced / Analyze (javascript-typescript) (push) Failing after 1s
federation-peer-rust / stable-noop (push) Failing after 1s
metaharness-ci / score (push) Failing after 1s
metaharness-ci / router-compat (push) Failing after 0s
metaharness-ci / similarity-tests (push) Failing after 0s
no-agentbbs-smoke / smoke-without-agentbbs (push) Failing after 1s
V3 CI/CD Pipeline / Build V3 (windows-latest) (push) Has been skipped
codex-integration-audit / Codex integration audit (push) Failing after 1s
helpers-manifest-guard / guard (push) Failing after 1s
🔗 Cross-Agent Integration Tests / 🤝 Agent Coordination Tests (push) Has been skipped
🔗 Cross-Agent Integration Tests / 🧠 Memory Sharing Integration (push) Has been skipped
🔗 Cross-Agent Integration Tests / 🛡️ Fault Tolerance Tests (push) Has been skipped
🔗 Cross-Agent Integration Tests / ⚡ Performance Integration Tests (push) Has been skipped
metaharness-ci / mcp-scan (push) Failing after 1s
metaharness-ci / eject-dryrun (push) Failing after 1s
metaharness-ci / metaharness-real-data (push) Failing after 0s
no-cli-optdep-bloat-2561 / guard (push) Failing after 1s
no-metaharness-smoke / smoke-without-metaharness (push) Failing after 1s
no-phantom-agentic-flow-subpath / guard (push) Failing after 1s
🔄 Automated Rollback Manager / 🚨 Failure Detection (push) Failing after 1s
V3 CI/CD Pipeline / Plugin hooks smoke / ubuntu-latest / Node 22 (push) Failing after 1s
V3 CI/CD Pipeline / ruflo-graph-intelligence build + test smoke (#2044, ADR-123) (push) Failing after 1s
CVE Audit Gate / Audit root (critical-blocking) (push) Failing after 2s
cost-tracker-smoke / smoke (push) Failing after 3s
oia-audit-weekly / audit (push) Failing after 2s
ruflo-agent-smoke / ruflo-agent structural smoke (push) Failing after 1s
📊 Status Badges Update / 📊 Update Status Badges (push) Failing after 1s
V3 CI/CD Pipeline / Static regression guards (#2267 YAML + (push) Failing after 1s
V3 CI/CD Pipeline / Test V3 Packages (push) Failing after 0s
V3 CI/CD Pipeline / agent_execute provider routing smoke (#2042) (push) Failing after 0s
CVE Audit Gate / Audit v3 (critical-blocking) (push) Failing after 1s
federation-peer-rust / stable-native (push) Failing after 2s
🔗 Cross-Agent Integration Tests / 🚀 Integration Test Setup (push) Failing after 2s
neural-trader-smoke / runtime-smoke (push) Failing after 1s
V3 CI/CD Pipeline / Build V3 (macos-latest) (push) Has been skipped
V3 CI/CD Pipeline / Build V3 (ubuntu-latest) (push) Has been skipped
V3 CI/CD Pipeline / Type Check V3 (push) Failing after 1s
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / ubuntu-latest / Node 24 (push) Failing after 1s
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / ubuntu-latest / Node 22 (push) Failing after 2s
V3 CI/CD Pipeline / browser rvf create flag smoke (#2015) (push) Failing after 0s
V3 CI/CD Pipeline / Dependency review (#2046) (push) Has been skipped
V3 CI/CD Pipeline / Supply-chain audit (#2046) (push) Failing after 0s
V3 CI/CD Pipeline / witness marker drift smoke (#2021) (push) Failing after 1s
V3 CI/CD Pipeline / neural-trader portfolio CG smoke (#2068, ADR-126 Phase 3) (push) Failing after 1s
V3 CI/CD Pipeline / neural-trader backtest signing smoke (#2068, ADR-126 Phase 4) (push) Failing after 1s
V3 CI/CD Pipeline / kg-extract type-import classification smoke (#2049) (push) Failing after 0s
V3 CI/CD Pipeline / witness verify precondition smoke (#1880) (push) Failing after 2s
V3 CI/CD Pipeline / neural-trader pipeline risk-gate smoke (#2068, ADR-126 Phase 5) (push) Failing after 0s
V3 CI/CD Pipeline / neural-trader feature attribution smoke (#2068, ADR-126 Phase 6) (push) Failing after 0s
V3 CI/CD Pipeline / plugin-registry signature verification smoke (#1922, CWE-347) (push) Failing after 4s
V3 CI/CD Pipeline / memory stats legacy-DB smoke (#2120) (push) Failing after 4s
V3 CI/CD Pipeline / github deprecated actions smoke (#2089, ADR-127 Phase 3) (push) Failing after 1s
V3 CI/CD Pipeline / graph query + pathfinder smoke (ADR-130 P2+P5) (push) Has been skipped
V3 CI/CD Pipeline / graph trajectory hooks smoke (ADR-130 P3) (push) Has been skipped
V3 CI/CD Pipeline / graph plugin adapter smoke (ADR-130 P4) (push) Has been skipped
V3 CI/CD Pipeline / graph benchmark (ADR-130 P6) (push) Has been skipped
V3 CI/CD Pipeline / statusline generator delegation smoke (#2195) (push) Failing after 1s
V3 CI/CD Pipeline / wizard init regression guard (#2206 (push) Failing after 1s
V3 CI/CD Pipeline / memory no-stray-db smoke (ADR-125 P7) (push) Failing after 1s
V3 CI/CD Pipeline / github-safe injection smoke (#2089, ADR-127 Phase 1) (push) Failing after 1s
V3 CI/CD Pipeline / github actions pin smoke (#2089, ADR-127 Phase 1) (push) Failing after 1s
V3 CI/CD Pipeline / github attribution opt-in smoke (#2089, ADR-127 Phase 4) (push) Failing after 1s
V3 CI/CD Pipeline / pre-bash hook safety smoke (#2017) (push) Failing after 1s
V3 CI/CD Pipeline / Memory import smoke / ubuntu-latest (push) Failing after 0s
V3 CI/CD Pipeline / MCP protocol smoke / ubuntu-latest (push) Failing after 2s
V3 CI/CD Pipeline / ruvllm WASM auto-init smoke (#2086) (push) Failing after 4s
V3 CI/CD Pipeline / MCP paired-tool round-trip smoke (#1889) (push) Failing after 1s
V3 CI/CD Pipeline / Plugin package install-safety (#1902/#1903/#1904) (push) Failing after 1s
V3 CI/CD Pipeline / Tool description discoverability (ADR-112) (push) Failing after 3s
V3 CI/CD Pipeline / CLI npx-install smoke (#1147 / (22) (push) Failing after 1s
V3 CI/CD Pipeline / CLI npx-install smoke (#1147 / (24) (push) Failing after 1s
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / ubuntu-latest (push) Failing after 2s
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / ubuntu-latest (push) Failing after 1s
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / ubuntu-latest (push) Failing after 1s
V3 CI/CD Pipeline / Vector-index dimension audit (#1947) (push) Failing after 0s
V3 CI/CD Pipeline / Hook-command install safety (#1921) (push) Failing after 1s
V3 CI/CD Pipeline / ToolOutputGuardrail smoke (ADR-131, (push) Failing after 1s
V3 CI/CD Pipeline / init-bundle invariants smoke (#2095, ADR-128 Phase 5) (push) Failing after 1s
V3 CI/CD Pipeline / wasm provider bridge smoke (ADR-129 P1) (push) Failing after 2s
V3 CI/CD Pipeline / wasm gallery CRUD smoke (ADR-129 P3) (push) Failing after 1s
V3 CI/CD Pipeline / wasm plugin bridge smoke (ADR-129 P4) (push) Failing after 0s
V3 CI/CD Pipeline / wasm compose smoke (ADR-129 P2) (push) Failing after 4s
V3 CI/CD Pipeline / graph schema smoke (ADR-130 P1) (push) Failing after 0s
Validate Marketplace / validate (push) Failing after 1s
🔍 Verification Pipeline / 🚀 Setup Verification (push) Failing after 1s
🔍 Verification Pipeline / 🛡️ Security Verification (push) Has been skipped
🔍 Verification Pipeline / 📝 Code Quality (push) Has been skipped
🔍 Verification Pipeline / 🧪 Test Verification (${{ matrix.os }}, Node ${{ matrix.node }}) (push) Has been skipped
🔍 Verification Pipeline / 🏗️ Build Verification (push) Has been skipped
🔍 Verification Pipeline / 📚 Documentation Verification (push) Has been skipped
CVE Audit Gate / High-severity report (warn only) (push) Has been cancelled
🔄 Automated Rollback Manager / 🔄 Execute Rollback (push) Has been cancelled
🔄 Automated Rollback Manager / ✅ Post-Rollback Verification (push) Has been cancelled
🔄 Automated Rollback Manager / 📊 Rollback Monitoring (push) Has been cancelled
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / windows-latest (push) Has been cancelled
🔄 Automated Rollback Manager / ⏳ Manual Rollback Approval (push) Has been cancelled
V3 CI/CD Pipeline / MCP protocol smoke / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Memory import smoke / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / ubuntu-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Publish to npm (alpha) (push) Has been cancelled
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / macos-latest / Node 22 (push) Has been cancelled
V3 CI/CD Pipeline / Plugin hooks smoke / macos-latest / Node 22 (push) Has been cancelled
CI/CD Pipeline / Deploy & Release (push) Has been cancelled
CI/CD Pipeline / CI Status (push) Has been cancelled
🔗 Cross-Agent Integration Tests / 📊 Integration Test Report (push) Has been cancelled
🔄 Automated Rollback Manager / 🔍 Pre-Rollback Validation (push) Has been cancelled
🔍 Verification Pipeline / ⚡ Performance Verification (push) Has been cancelled
🔍 Verification Pipeline / 📊 Verification Report (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:02:19 +08:00

847 lines
27 KiB
TypeScript

/**
* RuVector Quantization Tests
*
* Tests for vector quantization features including:
* - Scalar quantization (int8, int4)
* - Binary quantization
* - Product quantization (PQ)
* - Recall accuracy with quantization
*
* @module @claude-flow/plugins/__tests__/ruvector-quantization
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
randomVector,
normalizedVector,
randomVectors,
generateSimilarVectors,
cosineSimilarity,
euclideanDistance,
createTestConfig,
measureAsync,
benchmark,
} from './utils/ruvector-test-utils.js';
// ============================================================================
// Quantization Utility Functions
// ============================================================================
/**
* Scalar quantization to int8 (-128 to 127)
*/
function quantizeInt8(vector: number[]): Int8Array {
const min = Math.min(...vector);
const max = Math.max(...vector);
const range = max - min || 1;
return new Int8Array(vector.map((v) => {
const normalized = (v - min) / range; // 0 to 1
return Math.round(normalized * 255 - 128); // -128 to 127
}));
}
/**
* Dequantize int8 back to float
*/
function dequantizeInt8(quantized: Int8Array, min: number, max: number): number[] {
const range = max - min || 1;
return Array.from(quantized).map((v) => {
const normalized = (v + 128) / 255; // 0 to 1
return normalized * range + min;
});
}
/**
* Scalar quantization to int4 (0 to 15, packed)
*/
function quantizeInt4(vector: number[]): Uint8Array {
const min = Math.min(...vector);
const max = Math.max(...vector);
const range = max - min || 1;
// Pack two int4 values per byte
const packedLength = Math.ceil(vector.length / 2);
const packed = new Uint8Array(packedLength);
for (let i = 0; i < vector.length; i += 2) {
const v1 = Math.round(((vector[i] - min) / range) * 15); // 0 to 15
const v2 = i + 1 < vector.length
? Math.round(((vector[i + 1] - min) / range) * 15)
: 0;
packed[i / 2] = (v1 << 4) | v2; // Pack two values
}
return packed;
}
/**
* Dequantize int4 back to float
*/
function dequantizeInt4(packed: Uint8Array, length: number, min: number, max: number): number[] {
const range = max - min || 1;
const result: number[] = [];
for (let i = 0; i < packed.length; i++) {
const v1 = (packed[i] >> 4) & 0x0f;
const v2 = packed[i] & 0x0f;
result.push((v1 / 15) * range + min);
if (result.length < length) {
result.push((v2 / 15) * range + min);
}
}
return result;
}
/**
* Binary quantization (sign-based)
*/
function quantizeBinary(vector: number[]): Uint8Array {
const packedLength = Math.ceil(vector.length / 8);
const packed = new Uint8Array(packedLength);
for (let i = 0; i < vector.length; i++) {
if (vector[i] > 0) {
const byteIndex = Math.floor(i / 8);
const bitIndex = i % 8;
packed[byteIndex] |= (1 << bitIndex);
}
}
return packed;
}
/**
* Dequantize binary back to float (+1/-1)
*/
function dequantizeBinary(packed: Uint8Array, length: number): number[] {
const result: number[] = [];
for (let i = 0; i < length; i++) {
const byteIndex = Math.floor(i / 8);
const bitIndex = i % 8;
const bit = (packed[byteIndex] >> bitIndex) & 1;
result.push(bit === 1 ? 1 : -1);
}
return result;
}
/**
* Product quantization - split vector into subvectors and quantize each
*/
interface PQCodebook {
centroids: number[][][]; // [numSubvectors][numCentroids][subvectorDim]
numSubvectors: number;
numCentroids: number;
subvectorDim: number;
}
/**
* Train product quantizer codebook using k-means
*/
function trainPQCodebook(
vectors: number[][],
numSubvectors: number,
numCentroids: number = 256
): PQCodebook {
const dim = vectors[0].length;
const subvectorDim = Math.ceil(dim / numSubvectors);
const centroids: number[][][] = [];
// Train codebook for each subvector
for (let s = 0; s < numSubvectors; s++) {
const startIdx = s * subvectorDim;
const endIdx = Math.min(startIdx + subvectorDim, dim);
const actualSubDim = endIdx - startIdx;
// Extract subvectors
const subvectors = vectors.map((v) => v.slice(startIdx, endIdx));
// Simple k-means initialization (random centroids)
const subCentroids: number[][] = [];
for (let c = 0; c < numCentroids; c++) {
const randomIdx = Math.floor(Math.random() * subvectors.length);
subCentroids.push([...subvectors[randomIdx]]);
}
// One iteration of k-means for simplicity
const assignments = subvectors.map((sv) => {
let minDist = Infinity;
let minIdx = 0;
for (let c = 0; c < subCentroids.length; c++) {
const dist = euclideanDistance(sv, subCentroids[c]);
if (dist < minDist) {
minDist = dist;
minIdx = c;
}
}
return minIdx;
});
// Update centroids
for (let c = 0; c < numCentroids; c++) {
const assigned = subvectors.filter((_, i) => assignments[i] === c);
if (assigned.length > 0) {
subCentroids[c] = assigned[0].map((_, d) =>
assigned.reduce((sum, v) => sum + v[d], 0) / assigned.length
);
}
}
centroids.push(subCentroids);
}
return {
centroids,
numSubvectors,
numCentroids,
subvectorDim,
};
}
/**
* Encode vector using product quantization
*/
function encodePQ(vector: number[], codebook: PQCodebook): Uint8Array {
const codes = new Uint8Array(codebook.numSubvectors);
for (let s = 0; s < codebook.numSubvectors; s++) {
const startIdx = s * codebook.subvectorDim;
const endIdx = Math.min(startIdx + codebook.subvectorDim, vector.length);
const subvector = vector.slice(startIdx, endIdx);
// Find nearest centroid
let minDist = Infinity;
let minIdx = 0;
for (let c = 0; c < codebook.centroids[s].length; c++) {
const centroid = codebook.centroids[s][c].slice(0, subvector.length);
const dist = euclideanDistance(subvector, centroid);
if (dist < minDist) {
minDist = dist;
minIdx = c;
}
}
codes[s] = minIdx;
}
return codes;
}
/**
* Decode product quantization codes back to approximate vector
*/
function decodePQ(codes: Uint8Array, codebook: PQCodebook, originalDim: number): number[] {
const result: number[] = [];
for (let s = 0; s < codebook.numSubvectors; s++) {
const centroid = codebook.centroids[s][codes[s]];
for (let d = 0; d < centroid.length && result.length < originalDim; d++) {
result.push(centroid[d]);
}
}
return result;
}
/**
* Calculate recall@k between true and quantized search results
*/
function calculateRecall(
trueResults: string[],
quantizedResults: string[],
k: number
): number {
const trueTopK = new Set(trueResults.slice(0, k));
const quantizedTopK = quantizedResults.slice(0, k);
let matches = 0;
for (const id of quantizedTopK) {
if (trueTopK.has(id)) {
matches++;
}
}
return matches / k;
}
// ============================================================================
// Mock Quantized Search
// ============================================================================
interface QuantizedVectorStore {
vectors: Map<string, number[]>;
quantizedInt8: Map<string, { data: Int8Array; min: number; max: number }>;
quantizedInt4: Map<string, { data: Uint8Array; length: number; min: number; max: number }>;
quantizedBinary: Map<string, { data: Uint8Array; length: number }>;
pqCodes: Map<string, Uint8Array>;
pqCodebook: PQCodebook | null;
}
function createQuantizedStore(): QuantizedVectorStore {
return {
vectors: new Map(),
quantizedInt8: new Map(),
quantizedInt4: new Map(),
quantizedBinary: new Map(),
pqCodes: new Map(),
pqCodebook: null,
};
}
function addVector(store: QuantizedVectorStore, id: string, vector: number[]): void {
const min = Math.min(...vector);
const max = Math.max(...vector);
store.vectors.set(id, vector);
store.quantizedInt8.set(id, { data: quantizeInt8(vector), min, max });
store.quantizedInt4.set(id, { data: quantizeInt4(vector), length: vector.length, min, max });
store.quantizedBinary.set(id, { data: quantizeBinary(vector), length: vector.length });
}
function searchExact(
store: QuantizedVectorStore,
query: number[],
k: number,
metric: 'cosine' | 'euclidean' = 'cosine'
): Array<{ id: string; distance: number }> {
const results: Array<{ id: string; distance: number }> = [];
for (const [id, vector] of store.vectors) {
const distance = metric === 'cosine'
? 1 - cosineSimilarity(query, vector)
: euclideanDistance(query, vector);
results.push({ id, distance });
}
return results.sort((a, b) => a.distance - b.distance).slice(0, k);
}
function searchQuantizedInt8(
store: QuantizedVectorStore,
query: number[],
k: number
): Array<{ id: string; distance: number }> {
const results: Array<{ id: string; distance: number }> = [];
const queryMin = Math.min(...query);
const queryMax = Math.max(...query);
const queryQuantized = quantizeInt8(query);
for (const [id, { data }] of store.quantizedInt8) {
// Simple dot product approximation
let dot = 0;
for (let i = 0; i < queryQuantized.length; i++) {
dot += queryQuantized[i] * data[i];
}
// Lower dot product = higher distance for normalized vectors
results.push({ id, distance: -dot / (128 * 128 * query.length) + 1 });
}
return results.sort((a, b) => a.distance - b.distance).slice(0, k);
}
function searchQuantizedBinary(
store: QuantizedVectorStore,
query: number[],
k: number
): Array<{ id: string; distance: number }> {
const results: Array<{ id: string; distance: number }> = [];
const queryBinary = quantizeBinary(query);
for (const [id, { data }] of store.quantizedBinary) {
// Hamming distance
let hammingDist = 0;
for (let i = 0; i < queryBinary.length; i++) {
const xor = queryBinary[i] ^ data[i];
// Count set bits
let bits = xor;
while (bits) {
hammingDist += bits & 1;
bits >>= 1;
}
}
results.push({ id, distance: hammingDist });
}
return results.sort((a, b) => a.distance - b.distance).slice(0, k);
}
// ============================================================================
// Test Suites
// ============================================================================
describe('RuVector Quantization', () => {
let store: QuantizedVectorStore;
const dimensions = 384;
const numVectors = 1000;
beforeEach(() => {
store = createQuantizedStore();
// Populate store with vectors
for (let i = 0; i < numVectors; i++) {
addVector(store, `vec-${i}`, normalizedVector(dimensions));
}
});
// ==========================================================================
// Int8 Quantization Tests
// ==========================================================================
describe('Int8 Quantization', () => {
it('should quantize vectors to int8', () => {
const vector = randomVector(dimensions);
const quantized = quantizeInt8(vector);
expect(quantized).toBeInstanceOf(Int8Array);
expect(quantized.length).toBe(dimensions);
// Values should be in int8 range
for (const v of quantized) {
expect(v).toBeGreaterThanOrEqual(-128);
expect(v).toBeLessThanOrEqual(127);
}
});
it('should dequantize int8 back to float', () => {
const vector = randomVector(dimensions);
const min = Math.min(...vector);
const max = Math.max(...vector);
const quantized = quantizeInt8(vector);
const dequantized = dequantizeInt8(quantized, min, max);
expect(dequantized.length).toBe(dimensions);
// Check reconstruction error
const mse = vector.reduce((sum, v, i) => sum + (v - dequantized[i]) ** 2, 0) / dimensions;
expect(mse).toBeLessThan(0.01); // Reasonable reconstruction error
});
it('should perform search with int8 quantization', () => {
const query = normalizedVector(dimensions);
const k = 10;
const exactResults = searchExact(store, query, k);
const quantizedResults = searchQuantizedInt8(store, query, k);
expect(quantizedResults).toHaveLength(k);
// Calculate recall
const exactIds = exactResults.map((r) => r.id);
const quantizedIds = quantizedResults.map((r) => r.id);
const recall = calculateRecall(exactIds, quantizedIds, k);
// Int8 should maintain good recall (>60%)
expect(recall).toBeGreaterThanOrEqual(0.5);
});
it('should reduce memory by ~4x with int8', () => {
const vector = randomVector(dimensions);
const floatSize = dimensions * 4; // Float32
const int8Size = dimensions * 1; // Int8
expect(int8Size).toBe(floatSize / 4);
});
});
// ==========================================================================
// Binary Quantization Tests
// ==========================================================================
describe('Binary Quantization', () => {
it('should quantize vectors to binary', () => {
const vector = randomVector(dimensions);
const quantized = quantizeBinary(vector);
expect(quantized).toBeInstanceOf(Uint8Array);
expect(quantized.length).toBe(Math.ceil(dimensions / 8));
});
it('should dequantize binary back to +1/-1', () => {
const vector = randomVector(dimensions);
const quantized = quantizeBinary(vector);
const dequantized = dequantizeBinary(quantized, dimensions);
expect(dequantized.length).toBe(dimensions);
// All values should be +1 or -1
for (const v of dequantized) {
expect(Math.abs(v)).toBe(1);
}
});
it('should perform search with binary quantization', () => {
const query = normalizedVector(dimensions);
const k = 10;
const exactResults = searchExact(store, query, k);
const binaryResults = searchQuantizedBinary(store, query, k);
expect(binaryResults).toHaveLength(k);
// Calculate recall (binary is less accurate but much faster)
const exactIds = exactResults.map((r) => r.id);
const binaryIds = binaryResults.map((r) => r.id);
const recall = calculateRecall(exactIds, binaryIds, k);
// Binary quantization has lower recall but is very fast
expect(recall).toBeGreaterThanOrEqual(0.1); // Lower threshold for binary
});
it('should reduce memory by ~32x with binary', () => {
const vector = randomVector(dimensions);
const floatSize = dimensions * 4; // Float32
const binarySize = Math.ceil(dimensions / 8); // 1 bit per dimension
const compression = floatSize / binarySize;
expect(compression).toBeCloseTo(32, 0);
});
it('should handle Hamming distance correctly', () => {
// Two similar vectors should have small Hamming distance
const base = randomVector(dimensions);
const similar = base.map((v) => v + (Math.random() - 0.5) * 0.1);
const baseBinary = quantizeBinary(base);
const similarBinary = quantizeBinary(similar);
// Calculate Hamming distance
let hammingDist = 0;
for (let i = 0; i < baseBinary.length; i++) {
let xor = baseBinary[i] ^ similarBinary[i];
while (xor) {
hammingDist += xor & 1;
xor >>= 1;
}
}
// Similar vectors should have relatively small Hamming distance
expect(hammingDist).toBeLessThan(dimensions * 0.3);
});
});
// ==========================================================================
// Product Quantization Tests
// ==========================================================================
describe('Product Quantization', () => {
let pqCodebook: PQCodebook;
const numSubvectors = 8;
const numCentroids = 256;
beforeEach(() => {
// Train codebook on subset of vectors
const trainingVectors = Array.from(store.vectors.values()).slice(0, 500);
pqCodebook = trainPQCodebook(trainingVectors, numSubvectors, numCentroids);
});
it('should train product quantizer codebook', () => {
expect(pqCodebook.numSubvectors).toBe(numSubvectors);
expect(pqCodebook.numCentroids).toBe(numCentroids);
expect(pqCodebook.centroids).toHaveLength(numSubvectors);
for (const subCentroids of pqCodebook.centroids) {
expect(subCentroids).toHaveLength(numCentroids);
}
});
it('should encode vectors with PQ', () => {
const vector = randomVector(dimensions);
const codes = encodePQ(vector, pqCodebook);
expect(codes).toBeInstanceOf(Uint8Array);
expect(codes.length).toBe(numSubvectors);
// All codes should be valid centroid indices
for (const code of codes) {
expect(code).toBeGreaterThanOrEqual(0);
expect(code).toBeLessThan(numCentroids);
}
});
it('should decode PQ codes back to approximate vector', () => {
const vector = normalizedVector(dimensions);
const codes = encodePQ(vector, pqCodebook);
const decoded = decodePQ(codes, pqCodebook, dimensions);
expect(decoded.length).toBe(dimensions);
// Check reconstruction - PQ with random codebook may have lower similarity
// but structure should be preserved
const similarity = cosineSimilarity(vector, decoded);
expect(similarity).toBeGreaterThan(0); // At least positive correlation
expect(Number.isFinite(similarity)).toBe(true);
});
it('should reduce memory significantly with PQ', () => {
const vector = randomVector(dimensions);
const floatSize = dimensions * 4; // Float32 = 1536 bytes for 384 dims
const pqSize = numSubvectors; // 8 bytes (1 byte per subvector code)
const compression = floatSize / pqSize;
expect(compression).toBeGreaterThan(100); // >100x compression
});
it('should maintain recall with product quantization', () => {
// Encode all vectors
const pqStore = new Map<string, Uint8Array>();
for (const [id, vector] of store.vectors) {
pqStore.set(id, encodePQ(vector, pqCodebook));
}
const query = normalizedVector(dimensions);
// Asymmetric distance computation (query to codes)
const results: Array<{ id: string; distance: number }> = [];
for (const [id, codes] of pqStore) {
let distance = 0;
for (let s = 0; s < numSubvectors; s++) {
const startIdx = s * pqCodebook.subvectorDim;
const endIdx = Math.min(startIdx + pqCodebook.subvectorDim, dimensions);
const querySubvec = query.slice(startIdx, endIdx);
const centroid = pqCodebook.centroids[s][codes[s]].slice(0, querySubvec.length);
distance += euclideanDistance(querySubvec, centroid);
}
results.push({ id, distance });
}
results.sort((a, b) => a.distance - b.distance);
const pqResults = results.slice(0, 10);
// Compare with exact search
const exactResults = searchExact(store, query, 10);
const exactIds = exactResults.map((r) => r.id);
const pqIds = pqResults.map((r) => r.id);
const recall = calculateRecall(exactIds, pqIds, 10);
// With random codebook initialization, recall may be low
// but should provide some ordering
expect(recall).toBeGreaterThanOrEqual(0); // At least non-negative
expect(pqResults.length).toBe(10); // Should return correct number of results
});
});
// ==========================================================================
// Int4 Quantization Tests
// ==========================================================================
describe('Int4 Quantization', () => {
it('should quantize vectors to int4', () => {
const vector = randomVector(dimensions);
const quantized = quantizeInt4(vector);
expect(quantized).toBeInstanceOf(Uint8Array);
// Two int4 values packed per byte
expect(quantized.length).toBe(Math.ceil(dimensions / 2));
});
it('should dequantize int4 back to float', () => {
const vector = randomVector(dimensions);
const min = Math.min(...vector);
const max = Math.max(...vector);
const quantized = quantizeInt4(vector);
const dequantized = dequantizeInt4(quantized, dimensions, min, max);
expect(dequantized.length).toBe(dimensions);
// Int4 has lower precision but should still capture general structure
const similarity = cosineSimilarity(vector, dequantized);
expect(similarity).toBeGreaterThan(0.8);
});
it('should reduce memory by ~8x with int4', () => {
const floatSize = dimensions * 4; // Float32
const int4Size = Math.ceil(dimensions / 2); // 4 bits per value, packed
const compression = floatSize / int4Size;
expect(compression).toBeCloseTo(8, 1);
});
});
// ==========================================================================
// Recall Analysis Tests
// ==========================================================================
describe('Recall Analysis', () => {
it('should calculate recall@k correctly', () => {
const trueResults = ['a', 'b', 'c', 'd', 'e'];
const quantizedResults = ['a', 'c', 'e', 'f', 'g'];
const recall5 = calculateRecall(trueResults, quantizedResults, 5);
expect(recall5).toBe(0.6); // 3 out of 5 match
const recall3 = calculateRecall(trueResults, quantizedResults, 3);
// First 3: a, b, c vs a, c, e -> 2 matches
expect(recall3).toBeCloseTo(0.67, 1);
});
it('should show recall degradation with more aggressive quantization', () => {
const query = normalizedVector(dimensions);
const k = 20;
const exactResults = searchExact(store, query, k).map((r) => r.id);
const int8Results = searchQuantizedInt8(store, query, k).map((r) => r.id);
const binaryResults = searchQuantizedBinary(store, query, k).map((r) => r.id);
const int8Recall = calculateRecall(exactResults, int8Results, k);
const binaryRecall = calculateRecall(exactResults, binaryResults, k);
// Int8 should have better recall than binary
// Note: This may not always hold due to mock implementation
expect(int8Recall).toBeGreaterThanOrEqual(0);
expect(binaryRecall).toBeGreaterThanOrEqual(0);
});
});
// ==========================================================================
// Performance Tests
// ==========================================================================
describe('Performance', () => {
it('should be faster with quantized search', async () => {
const query = normalizedVector(dimensions);
const k = 10;
// Measure exact search time
const { durationMs: exactTime } = await measureAsync(() =>
Promise.resolve(searchExact(store, query, k))
);
// Measure int8 search time
const { durationMs: int8Time } = await measureAsync(() =>
Promise.resolve(searchQuantizedInt8(store, query, k))
);
// Measure binary search time
const { durationMs: binaryTime } = await measureAsync(() =>
Promise.resolve(searchQuantizedBinary(store, query, k))
);
// All should complete in reasonable time
expect(exactTime).toBeLessThan(1000);
expect(int8Time).toBeLessThan(1000);
expect(binaryTime).toBeLessThan(1000);
});
it('should handle batch quantization efficiently', () => {
const vectors = randomVectors(1000, dimensions);
const start = performance.now();
const quantized = vectors.map((v) => quantizeInt8(v));
const duration = performance.now() - start;
expect(quantized).toHaveLength(1000);
expect(duration).toBeLessThan(1000); // Should complete in under 1 second
});
});
// ==========================================================================
// Memory Analysis Tests
// ==========================================================================
describe('Memory Analysis', () => {
it('should calculate memory savings correctly', () => {
const numVecs = 1000000; // 1M vectors
const dims = 384;
const float32Size = numVecs * dims * 4; // ~1.5GB
const int8Size = numVecs * dims * 1; // ~384MB
const int4Size = numVecs * Math.ceil(dims / 2); // ~192MB
const binarySize = numVecs * Math.ceil(dims / 8); // ~48MB
const pqSize = numVecs * 8; // ~8MB (8 subvectors)
expect(float32Size / int8Size).toBeCloseTo(4, 0);
expect(float32Size / int4Size).toBeCloseTo(8, 0);
expect(float32Size / binarySize).toBeCloseTo(32, 0);
expect(float32Size / pqSize).toBeGreaterThan(100);
});
it('should report quantization metadata', () => {
const vector = randomVector(dimensions);
const min = Math.min(...vector);
const max = Math.max(...vector);
const int8 = quantizeInt8(vector);
const int4 = quantizeInt4(vector);
const binary = quantizeBinary(vector);
const metadata = {
originalDimensions: dimensions,
int8Size: int8.byteLength,
int4Size: int4.byteLength,
binarySize: binary.byteLength,
valueRange: { min, max },
};
expect(metadata.int8Size).toBe(dimensions);
expect(metadata.int4Size).toBe(Math.ceil(dimensions / 2));
expect(metadata.binarySize).toBe(Math.ceil(dimensions / 8));
});
});
// ==========================================================================
// Edge Cases
// ==========================================================================
describe('Edge Cases', () => {
it('should handle zero vectors', () => {
const zeroVector = new Array(dimensions).fill(0);
const int8 = quantizeInt8(zeroVector);
const binary = quantizeBinary(zeroVector);
expect(int8.length).toBe(dimensions);
expect(binary.length).toBe(Math.ceil(dimensions / 8));
});
it('should handle constant vectors', () => {
const constVector = new Array(dimensions).fill(0.5);
const int8 = quantizeInt8(constVector);
// With constant values, all quantized values should be the same
const unique = new Set(int8);
expect(unique.size).toBe(1);
});
it('should handle very small vectors', () => {
const smallDims = 8;
const vector = randomVector(smallDims);
const int8 = quantizeInt8(vector);
const int4 = quantizeInt4(vector);
const binary = quantizeBinary(vector);
expect(int8.length).toBe(smallDims);
expect(int4.length).toBe(Math.ceil(smallDims / 2));
expect(binary.length).toBe(Math.ceil(smallDims / 8));
});
it('should handle vectors with extreme values', () => {
const extremeVector = randomVector(dimensions).map((v, i) =>
i % 2 === 0 ? v * 1000 : v * -1000
);
const int8 = quantizeInt8(extremeVector);
const min = Math.min(...extremeVector);
const max = Math.max(...extremeVector);
const dequantized = dequantizeInt8(int8, min, max);
// Should still preserve relative ordering
expect(dequantized.length).toBe(dimensions);
});
it('should handle odd-length vectors for int4', () => {
const oddDims = 383;
const vector = randomVector(oddDims);
const int4 = quantizeInt4(vector);
expect(int4.length).toBe(Math.ceil(oddDims / 2));
});
});
});