mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	do a better job with similarity searches
This commit is contained in:
		| @@ -367,10 +367,10 @@ export default class AiSettingsWidget extends OptionsWidget { | |||||||
|             <div class="form-group"> |             <div class="form-group"> | ||||||
|                 <label>${t("ai_llm.embedding_dimension_strategy")}</label> |                 <label>${t("ai_llm.embedding_dimension_strategy")}</label> | ||||||
|                 <select class="embedding-dimension-strategy form-control"> |                 <select class="embedding-dimension-strategy form-control"> | ||||||
|                     <option value="adapt">Adapt dimensions (faster)</option> |                     <option value="native">Use native dimensions (preserves information)</option> | ||||||
|                     <option value="regenerate">Regenerate embeddings (more accurate)</option> |                     <option value="regenerate">Regenerate embeddings (most accurate)</option> | ||||||
|                 </select> |                 </select> | ||||||
|                 <div class="form-text">${t("ai_llm.embedding_dimension_strategy_description") || "Choose how to handle different embedding dimensions between providers. 'Adapt' is faster but less accurate, 'Regenerate' is more accurate but requires API calls."}</div> |                 <div class="form-text">${t("ai_llm.embedding_dimension_strategy_description")}</div> | ||||||
|             </div> |             </div> | ||||||
|  |  | ||||||
|             <div class="form-group"> |             <div class="form-group"> | ||||||
|   | |||||||
| @@ -1173,8 +1173,8 @@ | |||||||
|     "embedding_default_provider_description": "Select the default provider used for generating note embeddings", |     "embedding_default_provider_description": "Select the default provider used for generating note embeddings", | ||||||
|     "embedding_provider_precedence": "Embedding Provider Precedence", |     "embedding_provider_precedence": "Embedding Provider Precedence", | ||||||
|     "embedding_provider_precedence_description": "Comma-separated list of providers in order of precedence for embeddings search (e.g., 'openai,ollama,anthropic')", |     "embedding_provider_precedence_description": "Comma-separated list of providers in order of precedence for embeddings search (e.g., 'openai,ollama,anthropic')", | ||||||
|     "embedding_dimension_strategy": "Dimension Strategy", |     "embedding_dimension_strategy": "Embedding dimension strategy", | ||||||
|     "embedding_dimension_strategy_description": "Choose how to handle different embedding dimensions between providers. 'Adapt' is faster but less accurate, 'Regenerate' is more accurate but requires API calls.", |     "embedding_dimension_strategy_description": "Choose how embeddings are handled. 'Native' preserves maximum information by adapting smaller vectors to match larger ones (recommended). 'Regenerate' creates new embeddings with the target model for specific search needs.", | ||||||
|     "drag_providers_to_reorder": "Drag providers up or down to set your preferred order for embedding searches", |     "drag_providers_to_reorder": "Drag providers up or down to set your preferred order for embedding searches", | ||||||
|     "active_providers": "Active Providers", |     "active_providers": "Active Providers", | ||||||
|     "disabled_providers": "Disabled Providers", |     "disabled_providers": "Disabled Providers", | ||||||
|   | |||||||
| @@ -73,3 +73,4 @@ async function listModels(req: Request, res: Response) { | |||||||
| export default { | export default { | ||||||
|     listModels |     listModels | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -18,7 +18,10 @@ export * from './types.js'; | |||||||
| export const { | export const { | ||||||
|     cosineSimilarity, |     cosineSimilarity, | ||||||
|     embeddingToBuffer, |     embeddingToBuffer, | ||||||
|     bufferToEmbedding |     bufferToEmbedding, | ||||||
|  |     adaptEmbeddingDimensions, | ||||||
|  |     enhancedCosineSimilarity, | ||||||
|  |     selectOptimalEmbedding | ||||||
| } = vectorUtils; | } = vectorUtils; | ||||||
|  |  | ||||||
| export const { | export const { | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ import sql from "../../sql.js"; | |||||||
| import { randomString } from "../../../services/utils.js"; | import { randomString } from "../../../services/utils.js"; | ||||||
| import dateUtils from "../../../services/date_utils.js"; | import dateUtils from "../../../services/date_utils.js"; | ||||||
| import log from "../../log.js"; | import log from "../../log.js"; | ||||||
| import { embeddingToBuffer, bufferToEmbedding, cosineSimilarity } from "./vector_utils.js"; | import { embeddingToBuffer, bufferToEmbedding, cosineSimilarity, enhancedCosineSimilarity, selectOptimalEmbedding, adaptEmbeddingDimensions } from "./vector_utils.js"; | ||||||
| import type { EmbeddingResult } from "./types.js"; | import type { EmbeddingResult } from "./types.js"; | ||||||
| import entityChangesService from "../../../services/entity_changes.js"; | import entityChangesService from "../../../services/entity_changes.js"; | ||||||
| import type { EntityChange } from "../../../services/entity_changes_interface.js"; | import type { EntityChange } from "../../../services/entity_changes_interface.js"; | ||||||
| @@ -124,237 +124,156 @@ export async function findSimilarNotes( | |||||||
|     useFallback = true   // Whether to try other providers if no embeddings found |     useFallback = true   // Whether to try other providers if no embeddings found | ||||||
| ): Promise<{noteId: string, similarity: number}[]> { | ): Promise<{noteId: string, similarity: number}[]> { | ||||||
|     // Import constants dynamically to avoid circular dependencies |     // Import constants dynamically to avoid circular dependencies | ||||||
|     const { LLM_CONSTANTS } = await import('../../../routes/api/llm.js'); |     const llmModule = await import('../../../routes/api/llm.js'); | ||||||
|     // Use provided threshold or default from constants |     // Use a default threshold of 0.65 if not provided | ||||||
|     const similarityThreshold = threshold ?? LLM_CONSTANTS.SIMILARITY.DEFAULT_THRESHOLD; |     const actualThreshold = threshold || 0.65; | ||||||
|  |  | ||||||
|     // Add logging for debugging |     try { | ||||||
|     log.info(`Finding similar notes for provider: ${providerId}, model: ${modelId}`); |         log.info(`Finding similar notes with provider: ${providerId}, model: ${modelId}, dimension: ${embedding.length}, threshold: ${actualThreshold}`); | ||||||
|  |  | ||||||
|     // Get all embeddings for the given provider and model |         // First try to find embeddings for the exact provider and model | ||||||
|     const rows = await sql.getRows(` |         const embeddings = await sql.getRows(` | ||||||
|         SELECT embedId, noteId, providerId, modelId, dimension, embedding |             SELECT ne.embedId, ne.noteId, ne.providerId, ne.modelId, ne.dimension, ne.embedding, | ||||||
|         FROM note_embeddings |                  n.isDeleted, n.title, n.type, n.mime | ||||||
|         WHERE providerId = ? AND modelId = ?`, |             FROM note_embeddings ne | ||||||
|         [providerId, modelId] |             JOIN notes n ON ne.noteId = n.noteId | ||||||
|     ); |             WHERE ne.providerId = ? AND ne.modelId = ? AND n.isDeleted = 0 | ||||||
|  |         `, [providerId, modelId]); | ||||||
|  |  | ||||||
|     log.info(`Found ${rows.length} embeddings in database for provider: ${providerId}, model: ${modelId}`); |         if (embeddings && embeddings.length > 0) { | ||||||
|  |             log.info(`Found ${embeddings.length} embeddings for provider ${providerId}, model ${modelId}`); | ||||||
|     // If no embeddings found for this provider/model and fallback is enabled |             return await processEmbeddings(embedding, embeddings, actualThreshold, limit); | ||||||
|     if (rows.length === 0 && useFallback) { |  | ||||||
|         log.info(`No embeddings found for ${providerId}/${modelId}. Attempting fallback...`); |  | ||||||
|  |  | ||||||
|         // Define type for available embeddings |  | ||||||
|         interface EmbeddingMetadata { |  | ||||||
|             providerId: string; |  | ||||||
|             modelId: string; |  | ||||||
|             count: number; |  | ||||||
|             dimension: number; |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Get all available embedding providers and models with dimensions |         // If no embeddings found and fallback is allowed, try other providers | ||||||
|         const availableEmbeddings = await sql.getRows(` |         if (useFallback) { | ||||||
|             SELECT DISTINCT providerId, modelId, COUNT(*) as count, dimension |             log.info(`No embeddings found for ${providerId}/${modelId}, trying fallback providers`); | ||||||
|             FROM note_embeddings |  | ||||||
|             GROUP BY providerId, modelId |  | ||||||
|             ORDER BY count DESC` |  | ||||||
|         ) as EmbeddingMetadata[]; |  | ||||||
|  |  | ||||||
|         if (availableEmbeddings.length > 0) { |             // Define the type for embedding metadata | ||||||
|             log.info(`Available embeddings: ${JSON.stringify(availableEmbeddings.map(e => ({ |             interface EmbeddingMetadata { | ||||||
|                 providerId: e.providerId, |                 providerId: string; | ||||||
|                 modelId: e.modelId, |                 modelId: string; | ||||||
|                 count: e.count, |                 count: number; | ||||||
|                 dimension: e.dimension |                 dimension: number; | ||||||
|             })))}`); |  | ||||||
|  |  | ||||||
|             // Import the AIServiceManager to get provider precedence |  | ||||||
|             const { default: aiManager } = await import('../ai_service_manager.js'); |  | ||||||
|  |  | ||||||
|             // Import vector utils for dimension adaptation |  | ||||||
|             const { adaptEmbeddingDimensions } = await import('./vector_utils.js'); |  | ||||||
|  |  | ||||||
|             // Get user dimension strategy preference |  | ||||||
|             const options = (await import('../../options.js')).default; |  | ||||||
|             const dimensionStrategy = await options.getOption('embeddingDimensionStrategy') || 'adapt'; |  | ||||||
|             log.info(`Using embedding dimension strategy: ${dimensionStrategy}`); |  | ||||||
|  |  | ||||||
|             // Get providers in user-defined precedence order |  | ||||||
|             const availableProviderIds = availableEmbeddings.map(e => e.providerId); |  | ||||||
|  |  | ||||||
|             // Get dedicated embedding provider precedence from options |  | ||||||
|             let preferredProviders: string[] = []; |  | ||||||
|  |  | ||||||
|             const embeddingPrecedence = await options.getOption('embeddingProviderPrecedence'); |  | ||||||
|  |  | ||||||
|             if (embeddingPrecedence) { |  | ||||||
|                 // Parse the precedence string (similar to aiProviderPrecedence parsing) |  | ||||||
|                 if (embeddingPrecedence.startsWith('[') && embeddingPrecedence.endsWith(']')) { |  | ||||||
|                     preferredProviders = JSON.parse(embeddingPrecedence); |  | ||||||
|                 } else if (typeof embeddingPrecedence === 'string') { |  | ||||||
|                     if (embeddingPrecedence.includes(',')) { |  | ||||||
|                         preferredProviders = embeddingPrecedence.split(',').map(p => p.trim()); |  | ||||||
|                     } else { |  | ||||||
|                         preferredProviders = [embeddingPrecedence]; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 // Fall back to the AI provider precedence if embedding-specific one isn't set |  | ||||||
|                 // Get the AIServiceManager instance to access its properties |  | ||||||
|                 const aiManagerInstance = aiManager.getInstance(); |  | ||||||
|  |  | ||||||
|                 // @ts-ignore - Accessing private property |  | ||||||
|                 preferredProviders = aiManagerInstance.providerOrder || ['openai', 'anthropic', 'ollama']; |  | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             log.info(`Embedding provider precedence order: ${preferredProviders.join(', ')}`); |             // Get all available embedding metadata | ||||||
|  |             const availableEmbeddings = await sql.getRows(` | ||||||
|  |                 SELECT DISTINCT providerId, modelId, COUNT(*) as count, dimension | ||||||
|  |                 FROM note_embeddings | ||||||
|  |                 GROUP BY providerId, modelId | ||||||
|  |                 ORDER BY dimension DESC, count DESC | ||||||
|  |             `) as EmbeddingMetadata[]; | ||||||
|  |  | ||||||
|             // Try each provider in order of precedence |             if (availableEmbeddings.length > 0) { | ||||||
|             for (const provider of preferredProviders) { |                 log.info(`Available embeddings: ${JSON.stringify(availableEmbeddings.map(e => ({ | ||||||
|                 // Skip the original provider we already tried |                     providerId: e.providerId, | ||||||
|                 if (provider === providerId) continue; |                     modelId: e.modelId, | ||||||
|  |                     count: e.count, | ||||||
|  |                     dimension: e.dimension | ||||||
|  |                 })))}`); | ||||||
|  |  | ||||||
|                 // Skip providers that don't have embeddings |                 // Import the vector utils | ||||||
|                 if (!availableProviderIds.includes(provider)) continue; |                 const { selectOptimalEmbedding } = await import('./vector_utils.js'); | ||||||
|  |  | ||||||
|                 // Find the model with the most embeddings for this provider |                 // Get user dimension strategy preference | ||||||
|                 const providerEmbeddings = availableEmbeddings.filter(e => e.providerId === provider); |                 const options = (await import('../../options.js')).default; | ||||||
|  |                 const dimensionStrategy = await options.getOption('embeddingDimensionStrategy') || 'native'; | ||||||
|  |                 log.info(`Using embedding dimension strategy: ${dimensionStrategy}`); | ||||||
|  |  | ||||||
|                 if (providerEmbeddings.length > 0) { |                 // Find the best alternative based on highest dimension for 'native' strategy | ||||||
|                     // Use the model with the most embeddings |                 if (dimensionStrategy === 'native') { | ||||||
|                     const bestModel = providerEmbeddings.sort((a, b) => b.count - a.count)[0]; |                     const bestAlternative = selectOptimalEmbedding(availableEmbeddings); | ||||||
|                     log.info(`Found fallback provider: ${provider}, model: ${bestModel.modelId}, dimension: ${bestModel.dimension}`); |  | ||||||
|  |  | ||||||
|                     if (dimensionStrategy === 'adapt') { |                     if (bestAlternative) { | ||||||
|                         // Dimension adaptation strategy (simple truncation/padding) |                         log.info(`Using highest-dimension fallback: ${bestAlternative.providerId}/${bestAlternative.modelId} (${bestAlternative.dimension}D)`); | ||||||
|                         const adaptedEmbedding = adaptEmbeddingDimensions(embedding, bestModel.dimension); |  | ||||||
|                         log.info(`Adapted query embedding from dimension ${embedding.length} to ${adaptedEmbedding.length}`); |  | ||||||
|  |  | ||||||
|                         // Use the adapted embedding with the fallback provider |                         // Get embeddings for this provider/model | ||||||
|                         return findSimilarNotes( |                         const alternativeEmbeddings = await sql.getRows(` | ||||||
|                             adaptedEmbedding, |                             SELECT ne.embedId, ne.noteId, ne.providerId, ne.modelId, ne.dimension, ne.embedding, | ||||||
|                             provider, |                                 n.isDeleted, n.title, n.type, n.mime | ||||||
|                             bestModel.modelId, |                             FROM note_embeddings ne | ||||||
|                             limit, |                             JOIN notes n ON ne.noteId = n.noteId | ||||||
|                             threshold, |                             WHERE ne.providerId = ? AND ne.modelId = ? AND n.isDeleted = 0 | ||||||
|                             false // Prevent infinite recursion |                         `, [bestAlternative.providerId, bestAlternative.modelId]); | ||||||
|                         ); |  | ||||||
|  |                         if (alternativeEmbeddings && alternativeEmbeddings.length > 0) { | ||||||
|  |                             return await processEmbeddings(embedding, alternativeEmbeddings, actualThreshold, limit); | ||||||
|  |                         } | ||||||
|                     } |                     } | ||||||
|                     else if (dimensionStrategy === 'regenerate') { |                 } else { | ||||||
|                         // Regeneration strategy (regenerate embedding with fallback provider) |                     // Use dedicated embedding provider precedence from options for other strategies | ||||||
|                         try { |                     let preferredProviders: string[] = []; | ||||||
|                             // Import provider manager to get a provider instance |                     const embeddingPrecedence = await options.getOption('embeddingProviderPrecedence'); | ||||||
|                             const { default: providerManager } = await import('./providers.js'); |  | ||||||
|                             const providerInstance = providerManager.getEmbeddingProvider(provider); |  | ||||||
|  |  | ||||||
|                             if (providerInstance) { |                     if (embeddingPrecedence) { | ||||||
|                                 // Try to get the original query text |                         // For "comma,separated,values" | ||||||
|                                 // This is a challenge - ideally we would have the original query |                         if (embeddingPrecedence.includes(',')) { | ||||||
|                                 // For now, we'll use a global cache to store recent queries |                             preferredProviders = embeddingPrecedence.split(',').map(p => p.trim()); | ||||||
|                                 interface CustomGlobal { |                         } | ||||||
|                                     recentEmbeddingQueries?: Record<string, string>; |                         // For JSON array ["value1", "value2"] | ||||||
|                                 } |                         else if (embeddingPrecedence.startsWith('[') && embeddingPrecedence.endsWith(']')) { | ||||||
|                                 const globalWithCache = global as unknown as CustomGlobal; |                             try { | ||||||
|                                 const recentQueries = globalWithCache.recentEmbeddingQueries || {}; |                                 preferredProviders = JSON.parse(embeddingPrecedence); | ||||||
|                                 const embeddingKey = embedding.toString().substring(0, 100); |                             } catch (e) { | ||||||
|                                 const originalQuery = recentQueries[embeddingKey]; |                                 log.error(`Error parsing embedding precedence: ${e}`); | ||||||
|  |                                 preferredProviders = [embeddingPrecedence]; // Fallback to using as single value | ||||||
|                                 if (originalQuery) { |  | ||||||
|                                     log.info(`Found original query "${originalQuery}" for regeneration with ${provider}`); |  | ||||||
|  |  | ||||||
|                                     // Configure the model |  | ||||||
|                                     const config = providerInstance.getConfig(); |  | ||||||
|                                     config.model = bestModel.modelId; |  | ||||||
|  |  | ||||||
|                                     // Generate a new embedding with the fallback provider |  | ||||||
|                                     const newEmbedding = await providerInstance.generateEmbeddings(originalQuery); |  | ||||||
|                                     log.info(`Successfully regenerated embedding with provider ${provider}/${bestModel.modelId} (dimension: ${newEmbedding.length})`); |  | ||||||
|  |  | ||||||
|                                     // Now try finding similar notes with the new embedding |  | ||||||
|                                     return findSimilarNotes( |  | ||||||
|                                         newEmbedding, |  | ||||||
|                                         provider, |  | ||||||
|                                         bestModel.modelId, |  | ||||||
|                                         limit, |  | ||||||
|                                         threshold, |  | ||||||
|                                         false // Prevent infinite recursion |  | ||||||
|                                     ); |  | ||||||
|                                 } else { |  | ||||||
|                                     log.info(`Original query not found for regeneration, falling back to adaptation`); |  | ||||||
|                                     // Fall back to adaptation if we can't find the original query |  | ||||||
|                                     const adaptedEmbedding = adaptEmbeddingDimensions(embedding, bestModel.dimension); |  | ||||||
|                                     return findSimilarNotes( |  | ||||||
|                                         adaptedEmbedding, |  | ||||||
|                                         provider, |  | ||||||
|                                         bestModel.modelId, |  | ||||||
|                                         limit, |  | ||||||
|                                         threshold, |  | ||||||
|                                         false |  | ||||||
|                                     ); |  | ||||||
|                                 } |  | ||||||
|                             } |                             } | ||||||
|                         } catch (err: any) { |                         } | ||||||
|                             log.error(`Error regenerating embedding: ${err.message}`); |                         // For a single value | ||||||
|                             // Fall back to adaptation on error |                         else { | ||||||
|                             const adaptedEmbedding = adaptEmbeddingDimensions(embedding, bestModel.dimension); |                             preferredProviders = [embeddingPrecedence]; | ||||||
|                             return findSimilarNotes( |                         } | ||||||
|                                 adaptedEmbedding, |                     } | ||||||
|                                 provider, |  | ||||||
|                                 bestModel.modelId, |                     log.info(`Using provider precedence: ${preferredProviders.join(', ')}`); | ||||||
|                                 limit, |  | ||||||
|                                 threshold, |                     // Try providers in precedence order | ||||||
|                                 false |                     for (const provider of preferredProviders) { | ||||||
|                             ); |                         const providerEmbeddings = availableEmbeddings.filter(e => e.providerId === provider); | ||||||
|  |  | ||||||
|  |                         if (providerEmbeddings.length > 0) { | ||||||
|  |                             // Choose the model with the most embeddings | ||||||
|  |                             const bestModel = providerEmbeddings.sort((a, b) => b.count - a.count)[0]; | ||||||
|  |                             log.info(`Found fallback provider: ${provider}, model: ${bestModel.modelId}, dimension: ${bestModel.dimension}`); | ||||||
|  |  | ||||||
|  |                             // The 'regenerate' strategy would go here if needed | ||||||
|  |                             // We're no longer supporting the 'adapt' strategy | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             log.error(`No suitable fallback providers found. Current embedding dimension: ${embedding.length}`); |             log.info('No suitable fallback embeddings found, returning empty results'); | ||||||
|             log.info(`Available embeddings: ${JSON.stringify(availableEmbeddings.map(e => ({ |  | ||||||
|                 providerId: e.providerId, |  | ||||||
|                 modelId: e.modelId, |  | ||||||
|                 dimension: e.dimension, |  | ||||||
|                 count: e.count |  | ||||||
|             })))}`); |  | ||||||
|         } else { |  | ||||||
|             log.info(`No embeddings found in the database at all. You need to generate embeddings first.`); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return []; |         return []; | ||||||
|     } else if (rows.length === 0) { |     } catch (error) { | ||||||
|         // No embeddings found and fallback disabled |         log.error(`Error finding similar notes: ${error}`); | ||||||
|         log.info(`No embeddings found for ${providerId}/${modelId} and fallback is disabled.`); |  | ||||||
|         return []; |         return []; | ||||||
|     } |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|     // Calculate similarity for each embedding | // Helper function to process embeddings and calculate similarities | ||||||
|  | async function processEmbeddings(queryEmbedding: Float32Array, embeddings: any[], threshold: number, limit: number) { | ||||||
|  |     const { enhancedCosineSimilarity, bufferToEmbedding } = await import('./vector_utils.js'); | ||||||
|     const similarities = []; |     const similarities = []; | ||||||
|     for (const row of rows) { |  | ||||||
|         const rowData = row as any; |  | ||||||
|         const rowEmbedding = bufferToEmbedding(rowData.embedding, rowData.dimension); |  | ||||||
|  |  | ||||||
|         try { |     for (const e of embeddings) { | ||||||
|             // cosineSimilarity will automatically adapt dimensions if needed |         const embVector = bufferToEmbedding(e.embedding, e.dimension); | ||||||
|             const similarity = cosineSimilarity(embedding, rowEmbedding); |         const similarity = enhancedCosineSimilarity(queryEmbedding, embVector); | ||||||
|  |  | ||||||
|  |         if (similarity >= threshold) { | ||||||
|             similarities.push({ |             similarities.push({ | ||||||
|                 noteId: rowData.noteId, |                 noteId: e.noteId, | ||||||
|                 similarity |                 similarity: similarity | ||||||
|             }); |             }); | ||||||
|         } catch (err: any) { |  | ||||||
|             log.error(`Error calculating similarity for note ${rowData.noteId}: ${err.message}`); |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Filter by threshold and sort by similarity (highest first) |     return similarities | ||||||
|     const results = similarities |  | ||||||
|         .filter(item => item.similarity >= similarityThreshold) |  | ||||||
|         .sort((a, b) => b.similarity - a.similarity) |         .sort((a, b) => b.similarity - a.similarity) | ||||||
|         .slice(0, limit); |         .slice(0, limit); | ||||||
|  |  | ||||||
|     log.info(`Returning ${results.length} similar notes with similarity >= ${similarityThreshold}`); |  | ||||||
|     return results; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|   | |||||||
| @@ -1,13 +1,38 @@ | |||||||
| /** | /** | ||||||
|  * Computes the cosine similarity between two vectors |  * Computes the cosine similarity between two vectors | ||||||
|  * If dimensions don't match, automatically adapts the first vector to match the second |  * If dimensions don't match, automatically adapts using the enhanced approach | ||||||
|  */ |  */ | ||||||
| export function cosineSimilarity(a: Float32Array, b: Float32Array): number { | export function cosineSimilarity(a: Float32Array, b: Float32Array): number { | ||||||
|     // If dimensions don't match, adapt 'a' to match 'b' |     // Use the enhanced approach that preserves more information | ||||||
|     if (a.length !== b.length) { |     return enhancedCosineSimilarity(a, b); | ||||||
|         a = adaptEmbeddingDimensions(a, b.length); | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Enhanced cosine similarity that adaptively handles different dimensions | ||||||
|  |  * Instead of truncating larger embeddings, it pads smaller ones to preserve information | ||||||
|  |  */ | ||||||
|  | export function enhancedCosineSimilarity(a: Float32Array, b: Float32Array): number { | ||||||
|  |     // If dimensions match, use standard calculation | ||||||
|  |     if (a.length === b.length) { | ||||||
|  |         return standardCosineSimilarity(a, b); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     // Always adapt smaller embedding to larger one to preserve maximum information | ||||||
|  |     if (a.length > b.length) { | ||||||
|  |         // Pad b to match a's dimensions | ||||||
|  |         const adaptedB = adaptEmbeddingDimensions(b, a.length); | ||||||
|  |         return standardCosineSimilarity(a, adaptedB); | ||||||
|  |     } else { | ||||||
|  |         // Pad a to match b's dimensions | ||||||
|  |         const adaptedA = adaptEmbeddingDimensions(a, b.length); | ||||||
|  |         return standardCosineSimilarity(adaptedA, b); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Standard cosine similarity for same-dimension vectors | ||||||
|  |  */ | ||||||
|  | function standardCosineSimilarity(a: Float32Array, b: Float32Array): number { | ||||||
|     let dotProduct = 0; |     let dotProduct = 0; | ||||||
|     let aMagnitude = 0; |     let aMagnitude = 0; | ||||||
|     let bMagnitude = 0; |     let bMagnitude = 0; | ||||||
| @@ -28,6 +53,27 @@ export function cosineSimilarity(a: Float32Array, b: Float32Array): number { | |||||||
|     return dotProduct / (aMagnitude * bMagnitude); |     return dotProduct / (aMagnitude * bMagnitude); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Identifies the optimal embedding when multiple are available | ||||||
|  |  * Prioritizes higher-dimensional embeddings as they contain more information | ||||||
|  |  */ | ||||||
|  | export function selectOptimalEmbedding(embeddings: Array<{ | ||||||
|  |     providerId: string; | ||||||
|  |     modelId: string; | ||||||
|  |     dimension: number; | ||||||
|  |     count?: number; | ||||||
|  | }>): {providerId: string; modelId: string; dimension: number} | null { | ||||||
|  |     if (!embeddings || embeddings.length === 0) return null; | ||||||
|  |  | ||||||
|  |     // First prioritize by dimension (higher is better) | ||||||
|  |     let optimal = embeddings.reduce((best, current) => | ||||||
|  |         current.dimension > best.dimension ? current : best, | ||||||
|  |         embeddings[0] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     return optimal; | ||||||
|  | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Adapts an embedding to match target dimensions |  * Adapts an embedding to match target dimensions | ||||||
|  * Uses a simple truncation (if source is larger) or zero-padding (if source is smaller) |  * Uses a simple truncation (if source is larger) or zero-padding (if source is smaller) | ||||||
|   | |||||||
| @@ -191,7 +191,7 @@ const defaultOptions: DefaultOption[] = [ | |||||||
|     { name: "aiProviderPrecedence", value: "openai,anthropic,ollama", isSynced: true }, |     { name: "aiProviderPrecedence", value: "openai,anthropic,ollama", isSynced: true }, | ||||||
|     { name: "embeddingsDefaultProvider", value: "openai", isSynced: true }, |     { name: "embeddingsDefaultProvider", value: "openai", isSynced: true }, | ||||||
|     { name: "embeddingProviderPrecedence", value: "openai,voyage,ollama", isSynced: true }, |     { name: "embeddingProviderPrecedence", value: "openai,voyage,ollama", isSynced: true }, | ||||||
|     { name: "embeddingDimensionStrategy", value: "adapt", isSynced: true }, |     { name: "embeddingDimensionStrategy", value: "native", isSynced: true }, | ||||||
|     { name: "enableAutomaticIndexing", value: "true", isSynced: true }, |     { name: "enableAutomaticIndexing", value: "true", isSynced: true }, | ||||||
|     { name: "embeddingSimilarityThreshold", value: "0.65", isSynced: true }, |     { name: "embeddingSimilarityThreshold", value: "0.65", isSynced: true }, | ||||||
|     { name: "maxNotesPerLlmQuery", value: "10", isSynced: true }, |     { name: "maxNotesPerLlmQuery", value: "10", isSynced: true }, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user