/** * Distributed Search System for Large-Scale HNSW Indices * Implements parallel search across multiple partitions and instances */ import { executeInThread } from '../utils/workerUtils.js'; // Search coordination strategies export var SearchStrategy; (function (SearchStrategy) { SearchStrategy["BROADCAST"] = "broadcast"; SearchStrategy["SELECTIVE"] = "selective"; SearchStrategy["ADAPTIVE"] = "adaptive"; SearchStrategy["HIERARCHICAL"] = "hierarchical"; // Multi-level search })(SearchStrategy || (SearchStrategy = {})); /** * Distributed search coordinator for large-scale vector search */ export class DistributedSearchSystem { constructor(config = {}) { this.searchWorkers = new Map(); this.searchQueue = []; this.activeSearches = new Map(); this.partitionStats = new Map(); // Performance monitoring this.searchStats = { totalSearches: 0, averageLatency: 0, parallelEfficiency: 0, cacheHitRate: 0, partitionUtilization: new Map() }; this.config = { maxConcurrentSearches: 10, searchTimeout: 30000, // 30 seconds resultMergeStrategy: 'hybrid', adaptivePartitionSelection: true, redundantSearches: 0, loadBalancing: true, ...config }; this.initializeWorkerPool(); } /** * Execute distributed search across multiple partitions */ async distributedSearch(partitionedIndex, queryVector, k, strategy = SearchStrategy.ADAPTIVE) { const searchId = this.generateSearchId(); const startTime = Date.now(); try { // Select partitions to search based on strategy const partitionsToSearch = await this.selectPartitions(partitionedIndex, queryVector, strategy); // Create search tasks const searchTasks = this.createSearchTasks(partitionsToSearch, queryVector, k, searchId); // Execute searches in parallel const searchResults = await this.executeParallelSearches(partitionedIndex, searchTasks); // Merge results from all partitions const mergedResults = this.mergeSearchResults(searchResults, k); // Update statistics this.updateSearchStats(searchId, startTime, searchResults); return mergedResults; } catch (error) { console.error(`Distributed search ${searchId} failed:`, error); throw error; } } /** * Select partitions to search based on strategy */ async selectPartitions(partitionedIndex, queryVector, strategy) { const stats = partitionedIndex.getPartitionStats(); const allPartitionIds = stats.partitionDetails.map(p => p.id); switch (strategy) { case SearchStrategy.BROADCAST: return allPartitionIds; case SearchStrategy.SELECTIVE: return this.selectTopPartitions(allPartitionIds, 3); case SearchStrategy.ADAPTIVE: return await this.adaptivePartitionSelection(allPartitionIds, queryVector); case SearchStrategy.HIERARCHICAL: return this.hierarchicalPartitionSelection(allPartitionIds); default: return allPartitionIds; } } /** * Adaptive partition selection based on historical performance */ async adaptivePartitionSelection(partitionIds, queryVector) { const candidates = []; for (const partitionId of partitionIds) { const stats = this.partitionStats.get(partitionId); let score = 1.0; if (stats) { // Score based on performance metrics const speedScore = 1000 / Math.max(stats.averageSearchTime, 1); const loadScore = Math.max(0, 1 - stats.load); const qualityScore = stats.quality; const recencyScore = Math.max(0, 1 - (Date.now() - stats.lastUsed) / 3600000); score = speedScore * 0.3 + loadScore * 0.25 + qualityScore * 0.3 + recencyScore * 0.15; } candidates.push({ id: partitionId, score }); } // Sort by score and select top partitions candidates.sort((a, b) => b.score - a.score); const selectedCount = Math.min(Math.ceil(partitionIds.length * 0.6), 8); return candidates.slice(0, selectedCount).map(c => c.id); } /** * Select top-performing partitions */ selectTopPartitions(partitionIds, count) { const withStats = partitionIds.map(id => ({ id, stats: this.partitionStats.get(id) })); // Sort by average search time (faster is better) withStats.sort((a, b) => { const timeA = a.stats?.averageSearchTime || 1000; const timeB = b.stats?.averageSearchTime || 1000; return timeA - timeB; }); return withStats.slice(0, count).map(p => p.id); } /** * Hierarchical partition selection for very large datasets */ hierarchicalPartitionSelection(partitionIds) { // First level: select representative partitions const firstLevel = partitionIds.filter((_, index) => index % 3 === 0); // Could implement a two-phase search here: // 1. Quick search on representative partitions // 2. Detailed search on promising partitions return firstLevel; } /** * Create search tasks for parallel execution */ createSearchTasks(partitionIds, queryVector, k, searchId) { const tasks = []; for (let i = 0; i < partitionIds.length; i++) { const partitionId = partitionIds[i]; const stats = this.partitionStats.get(partitionId); // Calculate priority based on partition performance const priority = stats ? (1000 - stats.averageSearchTime) : 500; tasks.push({ partitionId, queryVector: [...queryVector], // Clone vector k: Math.max(k * 2, 20), // Search for more results per partition searchId, priority }); // Add redundant searches if configured if (this.config.redundantSearches > 0 && i < this.config.redundantSearches) { tasks.push({ partitionId, queryVector: [...queryVector], k: Math.max(k * 2, 20), searchId: `${searchId}_redundant_${i}`, priority: priority - 100 // Lower priority for redundant searches }); } } // Sort tasks by priority tasks.sort((a, b) => b.priority - a.priority); return tasks; } /** * Execute searches in parallel across selected partitions */ async executeParallelSearches(partitionedIndex, searchTasks) { const results = []; const semaphore = new Semaphore(this.config.maxConcurrentSearches); // Execute tasks with controlled concurrency const taskPromises = searchTasks.map(async (task) => { await semaphore.acquire(); try { const startTime = Date.now(); // Execute search with timeout const searchPromise = this.executePartitionSearch(partitionedIndex, task); const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('Search timeout')), this.config.searchTimeout); }); const result = await Promise.race([searchPromise, timeoutPromise]); result.searchTime = Date.now() - startTime; return result; } catch (error) { return { partitionId: task.partitionId, results: [], searchTime: this.config.searchTimeout, nodesVisited: 0, error: error }; } finally { semaphore.release(); } }); // Wait for all searches to complete const taskResults = await Promise.allSettled(taskPromises); for (const result of taskResults) { if (result.status === 'fulfilled') { results.push(result.value); } } return results; } /** * Execute search on a single partition */ async executePartitionSearch(partitionedIndex, task) { try { // Use thread pool for compute-intensive operations if (this.shouldUseWorkerThread(task)) { return await this.executeInWorkerThread(partitionedIndex, task); } // Execute search directly const results = await partitionedIndex.search(task.queryVector, task.k, { partitionIds: [task.partitionId] }); return { partitionId: task.partitionId, results, searchTime: 0, // Will be set by caller nodesVisited: results.length // Approximation }; } catch (error) { throw new Error(`Partition search failed: ${error}`); } } /** * Determine if search should use worker thread */ shouldUseWorkerThread(task) { // Use worker threads for high-dimensional vectors or large k return task.queryVector.length > 512 || task.k > 100; } /** * Execute search in worker thread */ async executeInWorkerThread(partitionedIndex, task) { const worker = this.getAvailableWorker(); if (!worker) { // No available workers, execute synchronously return this.executePartitionSearch(partitionedIndex, task); } try { worker.busy = true; const startTime = Date.now(); // Execute in thread (simplified - would need proper worker setup) const searchFunction = ` return partitionedIndex.search( task.queryVector, task.k, { partitionIds: [task.partitionId] } ) `; const results = await executeInThread(searchFunction, { queryVector: task.queryVector, k: task.k, partitionId: task.partitionId }); const searchTime = Date.now() - startTime; worker.averageTaskTime = (worker.averageTaskTime + searchTime) / 2; worker.tasksCompleted++; return { partitionId: task.partitionId, results: results || [], searchTime, nodesVisited: results ? results.length : 0 }; } finally { worker.busy = false; worker.lastTaskTime = Date.now(); } } /** * Get available worker from pool */ getAvailableWorker() { for (const worker of this.searchWorkers.values()) { if (!worker.busy) { return worker; } } return null; } /** * Merge search results from multiple partitions */ mergeSearchResults(partitionResults, k) { const allResults = []; const seenIds = new Set(); // Collect all unique results for (const partitionResult of partitionResults) { if (partitionResult.error) { console.warn(`Partition ${partitionResult.partitionId} failed:`, partitionResult.error); continue; } for (const [id, distance] of partitionResult.results) { if (!seenIds.has(id)) { allResults.push([id, distance]); seenIds.add(id); } } } // Sort and return top k results switch (this.config.resultMergeStrategy) { case 'distance': allResults.sort((a, b) => a[1] - b[1]); break; case 'score': // Convert distance to score (1 / (1 + distance)) allResults.sort((a, b) => { const scoreA = 1 / (1 + a[1]); const scoreB = 1 / (1 + b[1]); return scoreB - scoreA; }); break; case 'hybrid': // Weighted combination of distance and partition quality allResults.sort((a, b) => { const qualityWeightA = this.getPartitionQuality(a[0]); const qualityWeightB = this.getPartitionQuality(b[0]); const adjustedDistanceA = a[1] / (qualityWeightA + 0.1); const adjustedDistanceB = b[1] / (qualityWeightB + 0.1); return adjustedDistanceA - adjustedDistanceB; }); break; } return allResults.slice(0, k); } /** * Get partition quality score */ getPartitionQuality(nodeId) { // This would require knowing which partition a node came from // For now, return a default quality score return 1.0; } /** * Update search statistics */ updateSearchStats(searchId, startTime, results) { const totalTime = Date.now() - startTime; const successfulSearches = results.filter(r => !r.error); // Update global stats this.searchStats.totalSearches++; this.searchStats.averageLatency = (this.searchStats.averageLatency + totalTime) / 2; // Calculate parallel efficiency const totalPartitionTime = results.reduce((sum, r) => sum + r.searchTime, 0); this.searchStats.parallelEfficiency = totalPartitionTime > 0 ? totalTime / totalPartitionTime : 0; // Update partition statistics for (const result of successfulSearches) { let stats = this.partitionStats.get(result.partitionId); if (!stats) { stats = { averageSearchTime: result.searchTime, load: 0, quality: 1.0, lastUsed: Date.now() }; } else { stats.averageSearchTime = (stats.averageSearchTime + result.searchTime) / 2; stats.lastUsed = Date.now(); } this.partitionStats.set(result.partitionId, stats); this.searchStats.partitionUtilization.set(result.partitionId, (this.searchStats.partitionUtilization.get(result.partitionId) || 0) + 1); } } /** * Initialize worker thread pool */ initializeWorkerPool() { const workerCount = Math.min(navigator.hardwareConcurrency || 4, 8); for (let i = 0; i < workerCount; i++) { const worker = { id: `worker_${i}`, busy: false, tasksCompleted: 0, averageTaskTime: 0, lastTaskTime: 0 }; this.searchWorkers.set(worker.id, worker); } console.log(`Initialized worker pool with ${workerCount} workers`); } /** * Generate unique search ID */ generateSearchId() { return `search_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Get search performance statistics */ getSearchStats() { return { ...this.searchStats, workerStats: Array.from(this.searchWorkers.values()), partitionStats: Array.from(this.partitionStats.entries()).map(([id, stats]) => ({ id, stats })) }; } /** * Cleanup resources */ cleanup() { // Clear active searches this.activeSearches.clear(); // Reset worker states for (const worker of this.searchWorkers.values()) { worker.busy = false; } // Clear statistics this.partitionStats.clear(); } } /** * Simple semaphore for concurrency control */ class Semaphore { constructor(permits) { this.waiting = []; this.permits = permits; } async acquire() { if (this.permits > 0) { this.permits--; return Promise.resolve(); } return new Promise((resolve) => { this.waiting.push(resolve); }); } release() { if (this.waiting.length > 0) { const resolve = this.waiting.shift(); resolve(); } else { this.permits++; } } } //# sourceMappingURL=distributedSearch.js.map