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'
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
*
|
|
|
|
|
* This implementation will use GPU acceleration via WebGL when available,
|
|
|
|
|
* falling back to CPU processing when GPU is not available or fails to initialize.
|
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
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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-06-26 19:04:37 -07:00
|
|
|
// TensorFlow.js will use its default EPSILON value
|
|
|
|
|
|
|
|
|
|
// Dynamically import TensorFlow.js core module and backends
|
|
|
|
|
// Use type assertions to tell TypeScript these modules exist
|
|
|
|
|
this.tf = await import('@tensorflow/tfjs-core')
|
|
|
|
|
|
|
|
|
|
// Import CPU and WebGL backends
|
|
|
|
|
await import('@tensorflow/tfjs-backend-cpu')
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Try to import and use WebGL backend first (GPU)
|
|
|
|
|
await import('@tensorflow/tfjs-backend-webgl')
|
|
|
|
|
|
|
|
|
|
// Check if WebGL is available and set it as the backend
|
|
|
|
|
if (
|
|
|
|
|
(await this.tf.findBackend('webgl')) ||
|
|
|
|
|
(await this.tf.ready().then(() => this.tf.findBackend('webgl')))
|
|
|
|
|
) {
|
|
|
|
|
console.log('Using WebGL backend (GPU acceleration)')
|
|
|
|
|
await this.tf.setBackend('webgl')
|
|
|
|
|
} else {
|
|
|
|
|
console.log('WebGL backend not available, falling back to CPU')
|
|
|
|
|
await this.tf.setBackend('cpu')
|
2025-06-24 11:41:30 -07:00
|
|
|
}
|
2025-06-26 19:04:37 -07:00
|
|
|
} catch (err) {
|
|
|
|
|
console.warn(
|
|
|
|
|
'WebGL backend failed to initialize, using CPU backend:',
|
|
|
|
|
err
|
|
|
|
|
)
|
|
|
|
|
await this.tf.setBackend('cpu')
|
2025-06-24 11:41:30 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.use = await import('@tensorflow-models/universal-sentence-encoder')
|
|
|
|
|
|
|
|
|
|
// Load the model
|
|
|
|
|
this.model = await this.use.load()
|
|
|
|
|
this.initialized = true
|
|
|
|
|
|
|
|
|
|
// Restore original console.warn
|
|
|
|
|
console.warn = originalWarn
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to initialize Universal Sentence Encoder:', error)
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Failed to initialize Universal Sentence Encoder: ${error}`
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Embed text into a vector using Universal Sentence Encoder
|
|
|
|
|
* @param data Text to embed
|
|
|
|
|
*/
|
|
|
|
|
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'
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get embeddings
|
|
|
|
|
const embeddings = await this.model.embed(textToEmbed)
|
|
|
|
|
|
|
|
|
|
// Convert to array and return the first embedding
|
|
|
|
|
const embeddingArray = await embeddings.array()
|
|
|
|
|
return embeddingArray[0]
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(
|
|
|
|
|
'Failed to embed text with Universal Sentence Encoder:',
|
|
|
|
|
error
|
|
|
|
|
)
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Failed to embed text with Universal Sentence Encoder: ${error}`
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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) {
|
|
|
|
|
console.error('Failed to dispose Universal Sentence Encoder:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return Promise.resolve()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create an embedding function from an embedding model
|
|
|
|
|
* @param model Embedding model to use
|
|
|
|
|
*/
|
|
|
|
|
export function createEmbeddingFunction(
|
|
|
|
|
model: EmbeddingModel
|
|
|
|
|
): EmbeddingFunction {
|
|
|
|
|
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
|
|
|
|
|
*/
|
|
|
|
|
export function createTensorFlowEmbeddingFunction(): EmbeddingFunction {
|
|
|
|
|
// Create a single shared instance of the model
|
|
|
|
|
const model = new UniversalSentenceEncoder()
|
|
|
|
|
let modelInitialized = false
|
|
|
|
|
|
|
|
|
|
return async (data: any): Promise<Vector> => {
|
|
|
|
|
try {
|
|
|
|
|
// Initialize the model if it hasn't been initialized yet
|
|
|
|
|
if (!modelInitialized) {
|
|
|
|
|
await model.init()
|
|
|
|
|
modelInitialized = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return await model.embed(data)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to use TensorFlow embedding:', error)
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Universal Sentence Encoder is required but failed: ${error}`
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates a TensorFlow-based Universal Sentence Encoder embedding function that runs in a separate thread
|
2025-06-26 19:04:37 -07:00
|
|
|
* This provides better performance for embedding operations by:
|
|
|
|
|
* 1. Using GPU acceleration via WebGL when available
|
|
|
|
|
* 2. Running in a separate thread to avoid blocking the main thread
|
|
|
|
|
* 3. Falling back to CPU processing when GPU is not available
|
|
|
|
|
*
|
2025-06-24 11:41:30 -07:00
|
|
|
* @param options Configuration options
|
2025-06-26 19:04:37 -07:00
|
|
|
* @returns An embedding function that runs in a separate thread with GPU acceleration when available
|
2025-06-24 11:41:30 -07:00
|
|
|
*/
|
|
|
|
|
export function createThreadedEmbeddingFunction(
|
|
|
|
|
options: { fallbackToMain?: boolean } = {}
|
|
|
|
|
): EmbeddingFunction {
|
|
|
|
|
// Create a standard embedding function to use as fallback
|
|
|
|
|
const standardEmbedding = createTensorFlowEmbeddingFunction()
|
|
|
|
|
|
|
|
|
|
// Flag to track if we've fallen back to main thread
|
|
|
|
|
let useFallback = false
|
|
|
|
|
|
|
|
|
|
return async (data: any): Promise<Vector> => {
|
|
|
|
|
// If we've already determined that threading doesn't work, use the fallback
|
|
|
|
|
if (useFallback) {
|
|
|
|
|
return standardEmbedding(data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Function to be executed in a worker thread
|
|
|
|
|
// This must be a regular function (not async) to avoid Promise cloning issues
|
|
|
|
|
const embedInWorker = (inputData: any) => {
|
|
|
|
|
// Return a plain object with the input data
|
|
|
|
|
// All async operations will be performed inside the worker
|
|
|
|
|
return { data: inputData }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Worker implementation function that will be stringified and run in the worker
|
|
|
|
|
const workerImplementation = async ({ data }: { data: any }) => {
|
2025-06-26 19:04:37 -07:00
|
|
|
try {
|
|
|
|
|
// We need to dynamically import TensorFlow.js core module and USE in the worker
|
|
|
|
|
// Use a variable name that won't conflict with any minified variables
|
|
|
|
|
|
|
|
|
|
// TensorFlow.js will use its default EPSILON value
|
|
|
|
|
|
|
|
|
|
// Import TensorFlow.js modules
|
|
|
|
|
const tf = await import('@tensorflow/tfjs-core')
|
|
|
|
|
|
|
|
|
|
// Import CPU backend in the worker
|
|
|
|
|
await import('@tensorflow/tfjs-backend-cpu')
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Try to import and use WebGL backend first (GPU)
|
|
|
|
|
await import('@tensorflow/tfjs-backend-webgl')
|
|
|
|
|
|
|
|
|
|
// Check if WebGL is available and set it as the backend
|
|
|
|
|
if (
|
|
|
|
|
(tf.findBackend && (await tf.findBackend('webgl'))) ||
|
|
|
|
|
(tf.ready &&
|
|
|
|
|
(await tf
|
|
|
|
|
.ready()
|
|
|
|
|
.then(() => tf.findBackend && tf.findBackend('webgl'))))
|
|
|
|
|
) {
|
|
|
|
|
console.log('Worker: Using WebGL backend (GPU acceleration)')
|
|
|
|
|
if (tf.setBackend) {
|
|
|
|
|
await tf.setBackend('webgl')
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
console.log(
|
|
|
|
|
'Worker: WebGL backend not available, falling back to CPU'
|
|
|
|
|
)
|
|
|
|
|
if (tf.setBackend) {
|
|
|
|
|
await tf.setBackend('cpu')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.warn(
|
|
|
|
|
'Worker: WebGL backend failed to initialize, using CPU backend:',
|
|
|
|
|
err
|
|
|
|
|
)
|
|
|
|
|
if (tf.setBackend) {
|
|
|
|
|
await tf.setBackend('cpu')
|
|
|
|
|
}
|
2025-06-24 11:41:30 -07:00
|
|
|
}
|
2025-06-26 19:04:37 -07:00
|
|
|
|
|
|
|
|
// Import the Universal Sentence Encoder
|
|
|
|
|
const sentenceEncoderModule = await import(
|
|
|
|
|
'@tensorflow-models/universal-sentence-encoder'
|
2025-06-24 11:41:30 -07:00
|
|
|
)
|
|
|
|
|
|
2025-06-26 19:04:37 -07:00
|
|
|
// Load the model directly from the module
|
|
|
|
|
const model = await sentenceEncoderModule.load()
|
|
|
|
|
|
|
|
|
|
// Handle different input types
|
|
|
|
|
let textToEmbed: string[]
|
|
|
|
|
if (typeof data === 'string') {
|
|
|
|
|
if (data.trim() === '') {
|
|
|
|
|
return new Array(512).fill(0)
|
|
|
|
|
}
|
|
|
|
|
textToEmbed = [data]
|
|
|
|
|
} else if (
|
|
|
|
|
Array.isArray(data) &&
|
|
|
|
|
data.every((item) => typeof item === 'string')
|
|
|
|
|
) {
|
|
|
|
|
if (data.length === 0 || data.every((item) => item.trim() === '')) {
|
|
|
|
|
return new Array(512).fill(0)
|
|
|
|
|
}
|
|
|
|
|
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'
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get embeddings
|
|
|
|
|
const embeddings = await model.embed(textToEmbed)
|
2025-06-24 11:41:30 -07:00
|
|
|
|
2025-06-26 19:04:37 -07:00
|
|
|
// Convert to array and return the first embedding
|
|
|
|
|
const embeddingArray = await embeddings.array()
|
2025-06-24 11:41:30 -07:00
|
|
|
|
2025-06-26 19:04:37 -07:00
|
|
|
// Dispose of the tensor to free memory
|
|
|
|
|
embeddings.dispose()
|
2025-06-24 11:41:30 -07:00
|
|
|
|
2025-06-26 19:04:37 -07:00
|
|
|
return embeddingArray[0]
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Worker error:', error)
|
|
|
|
|
throw error
|
|
|
|
|
}
|
2025-06-24 11:41:30 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Execute the embedding function in a separate thread
|
|
|
|
|
// Pass the worker implementation as a string to avoid Promise cloning issues
|
2025-06-26 19:04:37 -07:00
|
|
|
return await executeInThread<Vector>(
|
|
|
|
|
workerImplementation.toString(),
|
|
|
|
|
embedInWorker(data)
|
|
|
|
|
)
|
2025-06-24 11:41:30 -07:00
|
|
|
} catch (error) {
|
|
|
|
|
// If threading fails and fallback is enabled, use the standard embedding function
|
|
|
|
|
if (options.fallbackToMain) {
|
2025-06-26 19:04:37 -07:00
|
|
|
console.warn(
|
|
|
|
|
'Threaded embedding failed, falling back to main thread:',
|
|
|
|
|
error
|
|
|
|
|
)
|
2025-06-24 11:41:30 -07:00
|
|
|
useFallback = true
|
|
|
|
|
return standardEmbedding(data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Otherwise, propagate the error
|
|
|
|
|
throw new Error(`Threaded embedding failed: ${error}`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Default embedding function
|
|
|
|
|
* Uses UniversalSentenceEncoder for all text embeddings
|
|
|
|
|
* TensorFlow.js is required for this to work
|
2025-06-26 19:04:37 -07:00
|
|
|
* Uses GPU acceleration via WebGL when available for optimal performance
|
|
|
|
|
* Uses threading when available to avoid blocking the main thread
|
|
|
|
|
* Falls back to CPU processing when GPU is not available
|
2025-06-24 11:41:30 -07:00
|
|
|
*/
|
|
|
|
|
export const defaultEmbeddingFunction: EmbeddingFunction =
|
|
|
|
|
createThreadedEmbeddingFunction({ fallbackToMain: true })
|