rag-lite-ts 2.0.4 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,13 +24,29 @@
24
24
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
25
25
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
26
26
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
27
- import { existsSync, statSync } from 'fs';
27
+ import { existsSync, statSync, createWriteStream } from 'fs';
28
28
  import { resolve } from 'path';
29
29
  import { SearchFactory } from './factories/search-factory.js';
30
30
  import { IngestionFactory } from './factories/ingestion-factory.js';
31
- import { openDatabase, getSystemInfo } from './core/db.js';
31
+ import { getSystemInfo } from './core/db.js';
32
32
  import { DatabaseConnectionManager } from './core/database-connection-manager.js';
33
33
  import { config, validateCoreConfig, ConfigurationError } from './core/config.js';
34
+ /**
35
+ * Detect MIME type from file path or extension
36
+ */
37
+ function getMimeTypeFromPath(filePath) {
38
+ const ext = filePath.toLowerCase().split('.').pop() || '';
39
+ const mimeTypes = {
40
+ 'jpg': 'image/jpeg',
41
+ 'jpeg': 'image/jpeg',
42
+ 'png': 'image/png',
43
+ 'gif': 'image/gif',
44
+ 'webp': 'image/webp',
45
+ 'bmp': 'image/bmp',
46
+ 'svg': 'image/svg+xml'
47
+ };
48
+ return mimeTypes[ext] || 'image/jpeg'; // Default to JPEG if unknown
49
+ }
34
50
  /**
35
51
  * MCP Server class that wraps RAG-lite TS functionality
36
52
  * Implements MCP protocol interface without creating REST/GraphQL endpoints
@@ -118,8 +134,8 @@ class RagLiteMCPServer {
118
134
  },
119
135
  rerank_strategy: {
120
136
  type: 'string',
121
- description: 'Reranking strategy for multimodal mode. Options: text-derived (default), metadata, hybrid, disabled',
122
- enum: ['text-derived', 'metadata', 'hybrid', 'disabled']
137
+ description: 'Reranking strategy for multimodal mode. Options: text-derived (default), disabled',
138
+ enum: ['text-derived', 'disabled']
123
139
  },
124
140
  force_rebuild: {
125
141
  type: 'boolean',
@@ -152,8 +168,8 @@ class RagLiteMCPServer {
152
168
  },
153
169
  rerank_strategy: {
154
170
  type: 'string',
155
- description: 'Reranking strategy for multimodal mode. Options: text-derived (default), metadata, hybrid, disabled',
156
- enum: ['text-derived', 'metadata', 'hybrid', 'disabled'],
171
+ description: 'Reranking strategy for multimodal mode. Options: text-derived (default), disabled',
172
+ enum: ['text-derived', 'disabled'],
157
173
  default: 'text-derived'
158
174
  },
159
175
  title: {
@@ -375,50 +391,60 @@ class RagLiteMCPServer {
375
391
  const startTime = Date.now();
376
392
  const results = await this.searchEngine.search(args.query, searchOptions);
377
393
  const searchTime = Date.now() - startTime;
378
- // Format results for MCP response with content type information
379
- const formattedResults = {
394
+ // Format results for MCP response with proper image content support
395
+ const textResults = {
380
396
  query: args.query,
381
397
  results_count: results.length,
382
398
  search_time_ms: searchTime,
383
- results: await Promise.all(results.map(async (result, index) => {
384
- const formattedResult = {
385
- rank: index + 1,
386
- score: Math.round(result.score * 100) / 100, // Round to 2 decimal places
387
- content_type: result.contentType,
388
- document: {
389
- id: result.document.id,
390
- title: result.document.title,
391
- source: result.document.source,
392
- content_type: result.document.contentType
393
- },
394
- text: result.content
395
- };
396
- // For image content, include base64-encoded image data for MCP clients
397
- if (result.contentType === 'image' && result.document.contentId) {
398
- try {
399
- const imageData = await this.searchEngine.getContent(result.document.contentId, 'base64');
400
- formattedResult.image_data = imageData;
401
- formattedResult.image_format = 'base64';
402
- }
403
- catch (error) {
404
- // If image retrieval fails, include error but don't fail the entire search
405
- formattedResult.image_error = error instanceof Error ? error.message : 'Failed to retrieve image';
406
- }
407
- }
408
- // Include metadata if available
409
- if (result.metadata) {
410
- formattedResult.metadata = result.metadata;
411
- }
412
- return formattedResult;
399
+ results: results.map((result, index) => ({
400
+ rank: index + 1,
401
+ score: Math.round(result.score * 100) / 100,
402
+ content_type: result.contentType,
403
+ document: {
404
+ id: result.document.id,
405
+ title: result.document.title,
406
+ source: result.document.source,
407
+ content_type: result.document.contentType
408
+ },
409
+ text: result.content,
410
+ metadata: result.metadata,
411
+ // Reference to image content if applicable
412
+ has_image: result.contentType === 'image' && !!result.document.contentId
413
413
  }))
414
414
  };
415
+ // Build MCP response content array
416
+ const responseContent = [
417
+ {
418
+ type: 'text',
419
+ text: JSON.stringify(textResults, null, 2)
420
+ }
421
+ ];
422
+ // Add proper MCP image content for each image result
423
+ for (const result of results) {
424
+ if (result.contentType === 'image' && result.document.contentId) {
425
+ try {
426
+ const imageData = await this.searchEngine.getContent(result.document.contentId, 'base64');
427
+ const mimeType = getMimeTypeFromPath(result.document.source);
428
+ responseContent.push({
429
+ type: 'image',
430
+ data: imageData,
431
+ mimeType: mimeType,
432
+ annotations: {
433
+ audience: ['user'],
434
+ priority: 0.8,
435
+ title: result.document.title,
436
+ source: result.document.source
437
+ }
438
+ });
439
+ }
440
+ catch (error) {
441
+ // If image retrieval fails, log but don't fail the entire search
442
+ console.error(`Failed to retrieve image for ${result.document.source}:`, error);
443
+ }
444
+ }
445
+ }
415
446
  return {
416
- content: [
417
- {
418
- type: 'text',
419
- text: JSON.stringify(formattedResults, null, 2),
420
- },
421
- ],
447
+ content: responseContent
422
448
  };
423
449
  }
424
450
  catch (error) {
@@ -549,7 +575,7 @@ class RagLiteMCPServer {
549
575
  if (mode === 'text') {
550
576
  throw new Error('Reranking strategy parameter is only supported in multimodal mode');
551
577
  }
552
- const validStrategies = ['text-derived', 'metadata', 'hybrid', 'disabled'];
578
+ const validStrategies = ['text-derived', 'disabled'];
553
579
  if (!validStrategies.includes(args.rerank_strategy)) {
554
580
  throw new Error(`Invalid reranking strategy: ${args.rerank_strategy}. Supported strategies: ${validStrategies.join(', ')}`);
555
581
  }
@@ -746,7 +772,7 @@ class RagLiteMCPServer {
746
772
  reject(new Error(`Failed to download image: HTTP ${redirectResponse.statusCode}`));
747
773
  return;
748
774
  }
749
- const fileStream = require('fs').createWriteStream(tempFilePath);
775
+ const fileStream = createWriteStream(tempFilePath);
750
776
  redirectResponse.pipe(fileStream);
751
777
  fileStream.on('finish', () => {
752
778
  fileStream.close();
@@ -761,7 +787,7 @@ class RagLiteMCPServer {
761
787
  return;
762
788
  }
763
789
  else {
764
- const fileStream = require('fs').createWriteStream(tempFilePath);
790
+ const fileStream = createWriteStream(tempFilePath);
765
791
  response.pipe(fileStream);
766
792
  fileStream.on('finish', () => {
767
793
  fileStream.close();
@@ -916,8 +942,8 @@ class RagLiteMCPServer {
916
942
  // Create ingestion pipeline with force rebuild using factory
917
943
  const pipeline = await IngestionFactory.create(config.db_file, config.index_file, { forceRebuild: true });
918
944
  try {
919
- // Get all documents from database and re-ingest them
920
- const db = await openDatabase(config.db_file);
945
+ // Get all documents from database and re-ingest them - use shared connection
946
+ const db = await DatabaseConnectionManager.getConnection(config.db_file);
921
947
  try {
922
948
  const documents = await db.all('SELECT DISTINCT source FROM documents ORDER BY source');
923
949
  if (documents.length === 0) {
@@ -971,7 +997,8 @@ class RagLiteMCPServer {
971
997
  };
972
998
  }
973
999
  finally {
974
- await db.close();
1000
+ // Release instead of close - keeps connection alive for reuse
1001
+ await DatabaseConnectionManager.releaseConnection(config.db_file);
975
1002
  }
976
1003
  }
977
1004
  finally {
@@ -1009,7 +1036,7 @@ class RagLiteMCPServer {
1009
1036
  // Check model compatibility if database exists
1010
1037
  if (stats.database_exists) {
1011
1038
  try {
1012
- const db = await openDatabase(config.db_file);
1039
+ const db = await DatabaseConnectionManager.getConnection(config.db_file);
1013
1040
  try {
1014
1041
  const systemInfo = await getSystemInfo(db);
1015
1042
  if (systemInfo && systemInfo.modelName && systemInfo.modelDimensions) {
@@ -1040,7 +1067,8 @@ class RagLiteMCPServer {
1040
1067
  stats.total_chunks = chunkCount?.count || 0;
1041
1068
  }
1042
1069
  finally {
1043
- await db.close();
1070
+ // Release instead of close - keeps connection alive for reuse
1071
+ await DatabaseConnectionManager.releaseConnection(config.db_file);
1044
1072
  }
1045
1073
  }
1046
1074
  catch (error) {
@@ -1221,48 +1249,61 @@ class RagLiteMCPServer {
1221
1249
  const startTime = Date.now();
1222
1250
  const results = await this.searchEngine.search(args.query, searchOptions);
1223
1251
  const searchTime = Date.now() - startTime;
1224
- // Format results for MCP response with content type information and image data
1225
- const formattedResults = {
1252
+ // Format results for MCP response with proper image content support
1253
+ const textResults = {
1226
1254
  query: args.query,
1227
1255
  content_type_filter: args.content_type || 'all',
1228
1256
  results_count: results.length,
1229
1257
  search_time_ms: searchTime,
1230
- results: await Promise.all(results.map(async (result, index) => {
1231
- const formattedResult = {
1232
- rank: index + 1,
1233
- score: Math.round(result.score * 100) / 100,
1234
- content_type: result.contentType,
1235
- document: {
1236
- id: result.document.id,
1237
- title: result.document.title,
1238
- source: result.document.source,
1239
- content_type: result.document.contentType
1240
- },
1241
- text: result.content,
1242
- metadata: result.metadata
1243
- };
1244
- // For image content, include base64-encoded image data for MCP clients
1245
- if (result.contentType === 'image' && result.document.contentId) {
1246
- try {
1247
- const imageData = await this.searchEngine.getContent(result.document.contentId, 'base64');
1248
- formattedResult.image_data = imageData;
1249
- formattedResult.image_format = 'base64';
1250
- }
1251
- catch (error) {
1252
- // If image retrieval fails, include error but don't fail the entire search
1253
- formattedResult.image_error = error instanceof Error ? error.message : 'Failed to retrieve image';
1254
- }
1255
- }
1256
- return formattedResult;
1258
+ results: results.map((result, index) => ({
1259
+ rank: index + 1,
1260
+ score: Math.round(result.score * 100) / 100,
1261
+ content_type: result.contentType,
1262
+ document: {
1263
+ id: result.document.id,
1264
+ title: result.document.title,
1265
+ source: result.document.source,
1266
+ content_type: result.document.contentType
1267
+ },
1268
+ text: result.content,
1269
+ metadata: result.metadata,
1270
+ // Reference to image content if applicable
1271
+ has_image: result.contentType === 'image' && !!result.document.contentId
1257
1272
  }))
1258
1273
  };
1274
+ // Build MCP response content array
1275
+ const responseContent = [
1276
+ {
1277
+ type: 'text',
1278
+ text: JSON.stringify(textResults, null, 2)
1279
+ }
1280
+ ];
1281
+ // Add proper MCP image content for each image result
1282
+ for (const result of results) {
1283
+ if (result.contentType === 'image' && result.document.contentId) {
1284
+ try {
1285
+ const imageData = await this.searchEngine.getContent(result.document.contentId, 'base64');
1286
+ const mimeType = getMimeTypeFromPath(result.document.source);
1287
+ responseContent.push({
1288
+ type: 'image',
1289
+ data: imageData,
1290
+ mimeType: mimeType,
1291
+ annotations: {
1292
+ audience: ['user'],
1293
+ priority: 0.8,
1294
+ title: result.document.title,
1295
+ source: result.document.source
1296
+ }
1297
+ });
1298
+ }
1299
+ catch (error) {
1300
+ // If image retrieval fails, log but don't fail the entire search
1301
+ console.error(`Failed to retrieve image for ${result.document.source}:`, error);
1302
+ }
1303
+ }
1304
+ }
1259
1305
  return {
1260
- content: [
1261
- {
1262
- type: 'text',
1263
- text: JSON.stringify(formattedResults, null, 2),
1264
- },
1265
- ],
1306
+ content: responseContent
1266
1307
  };
1267
1308
  }
1268
1309
  catch (error) {
@@ -1416,23 +1457,6 @@ class RagLiteMCPServer {
1416
1457
  strategyInfo.accuracy = 'high';
1417
1458
  strategyInfo.use_cases = ['Mixed content with images', 'Visual documentation', 'Diagrams and charts'];
1418
1459
  break;
1419
- case 'metadata':
1420
- strategyInfo.description = 'Uses file metadata, filenames, and content properties for scoring without model inference';
1421
- strategyInfo.requirements = ['None - uses file system metadata only'];
1422
- strategyInfo.supported_content_types = ['text', 'image', 'pdf', 'docx'];
1423
- strategyInfo.performance_impact = 'low';
1424
- strategyInfo.accuracy = 'medium';
1425
- strategyInfo.use_cases = ['Fast retrieval', 'Filename-based search', 'Content type filtering'];
1426
- break;
1427
- case 'hybrid':
1428
- strategyInfo.description = 'Combines multiple reranking signals (semantic + metadata) with configurable weights';
1429
- strategyInfo.requirements = ['Text-derived reranker', 'Metadata reranker'];
1430
- strategyInfo.supported_content_types = ['text', 'image', 'pdf', 'docx'];
1431
- strategyInfo.performance_impact = 'high';
1432
- strategyInfo.accuracy = 'very high';
1433
- strategyInfo.use_cases = ['Best overall accuracy', 'Complex multimodal collections', 'Production systems'];
1434
- strategyInfo.default_weights = { semantic: 0.7, metadata: 0.3 };
1435
- break;
1436
1460
  case 'disabled':
1437
1461
  strategyInfo.description = 'No reranking applied - results ordered by vector similarity scores only';
1438
1462
  strategyInfo.requirements = ['None'];
@@ -1453,8 +1477,8 @@ class RagLiteMCPServer {
1453
1477
  strategies_by_mode: strategiesByMode,
1454
1478
  recommendations: {
1455
1479
  text_mode: 'Use cross-encoder for best accuracy, disabled for best performance',
1456
- multimodal_mode: 'Use hybrid for best accuracy, text-derived for good balance, metadata for fast retrieval',
1457
- development: 'Start with disabled or metadata for fast iteration, upgrade to cross-encoder/text-derived for production'
1480
+ multimodal_mode: 'Use text-derived for best accuracy, disabled for best performance',
1481
+ development: 'Start with disabled for fast iteration, upgrade to cross-encoder/text-derived for production'
1458
1482
  }
1459
1483
  };
1460
1484
  return {
@@ -1500,19 +1524,19 @@ class RagLiteMCPServer {
1500
1524
  };
1501
1525
  // Add content breakdown if requested
1502
1526
  if (args.include_content_breakdown) {
1503
- const db = await openDatabase(config.db_file);
1527
+ const db = await DatabaseConnectionManager.getConnection(config.db_file);
1504
1528
  try {
1505
1529
  // Get document count by content type
1506
- const docsByType = await db.all(`
1507
- SELECT content_type, COUNT(*) as count
1508
- FROM documents
1509
- GROUP BY content_type
1530
+ const docsByType = await db.all(`
1531
+ SELECT content_type, COUNT(*) as count
1532
+ FROM documents
1533
+ GROUP BY content_type
1510
1534
  `);
1511
1535
  // Get chunk count by content type
1512
- const chunksByType = await db.all(`
1513
- SELECT content_type, COUNT(*) as count
1514
- FROM chunks
1515
- GROUP BY content_type
1536
+ const chunksByType = await db.all(`
1537
+ SELECT content_type, COUNT(*) as count
1538
+ FROM chunks
1539
+ GROUP BY content_type
1516
1540
  `);
1517
1541
  enhancedStats.content_breakdown = {
1518
1542
  documents_by_type: docsByType.reduce((acc, row) => {
@@ -1526,7 +1550,8 @@ class RagLiteMCPServer {
1526
1550
  };
1527
1551
  }
1528
1552
  finally {
1529
- await db.close();
1553
+ // Release instead of close - keeps connection alive for reuse
1554
+ await DatabaseConnectionManager.releaseConnection(config.db_file);
1530
1555
  }
1531
1556
  }
1532
1557
  // Add performance metrics if requested
@@ -1600,6 +1625,8 @@ class RagLiteMCPServer {
1600
1625
  this.isSearchEngineInitialized = true;
1601
1626
  }
1602
1627
  catch (error) {
1628
+ console.error('❌ MCP Server: Search engine initialization failed');
1629
+ console.error(`❌ Error: ${error instanceof Error ? error.message : String(error)}`);
1603
1630
  // Check if this is a mode detection error
1604
1631
  if (error instanceof Error && error.message.includes('mode detection')) {
1605
1632
  console.error('⚠️ MCP Server: Mode detection failed, falling back to text mode');
@@ -1612,7 +1639,6 @@ class RagLiteMCPServer {
1612
1639
  throw error;
1613
1640
  }
1614
1641
  // For other initialization errors, provide a generic wrapper
1615
- console.error('❌ MCP Server: Search engine initialization failed');
1616
1642
  throw new Error(`Failed to initialize search engine: ${error instanceof Error ? error.message : 'Unknown error'}`);
1617
1643
  }
1618
1644
  }
@@ -1635,8 +1661,8 @@ class RagLiteMCPServer {
1635
1661
  const { ModeDetectionService } = await import('./core/mode-detection-service.js');
1636
1662
  const modeService = new ModeDetectionService(config.db_file);
1637
1663
  const systemInfo = await modeService.detectMode();
1638
- // Check if database has any images
1639
- const db = await openDatabase(config.db_file);
1664
+ // Check if database has any images - use shared connection
1665
+ const db = await DatabaseConnectionManager.getConnection(config.db_file);
1640
1666
  let hasImages = false;
1641
1667
  let documentCount = 0;
1642
1668
  try {
@@ -1646,7 +1672,8 @@ class RagLiteMCPServer {
1646
1672
  documentCount = docCount?.count || 0;
1647
1673
  }
1648
1674
  finally {
1649
- await db.close();
1675
+ // Release instead of close - keeps connection alive for reuse
1676
+ await DatabaseConnectionManager.releaseConnection(config.db_file);
1650
1677
  }
1651
1678
  return {
1652
1679
  mode: systemInfo.mode,
@@ -338,76 +338,73 @@ export class CLIPEmbedder extends BaseUniversalEmbedder {
338
338
  if (!this.textModel || !this.tokenizer) {
339
339
  throw new Error('CLIP text model or tokenizer not initialized');
340
340
  }
341
- try {
342
- // Use the validated CLIPTextModelWithProjection approach (no pixel_values errors)
343
- // Tokenize text with CLIP's requirements
344
- // The tokenizer handles truncation at 77 TOKENS (not characters)
345
- const tokens = await this.tokenizer(processedText, {
346
- padding: true,
347
- truncation: true,
348
- max_length: 77, // CLIP's text sequence length limit (77 tokens)
349
- return_tensors: 'pt'
350
- });
351
- // Log token information for debugging (only in development)
352
- if (process.env.NODE_ENV === 'development') {
353
- const tokenIds = tokens.input_ids?.data || [];
354
- const actualTokenCount = Array.from(tokenIds).filter((id) => id !== 0).length;
355
- if (actualTokenCount >= 77) {
356
- console.warn(`Text truncated by tokenizer: "${processedText.substring(0, 50)}..." (truncated to 77 tokens)`);
357
- }
358
- }
359
- // Generate text embedding using CLIPTextModelWithProjection
360
- const output = await this.textModel(tokens);
361
- // Extract embedding from text_embeds (no pixel_values dependency)
362
- const embedding = new Float32Array(output.text_embeds.data);
363
- // Validate embedding dimensions and values
364
- if (embedding.length !== this.dimensions) {
365
- throw new Error(`CLIP embedding dimension mismatch: expected ${this.dimensions}, got ${embedding.length}`);
366
- }
367
- // Validate that all values are finite numbers
368
- const invalidValues = Array.from(embedding).filter(val => !isFinite(val) || isNaN(val));
369
- if (invalidValues.length > 0) {
370
- throw new Error(`CLIP embedding contains ${invalidValues.length} invalid values`);
371
- }
372
- // Validate embedding quality - should not be all zeros
373
- const nonZeroValues = Array.from(embedding).filter(val => Math.abs(val) > 1e-8);
374
- if (nonZeroValues.length === 0) {
375
- throw new Error('CLIP embedding is all zeros');
376
- }
377
- // Calculate embedding magnitude before normalization for quality assessment
378
- const magnitudeBeforeNorm = Math.sqrt(Array.from(embedding).reduce((sum, val) => sum + val * val, 0));
379
- if (magnitudeBeforeNorm < 1e-6) {
380
- throw new Error(`CLIP embedding has critically low magnitude: ${magnitudeBeforeNorm.toExponential(3)}`);
381
- }
382
- // Apply L2-normalization (CLIP models are trained with normalized embeddings)
383
- this.normalizeEmbedding(embedding);
384
- // Verify normalization was successful
385
- const magnitudeAfterNorm = Math.sqrt(Array.from(embedding).reduce((sum, val) => sum + val * val, 0));
386
- if (Math.abs(magnitudeAfterNorm - 1.0) > 0.01) {
387
- console.warn(`Warning: Embedding normalization may be imprecise (magnitude: ${magnitudeAfterNorm.toFixed(6)})`);
341
+ // Use the validated CLIPTextModelWithProjection approach (no pixel_values errors)
342
+ // Tokenize text with CLIP's requirements
343
+ // The tokenizer handles truncation at 77 TOKENS (not characters)
344
+ const tokens = await this.tokenizer(processedText, {
345
+ padding: true,
346
+ truncation: true,
347
+ max_length: 77, // CLIP's text sequence length limit (77 tokens)
348
+ return_tensors: 'pt'
349
+ });
350
+ // Log token information for debugging (only in development)
351
+ if (process.env.NODE_ENV === 'development') {
352
+ const tokenIds = tokens.input_ids?.data || [];
353
+ const actualTokenCount = Array.from(tokenIds).filter((id) => id !== 0).length;
354
+ if (actualTokenCount >= 77) {
355
+ console.warn(`Text truncated by tokenizer: "${processedText.substring(0, 50)}..." (truncated to 77 tokens)`);
356
+ }
357
+ }
358
+ // Generate text embedding using CLIPTextModelWithProjection
359
+ const output = await this.textModel(tokens);
360
+ // Extract embedding from text_embeds (no pixel_values dependency)
361
+ const embedding = new Float32Array(output.text_embeds.data);
362
+ // Validate embedding dimensions and values
363
+ if (embedding.length !== this.dimensions) {
364
+ throw new Error(`CLIP embedding dimension mismatch: expected ${this.dimensions}, got ${embedding.length}`);
365
+ }
366
+ // Validate that all values are finite numbers
367
+ const invalidValues = Array.from(embedding).filter(val => !isFinite(val) || isNaN(val));
368
+ if (invalidValues.length > 0) {
369
+ throw new Error(`CLIP embedding contains ${invalidValues.length} invalid values`);
370
+ }
371
+ // Validate embedding quality - should not be all zeros
372
+ const nonZeroValues = Array.from(embedding).filter(val => Math.abs(val) > 1e-8);
373
+ if (nonZeroValues.length === 0) {
374
+ throw new Error('CLIP embedding is all zeros');
375
+ }
376
+ // Calculate embedding magnitude before normalization for quality assessment
377
+ const magnitudeBeforeNorm = Math.sqrt(Array.from(embedding).reduce((sum, val) => sum + val * val, 0));
378
+ if (magnitudeBeforeNorm < 1e-6) {
379
+ throw new Error(`CLIP embedding has critically low magnitude: ${magnitudeBeforeNorm.toExponential(3)}`);
380
+ }
381
+ // Apply L2-normalization (CLIP models are trained with normalized embeddings)
382
+ this.normalizeEmbedding(embedding);
383
+ // Verify normalization was successful
384
+ const magnitudeAfterNorm = Math.sqrt(Array.from(embedding).reduce((sum, val) => sum + val * val, 0));
385
+ if (Math.abs(magnitudeAfterNorm - 1.0) > 0.01) {
386
+ console.warn(`Warning: Embedding normalization may be imprecise (magnitude: ${magnitudeAfterNorm.toFixed(6)})`);
387
+ }
388
+ // Log text embedding generation
389
+ console.log(`[CLIP] Generated text embedding for: "${processedText.substring(0, 30)}${processedText.length > 30 ? '...' : ''}"`);
390
+ // Generate unique embedding ID
391
+ const embeddingId = this.generateEmbeddingId(processedText, 'text');
392
+ return {
393
+ embedding_id: embeddingId,
394
+ vector: embedding,
395
+ contentType: 'text',
396
+ metadata: {
397
+ originalText: text,
398
+ processedText: processedText,
399
+ textLength: processedText.length,
400
+ embeddingMagnitudeBeforeNorm: magnitudeBeforeNorm,
401
+ embeddingMagnitudeAfterNorm: magnitudeAfterNorm,
402
+ normalized: true,
403
+ modelName: this.modelName,
404
+ modelType: this.modelType,
405
+ dimensions: this.dimensions
388
406
  }
389
- // Generate unique embedding ID
390
- const embeddingId = this.generateEmbeddingId(processedText, 'text');
391
- return {
392
- embedding_id: embeddingId,
393
- vector: embedding,
394
- contentType: 'text',
395
- metadata: {
396
- originalText: text,
397
- processedText: processedText,
398
- textLength: processedText.length,
399
- embeddingMagnitudeBeforeNorm: magnitudeBeforeNorm,
400
- embeddingMagnitudeAfterNorm: magnitudeAfterNorm,
401
- normalized: true,
402
- modelName: this.modelName,
403
- modelType: this.modelType,
404
- dimensions: this.dimensions
405
- }
406
- };
407
- }
408
- catch (error) {
409
- throw error;
410
- }
407
+ };
411
408
  }
412
409
  // =============================================================================
413
410
  // IMAGE EMBEDDING METHODS
@@ -602,9 +599,11 @@ export class CLIPEmbedder extends BaseUniversalEmbedder {
602
599
  const absolutePath = path.resolve(imagePath);
603
600
  // Try to use Sharp for better Node.js support
604
601
  try {
605
- const sharp = await import('sharp');
602
+ const sharpModule = await import('sharp');
603
+ const sharp = sharpModule.default;
604
+ sharp.concurrency(2);
606
605
  // Use Sharp to load and get raw pixel data
607
- const { data, info } = await sharp.default(absolutePath)
606
+ const { data, info } = await sharp(absolutePath)
608
607
  .resize(variant.imageSize, variant.imageSize, {
609
608
  fit: 'cover',
610
609
  position: 'center'