2025-06-24 11:41:30 -07:00
/ * *
* Embedding functions for converting data to vectors
* /
import { EmbeddingFunction , EmbeddingModel , Vector } from '../coreTypes.js'
import { executeInThread } from './workerUtils.js'
2025-07-28 10:04:45 -07:00
import { isBrowser } from './environment.js'
2025-06-24 11:41:30 -07:00
/ * *
* TensorFlow Universal Sentence Encoder embedding model
* This model provides high - quality text embeddings using TensorFlow . js
* The required TensorFlow . js dependencies are automatically installed with this package
2025-06-26 19:04:37 -07:00
*
2025-06-27 14:06:59 -07:00
* This implementation attempts to use GPU processing when available for better performance ,
* falling back to CPU processing for compatibility across all environments .
2025-06-24 11:41:30 -07:00
* /
export class UniversalSentenceEncoder implements EmbeddingModel {
private model : any = null
private initialized = false
private tf : any = null
private use : any = null
2025-06-27 14:06:59 -07:00
private backend : string = 'cpu' // Default to CPU
2025-07-18 10:40:37 -07:00
private verbose : boolean = true // Whether to log non-essential messages
/ * *
* Create a new UniversalSentenceEncoder instance
* @param options Configuration options
* /
constructor ( options : { verbose? : boolean } = { } ) {
this . verbose = options . verbose !== undefined ? options.verbose : true
}
2025-06-24 11:41:30 -07:00
2025-07-02 16:16:19 -07:00
/ * *
* Add polyfills and patches for TensorFlow . js compatibility
2025-07-16 13:51:00 -07:00
* This addresses issues with TensorFlow . js across all server environments
* ( Node . js , serverless , and other server environments )
*
2025-07-14 11:12:51 -07:00
* Note : The main TensorFlow . js patching is now centralized in textEncoding . ts
* and applied through setup . ts . This method only adds additional utility functions
* that might be needed by TensorFlow . js .
2025-07-02 16:16:19 -07:00
* /
2025-07-16 13:51:00 -07:00
private addServerCompatibilityPolyfills ( ) : void {
// Apply in all non-browser environments (Node.js, serverless, server environments)
2025-07-28 10:04:45 -07:00
if ( isBrowser ( ) ) {
2025-07-16 13:51:00 -07:00
return // Browser environments don't need these polyfills
2025-07-02 16:16:19 -07:00
}
2025-07-16 13:51:00 -07:00
// Get the appropriate global object for the current environment
const globalObj = ( ( ) = > {
if ( typeof globalThis !== 'undefined' ) return globalThis
if ( typeof global !== 'undefined' ) return global
if ( typeof self !== 'undefined' ) return self
return { } as any // Fallback for unknown environments
} ) ( )
// Add polyfill for utility functions across all server environments
// This fixes issues like "Cannot read properties of undefined (reading 'isFloat32Array')"
try {
// Ensure the util object exists
if ( ! globalObj . util ) {
globalObj . util = { }
}
// Add isFloat32Array method if it doesn't exist
if ( ! globalObj . util . isFloat32Array ) {
globalObj . util . isFloat32Array = ( obj : any ) = > {
return ! ! (
obj instanceof Float32Array ||
( obj &&
Object . prototype . toString . call ( obj ) === '[object Float32Array]' )
)
2025-07-14 11:12:51 -07:00
}
2025-07-16 13:51:00 -07:00
}
2025-07-11 11:11:56 -07:00
2025-07-16 13:51:00 -07:00
// Add isTypedArray method if it doesn't exist
if ( ! globalObj . util . isTypedArray ) {
globalObj . util . isTypedArray = ( obj : any ) = > {
return ! ! ( ArrayBuffer . isView ( obj ) && ! ( obj instanceof DataView ) )
2025-07-14 11:12:51 -07:00
}
2025-07-16 13:51:00 -07:00
}
} catch ( error ) {
console . warn ( 'Failed to add utility polyfills:' , error )
}
}
2025-07-11 11:11:56 -07:00
2025-07-16 13:51:00 -07:00
/ * *
* Check if we ' re running in a test environment
* /
private isTestEnvironment ( ) : boolean {
// Safely check for Node.js environment first
if ( typeof process === 'undefined' ) {
return false
}
2025-07-17 10:00:28 -07:00
2025-07-16 13:51:00 -07:00
return (
process . env . NODE_ENV === 'test' ||
process . env . VITEST === 'true' ||
( typeof global !== 'undefined' && global . __vitest__ ) ||
process . argv . some ( ( arg ) = > arg . includes ( 'vitest' ) )
)
}
/ * *
2025-07-18 10:40:37 -07:00
* Log message only if verbose mode is enabled or if it ' s an error
* This helps suppress non - essential log messages
2025-07-16 13:51:00 -07:00
* /
2025-07-17 10:00:28 -07:00
private logger (
2025-07-16 13:51:00 -07:00
level : 'log' | 'warn' | 'error' ,
message : string ,
. . . args : any [ ]
) : void {
2025-07-18 10:40:37 -07:00
// Always log errors, but only log other messages if verbose mode is enabled
if ( level === 'error' || this . verbose ) {
console [ level ] ( message , . . . args )
}
2025-07-16 13:51:00 -07:00
}
/ * *
* Load the Universal Sentence Encoder model with retry logic
* This helps handle network failures and JSON parsing errors from TensorFlow Hub
* @param loadFunction The function to load the model
* @param maxRetries Maximum number of retry attempts
* @param baseDelay Base delay in milliseconds for exponential backoff
* /
private async loadModelWithRetry (
loadFunction : ( ) = > Promise < EmbeddingModel > ,
maxRetries : number = 3 ,
baseDelay : number = 1000
) : Promise < EmbeddingModel > {
let lastError : Error | null = null
2025-07-28 16:00:05 -07:00
// Define alternative model URLs to try if the default one fails
const alternativeLoadFunctions : Array < ( ) = > Promise < EmbeddingModel > > = [ ]
// Try to create alternative load functions using different model URLs
if ( this . use ) {
// Add alternative model URLs to try
const alternativeUrls = [
'https://storage.googleapis.com/tfjs-models/savedmodel/universal_sentence_encoder/model.json' ,
'https://tfhub.dev/tensorflow/tfjs-model/universal-sentence-encoder-lite/1/default/1/model.json' ,
'https://tfhub.dev/tensorflow/tfjs-model/universal-sentence-encoder/1/default/1/model.json' ,
'https://tfhub.dev/tensorflow/tfjs-model/universal-sentence-encoder/1/default/1' ,
'https://tfhub.dev/tensorflow/universal-sentence-encoder/4' ,
'https://tfhub.dev/tensorflow/universal-sentence-encoder/4/default/1/model.json'
]
// Create load functions for each alternative URL
for ( const url of alternativeUrls ) {
if ( this . use . load ) {
alternativeLoadFunctions . push ( ( ) = > this . use ! . load ( url ) )
} else if ( this . use . default && this . use . default . load ) {
alternativeLoadFunctions . push ( ( ) = > this . use ! . default . load ( url ) )
}
}
}
// First try with the original load function
2025-07-16 13:51:00 -07:00
for ( let attempt = 0 ; attempt <= maxRetries ; attempt ++ ) {
try {
2025-07-17 10:00:28 -07:00
this . logger (
2025-07-16 13:51:00 -07:00
'log' ,
attempt === 0
? 'Loading Universal Sentence Encoder model...'
: ` Retrying Universal Sentence Encoder model loading (attempt ${ attempt + 1 } / ${ maxRetries + 1 } )... `
)
const model = await loadFunction ( )
if ( attempt > 0 ) {
2025-07-17 10:00:28 -07:00
this . logger (
2025-07-16 13:51:00 -07:00
'log' ,
'Universal Sentence Encoder model loaded successfully after retry'
)
2025-07-11 11:11:56 -07:00
}
2025-07-16 13:51:00 -07:00
return model
2025-07-11 11:11:56 -07:00
} catch ( error ) {
2025-07-16 13:51:00 -07:00
lastError = error as Error
const errorMessage = lastError . message || String ( lastError )
// Check if this is a network-related error that might benefit from retry
const isRetryableError =
errorMessage . includes ( 'Failed to parse model JSON' ) ||
errorMessage . includes ( 'Failed to fetch' ) ||
errorMessage . includes ( 'Network error' ) ||
errorMessage . includes ( 'ENOTFOUND' ) ||
errorMessage . includes ( 'ECONNRESET' ) ||
errorMessage . includes ( 'ETIMEDOUT' ) ||
errorMessage . includes ( 'JSON' ) ||
2025-07-28 16:00:05 -07:00
errorMessage . includes ( 'model.json' ) ||
errorMessage . includes ( 'byte length' ) ||
errorMessage . includes ( 'tensor should have' ) ||
errorMessage . includes ( 'shape' ) ||
errorMessage . includes ( 'dimensions' )
2025-07-16 13:51:00 -07:00
if ( attempt < maxRetries && isRetryableError ) {
const delay = baseDelay * Math . pow ( 2 , attempt ) // Exponential backoff
2025-07-17 10:00:28 -07:00
this . logger (
2025-07-16 13:51:00 -07:00
'warn' ,
` Universal Sentence Encoder model loading failed (attempt ${ attempt + 1 } ): ${ errorMessage } . Retrying in ${ delay } ms... `
)
await new Promise ( ( resolve ) = > setTimeout ( resolve , delay ) )
} else {
// Either we've exhausted retries or this is not a retryable error
if ( attempt >= maxRetries ) {
2025-07-28 16:00:05 -07:00
this . logger (
'warn' ,
` Universal Sentence Encoder model loading failed after ${ maxRetries + 1 } attempts. Last error: ${ errorMessage } . Trying alternative URLs... `
)
// Try alternative URLs if available
if ( alternativeLoadFunctions . length > 0 ) {
for ( let i = 0 ; i < alternativeLoadFunctions . length ; i ++ ) {
try {
this . logger (
'log' ,
` Trying alternative model URL ${ i + 1 } / ${ alternativeLoadFunctions . length } ... `
)
const model = await alternativeLoadFunctions [ i ] ( )
this . logger (
'log' ,
` Successfully loaded Universal Sentence Encoder from alternative URL ${ i + 1 } `
)
return model
} catch ( altError ) {
this . logger (
'warn' ,
` Failed to load from alternative URL ${ i + 1 } : ${ altError } `
)
// Continue to the next alternative
}
}
}
// If we get here, all alternatives failed
2025-07-17 10:00:28 -07:00
this . logger (
2025-07-16 13:51:00 -07:00
'error' ,
2025-07-28 16:00:05 -07:00
` Universal Sentence Encoder model loading failed after trying all alternatives. Last error: ${ errorMessage } `
2025-07-16 13:51:00 -07:00
)
} else {
2025-07-17 10:00:28 -07:00
this . logger (
2025-07-16 13:51:00 -07:00
'error' ,
` Universal Sentence Encoder model loading failed with non-retryable error: ${ errorMessage } `
)
}
throw lastError
}
2025-07-11 11:11:56 -07:00
}
}
2025-07-16 13:51:00 -07:00
// This should never be reached, but just in case
throw lastError || new Error ( 'Unknown error during model loading' )
2025-07-02 16:16:19 -07:00
}
2025-06-24 11:41:30 -07:00
/ * *
* Initialize the embedding model
* /
public async init ( ) : Promise < void > {
try {
// Save original console.warn
const originalWarn = console . warn
// Override console.warn to suppress TensorFlow.js Node.js backend message
console . warn = function ( message? : any , . . . optionalParams : any [ ] ) {
if (
message &&
typeof message === 'string' &&
message . includes (
'Hi, looks like you are running TensorFlow.js in Node.js'
)
) {
return // Suppress the specific warning
}
originalWarn ( message , . . . optionalParams )
}
2025-07-02 16:16:19 -07:00
// Add polyfills for TensorFlow.js compatibility
2025-07-16 13:51:00 -07:00
this . addServerCompatibilityPolyfills ( )
2025-07-02 16:16:19 -07:00
2025-06-26 19:04:37 -07:00
// TensorFlow.js will use its default EPSILON value
2025-07-16 13:51:00 -07:00
// CRITICAL: Ensure TextEncoder/TextDecoder are available before TensorFlow.js loads
2025-06-26 19:04:37 -07:00
try {
2025-07-16 13:51:00 -07:00
// Get the appropriate global object for the current environment
const globalObj = ( ( ) = > {
if ( typeof globalThis !== 'undefined' ) return globalThis
if ( typeof global !== 'undefined' ) return global
if ( typeof self !== 'undefined' ) return self
return null
} ) ( )
// Ensure TextEncoder/TextDecoder are globally available in server environments
if ( globalObj ) {
// Try to use Node.js util module if available (Node.js environments)
try {
2025-07-17 10:00:28 -07:00
if (
typeof process !== 'undefined' &&
process . versions &&
process . versions . node
) {
2025-07-16 13:51:00 -07:00
const util = await import ( 'util' )
if ( ! globalObj . TextEncoder ) {
globalObj . TextEncoder = util . TextEncoder
}
if ( ! globalObj . TextDecoder ) {
2025-07-28 16:00:05 -07:00
globalObj . TextDecoder = util . TextDecoder as unknown as typeof TextDecoder
2025-07-16 13:51:00 -07:00
}
}
} catch ( utilError ) {
// Fallback to standard TextEncoder/TextDecoder for non-Node.js server environments
if ( ! globalObj . TextEncoder ) {
globalObj . TextEncoder = TextEncoder
}
if ( ! globalObj . TextDecoder ) {
globalObj . TextDecoder = TextDecoder
}
}
}
// Apply the TensorFlow.js patch
const { applyTensorFlowPatch } = await import ( './textEncoding.js' )
await applyTensorFlowPatch ( )
2025-07-14 11:12:51 -07:00
2025-07-16 13:51:00 -07:00
// Now load TensorFlow.js core module using dynamic imports
this . tf = await import ( '@tensorflow/tfjs-core' )
2025-07-14 11:12:51 -07:00
2025-07-16 13:51:00 -07:00
// Import CPU backend (always needed as fallback)
await import ( '@tensorflow/tfjs-backend-cpu' )
2025-07-14 11:12:51 -07:00
2025-07-16 13:51:00 -07:00
// Try to import WebGL backend for GPU acceleration in browser environments
try {
2025-07-28 10:04:45 -07:00
if ( isBrowser ( ) ) {
2025-07-16 13:51:00 -07:00
await import ( '@tensorflow/tfjs-backend-webgl' )
// Check if WebGL is available
2025-07-14 11:12:51 -07:00
try {
if ( this . tf . setBackend ) {
2025-07-16 13:51:00 -07:00
await this . tf . setBackend ( 'webgl' )
2025-07-14 11:12:51 -07:00
this . backend = 'webgl'
console . log ( 'Using WebGL backend for TensorFlow.js' )
} else {
console . warn (
'tf.setBackend is not available, falling back to CPU'
)
}
} catch ( e ) {
2025-06-27 14:06:59 -07:00
console . warn (
2025-07-14 11:12:51 -07:00
'WebGL backend not available, falling back to CPU:' ,
e
2025-06-27 14:06:59 -07:00
)
2025-07-14 11:12:51 -07:00
this . backend = 'cpu'
}
}
2025-07-16 13:51:00 -07:00
} catch ( error ) {
console . warn (
'WebGL backend not available, falling back to CPU:' ,
error
2025-07-14 11:12:51 -07:00
)
2025-07-16 13:51:00 -07:00
this . backend = 'cpu'
2025-06-24 11:41:30 -07:00
}
2025-07-16 13:51:00 -07:00
// Load Universal Sentence Encoder using dynamic import
this . use = await import ( '@tensorflow-models/universal-sentence-encoder' )
2025-06-27 14:06:59 -07:00
} catch ( error ) {
2025-07-17 10:00:28 -07:00
this . logger ( 'error' , 'Failed to initialize TensorFlow.js:' , error )
2025-08-01 11:02:01 -07:00
// No fallback allowed - throw error
throw new Error ( ` Universal Sentence Encoder initialization failed: ${ error } ` )
2025-06-27 14:06:59 -07:00
}
// Set the backend
2025-07-28 16:00:05 -07:00
if ( this . tf && this . tf . setBackend ) {
2025-06-27 14:06:59 -07:00
await this . tf . setBackend ( this . backend )
2025-06-24 11:41:30 -07:00
}
2025-06-27 14:06:59 -07:00
// Try to find the load function in different possible module structures
const loadFunction = findUSELoadFunction ( this . use )
if ( ! loadFunction ) {
2025-08-01 11:02:01 -07:00
this . logger ( 'error' , 'Could not find Universal Sentence Encoder load function' )
throw new Error ( 'Universal Sentence Encoder load function not found. Fallback mechanisms are not allowed.' )
2025-06-27 14:06:59 -07:00
}
2025-07-28 16:00:05 -07:00
try {
// Load the model with retry logic for network failures
this . model = await this . loadModelWithRetry ( loadFunction )
this . initialized = true
} catch ( modelError ) {
this . logger (
2025-08-01 11:02:01 -07:00
'error' ,
'Failed to load Universal Sentence Encoder model:' ,
2025-07-28 16:00:05 -07:00
modelError
)
2025-08-01 11:02:01 -07:00
// No fallback allowed - throw error
throw new Error ( ` Universal Sentence Encoder model loading failed: ${ modelError } ` )
2025-07-28 16:00:05 -07:00
}
2025-06-24 11:41:30 -07:00
// Restore original console.warn
console . warn = originalWarn
} catch ( error ) {
2025-07-17 10:00:28 -07:00
this . logger (
2025-07-16 13:51:00 -07:00
'error' ,
'Failed to initialize Universal Sentence Encoder:' ,
error
)
2025-08-01 11:02:01 -07:00
// No fallback allowed - throw error
throw new Error ( ` Universal Sentence Encoder initialization failed: ${ error } ` )
2025-06-24 11:41:30 -07:00
}
}
/ * *
* Embed text into a vector using Universal Sentence Encoder
* @param data Text to embed
* /
2025-07-28 16:00:05 -07:00
/ * *
2025-08-01 11:02:01 -07:00
* This method has been removed as we should always use Universal Sentence Encoder
* and never fall back to alternative vector generation methods
* @deprecated
2025-07-28 16:00:05 -07:00
* /
private generateFallbackVector ( text : string ) : Vector {
2025-08-01 11:02:01 -07:00
throw new Error ( 'Fallback vector generation is not allowed. Universal Sentence Encoder must be used for all embeddings.' )
2025-07-28 16:00:05 -07:00
}
2025-06-24 11:41:30 -07:00
public async embed ( data : string | string [ ] ) : Promise < Vector > {
if ( ! this . initialized ) {
await this . init ( )
}
try {
// Handle different input types
let textToEmbed : string [ ]
if ( typeof data === 'string' ) {
// Handle empty string case
if ( data . trim ( ) === '' ) {
// Return a zero vector of appropriate dimension (512 is the default for USE)
return new Array ( 512 ) . fill ( 0 )
}
textToEmbed = [ data ]
} else if (
Array . isArray ( data ) &&
data . every ( ( item ) = > typeof item === 'string' )
) {
// Handle empty array or array with empty strings
if ( data . length === 0 || data . every ( ( item ) = > item . trim ( ) === '' ) ) {
return new Array ( 512 ) . fill ( 0 )
}
// Filter out empty strings
textToEmbed = data . filter ( ( item ) = > item . trim ( ) !== '' )
if ( textToEmbed . length === 0 ) {
return new Array ( 512 ) . fill ( 0 )
}
} else {
throw new Error (
'UniversalSentenceEncoder only supports string or string[] data'
)
}
2025-08-01 11:02:01 -07:00
// Ensure the model is available - no fallbacks allowed
2025-07-28 16:00:05 -07:00
if ( ! this . model ) {
2025-08-01 11:02:01 -07:00
throw new Error (
'Universal Sentence Encoder model is not available. Fallback mechanisms are not allowed.'
2025-07-28 16:00:05 -07:00
)
}
2025-06-24 11:41:30 -07:00
// Get embeddings
const embeddings = await this . model . embed ( textToEmbed )
// Convert to array and return the first embedding
const embeddingArray = await embeddings . array ( )
2025-06-27 14:06:59 -07:00
// Dispose of the tensor to free memory
embeddings . dispose ( )
2025-07-28 16:00:05 -07:00
// Get the first embedding
let embedding = embeddingArray [ 0 ]
// Ensure the embedding is exactly 512 dimensions
if ( embedding . length !== 512 ) {
this . logger (
'warn' ,
` Embedding dimension mismatch: expected 512, got ${ embedding . length } . Standardizing... `
)
// If the embedding is too short, pad with zeros
if ( embedding . length < 512 ) {
const paddedEmbedding = new Array ( 512 ) . fill ( 0 )
for ( let i = 0 ; i < embedding . length ; i ++ ) {
paddedEmbedding [ i ] = embedding [ i ]
}
embedding = paddedEmbedding
}
// If the embedding is too long, truncate
else if ( embedding . length > 512 ) {
// Special handling for 1536-dimensional vectors (common with newer models)
if ( embedding . length === 1536 ) {
// Take every third value to reduce from 1536 to 512
const reducedEmbedding = new Array ( 512 ) . fill ( 0 )
for ( let i = 0 ; i < 512 ; i ++ ) {
reducedEmbedding [ i ] = embedding [ i * 3 ]
}
embedding = reducedEmbedding
} else {
// For other dimensions, just truncate
embedding = embedding . slice ( 0 , 512 )
}
}
}
return embedding
2025-06-24 11:41:30 -07:00
} catch ( error ) {
2025-08-01 11:02:01 -07:00
// No fallback - throw the error
2025-07-17 10:00:28 -07:00
this . logger (
2025-08-01 11:02:01 -07:00
'error' ,
'Failed to embed text with Universal Sentence Encoder:' ,
2025-06-24 11:41:30 -07:00
error
)
2025-08-01 11:02:01 -07:00
throw new Error ( ` Universal Sentence Encoder embedding failed: ${ error } ` )
2025-06-24 11:41:30 -07:00
}
}
2025-06-27 14:06:59 -07:00
/ * *
* Embed multiple texts into vectors using Universal Sentence Encoder
* This is more efficient than calling embed ( ) multiple times
* @param dataArray Array of texts to embed
* @returns Array of embedding vectors
* /
public async embedBatch ( dataArray : string [ ] ) : Promise < Vector [ ] > {
if ( ! this . initialized ) {
await this . init ( )
}
try {
// Handle empty array case
if ( dataArray . length === 0 ) {
return [ ]
}
// Filter out empty strings and handle edge cases
const textToEmbed = dataArray . filter (
( text : string ) = > typeof text === 'string' && text . trim ( ) !== ''
)
// If all strings were empty, return appropriate zero vectors
if ( textToEmbed . length === 0 ) {
return dataArray . map ( ( ) = > new Array ( 512 ) . fill ( 0 ) )
}
2025-08-01 11:02:01 -07:00
// Ensure the model is available - no fallbacks allowed
2025-07-28 16:00:05 -07:00
if ( ! this . model ) {
2025-08-01 11:02:01 -07:00
throw new Error (
'Universal Sentence Encoder model is not available. Fallback mechanisms are not allowed.'
2025-07-28 16:00:05 -07:00
)
}
2025-06-27 14:06:59 -07:00
// Get embeddings for all texts in a single batch operation
const embeddings = await this . model . embed ( textToEmbed )
// Convert to array
const embeddingArray = await embeddings . array ( )
// Dispose of the tensor to free memory
embeddings . dispose ( )
2025-07-28 16:00:05 -07:00
// Standardize embeddings to ensure they're all 512 dimensions
const standardizedEmbeddings = embeddingArray . map ( ( embedding : Vector ) = > {
if ( embedding . length !== 512 ) {
this . logger (
'warn' ,
` Batch embedding dimension mismatch: expected 512, got ${ embedding . length } . Standardizing... `
)
// If the embedding is too short, pad with zeros
if ( embedding . length < 512 ) {
const paddedEmbedding = new Array ( 512 ) . fill ( 0 )
for ( let i = 0 ; i < embedding . length ; i ++ ) {
paddedEmbedding [ i ] = embedding [ i ]
}
return paddedEmbedding
}
// If the embedding is too long, truncate
else if ( embedding . length > 512 ) {
// Special handling for 1536-dimensional vectors (common with newer models)
if ( embedding . length === 1536 ) {
// Take every third value to reduce from 1536 to 512
const reducedEmbedding = new Array ( 512 ) . fill ( 0 )
for ( let i = 0 ; i < 512 ; i ++ ) {
reducedEmbedding [ i ] = embedding [ i * 3 ]
}
return reducedEmbedding
} else {
// For other dimensions, just truncate
return embedding . slice ( 0 , 512 )
}
}
}
return embedding
} )
2025-06-27 14:06:59 -07:00
// Map the results back to the original array order
const results : Vector [ ] = [ ]
let embeddingIndex = 0
for ( let i = 0 ; i < dataArray . length ; i ++ ) {
const text = dataArray [ i ]
if ( typeof text === 'string' && text . trim ( ) !== '' ) {
2025-07-28 16:00:05 -07:00
// Use the standardized embedding for non-empty strings
results . push ( standardizedEmbeddings [ embeddingIndex ] )
2025-06-27 14:06:59 -07:00
embeddingIndex ++
} else {
// Use a zero vector for empty strings
results . push ( new Array ( 512 ) . fill ( 0 ) )
}
}
return results
} catch ( error ) {
2025-08-01 11:02:01 -07:00
// No fallback - throw the error
2025-07-17 10:00:28 -07:00
this . logger (
2025-08-01 11:02:01 -07:00
'error' ,
'Failed to batch embed text with Universal Sentence Encoder:' ,
2025-06-27 14:06:59 -07:00
error
)
2025-08-01 11:02:01 -07:00
throw new Error ( ` Universal Sentence Encoder batch embedding failed: ${ error } ` )
2025-06-27 14:06:59 -07:00
}
}
2025-06-24 11:41:30 -07:00
/ * *
* Dispose of the model resources
* /
public async dispose ( ) : Promise < void > {
if ( this . model && this . tf ) {
try {
// Dispose of the model and tensors
this . model . dispose ( )
this . tf . disposeVariables ( )
this . initialized = false
} catch ( error ) {
2025-07-17 10:00:28 -07:00
this . logger (
2025-07-16 13:51:00 -07:00
'error' ,
'Failed to dispose Universal Sentence Encoder:' ,
error
)
2025-06-24 11:41:30 -07:00
}
}
return Promise . resolve ( )
}
}
2025-06-27 14:06:59 -07:00
/ * *
* Helper function to load the Universal Sentence Encoder model
* This tries multiple approaches to find the correct load function
* @param sentenceEncoderModule The imported module
* @returns The load function or null if not found
* /
2025-06-30 11:00:36 -07:00
function findUSELoadFunction (
sentenceEncoderModule : any
) : ( ( ) = > Promise < EmbeddingModel > ) | null {
2025-07-17 10:12:15 -07:00
// Module structure available for debugging if needed
2025-06-27 14:06:59 -07:00
2025-07-28 16:00:05 -07:00
// Find the appropriate load function from the module
2025-06-27 14:06:59 -07:00
let loadFunction = null
// Try sentenceEncoderModule.load first (direct export)
if (
sentenceEncoderModule . load &&
typeof sentenceEncoderModule . load === 'function'
) {
loadFunction = sentenceEncoderModule . load
}
// Then try sentenceEncoderModule.default.load (default export)
else if (
sentenceEncoderModule . default &&
sentenceEncoderModule . default . load &&
typeof sentenceEncoderModule . default . load === 'function'
) {
loadFunction = sentenceEncoderModule . default . load
}
// Try sentenceEncoderModule.default directly if it's a function
else if (
sentenceEncoderModule . default &&
typeof sentenceEncoderModule . default === 'function'
) {
loadFunction = sentenceEncoderModule . default
}
// Try sentenceEncoderModule directly if it's a function
else if ( typeof sentenceEncoderModule === 'function' ) {
loadFunction = sentenceEncoderModule
}
// Try additional common patterns
else if (
sentenceEncoderModule . UniversalSentenceEncoder &&
typeof sentenceEncoderModule . UniversalSentenceEncoder . load === 'function'
) {
loadFunction = sentenceEncoderModule . UniversalSentenceEncoder . load
} else if (
sentenceEncoderModule . default &&
sentenceEncoderModule . default . UniversalSentenceEncoder &&
2025-07-28 16:00:05 -07:00
typeof sentenceEncoderModule . default . UniversalSentenceEncoder . load === 'function'
2025-06-27 14:06:59 -07:00
) {
loadFunction = sentenceEncoderModule . default . UniversalSentenceEncoder . load
}
// Try to find the load function in the module's properties
else {
// Look for any property that might be a load function
for ( const key in sentenceEncoderModule ) {
if ( typeof sentenceEncoderModule [ key ] === 'function' ) {
// Check if the function name or key contains 'load'
2025-06-30 09:39:16 -07:00
const fnName = sentenceEncoderModule [ key ] . name || key
2025-06-27 14:06:59 -07:00
if ( fnName . toLowerCase ( ) . includes ( 'load' ) ) {
2025-06-30 09:39:16 -07:00
loadFunction = sentenceEncoderModule [ key ]
break
2025-06-27 14:06:59 -07:00
}
}
// Also check nested objects
2025-06-30 09:39:16 -07:00
else if (
typeof sentenceEncoderModule [ key ] === 'object' &&
sentenceEncoderModule [ key ] !== null
) {
2025-06-27 14:06:59 -07:00
for ( const nestedKey in sentenceEncoderModule [ key ] ) {
if ( typeof sentenceEncoderModule [ key ] [ nestedKey ] === 'function' ) {
2025-06-30 09:39:16 -07:00
const fnName =
sentenceEncoderModule [ key ] [ nestedKey ] . name || nestedKey
2025-06-27 14:06:59 -07:00
if ( fnName . toLowerCase ( ) . includes ( 'load' ) ) {
2025-06-30 09:39:16 -07:00
loadFunction = sentenceEncoderModule [ key ] [ nestedKey ]
break
2025-06-27 14:06:59 -07:00
}
}
}
2025-06-30 09:39:16 -07:00
if ( loadFunction ) break
2025-06-27 14:06:59 -07:00
}
}
}
2025-07-28 16:00:05 -07:00
// Return a function that calls the load function without arguments
// This will use the bundled model from the package
if ( loadFunction ) {
return async ( ) = > await loadFunction ( )
}
return null
2025-06-27 14:06:59 -07:00
}
2025-07-16 13:51:00 -07:00
/ * *
* Check if we ' re running in a test environment ( standalone version )
2025-07-28 10:04:45 -07:00
* Uses the same logic as the class method to avoid duplication
2025-07-16 13:51:00 -07:00
* /
function isTestEnvironment ( ) : boolean {
2025-07-28 10:04:45 -07:00
// Use the same implementation as the class method
2025-07-16 13:51:00 -07:00
// Safely check for Node.js environment first
if ( typeof process === 'undefined' ) {
return false
}
2025-07-17 10:00:28 -07:00
2025-07-16 13:51:00 -07:00
return (
process . env . NODE_ENV === 'test' ||
process . env . VITEST === 'true' ||
( typeof global !== 'undefined' && global . __vitest__ ) ||
process . argv . some ( ( arg ) = > arg . includes ( 'vitest' ) )
)
}
/ * *
2025-07-18 10:40:37 -07:00
* Log message only if not in test environment and verbose mode is enabled ( standalone version )
* @param level Log level ( 'log' , 'warn' , 'error' )
* @param message Message to log
* @param args Additional arguments to log
* @param verbose Whether to log non - essential messages ( default : true )
2025-07-16 13:51:00 -07:00
* /
function logIfNotTest (
level : 'log' | 'warn' | 'error' ,
message : string ,
2025-07-18 10:40:37 -07:00
args : any [ ] = [ ] ,
verbose : boolean = true
2025-07-16 13:51:00 -07:00
) : void {
2025-07-18 10:40:37 -07:00
// Always log errors, but only log other messages if verbose mode is enabled
if ( ( level === 'error' || verbose ) && ! isTestEnvironment ( ) ) {
2025-07-16 13:51:00 -07:00
console [ level ] ( message , . . . args )
}
}
2025-06-24 11:41:30 -07:00
/ * *
* Create an embedding function from an embedding model
2025-07-16 13:51:00 -07:00
* @param model Embedding model to use ( optional , defaults to UniversalSentenceEncoder )
2025-06-24 11:41:30 -07:00
* /
export function createEmbeddingFunction (
2025-07-16 13:51:00 -07:00
model? : EmbeddingModel
2025-06-24 11:41:30 -07:00
) : EmbeddingFunction {
2025-07-16 13:51:00 -07:00
// If no model is provided, use the default TensorFlow embedding function
if ( ! model ) {
return createTensorFlowEmbeddingFunction ( )
}
2025-06-24 11:41:30 -07:00
return async ( data : any ) : Promise < Vector > = > {
return await model . embed ( data )
}
}
/ * *
* Creates a TensorFlow - based Universal Sentence Encoder embedding function
* This is the required embedding function for all text embeddings
2025-06-27 14:06:59 -07:00
* Uses a shared model instance for better performance across multiple calls
2025-07-18 10:40:37 -07:00
* @param options Configuration options
* @param options . verbose Whether to log non - essential messages ( default : true )
2025-06-24 11:41:30 -07:00
* /
2025-06-27 14:06:59 -07:00
// Create a single shared instance of the model that persists across all embedding calls
2025-07-18 10:40:37 -07:00
let sharedModel : UniversalSentenceEncoder | null = null
2025-06-27 14:06:59 -07:00
let sharedModelInitialized = false
2025-07-18 10:40:37 -07:00
let sharedModelVerbose = true
2025-06-24 11:41:30 -07:00
2025-07-18 10:40:37 -07:00
export function createTensorFlowEmbeddingFunction ( options : { verbose? : boolean } = { } ) : EmbeddingFunction {
// Update verbose setting if provided
if ( options . verbose !== undefined ) {
sharedModelVerbose = options . verbose
}
// Create the shared model if it doesn't exist yet
if ( ! sharedModel ) {
sharedModel = new UniversalSentenceEncoder ( { verbose : sharedModelVerbose } )
}
2025-06-24 11:41:30 -07:00
return async ( data : any ) : Promise < Vector > = > {
try {
// Initialize the model if it hasn't been initialized yet
2025-06-27 14:06:59 -07:00
if ( ! sharedModelInitialized ) {
2025-07-16 13:51:00 -07:00
try {
2025-07-18 10:40:37 -07:00
await sharedModel ! . init ( )
2025-07-16 13:51:00 -07:00
sharedModelInitialized = true
} catch ( initError ) {
// Reset the flag so we can retry initialization on the next call
sharedModelInitialized = false
throw initError
}
2025-06-24 11:41:30 -07:00
}
2025-07-18 10:40:37 -07:00
return await sharedModel ! . embed ( data )
2025-06-24 11:41:30 -07:00
} catch ( error ) {
2025-08-01 11:02:01 -07:00
logIfNotTest ( 'error' , 'Failed to use Universal Sentence Encoder:' , [ error ] , sharedModelVerbose )
// No fallback - Universal Sentence Encoder is required
2025-06-24 11:41:30 -07:00
throw new Error (
2025-08-01 11:02:01 -07:00
` Universal Sentence Encoder is required and no fallbacks are allowed: ${ error } `
2025-06-24 11:41:30 -07:00
)
}
}
}
/ * *
2025-06-27 14:06:59 -07:00
* Default embedding function
* Uses UniversalSentenceEncoder for all text embeddings
* TensorFlow . js is required for this to work
* Uses CPU for compatibility
2025-07-18 10:40:37 -07:00
* @param options Configuration options
* @param options . verbose Whether to log non - essential messages ( default : true )
2025-06-24 11:41:30 -07:00
* /
2025-07-18 10:40:37 -07:00
export function getDefaultEmbeddingFunction ( options : { verbose? : boolean } = { } ) : EmbeddingFunction {
return createTensorFlowEmbeddingFunction ( options )
}
2025-06-24 11:41:30 -07:00
2025-06-27 14:06:59 -07:00
/ * *
2025-07-18 10:40:37 -07:00
* Default embedding function with default options
2025-06-27 14:06:59 -07:00
* Uses UniversalSentenceEncoder for all text embeddings
* TensorFlow . js is required for this to work
2025-07-18 10:40:37 -07:00
* Uses CPU for compatibility
* /
export const defaultEmbeddingFunction : EmbeddingFunction = getDefaultEmbeddingFunction ( )
/ * *
* Creates a batch embedding function that uses UniversalSentenceEncoder
* TensorFlow . js is required for this to work
2025-06-27 14:06:59 -07:00
* Processes all items in a single batch operation
* Uses a shared model instance for better performance across multiple calls
2025-07-18 10:40:37 -07:00
* @param options Configuration options
* @param options . verbose Whether to log non - essential messages ( default : true )
2025-06-27 14:06:59 -07:00
* /
// Create a single shared instance of the model that persists across function calls
2025-07-18 10:40:37 -07:00
let sharedBatchModel : UniversalSentenceEncoder | null = null
2025-06-27 14:06:59 -07:00
let sharedBatchModelInitialized = false
2025-07-18 10:40:37 -07:00
let sharedBatchModelVerbose = true
2025-06-27 14:06:59 -07:00
2025-07-18 10:40:37 -07:00
export function createBatchEmbeddingFunction ( options : { verbose? : boolean } = { } ) : (
2025-06-27 14:06:59 -07:00
dataArray : string [ ]
2025-07-18 10:40:37 -07:00
) = > Promise < Vector [ ] > {
// Update verbose setting if provided
if ( options . verbose !== undefined ) {
sharedBatchModelVerbose = options . verbose
}
// Create the shared model if it doesn't exist yet
if ( ! sharedBatchModel ) {
sharedBatchModel = new UniversalSentenceEncoder ( { verbose : sharedBatchModelVerbose } )
}
return async ( dataArray : string [ ] ) : Promise < Vector [ ] > = > {
try {
// Initialize the model if it hasn't been initialized yet
if ( ! sharedBatchModelInitialized ) {
2025-08-01 11:02:01 -07:00
try {
await sharedBatchModel ! . init ( )
sharedBatchModelInitialized = true
} catch ( initError ) {
// Reset the flag so we can retry initialization on the next call
sharedBatchModelInitialized = false
throw initError
}
2025-07-18 10:40:37 -07:00
}
2025-06-24 11:41:30 -07:00
2025-07-18 10:40:37 -07:00
return await sharedBatchModel ! . embedBatch ( dataArray )
} catch ( error ) {
2025-08-01 11:02:01 -07:00
logIfNotTest ( 'error' , 'Failed to use Universal Sentence Encoder batch embedding:' , [ error ] , sharedBatchModelVerbose )
// No fallback - Universal Sentence Encoder is required
2025-07-18 10:40:37 -07:00
throw new Error (
2025-08-01 11:02:01 -07:00
` Universal Sentence Encoder is required for batch embedding and no fallbacks are allowed: ${ error } `
2025-07-18 10:40:37 -07:00
)
}
2025-06-24 11:41:30 -07:00
}
}
2025-07-18 10:40:37 -07:00
/ * *
* Get a batch embedding function with custom options
* Uses UniversalSentenceEncoder for all text embeddings
* TensorFlow . js is required for this to work
* Processes all items in a single batch operation
* @param options Configuration options
* @param options . verbose Whether to log non - essential messages ( default : true )
* /
export function getDefaultBatchEmbeddingFunction ( options : { verbose? : boolean } = { } ) : (
dataArray : string [ ]
) = > Promise < Vector [ ] > {
return createBatchEmbeddingFunction ( options )
}
/ * *
* Default batch embedding function with default options
* Uses UniversalSentenceEncoder for all text embeddings
* TensorFlow . js is required for this to work
* Processes all items in a single batch operation
* /
export const defaultBatchEmbeddingFunction = getDefaultBatchEmbeddingFunction ( )
2025-06-24 11:41:30 -07:00
/ * *
2025-06-27 14:06:59 -07:00
* Creates an embedding function that runs in a separate thread
* This is a wrapper around createEmbeddingFunction that uses executeInThread
* @param model Embedding model to use
2025-06-24 11:41:30 -07:00
* /
2025-06-27 14:06:59 -07:00
export function createThreadedEmbeddingFunction (
model : EmbeddingModel
) : EmbeddingFunction {
const embeddingFunction = createEmbeddingFunction ( model )
return async ( data : any ) : Promise < Vector > = > {
// Convert the embedding function to a string
const fnString = embeddingFunction . toString ( )
// Execute the embedding function in a "thread" (main thread in this implementation)
return await executeInThread < Vector > ( fnString , data )
}
}