2025-06-24 11:41:30 -07:00
/ * *
* Distance functions for vector similarity calculations
* Optimized for Node . js 23.11 + using enhanced array methods
2025-06-26 19:04:37 -07:00
* GPU - accelerated versions available for high - performance computing
2025-06-24 11:41:30 -07:00
* /
import { DistanceFunction , Vector } from '../coreTypes.js'
2025-06-26 19:04:37 -07:00
import { executeInThread } from './workerUtils.js'
import { isThreadingAvailable } from './environment.js'
2025-06-24 11:41:30 -07:00
/ * *
* Calculates the Euclidean distance between two vectors
* Lower values indicate higher similarity
* Optimized using array methods for Node . js 23.11 +
* /
2025-06-27 14:06:59 -07:00
export const euclideanDistance : DistanceFunction = (
a : Vector ,
b : Vector
) : number = > {
2025-06-24 11:41:30 -07:00
if ( a . length !== b . length ) {
throw new Error ( 'Vectors must have the same dimensions' )
}
// Use array.reduce for better performance in Node.js 23.11+
const sum = a . reduce ( ( acc , val , i ) = > {
const diff = val - b [ i ]
2025-06-27 14:06:59 -07:00
return acc + diff * diff
2025-06-24 11:41:30 -07:00
} , 0 )
return Math . sqrt ( sum )
}
/ * *
* Calculates the cosine distance between two vectors
* Lower values indicate higher similarity
* Range : 0 ( identical ) to 2 ( opposite )
* Optimized using array methods for Node . js 23.11 +
* /
2025-06-27 14:06:59 -07:00
export const cosineDistance : DistanceFunction = (
a : Vector ,
b : Vector
) : number = > {
2025-06-24 11:41:30 -07:00
if ( a . length !== b . length ) {
throw new Error ( 'Vectors must have the same dimensions' )
}
// Use array.reduce to calculate all values in a single pass
2025-06-27 14:06:59 -07:00
const { dotProduct , normA , normB } = a . reduce (
( acc , val , i ) = > {
return {
dotProduct : acc.dotProduct + val * b [ i ] ,
normA : acc.normA + val * val ,
normB : acc.normB + b [ i ] * b [ i ]
}
} ,
{ dotProduct : 0 , normA : 0 , normB : 0 }
)
2025-06-24 11:41:30 -07:00
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
* Optimized using array methods for Node . js 23.11 +
* /
2025-06-27 14:06:59 -07:00
export const manhattanDistance : DistanceFunction = (
a : Vector ,
b : Vector
) : number = > {
2025-06-24 11:41:30 -07:00
if ( a . length !== b . length ) {
throw new Error ( 'Vectors must have the same dimensions' )
}
// Use array.reduce for better performance in Node.js 23.11+
return a . reduce ( ( sum , val , i ) = > sum + Math . abs ( val - b [ i ] ) , 0 )
}
/ * *
* Calculates the dot product similarity between two vectors
* Higher values indicate higher similarity
* Converted to a distance metric ( lower is better )
* Optimized using array methods for Node . js 23.11 +
* /
2025-06-27 14:06:59 -07:00
export const dotProductDistance : DistanceFunction = (
a : Vector ,
b : Vector
) : number = > {
2025-06-24 11:41:30 -07:00
if ( a . length !== b . length ) {
throw new Error ( 'Vectors must have the same dimensions' )
}
// Use array.reduce for better performance in Node.js 23.11+
2025-06-27 14:06:59 -07:00
const dotProduct = a . reduce ( ( sum , val , i ) = > sum + val * b [ i ] , 0 )
2025-06-24 11:41:30 -07:00
// Convert to a distance metric (lower is better)
return - dotProduct
}
2025-06-26 19:04:37 -07:00
/ * *
2025-06-27 14:06:59 -07:00
* Batch distance calculation
* Uses TensorFlow . js with CPU backend for optimized performance
2025-06-26 19:04:37 -07:00
*
* @param queryVector The query vector to compare against all vectors
* @param vectors Array of vectors to compare against
* @param distanceFunction The distance function to use
* @returns Promise resolving to array of distances
* /
2025-06-27 14:06:59 -07:00
export async function calculateDistancesBatch (
2025-06-26 19:04:37 -07:00
queryVector : Vector ,
vectors : Vector [ ] ,
distanceFunction : DistanceFunction = euclideanDistance
) : Promise < number [ ] > {
// For small batches, use the standard distance function
if ( vectors . length < 10 ) {
2025-06-27 14:06:59 -07:00
return vectors . map ( ( vector ) = > distanceFunction ( queryVector , vector ) )
2025-06-26 19:04:37 -07:00
}
try {
// Function to be executed in a worker thread
2025-06-27 14:06:59 -07:00
const distanceCalculator = async ( args : {
queryVector : Vector
vectors : Vector [ ]
distanceFnString : string
} ) = > {
2025-06-26 19:04:37 -07:00
const { queryVector , vectors , distanceFnString } = args
2025-06-27 14:06:59 -07:00
// Use TensorFlow.js with CPU processing
2025-06-26 19:04:37 -07:00
const useTensorFlow = async ( ) = > {
2025-06-27 14:06:59 -07:00
// TensorFlow.js will use its default EPSILON value
// Use the importTensorFlow function if available (in worker context)
// or directly import TensorFlow.js (in main thread)
let tf
if (
typeof self !== 'undefined' &&
typeof self . importTensorFlow === 'function'
) {
// In worker context, use the importTensorFlow function
tf = await self . importTensorFlow ( )
} else {
2025-07-16 13:51:00 -07:00
// CRITICAL: Ensure TextEncoder/TextDecoder are available before TensorFlow.js loads
2025-07-14 11:12:51 -07:00
try {
2025-07-16 13:51:00 -07:00
// Use dynamic imports for all environments to ensure TensorFlow loads after patch
if ( typeof process !== 'undefined' && process . versions && process . versions . node ) {
// Ensure TextEncoder/TextDecoder are globally available in Node.js
const util = await import ( 'util' )
if ( typeof global . TextEncoder === 'undefined' ) {
global . TextEncoder = util . TextEncoder
}
if ( typeof global . TextDecoder === 'undefined' ) {
2025-07-28 16:00:05 -07:00
global . TextDecoder = util . TextDecoder as unknown as typeof TextDecoder
2025-07-16 13:51:00 -07:00
}
2025-07-14 11:12:51 -07:00
}
2025-07-16 13:51:00 -07:00
// Apply the TensorFlow.js patch
const { applyTensorFlowPatch } = await import ( './textEncoding.js' )
await applyTensorFlowPatch ( )
// Now load TensorFlow.js core module using dynamic imports
tf = await import ( '@tensorflow/tfjs-core' )
await import ( '@tensorflow/tfjs-backend-cpu' )
await tf . setBackend ( 'cpu' )
2025-07-14 11:12:51 -07:00
} catch ( error ) {
console . error ( 'Failed to initialize TensorFlow.js:' , error )
throw error
}
2025-06-27 14:06:59 -07:00
}
// Convert vectors to tensors
const queryTensor = tf . tensor2d ( [ queryVector ] )
const vectorsTensor = tf . tensor2d ( vectors )
let distances : number [ ]
// Calculate distances based on the distance function type
if ( distanceFnString . includes ( 'euclideanDistance' ) ) {
// Euclidean distance using GPU-optimized operations
// Formula: sqrt(sum((a - b)^2))
const expanded = tf . sub (
( queryTensor as any ) . expandDims ( 1 ) ,
( vectorsTensor as any ) . expandDims ( 0 )
)
const squaredDiff = tf . square ( expanded )
const sumSquaredDiff = tf . sum ( squaredDiff , - 1 )
const distancesTensor = tf . sqrt ( sumSquaredDiff )
distances = ( await ( distancesTensor as any )
. squeeze ( )
. array ( ) ) as number [ ]
// Clean up tensors
queryTensor . dispose ( )
vectorsTensor . dispose ( )
expanded . dispose ( )
squaredDiff . dispose ( )
sumSquaredDiff . dispose ( )
distancesTensor . dispose ( )
} else if ( distanceFnString . includes ( 'cosineDistance' ) ) {
// Cosine distance using GPU-optimized operations
// Formula: 1 - (a·b / (||a|| * ||b||))
const dotProduct = tf . matMul (
queryTensor ,
( vectorsTensor as any ) . transpose ( )
)
const queryNorm = tf . norm ( queryTensor , 2 , 1 )
const vectorsNorm = tf . norm ( vectorsTensor , 2 , 1 )
const normProduct = tf . outerProduct (
queryNorm as any ,
vectorsNorm as any
)
const cosineSimilarity = tf . div ( dotProduct , normProduct )
const distancesTensor = tf . sub ( tf . scalar ( 1 ) , cosineSimilarity )
distances = ( await ( distancesTensor as any )
. squeeze ( )
. array ( ) ) as number [ ]
// Clean up tensors
queryTensor . dispose ( )
vectorsTensor . dispose ( )
dotProduct . dispose ( )
queryNorm . dispose ( )
vectorsNorm . dispose ( )
normProduct . dispose ( )
cosineSimilarity . dispose ( )
distancesTensor . dispose ( )
} else if ( distanceFnString . includes ( 'manhattanDistance' ) ) {
// Manhattan distance using GPU-optimized operations
// Formula: sum(|a - b|)
const diff = tf . sub (
( queryTensor as any ) . expandDims ( 1 ) ,
( vectorsTensor as any ) . expandDims ( 0 )
)
const absDiff = tf . abs ( diff )
const distancesTensor = tf . sum ( absDiff , - 1 )
distances = ( await ( distancesTensor as any )
. squeeze ( )
. array ( ) ) as number [ ]
// Clean up tensors
queryTensor . dispose ( )
vectorsTensor . dispose ( )
diff . dispose ( )
absDiff . dispose ( )
distancesTensor . dispose ( )
} else if ( distanceFnString . includes ( 'dotProductDistance' ) ) {
// Dot product distance using GPU-optimized operations
// Formula: -sum(a * b)
const dotProduct = tf . matMul (
queryTensor ,
( vectorsTensor as any ) . transpose ( )
)
const distancesTensor = tf . neg ( dotProduct )
distances = ( await ( distancesTensor as any )
. squeeze ( )
. array ( ) ) as number [ ]
// Clean up tensors
queryTensor . dispose ( )
vectorsTensor . dispose ( )
dotProduct . dispose ( )
distancesTensor . dispose ( )
} else {
// For unknown distance functions, fall back to direct CPU implementation
throw new Error (
'Unsupported distance function for TensorFlow optimization'
)
}
return {
distances
2025-06-26 19:04:37 -07:00
}
}
2025-06-27 14:06:59 -07:00
// Try to use TensorFlow.js with CPU optimization
2025-06-26 19:04:37 -07:00
try {
return await useTensorFlow ( )
} catch ( error ) {
2025-06-27 14:06:59 -07:00
// Fall back to direct CPU implementation if TensorFlow.js fails
2025-06-26 19:04:37 -07:00
// Recreate the distance function from its string representation
2025-06-27 14:06:59 -07:00
const distanceFunction = new Function (
'return ' + distanceFnString
) ( ) as DistanceFunction
2025-06-26 19:04:37 -07:00
// Calculate distances for all vectors
2025-06-27 14:06:59 -07:00
const distances = vectors . map ( ( vector ) = >
distanceFunction ( queryVector , vector )
)
2025-06-26 19:04:37 -07:00
return {
2025-06-27 14:06:59 -07:00
distances
2025-06-26 19:04:37 -07:00
}
}
}
2025-06-27 14:06:59 -07:00
// Threading is not available, so we'll always use the main thread implementation
// This comment is kept for clarity about the removed code
2025-06-26 19:04:37 -07:00
// If threading is not available or failed, calculate distances in the main thread
2025-06-27 14:06:59 -07:00
return vectors . map ( ( vector ) = > distanceFunction ( queryVector , vector ) )
2025-06-26 19:04:37 -07:00
} catch ( error ) {
// If anything fails, fall back to the standard distance function
2025-06-27 14:06:59 -07:00
console . error ( 'Batch distance calculation failed:' , error )
return vectors . map ( ( vector ) = > distanceFunction ( queryVector , vector ) )
2025-06-26 19:04:37 -07:00
}
}