widgettdc-api / apps /backend /src /platform /embeddings /TransformersEmbeddings.ts
Kraft102's picture
Update backend source
34367da verified
/**
* Transformers.js Embeddings Service
*
* Uses @xenova/transformers to generate embeddings locally without HuggingFace API.
* Supports sentence-transformers models for semantic similarity.
*
* NOTE: Disabled in Docker/production due to ONNX runtime compatibility issues with Alpine
*/
// Dynamic import to avoid ONNX runtime crash in Docker
let pipelineFactory: any = null;
export interface EmbeddingOptions {
model?: string;
normalize?: boolean;
}
export class TransformersEmbeddings {
private modelName: string;
private extractor: any = null; // Pipeline type from @xenova/transformers
private initialized: boolean = false;
constructor(modelName: string = 'Xenova/all-MiniLM-L6-v2') {
this.modelName = modelName;
}
/**
* Initialize the embedding model
*/
async initialize(): Promise<void> {
if (this.initialized && this.extractor) {
return;
}
// Skip transformers in Docker/production - ONNX runtime has architecture issues on Alpine
const isDocker = process.env.NODE_ENV === 'production' || process.cwd().startsWith('/app');
if (isDocker) {
throw new Error(
'Transformers.js disabled in Docker mode (ONNX runtime incompatibility with Alpine)'
);
}
try {
console.log(`🔄 Loading embedding model: ${this.modelName}`);
// Dynamic import to avoid top-level ONNX load
if (!pipelineFactory) {
const transformers = await import('@xenova/transformers');
pipelineFactory = transformers.pipeline;
}
this.extractor = await pipelineFactory('feature-extraction', this.modelName, {
quantized: true, // Use quantized model for faster loading
});
this.initialized = true;
console.log(`✅ Embedding model loaded: ${this.modelName}`);
} catch (error) {
console.error('❌ Failed to load embedding model:', error);
throw error;
}
}
/**
* Generate embedding for a single text
*/
async embed(text: string, options?: EmbeddingOptions): Promise<number[]> {
if (!this.extractor) {
await this.initialize();
}
if (!this.extractor) {
throw new Error('Embedding model not initialized');
}
try {
const output = await this.extractor(text, {
pooling: 'mean',
normalize: options?.normalize ?? true,
});
// Convert tensor to array
const embedding = Array.from(output.data) as number[];
return embedding;
} catch (error) {
console.error('❌ Failed to generate embedding:', error);
throw error;
}
}
/**
* Generate embeddings for multiple texts
*/
async embedBatch(texts: string[], options?: EmbeddingOptions): Promise<number[][]> {
if (!this.extractor) {
await this.initialize();
}
if (!this.extractor) {
throw new Error('Embedding model not initialized');
}
try {
const embeddings: number[][] = [];
// Process in batches to avoid memory issues
const batchSize = 10;
for (let i = 0; i < texts.length; i += batchSize) {
const batch = texts.slice(i, i + batchSize);
const batchEmbeddings = await Promise.all(batch.map(text => this.embed(text, options)));
embeddings.push(...batchEmbeddings);
}
return embeddings;
} catch (error) {
console.error('❌ Failed to generate batch embeddings:', error);
throw error;
}
}
/**
* Calculate cosine similarity between two embeddings
*/
cosineSimilarity(embedding1: number[], embedding2: number[]): number {
if (embedding1.length !== embedding2.length) {
throw new Error('Embeddings must have the same dimension');
}
let dotProduct = 0;
let norm1 = 0;
let norm2 = 0;
for (let i = 0; i < embedding1.length; i++) {
dotProduct += embedding1[i] * embedding2[i];
norm1 += embedding1[i] * embedding1[i];
norm2 += embedding2[i] * embedding2[i];
}
const similarity = dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
return similarity;
}
/**
* Find most similar embedding in a collection
*/
findMostSimilar(
queryEmbedding: number[],
candidateEmbeddings: number[][],
topK: number = 5
): Array<{ index: number; similarity: number }> {
const similarities = candidateEmbeddings.map((embedding, index) => ({
index,
similarity: this.cosineSimilarity(queryEmbedding, embedding),
}));
return similarities.sort((a, b) => b.similarity - a.similarity).slice(0, topK);
}
/**
* Get embedding dimension
*/
getDimension(): number {
// all-MiniLM-L6-v2 has 384 dimensions
return 384;
}
/**
* Check if model is initialized
*/
isInitialized(): boolean {
return this.initialized && this.extractor !== null;
}
}
// Singleton instance
let transformersEmbeddingsInstance: TransformersEmbeddings | null = null;
export function getTransformersEmbeddings(): TransformersEmbeddings {
if (!transformersEmbeddingsInstance) {
transformersEmbeddingsInstance = new TransformersEmbeddings();
}
return transformersEmbeddingsInstance;
}