/** * Distance functions for vector similarity calculations. * * Pure-JavaScript implementations using allocation-free indexed loops: a single * pass over the two vectors with scalar accumulators and no per-element closures * or intermediate objects. This is the open-core distance path (the native * provider owns the SIMD/quantized billion-scale path); for the small/medium * vectors it serves (e.g. 384-dim sentence embeddings) a tight loop keeps the * whole computation in registers with zero GC pressure. * * MEASURED (tests/benchmarks/distance-microbench.mjs, dim=384, N=20000, median * of 41): rewriting cosine from an object-accumulating `reduce` to this loop is * ~6x on `number[]`; euclidean ~1.4x. (`number[]` is also measurably faster than * `Float32Array` here — V8 widens f32→f64 on every element read — so the * resident representation stays `number[]`.) */ import { DistanceFunction, Vector } from '../coreTypes.js' /** * Calculates the Euclidean (L2) distance between two vectors. * Lower values indicate higher similarity. */ export const euclideanDistance: DistanceFunction = (a: Vector, b: Vector): number => { if (a.length !== b.length) { throw new Error('Vectors must have the same dimensions') } let sum = 0 const len = a.length for (let i = 0; i < len; i++) { const diff = a[i] - b[i] sum += diff * diff } return Math.sqrt(sum) } /** * Calculates the cosine distance between two vectors. * Lower values indicate higher similarity. Range: 0 (identical) to 2 (opposite). */ export const cosineDistance: DistanceFunction = (a: Vector, b: Vector): number => { if (a.length !== b.length) { throw new Error('Vectors must have the same dimensions') } let dotProduct = 0 let normA = 0 let normB = 0 const len = a.length for (let i = 0; i < len; i++) { const av = a[i] const bv = b[i] dotProduct += av * bv normA += av * av normB += bv * bv } if (normA === 0 || normB === 0) { return 2 // Maximum distance for zero vectors } const similarity = dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)) // Convert cosine similarity (-1 to 1) to distance (0 to 2) return 1 - similarity } /** * Calculates the Manhattan (L1) distance between two vectors. * Lower values indicate higher similarity. */ export const manhattanDistance: DistanceFunction = (a: Vector, b: Vector): number => { if (a.length !== b.length) { throw new Error('Vectors must have the same dimensions') } let sum = 0 const len = a.length for (let i = 0; i < len; i++) { sum += Math.abs(a[i] - b[i]) } return sum } /** * Calculates the dot-product similarity between two vectors, negated to a * distance metric (lower is better). */ export const dotProductDistance: DistanceFunction = (a: Vector, b: Vector): number => { if (a.length !== b.length) { throw new Error('Vectors must have the same dimensions') } let dotProduct = 0 const len = a.length for (let i = 0; i < len; i++) { dotProduct += a[i] * b[i] } return -dotProduct } /** * Batch distance calculation: the query vector against each candidate. * * With the distance functions now allocation-free indexed loops, this is a thin * map over the (monomorphic, JIT-inlined) `distanceFunction` — no worker, no * stringify/`new Function` reconstruction. Kept `async` for call-site * compatibility with the HNSW search path. * * @param queryVector The query vector to compare against all candidates. * @param vectors The candidate vectors. * @param distanceFunction The distance function to use (default: Euclidean). * @returns The distances, index-aligned with `vectors`. */ export async function calculateDistancesBatch( queryVector: Vector, vectors: Vector[], distanceFunction: DistanceFunction = euclideanDistance ): Promise { const out = new Array(vectors.length) for (let i = 0; i < vectors.length; i++) { out[i] = distanceFunction(queryVector, vectors[i]) } return out }