Initial commit: Brainy - Multi-Dimensional AI Database
Open source vector database with HNSW indexing, graph relationships, and metadata facets. Features CLI with professional augmentation registry integration for discovering extensions and capabilities.
This commit is contained in:
commit
f8c45f2d8d
448 changed files with 103294 additions and 0 deletions
118
dist/hnsw/distributedSearch.d.ts
vendored
Normal file
118
dist/hnsw/distributedSearch.d.ts
vendored
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
/**
|
||||
* Distributed Search System for Large-Scale HNSW Indices
|
||||
* Implements parallel search across multiple partitions and instances
|
||||
*/
|
||||
import { Vector } from '../coreTypes.js';
|
||||
import { PartitionedHNSWIndex } from './partitionedHNSWIndex.js';
|
||||
interface DistributedSearchConfig {
|
||||
maxConcurrentSearches?: number;
|
||||
searchTimeout?: number;
|
||||
resultMergeStrategy?: 'distance' | 'score' | 'hybrid';
|
||||
adaptivePartitionSelection?: boolean;
|
||||
redundantSearches?: number;
|
||||
loadBalancing?: boolean;
|
||||
}
|
||||
export declare enum SearchStrategy {
|
||||
BROADCAST = "broadcast",// Search all partitions
|
||||
SELECTIVE = "selective",// Search subset of partitions
|
||||
ADAPTIVE = "adaptive",// Dynamically adjust based on results
|
||||
HIERARCHICAL = "hierarchical"
|
||||
}
|
||||
interface SearchWorker {
|
||||
id: string;
|
||||
busy: boolean;
|
||||
tasksCompleted: number;
|
||||
averageTaskTime: number;
|
||||
lastTaskTime: number;
|
||||
}
|
||||
/**
|
||||
* Distributed search coordinator for large-scale vector search
|
||||
*/
|
||||
export declare class DistributedSearchSystem {
|
||||
private config;
|
||||
private searchWorkers;
|
||||
private searchQueue;
|
||||
private activeSearches;
|
||||
private partitionStats;
|
||||
private searchStats;
|
||||
constructor(config?: Partial<DistributedSearchConfig>);
|
||||
/**
|
||||
* Execute distributed search across multiple partitions
|
||||
*/
|
||||
distributedSearch(partitionedIndex: PartitionedHNSWIndex, queryVector: Vector, k: number, strategy?: SearchStrategy): Promise<Array<[string, number]>>;
|
||||
/**
|
||||
* Select partitions to search based on strategy
|
||||
*/
|
||||
private selectPartitions;
|
||||
/**
|
||||
* Adaptive partition selection based on historical performance
|
||||
*/
|
||||
private adaptivePartitionSelection;
|
||||
/**
|
||||
* Select top-performing partitions
|
||||
*/
|
||||
private selectTopPartitions;
|
||||
/**
|
||||
* Hierarchical partition selection for very large datasets
|
||||
*/
|
||||
private hierarchicalPartitionSelection;
|
||||
/**
|
||||
* Create search tasks for parallel execution
|
||||
*/
|
||||
private createSearchTasks;
|
||||
/**
|
||||
* Execute searches in parallel across selected partitions
|
||||
*/
|
||||
private executeParallelSearches;
|
||||
/**
|
||||
* Execute search on a single partition
|
||||
*/
|
||||
private executePartitionSearch;
|
||||
/**
|
||||
* Determine if search should use worker thread
|
||||
*/
|
||||
private shouldUseWorkerThread;
|
||||
/**
|
||||
* Execute search in worker thread
|
||||
*/
|
||||
private executeInWorkerThread;
|
||||
/**
|
||||
* Get available worker from pool
|
||||
*/
|
||||
private getAvailableWorker;
|
||||
/**
|
||||
* Merge search results from multiple partitions
|
||||
*/
|
||||
private mergeSearchResults;
|
||||
/**
|
||||
* Get partition quality score
|
||||
*/
|
||||
private getPartitionQuality;
|
||||
/**
|
||||
* Update search statistics
|
||||
*/
|
||||
private updateSearchStats;
|
||||
/**
|
||||
* Initialize worker thread pool
|
||||
*/
|
||||
private initializeWorkerPool;
|
||||
/**
|
||||
* Generate unique search ID
|
||||
*/
|
||||
private generateSearchId;
|
||||
/**
|
||||
* Get search performance statistics
|
||||
*/
|
||||
getSearchStats(): typeof this.searchStats & {
|
||||
workerStats: SearchWorker[];
|
||||
partitionStats: Array<{
|
||||
id: string;
|
||||
stats: any;
|
||||
}>;
|
||||
};
|
||||
/**
|
||||
* Cleanup resources
|
||||
*/
|
||||
cleanup(): void;
|
||||
}
|
||||
export {};
|
||||
452
dist/hnsw/distributedSearch.js
vendored
Normal file
452
dist/hnsw/distributedSearch.js
vendored
Normal file
|
|
@ -0,0 +1,452 @@
|
|||
/**
|
||||
* 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
|
||||
1
dist/hnsw/distributedSearch.js.map
vendored
Normal file
1
dist/hnsw/distributedSearch.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
121
dist/hnsw/hnswIndex.d.ts
vendored
Normal file
121
dist/hnsw/hnswIndex.d.ts
vendored
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* HNSW (Hierarchical Navigable Small World) Index implementation
|
||||
* Based on the paper: "Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs"
|
||||
*/
|
||||
import { DistanceFunction, HNSWConfig, HNSWNoun, Vector, VectorDocument } from '../coreTypes.js';
|
||||
export declare class HNSWIndex {
|
||||
private nouns;
|
||||
private entryPointId;
|
||||
private maxLevel;
|
||||
private config;
|
||||
private distanceFunction;
|
||||
private dimension;
|
||||
private useParallelization;
|
||||
constructor(config?: Partial<HNSWConfig>, distanceFunction?: DistanceFunction, options?: {
|
||||
useParallelization?: boolean;
|
||||
});
|
||||
/**
|
||||
* Set whether to use parallelization for performance-critical operations
|
||||
*/
|
||||
setUseParallelization(useParallelization: boolean): void;
|
||||
/**
|
||||
* Get whether parallelization is enabled
|
||||
*/
|
||||
getUseParallelization(): boolean;
|
||||
/**
|
||||
* Calculate distances between a query vector and multiple vectors in parallel
|
||||
* This is used to optimize performance for search operations
|
||||
* Uses optimized batch processing for optimal performance
|
||||
*
|
||||
* @param queryVector The query vector
|
||||
* @param vectors Array of vectors to compare against
|
||||
* @returns Array of distances
|
||||
*/
|
||||
private calculateDistancesInParallel;
|
||||
/**
|
||||
* Add a vector to the index
|
||||
*/
|
||||
addItem(item: VectorDocument): Promise<string>;
|
||||
/**
|
||||
* Search for nearest neighbors
|
||||
*/
|
||||
search(queryVector: Vector, k?: number, filter?: (id: string) => Promise<boolean>): Promise<Array<[string, number]>>;
|
||||
/**
|
||||
* Remove an item from the index
|
||||
*/
|
||||
removeItem(id: string): boolean;
|
||||
/**
|
||||
* Get all nouns in the index
|
||||
* @deprecated Use getNounsPaginated() instead for better scalability
|
||||
*/
|
||||
getNouns(): Map<string, HNSWNoun>;
|
||||
/**
|
||||
* Get nouns with pagination
|
||||
* @param options Pagination options
|
||||
* @returns Object containing paginated nouns and pagination info
|
||||
*/
|
||||
getNounsPaginated(options?: {
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
filter?: (noun: HNSWNoun) => boolean;
|
||||
}): {
|
||||
items: Map<string, HNSWNoun>;
|
||||
totalCount: number;
|
||||
hasMore: boolean;
|
||||
};
|
||||
/**
|
||||
* Clear the index
|
||||
*/
|
||||
clear(): void;
|
||||
/**
|
||||
* Get the size of the index
|
||||
*/
|
||||
size(): number;
|
||||
/**
|
||||
* Get the distance function used by the index
|
||||
*/
|
||||
getDistanceFunction(): DistanceFunction;
|
||||
/**
|
||||
* Get the entry point ID
|
||||
*/
|
||||
getEntryPointId(): string | null;
|
||||
/**
|
||||
* Get the maximum level
|
||||
*/
|
||||
getMaxLevel(): number;
|
||||
/**
|
||||
* Get the dimension
|
||||
*/
|
||||
getDimension(): number | null;
|
||||
/**
|
||||
* Get the configuration
|
||||
*/
|
||||
getConfig(): HNSWConfig;
|
||||
/**
|
||||
* Get index health metrics
|
||||
*/
|
||||
getIndexHealth(): {
|
||||
averageConnections: number;
|
||||
layerDistribution: number[];
|
||||
maxLayer: number;
|
||||
totalNodes: number;
|
||||
};
|
||||
/**
|
||||
* Search within a specific layer
|
||||
* Returns a map of noun IDs to distances, sorted by distance
|
||||
*/
|
||||
private searchLayer;
|
||||
/**
|
||||
* Select M nearest neighbors from the candidate set
|
||||
*/
|
||||
private selectNeighbors;
|
||||
/**
|
||||
* Ensure a noun doesn't have too many connections at a given level
|
||||
*/
|
||||
private pruneConnections;
|
||||
/**
|
||||
* Generate a random level for a new noun
|
||||
* Uses the same distribution as in the original HNSW paper
|
||||
*/
|
||||
private getRandomLevel;
|
||||
}
|
||||
621
dist/hnsw/hnswIndex.js
vendored
Normal file
621
dist/hnsw/hnswIndex.js
vendored
Normal file
|
|
@ -0,0 +1,621 @@
|
|||
/**
|
||||
* HNSW (Hierarchical Navigable Small World) Index implementation
|
||||
* Based on the paper: "Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs"
|
||||
*/
|
||||
import { euclideanDistance, calculateDistancesBatch } from '../utils/index.js';
|
||||
// Default HNSW parameters
|
||||
const DEFAULT_CONFIG = {
|
||||
M: 16, // Max number of connections per noun
|
||||
efConstruction: 200, // Size of a dynamic candidate list during construction
|
||||
efSearch: 50, // Size of a dynamic candidate list during search
|
||||
ml: 16 // Max level
|
||||
};
|
||||
export class HNSWIndex {
|
||||
constructor(config = {}, distanceFunction = euclideanDistance, options = {}) {
|
||||
this.nouns = new Map();
|
||||
this.entryPointId = null;
|
||||
this.maxLevel = 0;
|
||||
this.dimension = null;
|
||||
this.useParallelization = true; // Whether to use parallelization for performance-critical operations
|
||||
this.config = { ...DEFAULT_CONFIG, ...config };
|
||||
this.distanceFunction = distanceFunction;
|
||||
this.useParallelization =
|
||||
options.useParallelization !== undefined
|
||||
? options.useParallelization
|
||||
: true;
|
||||
}
|
||||
/**
|
||||
* Set whether to use parallelization for performance-critical operations
|
||||
*/
|
||||
setUseParallelization(useParallelization) {
|
||||
this.useParallelization = useParallelization;
|
||||
}
|
||||
/**
|
||||
* Get whether parallelization is enabled
|
||||
*/
|
||||
getUseParallelization() {
|
||||
return this.useParallelization;
|
||||
}
|
||||
/**
|
||||
* Calculate distances between a query vector and multiple vectors in parallel
|
||||
* This is used to optimize performance for search operations
|
||||
* Uses optimized batch processing for optimal performance
|
||||
*
|
||||
* @param queryVector The query vector
|
||||
* @param vectors Array of vectors to compare against
|
||||
* @returns Array of distances
|
||||
*/
|
||||
async calculateDistancesInParallel(queryVector, vectors) {
|
||||
// If parallelization is disabled or there are very few vectors, use sequential processing
|
||||
if (!this.useParallelization || vectors.length < 10) {
|
||||
return vectors.map((item) => ({
|
||||
id: item.id,
|
||||
distance: this.distanceFunction(queryVector, item.vector)
|
||||
}));
|
||||
}
|
||||
try {
|
||||
// Extract just the vectors from the input array
|
||||
const vectorsOnly = vectors.map((item) => item.vector);
|
||||
// Use optimized batch distance calculation
|
||||
const distances = await calculateDistancesBatch(queryVector, vectorsOnly, this.distanceFunction);
|
||||
// Map the distances back to their IDs
|
||||
return vectors.map((item, index) => ({
|
||||
id: item.id,
|
||||
distance: distances[index]
|
||||
}));
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error in batch distance calculation, falling back to sequential processing:', error);
|
||||
// Fall back to sequential processing if batch calculation fails
|
||||
return vectors.map((item) => ({
|
||||
id: item.id,
|
||||
distance: this.distanceFunction(queryVector, item.vector)
|
||||
}));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Add a vector to the index
|
||||
*/
|
||||
async addItem(item) {
|
||||
// Check if item is defined
|
||||
if (!item) {
|
||||
throw new Error('Item is undefined or null');
|
||||
}
|
||||
const { id, vector } = item;
|
||||
// Check if vector is defined
|
||||
if (!vector) {
|
||||
throw new Error('Vector is undefined or null');
|
||||
}
|
||||
// Set dimension on first insert
|
||||
if (this.dimension === null) {
|
||||
this.dimension = vector.length;
|
||||
}
|
||||
else if (vector.length !== this.dimension) {
|
||||
throw new Error(`Vector dimension mismatch: expected ${this.dimension}, got ${vector.length}`);
|
||||
}
|
||||
// Generate random level for this noun
|
||||
const nounLevel = this.getRandomLevel();
|
||||
// Create new noun
|
||||
const noun = {
|
||||
id,
|
||||
vector,
|
||||
connections: new Map(),
|
||||
level: nounLevel
|
||||
};
|
||||
// Initialize empty connection sets for each level
|
||||
for (let level = 0; level <= nounLevel; level++) {
|
||||
noun.connections.set(level, new Set());
|
||||
}
|
||||
// If this is the first noun, make it the entry point
|
||||
if (this.nouns.size === 0) {
|
||||
this.entryPointId = id;
|
||||
this.maxLevel = nounLevel;
|
||||
this.nouns.set(id, noun);
|
||||
return id;
|
||||
}
|
||||
// Find entry point
|
||||
if (!this.entryPointId) {
|
||||
console.error('Entry point ID is null');
|
||||
// If there's no entry point, this is the first noun, so we should have returned earlier
|
||||
// This is a safety check
|
||||
this.entryPointId = id;
|
||||
this.maxLevel = nounLevel;
|
||||
this.nouns.set(id, noun);
|
||||
return id;
|
||||
}
|
||||
const entryPoint = this.nouns.get(this.entryPointId);
|
||||
if (!entryPoint) {
|
||||
console.error(`Entry point with ID ${this.entryPointId} not found`);
|
||||
// If the entry point doesn't exist, treat this as the first noun
|
||||
this.entryPointId = id;
|
||||
this.maxLevel = nounLevel;
|
||||
this.nouns.set(id, noun);
|
||||
return id;
|
||||
}
|
||||
let currObj = entryPoint;
|
||||
let currDist = this.distanceFunction(vector, entryPoint.vector);
|
||||
// Traverse the graph from top to bottom to find the closest noun
|
||||
for (let level = this.maxLevel; level > nounLevel; level--) {
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
// Check all neighbors at current level
|
||||
const connections = currObj.connections.get(level) || new Set();
|
||||
for (const neighborId of connections) {
|
||||
const neighbor = this.nouns.get(neighborId);
|
||||
if (!neighbor) {
|
||||
// Skip neighbors that don't exist (expected during rapid additions/deletions)
|
||||
continue;
|
||||
}
|
||||
const distToNeighbor = this.distanceFunction(vector, neighbor.vector);
|
||||
if (distToNeighbor < currDist) {
|
||||
currDist = distToNeighbor;
|
||||
currObj = neighbor;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// For each level from nounLevel down to 0
|
||||
for (let level = Math.min(nounLevel, this.maxLevel); level >= 0; level--) {
|
||||
// Find ef nearest elements using greedy search
|
||||
const nearestNouns = await this.searchLayer(vector, currObj, this.config.efConstruction, level);
|
||||
// Select M nearest neighbors
|
||||
const neighbors = this.selectNeighbors(vector, nearestNouns, this.config.M);
|
||||
// Add bidirectional connections
|
||||
for (const [neighborId, _] of neighbors) {
|
||||
const neighbor = this.nouns.get(neighborId);
|
||||
if (!neighbor) {
|
||||
// Skip neighbors that don't exist (expected during rapid additions/deletions)
|
||||
continue;
|
||||
}
|
||||
noun.connections.get(level).add(neighborId);
|
||||
// Add reverse connection
|
||||
if (!neighbor.connections.has(level)) {
|
||||
neighbor.connections.set(level, new Set());
|
||||
}
|
||||
neighbor.connections.get(level).add(id);
|
||||
// Ensure neighbor doesn't have too many connections
|
||||
if (neighbor.connections.get(level).size > this.config.M) {
|
||||
this.pruneConnections(neighbor, level);
|
||||
}
|
||||
}
|
||||
// Update entry point for the next level
|
||||
if (nearestNouns.size > 0) {
|
||||
const [nearestId, nearestDist] = [...nearestNouns][0];
|
||||
if (nearestDist < currDist) {
|
||||
currDist = nearestDist;
|
||||
const nearestNoun = this.nouns.get(nearestId);
|
||||
if (!nearestNoun) {
|
||||
console.error(`Nearest noun with ID ${nearestId} not found in addItem`);
|
||||
// Keep the current object as is
|
||||
}
|
||||
else {
|
||||
currObj = nearestNoun;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update max level and entry point if needed
|
||||
if (nounLevel > this.maxLevel) {
|
||||
this.maxLevel = nounLevel;
|
||||
this.entryPointId = id;
|
||||
}
|
||||
// Add noun to the index
|
||||
this.nouns.set(id, noun);
|
||||
return id;
|
||||
}
|
||||
/**
|
||||
* Search for nearest neighbors
|
||||
*/
|
||||
async search(queryVector, k = 10, filter) {
|
||||
if (this.nouns.size === 0) {
|
||||
return [];
|
||||
}
|
||||
// Check if query vector is defined
|
||||
if (!queryVector) {
|
||||
throw new Error('Query vector is undefined or null');
|
||||
}
|
||||
if (this.dimension !== null && queryVector.length !== this.dimension) {
|
||||
throw new Error(`Query vector dimension mismatch: expected ${this.dimension}, got ${queryVector.length}`);
|
||||
}
|
||||
// Start from the entry point
|
||||
if (!this.entryPointId) {
|
||||
console.error('Entry point ID is null');
|
||||
return [];
|
||||
}
|
||||
const entryPoint = this.nouns.get(this.entryPointId);
|
||||
if (!entryPoint) {
|
||||
console.error(`Entry point with ID ${this.entryPointId} not found`);
|
||||
return [];
|
||||
}
|
||||
let currObj = entryPoint;
|
||||
let currDist = this.distanceFunction(queryVector, currObj.vector);
|
||||
// Traverse the graph from top to bottom to find the closest noun
|
||||
for (let level = this.maxLevel; level > 0; level--) {
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
// Check all neighbors at current level
|
||||
const connections = currObj.connections.get(level) || new Set();
|
||||
// If we have enough connections, use parallel distance calculation
|
||||
if (this.useParallelization && connections.size >= 10) {
|
||||
// Prepare vectors for parallel calculation
|
||||
const vectors = [];
|
||||
for (const neighborId of connections) {
|
||||
const neighbor = this.nouns.get(neighborId);
|
||||
if (!neighbor)
|
||||
continue;
|
||||
vectors.push({ id: neighborId, vector: neighbor.vector });
|
||||
}
|
||||
// Calculate distances in parallel
|
||||
const distances = await this.calculateDistancesInParallel(queryVector, vectors);
|
||||
// Find the closest neighbor
|
||||
for (const { id, distance } of distances) {
|
||||
if (distance < currDist) {
|
||||
currDist = distance;
|
||||
const neighbor = this.nouns.get(id);
|
||||
if (neighbor) {
|
||||
currObj = neighbor;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Use sequential processing for small number of connections
|
||||
for (const neighborId of connections) {
|
||||
const neighbor = this.nouns.get(neighborId);
|
||||
if (!neighbor) {
|
||||
// Skip neighbors that don't exist (expected during rapid additions/deletions)
|
||||
continue;
|
||||
}
|
||||
const distToNeighbor = this.distanceFunction(queryVector, neighbor.vector);
|
||||
if (distToNeighbor < currDist) {
|
||||
currDist = distToNeighbor;
|
||||
currObj = neighbor;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Search at level 0 with ef = k
|
||||
// If we have a filter, increase ef to compensate for filtered results
|
||||
const ef = filter ? Math.max(this.config.efSearch * 3, k * 3) : Math.max(this.config.efSearch, k);
|
||||
const nearestNouns = await this.searchLayer(queryVector, currObj, ef, 0, filter);
|
||||
// Convert to array and sort by distance
|
||||
return [...nearestNouns].slice(0, k);
|
||||
}
|
||||
/**
|
||||
* Remove an item from the index
|
||||
*/
|
||||
removeItem(id) {
|
||||
if (!this.nouns.has(id)) {
|
||||
return false;
|
||||
}
|
||||
const noun = this.nouns.get(id);
|
||||
// Remove connections to this noun from all neighbors
|
||||
for (const [level, connections] of noun.connections.entries()) {
|
||||
for (const neighborId of connections) {
|
||||
const neighbor = this.nouns.get(neighborId);
|
||||
if (!neighbor) {
|
||||
// Skip neighbors that don't exist (expected during rapid additions/deletions)
|
||||
continue;
|
||||
}
|
||||
if (neighbor.connections.has(level)) {
|
||||
neighbor.connections.get(level).delete(id);
|
||||
// Prune connections after removing this noun to ensure consistency
|
||||
this.pruneConnections(neighbor, level);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Also check all other nouns for references to this noun and remove them
|
||||
for (const [nounId, otherNoun] of this.nouns.entries()) {
|
||||
if (nounId === id)
|
||||
continue; // Skip the noun being removed
|
||||
for (const [level, connections] of otherNoun.connections.entries()) {
|
||||
if (connections.has(id)) {
|
||||
connections.delete(id);
|
||||
// Prune connections after removing this reference
|
||||
this.pruneConnections(otherNoun, level);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remove the noun
|
||||
this.nouns.delete(id);
|
||||
// If we removed the entry point, find a new one
|
||||
if (this.entryPointId === id) {
|
||||
if (this.nouns.size === 0) {
|
||||
this.entryPointId = null;
|
||||
this.maxLevel = 0;
|
||||
}
|
||||
else {
|
||||
// Find the noun with the highest level
|
||||
let maxLevel = 0;
|
||||
let newEntryPointId = null;
|
||||
for (const [nounId, noun] of this.nouns.entries()) {
|
||||
if (noun.connections.size === 0)
|
||||
continue; // Skip nouns with no connections
|
||||
const nounLevel = Math.max(...noun.connections.keys());
|
||||
if (nounLevel >= maxLevel) {
|
||||
maxLevel = nounLevel;
|
||||
newEntryPointId = nounId;
|
||||
}
|
||||
}
|
||||
this.entryPointId = newEntryPointId;
|
||||
this.maxLevel = maxLevel;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Get all nouns in the index
|
||||
* @deprecated Use getNounsPaginated() instead for better scalability
|
||||
*/
|
||||
getNouns() {
|
||||
return new Map(this.nouns);
|
||||
}
|
||||
/**
|
||||
* Get nouns with pagination
|
||||
* @param options Pagination options
|
||||
* @returns Object containing paginated nouns and pagination info
|
||||
*/
|
||||
getNounsPaginated(options = {}) {
|
||||
const offset = options.offset || 0;
|
||||
const limit = options.limit || 100;
|
||||
const filter = options.filter || (() => true);
|
||||
// Get all noun entries
|
||||
const entries = [...this.nouns.entries()];
|
||||
// Apply filter if provided
|
||||
const filteredEntries = entries.filter(([_, noun]) => filter(noun));
|
||||
// Get total count after filtering
|
||||
const totalCount = filteredEntries.length;
|
||||
// Apply pagination
|
||||
const paginatedEntries = filteredEntries.slice(offset, offset + limit);
|
||||
// Check if there are more items
|
||||
const hasMore = offset + limit < totalCount;
|
||||
// Create a new map with the paginated entries
|
||||
const items = new Map(paginatedEntries);
|
||||
return {
|
||||
items,
|
||||
totalCount,
|
||||
hasMore
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Clear the index
|
||||
*/
|
||||
clear() {
|
||||
this.nouns.clear();
|
||||
this.entryPointId = null;
|
||||
this.maxLevel = 0;
|
||||
}
|
||||
/**
|
||||
* Get the size of the index
|
||||
*/
|
||||
size() {
|
||||
return this.nouns.size;
|
||||
}
|
||||
/**
|
||||
* Get the distance function used by the index
|
||||
*/
|
||||
getDistanceFunction() {
|
||||
return this.distanceFunction;
|
||||
}
|
||||
/**
|
||||
* Get the entry point ID
|
||||
*/
|
||||
getEntryPointId() {
|
||||
return this.entryPointId;
|
||||
}
|
||||
/**
|
||||
* Get the maximum level
|
||||
*/
|
||||
getMaxLevel() {
|
||||
return this.maxLevel;
|
||||
}
|
||||
/**
|
||||
* Get the dimension
|
||||
*/
|
||||
getDimension() {
|
||||
return this.dimension;
|
||||
}
|
||||
/**
|
||||
* Get the configuration
|
||||
*/
|
||||
getConfig() {
|
||||
return { ...this.config };
|
||||
}
|
||||
/**
|
||||
* Get index health metrics
|
||||
*/
|
||||
getIndexHealth() {
|
||||
let totalConnections = 0;
|
||||
const layerCounts = new Array(this.maxLevel + 1).fill(0);
|
||||
// Count connections and layer distribution
|
||||
this.nouns.forEach(noun => {
|
||||
// Count connections at each layer
|
||||
for (let level = 0; level <= noun.level; level++) {
|
||||
totalConnections += noun.connections.get(level)?.size || 0;
|
||||
layerCounts[level]++;
|
||||
}
|
||||
});
|
||||
const totalNodes = this.nouns.size;
|
||||
const averageConnections = totalNodes > 0 ? totalConnections / totalNodes : 0;
|
||||
return {
|
||||
averageConnections,
|
||||
layerDistribution: layerCounts,
|
||||
maxLayer: this.maxLevel,
|
||||
totalNodes
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Search within a specific layer
|
||||
* Returns a map of noun IDs to distances, sorted by distance
|
||||
*/
|
||||
async searchLayer(queryVector, entryPoint, ef, level, filter) {
|
||||
// Set of visited nouns
|
||||
const visited = new Set([entryPoint.id]);
|
||||
// Check if entry point passes filter
|
||||
const entryPointDistance = this.distanceFunction(queryVector, entryPoint.vector);
|
||||
const entryPointPasses = filter ? await filter(entryPoint.id) : true;
|
||||
// Priority queue of candidates (closest first)
|
||||
const candidates = new Map();
|
||||
candidates.set(entryPoint.id, entryPointDistance);
|
||||
// Priority queue of nearest neighbors found so far (closest first)
|
||||
const nearest = new Map();
|
||||
if (entryPointPasses) {
|
||||
nearest.set(entryPoint.id, entryPointDistance);
|
||||
}
|
||||
// While there are candidates to explore
|
||||
while (candidates.size > 0) {
|
||||
// Get closest candidate
|
||||
const [closestId, closestDist] = [...candidates][0];
|
||||
candidates.delete(closestId);
|
||||
// If this candidate is farther than the farthest in our result set, we're done
|
||||
const farthestInNearest = [...nearest][nearest.size - 1];
|
||||
if (nearest.size >= ef && closestDist > farthestInNearest[1]) {
|
||||
break;
|
||||
}
|
||||
// Explore neighbors of the closest candidate
|
||||
const noun = this.nouns.get(closestId);
|
||||
if (!noun) {
|
||||
console.error(`Noun with ID ${closestId} not found in searchLayer`);
|
||||
continue;
|
||||
}
|
||||
const connections = noun.connections.get(level) || new Set();
|
||||
// If we have enough connections and parallelization is enabled, use parallel distance calculation
|
||||
if (this.useParallelization && connections.size >= 10) {
|
||||
// Collect unvisited neighbors
|
||||
const unvisitedNeighbors = [];
|
||||
for (const neighborId of connections) {
|
||||
if (!visited.has(neighborId)) {
|
||||
visited.add(neighborId);
|
||||
const neighbor = this.nouns.get(neighborId);
|
||||
if (!neighbor)
|
||||
continue;
|
||||
unvisitedNeighbors.push({ id: neighborId, vector: neighbor.vector });
|
||||
}
|
||||
}
|
||||
if (unvisitedNeighbors.length > 0) {
|
||||
// Calculate distances in parallel
|
||||
const distances = await this.calculateDistancesInParallel(queryVector, unvisitedNeighbors);
|
||||
// Process the results
|
||||
for (const { id, distance } of distances) {
|
||||
// Apply filter if provided
|
||||
const passes = filter ? await filter(id) : true;
|
||||
// Always add to candidates for graph traversal
|
||||
candidates.set(id, distance);
|
||||
// Only add to nearest if it passes the filter
|
||||
if (passes) {
|
||||
// If we haven't found ef nearest neighbors yet, or this neighbor is closer than the farthest one we've found
|
||||
if (nearest.size < ef || distance < farthestInNearest[1]) {
|
||||
nearest.set(id, distance);
|
||||
// If we have more than ef neighbors, remove the farthest one
|
||||
if (nearest.size > ef) {
|
||||
const sortedNearest = [...nearest].sort((a, b) => a[1] - b[1]);
|
||||
nearest.clear();
|
||||
for (let i = 0; i < ef; i++) {
|
||||
nearest.set(sortedNearest[i][0], sortedNearest[i][1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Use sequential processing for small number of connections
|
||||
for (const neighborId of connections) {
|
||||
if (!visited.has(neighborId)) {
|
||||
visited.add(neighborId);
|
||||
const neighbor = this.nouns.get(neighborId);
|
||||
if (!neighbor) {
|
||||
// Skip neighbors that don't exist (expected during rapid additions/deletions)
|
||||
continue;
|
||||
}
|
||||
const distToNeighbor = this.distanceFunction(queryVector, neighbor.vector);
|
||||
// Apply filter if provided
|
||||
const passes = filter ? await filter(neighborId) : true;
|
||||
// Always add to candidates for graph traversal
|
||||
candidates.set(neighborId, distToNeighbor);
|
||||
// Only add to nearest if it passes the filter
|
||||
if (passes) {
|
||||
// If we haven't found ef nearest neighbors yet, or this neighbor is closer than the farthest one we've found
|
||||
if (nearest.size < ef || distToNeighbor < farthestInNearest[1]) {
|
||||
nearest.set(neighborId, distToNeighbor);
|
||||
// If we have more than ef neighbors, remove the farthest one
|
||||
if (nearest.size > ef) {
|
||||
const sortedNearest = [...nearest].sort((a, b) => a[1] - b[1]);
|
||||
nearest.clear();
|
||||
for (let i = 0; i < ef; i++) {
|
||||
nearest.set(sortedNearest[i][0], sortedNearest[i][1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sort nearest by distance
|
||||
return new Map([...nearest].sort((a, b) => a[1] - b[1]));
|
||||
}
|
||||
/**
|
||||
* Select M nearest neighbors from the candidate set
|
||||
*/
|
||||
selectNeighbors(queryVector, candidates, M) {
|
||||
if (candidates.size <= M) {
|
||||
return candidates;
|
||||
}
|
||||
// Simple heuristic: just take the M closest
|
||||
const sortedCandidates = [...candidates].sort((a, b) => a[1] - b[1]);
|
||||
const result = new Map();
|
||||
for (let i = 0; i < Math.min(M, sortedCandidates.length); i++) {
|
||||
result.set(sortedCandidates[i][0], sortedCandidates[i][1]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Ensure a noun doesn't have too many connections at a given level
|
||||
*/
|
||||
pruneConnections(noun, level) {
|
||||
const connections = noun.connections.get(level);
|
||||
if (connections.size <= this.config.M) {
|
||||
return;
|
||||
}
|
||||
// Calculate distances to all neighbors
|
||||
const distances = new Map();
|
||||
const validNeighborIds = new Set();
|
||||
for (const neighborId of connections) {
|
||||
const neighbor = this.nouns.get(neighborId);
|
||||
if (!neighbor) {
|
||||
// Skip neighbors that don't exist (expected during rapid additions/deletions)
|
||||
continue;
|
||||
}
|
||||
// Only add valid neighbors to the distances map
|
||||
distances.set(neighborId, this.distanceFunction(noun.vector, neighbor.vector));
|
||||
validNeighborIds.add(neighborId);
|
||||
}
|
||||
// Only proceed if we have valid neighbors
|
||||
if (distances.size === 0) {
|
||||
// If no valid neighbors, clear connections at this level
|
||||
noun.connections.set(level, new Set());
|
||||
return;
|
||||
}
|
||||
// Select M closest neighbors from valid ones
|
||||
const selectedNeighbors = this.selectNeighbors(noun.vector, distances, this.config.M);
|
||||
// Update connections with only valid neighbors
|
||||
noun.connections.set(level, new Set(selectedNeighbors.keys()));
|
||||
}
|
||||
/**
|
||||
* Generate a random level for a new noun
|
||||
* Uses the same distribution as in the original HNSW paper
|
||||
*/
|
||||
getRandomLevel() {
|
||||
const r = Math.random();
|
||||
return Math.floor(-Math.log(r) * (1.0 / Math.log(this.config.M)));
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=hnswIndex.js.map
|
||||
1
dist/hnsw/hnswIndex.js.map
vendored
Normal file
1
dist/hnsw/hnswIndex.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
178
dist/hnsw/hnswIndexOptimized.d.ts
vendored
Normal file
178
dist/hnsw/hnswIndexOptimized.d.ts
vendored
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
/**
|
||||
* Optimized HNSW (Hierarchical Navigable Small World) Index implementation
|
||||
* Extends the base HNSW implementation with support for large datasets
|
||||
* Uses product quantization for dimensionality reduction and disk-based storage when needed
|
||||
*/
|
||||
import { DistanceFunction, HNSWConfig, Vector, VectorDocument } from '../coreTypes.js';
|
||||
import { HNSWIndex } from './hnswIndex.js';
|
||||
import { StorageAdapter } from '../coreTypes.js';
|
||||
export interface HNSWOptimizedConfig extends HNSWConfig {
|
||||
memoryThreshold?: number;
|
||||
productQuantization?: {
|
||||
enabled: boolean;
|
||||
numSubvectors?: number;
|
||||
numCentroids?: number;
|
||||
};
|
||||
useDiskBasedIndex?: boolean;
|
||||
}
|
||||
/**
|
||||
* Product Quantization implementation
|
||||
* Reduces vector dimensionality by splitting vectors into subvectors
|
||||
* and quantizing each subvector to the nearest centroid
|
||||
*/
|
||||
declare class ProductQuantizer {
|
||||
private numSubvectors;
|
||||
private numCentroids;
|
||||
private centroids;
|
||||
private subvectorSize;
|
||||
private initialized;
|
||||
private dimension;
|
||||
constructor(numSubvectors?: number, numCentroids?: number);
|
||||
/**
|
||||
* Initialize the product quantizer with training data
|
||||
* @param vectors Training vectors to use for learning centroids
|
||||
*/
|
||||
train(vectors: Vector[]): void;
|
||||
/**
|
||||
* Quantize a vector using product quantization
|
||||
* @param vector Vector to quantize
|
||||
* @returns Array of centroid indices, one for each subvector
|
||||
*/
|
||||
quantize(vector: Vector): number[];
|
||||
/**
|
||||
* Reconstruct a vector from its quantized representation
|
||||
* @param codes Array of centroid indices
|
||||
* @returns Reconstructed vector
|
||||
*/
|
||||
reconstruct(codes: number[]): Vector;
|
||||
/**
|
||||
* Compute squared Euclidean distance between two vectors
|
||||
* @param a First vector
|
||||
* @param b Second vector
|
||||
* @returns Squared Euclidean distance
|
||||
*/
|
||||
private euclideanDistanceSquared;
|
||||
/**
|
||||
* Implement k-means++ algorithm to initialize centroids
|
||||
* @param vectors Vectors to cluster
|
||||
* @param k Number of clusters
|
||||
* @returns Array of centroids
|
||||
*/
|
||||
private kMeansPlusPlus;
|
||||
/**
|
||||
* Get the centroids for each subvector
|
||||
* @returns Array of centroid arrays
|
||||
*/
|
||||
getCentroids(): Vector[][];
|
||||
/**
|
||||
* Set the centroids for each subvector
|
||||
* @param centroids Array of centroid arrays
|
||||
*/
|
||||
setCentroids(centroids: Vector[][]): void;
|
||||
/**
|
||||
* Get the dimension of the vectors
|
||||
* @returns Dimension
|
||||
*/
|
||||
getDimension(): number;
|
||||
/**
|
||||
* Set the dimension of the vectors
|
||||
* @param dimension Dimension
|
||||
*/
|
||||
setDimension(dimension: number): void;
|
||||
}
|
||||
/**
|
||||
* Optimized HNSW Index implementation
|
||||
* Extends the base HNSW implementation with support for large datasets
|
||||
* Uses product quantization for dimensionality reduction and disk-based storage when needed
|
||||
*/
|
||||
export declare class HNSWIndexOptimized extends HNSWIndex {
|
||||
private optimizedConfig;
|
||||
private productQuantizer;
|
||||
private storage;
|
||||
private useDiskBasedIndex;
|
||||
private useProductQuantization;
|
||||
private quantizedVectors;
|
||||
private memoryUsage;
|
||||
private vectorCount;
|
||||
private memoryUpdateLock;
|
||||
constructor(config: Partial<HNSWOptimizedConfig> | undefined, distanceFunction: DistanceFunction, storage?: StorageAdapter | null);
|
||||
/**
|
||||
* Thread-safe method to update memory usage
|
||||
* @param memoryDelta Change in memory usage (can be negative)
|
||||
* @param vectorCountDelta Change in vector count (can be negative)
|
||||
*/
|
||||
private updateMemoryUsage;
|
||||
/**
|
||||
* Thread-safe method to get current memory usage
|
||||
* @returns Current memory usage and vector count
|
||||
*/
|
||||
private getMemoryUsageAsync;
|
||||
/**
|
||||
* Add a vector to the index
|
||||
* Uses product quantization if enabled and memory threshold is exceeded
|
||||
*/
|
||||
addItem(item: VectorDocument): Promise<string>;
|
||||
/**
|
||||
* Search for nearest neighbors
|
||||
* Uses product quantization if enabled
|
||||
*/
|
||||
search(queryVector: Vector, k?: number): Promise<Array<[string, number]>>;
|
||||
/**
|
||||
* Remove an item from the index
|
||||
*/
|
||||
removeItem(id: string): boolean;
|
||||
/**
|
||||
* Clear the index
|
||||
*/
|
||||
clear(): Promise<void>;
|
||||
/**
|
||||
* Initialize product quantizer with existing vectors
|
||||
*/
|
||||
private initializeProductQuantizer;
|
||||
/**
|
||||
* Get the product quantizer
|
||||
* @returns Product quantizer or null if not enabled
|
||||
*/
|
||||
getProductQuantizer(): ProductQuantizer | null;
|
||||
/**
|
||||
* Get the optimized configuration
|
||||
* @returns Optimized configuration
|
||||
*/
|
||||
getOptimizedConfig(): HNSWOptimizedConfig;
|
||||
/**
|
||||
* Get the estimated memory usage
|
||||
* @returns Estimated memory usage in bytes
|
||||
*/
|
||||
getMemoryUsage(): number;
|
||||
/**
|
||||
* Set the storage adapter
|
||||
* @param storage Storage adapter
|
||||
*/
|
||||
setStorage(storage: StorageAdapter): void;
|
||||
/**
|
||||
* Get the storage adapter
|
||||
* @returns Storage adapter or null if not set
|
||||
*/
|
||||
getStorage(): StorageAdapter | null;
|
||||
/**
|
||||
* Set whether to use disk-based index
|
||||
* @param useDiskBasedIndex Whether to use disk-based index
|
||||
*/
|
||||
setUseDiskBasedIndex(useDiskBasedIndex: boolean): void;
|
||||
/**
|
||||
* Get whether disk-based index is used
|
||||
* @returns Whether disk-based index is used
|
||||
*/
|
||||
getUseDiskBasedIndex(): boolean;
|
||||
/**
|
||||
* Set whether to use product quantization
|
||||
* @param useProductQuantization Whether to use product quantization
|
||||
*/
|
||||
setUseProductQuantization(useProductQuantization: boolean): void;
|
||||
/**
|
||||
* Get whether product quantization is used
|
||||
* @returns Whether product quantization is used
|
||||
*/
|
||||
getUseProductQuantization(): boolean;
|
||||
}
|
||||
export {};
|
||||
471
dist/hnsw/hnswIndexOptimized.js
vendored
Normal file
471
dist/hnsw/hnswIndexOptimized.js
vendored
Normal file
|
|
@ -0,0 +1,471 @@
|
|||
/**
|
||||
* Optimized HNSW (Hierarchical Navigable Small World) Index implementation
|
||||
* Extends the base HNSW implementation with support for large datasets
|
||||
* Uses product quantization for dimensionality reduction and disk-based storage when needed
|
||||
*/
|
||||
import { HNSWIndex } from './hnswIndex.js';
|
||||
// Default configuration for the optimized HNSW index
|
||||
const DEFAULT_OPTIMIZED_CONFIG = {
|
||||
M: 16,
|
||||
efConstruction: 200,
|
||||
efSearch: 50,
|
||||
ml: 16,
|
||||
memoryThreshold: 1024 * 1024 * 1024, // 1GB default threshold
|
||||
productQuantization: {
|
||||
enabled: false,
|
||||
numSubvectors: 16,
|
||||
numCentroids: 256
|
||||
},
|
||||
useDiskBasedIndex: false
|
||||
};
|
||||
/**
|
||||
* Product Quantization implementation
|
||||
* Reduces vector dimensionality by splitting vectors into subvectors
|
||||
* and quantizing each subvector to the nearest centroid
|
||||
*/
|
||||
class ProductQuantizer {
|
||||
constructor(numSubvectors = 16, numCentroids = 256) {
|
||||
this.centroids = [];
|
||||
this.subvectorSize = 0;
|
||||
this.initialized = false;
|
||||
this.dimension = 0;
|
||||
this.numSubvectors = numSubvectors;
|
||||
this.numCentroids = numCentroids;
|
||||
}
|
||||
/**
|
||||
* Initialize the product quantizer with training data
|
||||
* @param vectors Training vectors to use for learning centroids
|
||||
*/
|
||||
train(vectors) {
|
||||
if (vectors.length === 0) {
|
||||
throw new Error('Cannot train product quantizer with empty vector set');
|
||||
}
|
||||
this.dimension = vectors[0].length;
|
||||
this.subvectorSize = Math.ceil(this.dimension / this.numSubvectors);
|
||||
// Initialize centroids for each subvector
|
||||
for (let i = 0; i < this.numSubvectors; i++) {
|
||||
// Extract subvectors from training data
|
||||
const subvectors = vectors.map((vector) => {
|
||||
const start = i * this.subvectorSize;
|
||||
const end = Math.min(start + this.subvectorSize, this.dimension);
|
||||
return vector.slice(start, end);
|
||||
});
|
||||
// Initialize centroids for this subvector using k-means++
|
||||
this.centroids[i] = this.kMeansPlusPlus(subvectors, this.numCentroids);
|
||||
}
|
||||
this.initialized = true;
|
||||
}
|
||||
/**
|
||||
* Quantize a vector using product quantization
|
||||
* @param vector Vector to quantize
|
||||
* @returns Array of centroid indices, one for each subvector
|
||||
*/
|
||||
quantize(vector) {
|
||||
if (!this.initialized) {
|
||||
throw new Error('Product quantizer not initialized. Call train() first.');
|
||||
}
|
||||
if (vector.length !== this.dimension) {
|
||||
throw new Error(`Vector dimension mismatch: expected ${this.dimension}, got ${vector.length}`);
|
||||
}
|
||||
const codes = [];
|
||||
// Quantize each subvector
|
||||
for (let i = 0; i < this.numSubvectors; i++) {
|
||||
const start = i * this.subvectorSize;
|
||||
const end = Math.min(start + this.subvectorSize, this.dimension);
|
||||
const subvector = vector.slice(start, end);
|
||||
// Find nearest centroid
|
||||
let minDist = Number.MAX_VALUE;
|
||||
let nearestCentroidIndex = 0;
|
||||
for (let j = 0; j < this.centroids[i].length; j++) {
|
||||
const centroid = this.centroids[i][j];
|
||||
const dist = this.euclideanDistanceSquared(subvector, centroid);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
nearestCentroidIndex = j;
|
||||
}
|
||||
}
|
||||
codes.push(nearestCentroidIndex);
|
||||
}
|
||||
return codes;
|
||||
}
|
||||
/**
|
||||
* Reconstruct a vector from its quantized representation
|
||||
* @param codes Array of centroid indices
|
||||
* @returns Reconstructed vector
|
||||
*/
|
||||
reconstruct(codes) {
|
||||
if (!this.initialized) {
|
||||
throw new Error('Product quantizer not initialized. Call train() first.');
|
||||
}
|
||||
if (codes.length !== this.numSubvectors) {
|
||||
throw new Error(`Code length mismatch: expected ${this.numSubvectors}, got ${codes.length}`);
|
||||
}
|
||||
const reconstructed = [];
|
||||
// Reconstruct each subvector
|
||||
for (let i = 0; i < this.numSubvectors; i++) {
|
||||
const centroidIndex = codes[i];
|
||||
const centroid = this.centroids[i][centroidIndex];
|
||||
// Add centroid components to reconstructed vector
|
||||
for (const component of centroid) {
|
||||
reconstructed.push(component);
|
||||
}
|
||||
}
|
||||
// Trim to original dimension if needed
|
||||
return reconstructed.slice(0, this.dimension);
|
||||
}
|
||||
/**
|
||||
* Compute squared Euclidean distance between two vectors
|
||||
* @param a First vector
|
||||
* @param b Second vector
|
||||
* @returns Squared Euclidean distance
|
||||
*/
|
||||
euclideanDistanceSquared(a, b) {
|
||||
let sum = 0;
|
||||
const length = Math.min(a.length, b.length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
const diff = a[i] - b[i];
|
||||
sum += diff * diff;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
/**
|
||||
* Implement k-means++ algorithm to initialize centroids
|
||||
* @param vectors Vectors to cluster
|
||||
* @param k Number of clusters
|
||||
* @returns Array of centroids
|
||||
*/
|
||||
kMeansPlusPlus(vectors, k) {
|
||||
if (vectors.length < k) {
|
||||
// If we have fewer vectors than centroids, use the vectors as centroids
|
||||
return [...vectors];
|
||||
}
|
||||
const centroids = [];
|
||||
// Choose first centroid randomly
|
||||
const firstIndex = Math.floor(Math.random() * vectors.length);
|
||||
centroids.push([...vectors[firstIndex]]);
|
||||
// Choose remaining centroids
|
||||
for (let i = 1; i < k; i++) {
|
||||
// Compute distances to nearest centroid for each vector
|
||||
const distances = vectors.map((vector) => {
|
||||
let minDist = Number.MAX_VALUE;
|
||||
for (const centroid of centroids) {
|
||||
const dist = this.euclideanDistanceSquared(vector, centroid);
|
||||
minDist = Math.min(minDist, dist);
|
||||
}
|
||||
return minDist;
|
||||
});
|
||||
// Compute sum of distances
|
||||
const distSum = distances.reduce((sum, dist) => sum + dist, 0);
|
||||
// Choose next centroid with probability proportional to distance
|
||||
let r = Math.random() * distSum;
|
||||
let nextIndex = 0;
|
||||
for (let j = 0; j < distances.length; j++) {
|
||||
r -= distances[j];
|
||||
if (r <= 0) {
|
||||
nextIndex = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
centroids.push([...vectors[nextIndex]]);
|
||||
}
|
||||
return centroids;
|
||||
}
|
||||
/**
|
||||
* Get the centroids for each subvector
|
||||
* @returns Array of centroid arrays
|
||||
*/
|
||||
getCentroids() {
|
||||
return this.centroids;
|
||||
}
|
||||
/**
|
||||
* Set the centroids for each subvector
|
||||
* @param centroids Array of centroid arrays
|
||||
*/
|
||||
setCentroids(centroids) {
|
||||
this.centroids = centroids;
|
||||
this.numSubvectors = centroids.length;
|
||||
this.numCentroids = centroids[0].length;
|
||||
this.initialized = true;
|
||||
}
|
||||
/**
|
||||
* Get the dimension of the vectors
|
||||
* @returns Dimension
|
||||
*/
|
||||
getDimension() {
|
||||
return this.dimension;
|
||||
}
|
||||
/**
|
||||
* Set the dimension of the vectors
|
||||
* @param dimension Dimension
|
||||
*/
|
||||
setDimension(dimension) {
|
||||
this.dimension = dimension;
|
||||
this.subvectorSize = Math.ceil(dimension / this.numSubvectors);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Optimized HNSW Index implementation
|
||||
* Extends the base HNSW implementation with support for large datasets
|
||||
* Uses product quantization for dimensionality reduction and disk-based storage when needed
|
||||
*/
|
||||
export class HNSWIndexOptimized extends HNSWIndex {
|
||||
constructor(config = {}, distanceFunction, storage = null) {
|
||||
// Initialize base HNSW index with standard config
|
||||
super(config, distanceFunction);
|
||||
this.productQuantizer = null;
|
||||
this.storage = null;
|
||||
this.useDiskBasedIndex = false;
|
||||
this.useProductQuantization = false;
|
||||
this.quantizedVectors = new Map();
|
||||
this.memoryUsage = 0;
|
||||
this.vectorCount = 0;
|
||||
// Thread safety for memory usage tracking
|
||||
this.memoryUpdateLock = Promise.resolve();
|
||||
// Set optimized config
|
||||
this.optimizedConfig = { ...DEFAULT_OPTIMIZED_CONFIG, ...config };
|
||||
// Set storage adapter
|
||||
this.storage = storage;
|
||||
// Initialize product quantizer if enabled
|
||||
if (this.optimizedConfig.productQuantization?.enabled) {
|
||||
this.useProductQuantization = true;
|
||||
this.productQuantizer = new ProductQuantizer(this.optimizedConfig.productQuantization.numSubvectors, this.optimizedConfig.productQuantization.numCentroids);
|
||||
}
|
||||
// Set disk-based index flag
|
||||
this.useDiskBasedIndex = this.optimizedConfig.useDiskBasedIndex || false;
|
||||
}
|
||||
/**
|
||||
* Thread-safe method to update memory usage
|
||||
* @param memoryDelta Change in memory usage (can be negative)
|
||||
* @param vectorCountDelta Change in vector count (can be negative)
|
||||
*/
|
||||
async updateMemoryUsage(memoryDelta, vectorCountDelta) {
|
||||
this.memoryUpdateLock = this.memoryUpdateLock.then(async () => {
|
||||
this.memoryUsage = Math.max(0, this.memoryUsage + memoryDelta);
|
||||
this.vectorCount = Math.max(0, this.vectorCount + vectorCountDelta);
|
||||
});
|
||||
await this.memoryUpdateLock;
|
||||
}
|
||||
/**
|
||||
* Thread-safe method to get current memory usage
|
||||
* @returns Current memory usage and vector count
|
||||
*/
|
||||
async getMemoryUsageAsync() {
|
||||
await this.memoryUpdateLock;
|
||||
return {
|
||||
memoryUsage: this.memoryUsage,
|
||||
vectorCount: this.vectorCount
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Add a vector to the index
|
||||
* Uses product quantization if enabled and memory threshold is exceeded
|
||||
*/
|
||||
async addItem(item) {
|
||||
// Check if item is defined
|
||||
if (!item) {
|
||||
throw new Error('Item is undefined or null');
|
||||
}
|
||||
const { id, vector } = item;
|
||||
// Check if vector is defined
|
||||
if (!vector) {
|
||||
throw new Error('Vector is undefined or null');
|
||||
}
|
||||
// Estimate memory usage for this vector
|
||||
const vectorMemory = vector.length * 8; // 8 bytes per number (Float64)
|
||||
const connectionsMemory = this.optimizedConfig.M * this.optimizedConfig.ml * 16; // Estimate for connections
|
||||
const totalMemory = vectorMemory + connectionsMemory;
|
||||
// Update memory usage estimate (thread-safe)
|
||||
await this.updateMemoryUsage(totalMemory, 1);
|
||||
// Check if we should switch to product quantization
|
||||
const currentMemoryUsage = await this.getMemoryUsageAsync();
|
||||
if (this.useProductQuantization &&
|
||||
currentMemoryUsage.memoryUsage > this.optimizedConfig.memoryThreshold &&
|
||||
this.productQuantizer &&
|
||||
!this.productQuantizer.getDimension()) {
|
||||
// Initialize product quantizer with existing vectors
|
||||
this.initializeProductQuantizer();
|
||||
}
|
||||
// If product quantization is active, quantize the vector
|
||||
if (this.useProductQuantization &&
|
||||
this.productQuantizer &&
|
||||
this.productQuantizer.getDimension() > 0) {
|
||||
// Quantize the vector
|
||||
const codes = this.productQuantizer.quantize(vector);
|
||||
// Store the quantized vector
|
||||
this.quantizedVectors.set(id, codes);
|
||||
// Reconstruct the vector for indexing
|
||||
const reconstructedVector = this.productQuantizer.reconstruct(codes);
|
||||
// Add the reconstructed vector to the index
|
||||
return await super.addItem({ id, vector: reconstructedVector });
|
||||
}
|
||||
// If disk-based index is active and storage is available, store the vector
|
||||
if (this.useDiskBasedIndex && this.storage) {
|
||||
// Create a noun object
|
||||
const noun = {
|
||||
id,
|
||||
vector,
|
||||
connections: new Map(),
|
||||
level: 0
|
||||
};
|
||||
// Store the noun
|
||||
this.storage.saveNoun(noun).catch((error) => {
|
||||
console.error(`Failed to save noun ${id} to storage:`, error);
|
||||
});
|
||||
}
|
||||
// Add the vector to the in-memory index
|
||||
return await super.addItem(item);
|
||||
}
|
||||
/**
|
||||
* Search for nearest neighbors
|
||||
* Uses product quantization if enabled
|
||||
*/
|
||||
async search(queryVector, k = 10) {
|
||||
// Check if query vector is defined
|
||||
if (!queryVector) {
|
||||
throw new Error('Query vector is undefined or null');
|
||||
}
|
||||
// If product quantization is active, quantize the query vector
|
||||
if (this.useProductQuantization &&
|
||||
this.productQuantizer &&
|
||||
this.productQuantizer.getDimension() > 0) {
|
||||
// Quantize the query vector
|
||||
const codes = this.productQuantizer.quantize(queryVector);
|
||||
// Reconstruct the query vector
|
||||
const reconstructedVector = this.productQuantizer.reconstruct(codes);
|
||||
// Search with the reconstructed vector
|
||||
return await super.search(reconstructedVector, k);
|
||||
}
|
||||
// Otherwise, use the standard search
|
||||
return await super.search(queryVector, k);
|
||||
}
|
||||
/**
|
||||
* Remove an item from the index
|
||||
*/
|
||||
removeItem(id) {
|
||||
// If product quantization is active, remove the quantized vector
|
||||
if (this.useProductQuantization) {
|
||||
this.quantizedVectors.delete(id);
|
||||
}
|
||||
// If disk-based index is active and storage is available, remove the vector from storage
|
||||
if (this.useDiskBasedIndex && this.storage) {
|
||||
this.storage.deleteNoun(id).catch((error) => {
|
||||
console.error(`Failed to delete noun ${id} from storage:`, error);
|
||||
});
|
||||
}
|
||||
// Update memory usage estimate (async operation, but don't block removal)
|
||||
this.getMemoryUsageAsync().then((currentMemoryUsage) => {
|
||||
if (currentMemoryUsage.vectorCount > 0) {
|
||||
const memoryPerVector = currentMemoryUsage.memoryUsage / currentMemoryUsage.vectorCount;
|
||||
this.updateMemoryUsage(-memoryPerVector, -1);
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error('Failed to update memory usage after removal:', error);
|
||||
});
|
||||
// Remove the item from the in-memory index
|
||||
return super.removeItem(id);
|
||||
}
|
||||
/**
|
||||
* Clear the index
|
||||
*/
|
||||
async clear() {
|
||||
// Clear product quantization data
|
||||
if (this.useProductQuantization) {
|
||||
this.quantizedVectors.clear();
|
||||
this.productQuantizer = new ProductQuantizer(this.optimizedConfig.productQuantization.numSubvectors, this.optimizedConfig.productQuantization.numCentroids);
|
||||
}
|
||||
// Reset memory usage (thread-safe)
|
||||
const currentMemoryUsage = await this.getMemoryUsageAsync();
|
||||
await this.updateMemoryUsage(-currentMemoryUsage.memoryUsage, -currentMemoryUsage.vectorCount);
|
||||
// Clear the in-memory index
|
||||
super.clear();
|
||||
}
|
||||
/**
|
||||
* Initialize product quantizer with existing vectors
|
||||
*/
|
||||
initializeProductQuantizer() {
|
||||
if (!this.productQuantizer) {
|
||||
return;
|
||||
}
|
||||
// Get all vectors from the index
|
||||
const nouns = super.getNouns();
|
||||
const vectors = [];
|
||||
// Extract vectors
|
||||
for (const [_, noun] of nouns) {
|
||||
vectors.push(noun.vector);
|
||||
}
|
||||
// Train the product quantizer
|
||||
if (vectors.length > 0) {
|
||||
this.productQuantizer.train(vectors);
|
||||
// Quantize all existing vectors
|
||||
for (const [id, noun] of nouns) {
|
||||
const codes = this.productQuantizer.quantize(noun.vector);
|
||||
this.quantizedVectors.set(id, codes);
|
||||
}
|
||||
console.log(`Initialized product quantizer with ${vectors.length} vectors`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get the product quantizer
|
||||
* @returns Product quantizer or null if not enabled
|
||||
*/
|
||||
getProductQuantizer() {
|
||||
return this.productQuantizer;
|
||||
}
|
||||
/**
|
||||
* Get the optimized configuration
|
||||
* @returns Optimized configuration
|
||||
*/
|
||||
getOptimizedConfig() {
|
||||
return { ...this.optimizedConfig };
|
||||
}
|
||||
/**
|
||||
* Get the estimated memory usage
|
||||
* @returns Estimated memory usage in bytes
|
||||
*/
|
||||
getMemoryUsage() {
|
||||
return this.memoryUsage;
|
||||
}
|
||||
/**
|
||||
* Set the storage adapter
|
||||
* @param storage Storage adapter
|
||||
*/
|
||||
setStorage(storage) {
|
||||
this.storage = storage;
|
||||
}
|
||||
/**
|
||||
* Get the storage adapter
|
||||
* @returns Storage adapter or null if not set
|
||||
*/
|
||||
getStorage() {
|
||||
return this.storage;
|
||||
}
|
||||
/**
|
||||
* Set whether to use disk-based index
|
||||
* @param useDiskBasedIndex Whether to use disk-based index
|
||||
*/
|
||||
setUseDiskBasedIndex(useDiskBasedIndex) {
|
||||
this.useDiskBasedIndex = useDiskBasedIndex;
|
||||
}
|
||||
/**
|
||||
* Get whether disk-based index is used
|
||||
* @returns Whether disk-based index is used
|
||||
*/
|
||||
getUseDiskBasedIndex() {
|
||||
return this.useDiskBasedIndex;
|
||||
}
|
||||
/**
|
||||
* Set whether to use product quantization
|
||||
* @param useProductQuantization Whether to use product quantization
|
||||
*/
|
||||
setUseProductQuantization(useProductQuantization) {
|
||||
this.useProductQuantization = useProductQuantization;
|
||||
}
|
||||
/**
|
||||
* Get whether product quantization is used
|
||||
* @returns Whether product quantization is used
|
||||
*/
|
||||
getUseProductQuantization() {
|
||||
return this.useProductQuantization;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=hnswIndexOptimized.js.map
|
||||
1
dist/hnsw/hnswIndexOptimized.js.map
vendored
Normal file
1
dist/hnsw/hnswIndexOptimized.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
97
dist/hnsw/optimizedHNSWIndex.d.ts
vendored
Normal file
97
dist/hnsw/optimizedHNSWIndex.d.ts
vendored
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* Optimized HNSW Index for Large-Scale Vector Search
|
||||
* Implements dynamic parameter tuning and performance optimizations
|
||||
*/
|
||||
import { DistanceFunction, HNSWConfig, Vector, VectorDocument } from '../coreTypes.js';
|
||||
import { HNSWIndex } from './hnswIndex.js';
|
||||
export interface OptimizedHNSWConfig extends HNSWConfig {
|
||||
dynamicParameterTuning?: boolean;
|
||||
targetSearchLatency?: number;
|
||||
targetRecall?: number;
|
||||
maxNodes?: number;
|
||||
memoryBudget?: number;
|
||||
diskCacheEnabled?: boolean;
|
||||
compressionEnabled?: boolean;
|
||||
performanceTracking?: boolean;
|
||||
adaptiveEfSearch?: boolean;
|
||||
levelMultiplier?: number;
|
||||
seedConnections?: number;
|
||||
pruningStrategy?: 'simple' | 'diverse' | 'hybrid';
|
||||
}
|
||||
interface PerformanceMetrics {
|
||||
averageSearchTime: number;
|
||||
averageRecall: number;
|
||||
memoryUsage: number;
|
||||
indexSize: number;
|
||||
apiCalls: number;
|
||||
cacheHitRate: number;
|
||||
}
|
||||
interface DynamicParameters {
|
||||
efSearch: number;
|
||||
efConstruction: number;
|
||||
M: number;
|
||||
ml: number;
|
||||
}
|
||||
/**
|
||||
* Optimized HNSW Index with dynamic parameter tuning for large datasets
|
||||
*/
|
||||
export declare class OptimizedHNSWIndex extends HNSWIndex {
|
||||
private optimizedConfig;
|
||||
private performanceMetrics;
|
||||
private dynamicParams;
|
||||
private searchHistory;
|
||||
private parameterTuningInterval?;
|
||||
constructor(config?: Partial<OptimizedHNSWConfig>, distanceFunction?: DistanceFunction);
|
||||
/**
|
||||
* Optimized search with dynamic parameter adjustment
|
||||
*/
|
||||
search(queryVector: Vector, k?: number, filter?: (id: string) => Promise<boolean>): Promise<Array<[string, number]>>;
|
||||
/**
|
||||
* Dynamically adjust efSearch based on performance requirements
|
||||
*/
|
||||
private adjustEfSearch;
|
||||
/**
|
||||
* Record search performance metrics
|
||||
*/
|
||||
private recordSearchMetrics;
|
||||
/**
|
||||
* Check memory usage and trigger optimizations
|
||||
*/
|
||||
private checkMemoryUsage;
|
||||
/**
|
||||
* Compress index to reduce memory usage (placeholder)
|
||||
*/
|
||||
private compressIndex;
|
||||
/**
|
||||
* Start automatic parameter tuning
|
||||
*/
|
||||
private startParameterTuning;
|
||||
/**
|
||||
* Automatic parameter tuning based on performance metrics
|
||||
*/
|
||||
private tuneParameters;
|
||||
/**
|
||||
* Get optimized configuration recommendations for current dataset size
|
||||
*/
|
||||
getOptimizedConfig(): OptimizedHNSWConfig;
|
||||
/**
|
||||
* Get current performance metrics
|
||||
*/
|
||||
getPerformanceMetrics(): PerformanceMetrics & {
|
||||
currentParams: DynamicParameters;
|
||||
searchHistorySize: number;
|
||||
};
|
||||
/**
|
||||
* Apply optimized bulk insertion strategy
|
||||
*/
|
||||
bulkInsert(items: VectorDocument[]): Promise<string[]>;
|
||||
/**
|
||||
* Optimize insertion order to improve index quality
|
||||
*/
|
||||
private optimizeInsertionOrder;
|
||||
/**
|
||||
* Cleanup resources
|
||||
*/
|
||||
destroy(): void;
|
||||
}
|
||||
export {};
|
||||
313
dist/hnsw/optimizedHNSWIndex.js
vendored
Normal file
313
dist/hnsw/optimizedHNSWIndex.js
vendored
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
/**
|
||||
* Optimized HNSW Index for Large-Scale Vector Search
|
||||
* Implements dynamic parameter tuning and performance optimizations
|
||||
*/
|
||||
import { HNSWIndex } from './hnswIndex.js';
|
||||
import { euclideanDistance } from '../utils/index.js';
|
||||
/**
|
||||
* Optimized HNSW Index with dynamic parameter tuning for large datasets
|
||||
*/
|
||||
export class OptimizedHNSWIndex extends HNSWIndex {
|
||||
constructor(config = {}, distanceFunction = euclideanDistance) {
|
||||
// Set optimized defaults for large scale
|
||||
const defaultConfig = {
|
||||
M: 32, // Higher connectivity for better recall
|
||||
efConstruction: 400, // Better build quality
|
||||
efSearch: 100, // Dynamic - will be tuned
|
||||
ml: 24, // Deeper hierarchy
|
||||
useDiskBasedIndex: false, // Added missing property
|
||||
dynamicParameterTuning: true,
|
||||
targetSearchLatency: 100, // 100ms target
|
||||
targetRecall: 0.95, // 95% recall target
|
||||
maxNodes: 1000000, // 1M node limit
|
||||
memoryBudget: 8 * 1024 * 1024 * 1024, // 8GB
|
||||
diskCacheEnabled: true,
|
||||
compressionEnabled: false, // Disabled by default for compatibility
|
||||
performanceTracking: true,
|
||||
adaptiveEfSearch: true,
|
||||
levelMultiplier: 16,
|
||||
seedConnections: 8,
|
||||
pruningStrategy: 'hybrid'
|
||||
};
|
||||
const mergedConfig = { ...defaultConfig, ...config };
|
||||
// Initialize parent with base config
|
||||
super({
|
||||
M: mergedConfig.M,
|
||||
efConstruction: mergedConfig.efConstruction,
|
||||
efSearch: mergedConfig.efSearch,
|
||||
ml: mergedConfig.ml
|
||||
}, distanceFunction, { useParallelization: true });
|
||||
this.searchHistory = [];
|
||||
this.optimizedConfig = mergedConfig;
|
||||
// Initialize dynamic parameters
|
||||
this.dynamicParams = {
|
||||
efSearch: mergedConfig.efSearch,
|
||||
efConstruction: mergedConfig.efConstruction,
|
||||
M: mergedConfig.M,
|
||||
ml: mergedConfig.ml
|
||||
};
|
||||
// Initialize performance metrics
|
||||
this.performanceMetrics = {
|
||||
averageSearchTime: 0,
|
||||
averageRecall: 0,
|
||||
memoryUsage: 0,
|
||||
indexSize: 0,
|
||||
apiCalls: 0,
|
||||
cacheHitRate: 0
|
||||
};
|
||||
// Start parameter tuning if enabled
|
||||
if (this.optimizedConfig.dynamicParameterTuning) {
|
||||
this.startParameterTuning();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Optimized search with dynamic parameter adjustment
|
||||
*/
|
||||
async search(queryVector, k = 10, filter) {
|
||||
const startTime = Date.now();
|
||||
// Adjust efSearch dynamically based on k and performance history
|
||||
if (this.optimizedConfig.adaptiveEfSearch) {
|
||||
this.adjustEfSearch(k);
|
||||
}
|
||||
// Check memory usage and trigger optimizations if needed
|
||||
if (this.optimizedConfig.performanceTracking) {
|
||||
this.checkMemoryUsage();
|
||||
}
|
||||
// Perform the search with current parameters
|
||||
const originalConfig = this.getConfig();
|
||||
// Temporarily update search parameters
|
||||
const tempConfig = {
|
||||
...originalConfig,
|
||||
efSearch: this.dynamicParams.efSearch
|
||||
};
|
||||
// Use the parent's search method with optimized parameters
|
||||
let results;
|
||||
try {
|
||||
// This is a simplified approach - in practice, we'd need to modify
|
||||
// the parent class to accept runtime parameter changes
|
||||
results = await super.search(queryVector, k, filter);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Optimized search failed, falling back to default:', error);
|
||||
results = await super.search(queryVector, k, filter);
|
||||
}
|
||||
// Record performance metrics
|
||||
const searchTime = Date.now() - startTime;
|
||||
this.recordSearchMetrics(searchTime, k, results.length);
|
||||
return results;
|
||||
}
|
||||
/**
|
||||
* Dynamically adjust efSearch based on performance requirements
|
||||
*/
|
||||
adjustEfSearch(k) {
|
||||
const recentSearches = this.searchHistory.slice(-10);
|
||||
if (recentSearches.length < 3) {
|
||||
// Not enough data, use heuristic
|
||||
this.dynamicParams.efSearch = Math.max(k * 2, 50);
|
||||
return;
|
||||
}
|
||||
const averageLatency = recentSearches.reduce((sum, s) => sum + s.latency, 0) / recentSearches.length;
|
||||
const targetLatency = this.optimizedConfig.targetSearchLatency;
|
||||
// Adjust efSearch based on latency performance
|
||||
if (averageLatency > targetLatency * 1.2) {
|
||||
// Too slow, reduce efSearch
|
||||
this.dynamicParams.efSearch = Math.max(Math.floor(this.dynamicParams.efSearch * 0.9), k);
|
||||
}
|
||||
else if (averageLatency < targetLatency * 0.8) {
|
||||
// Fast enough, can increase efSearch for better recall
|
||||
this.dynamicParams.efSearch = Math.min(Math.floor(this.dynamicParams.efSearch * 1.1), 500 // Maximum efSearch
|
||||
);
|
||||
}
|
||||
// Ensure efSearch is at least k
|
||||
this.dynamicParams.efSearch = Math.max(this.dynamicParams.efSearch, k);
|
||||
}
|
||||
/**
|
||||
* Record search performance metrics
|
||||
*/
|
||||
recordSearchMetrics(latency, k, resultCount) {
|
||||
if (!this.optimizedConfig.performanceTracking) {
|
||||
return;
|
||||
}
|
||||
// Add to search history
|
||||
this.searchHistory.push({
|
||||
latency,
|
||||
k,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
// Keep only recent history (last 100 searches)
|
||||
if (this.searchHistory.length > 100) {
|
||||
this.searchHistory.shift();
|
||||
}
|
||||
// Update performance metrics
|
||||
const recentSearches = this.searchHistory.slice(-20);
|
||||
this.performanceMetrics.averageSearchTime =
|
||||
recentSearches.reduce((sum, s) => sum + s.latency, 0) / recentSearches.length;
|
||||
// Estimate recall (simplified - would need ground truth for accurate measurement)
|
||||
this.performanceMetrics.averageRecall = Math.min(resultCount / k, 1.0);
|
||||
}
|
||||
/**
|
||||
* Check memory usage and trigger optimizations
|
||||
*/
|
||||
checkMemoryUsage() {
|
||||
// Estimate memory usage (simplified)
|
||||
const estimatedMemory = this.size() * 1000; // Rough estimate per node
|
||||
this.performanceMetrics.memoryUsage = estimatedMemory;
|
||||
if (estimatedMemory > this.optimizedConfig.memoryBudget * 0.9) {
|
||||
console.warn('Memory usage approaching limit, consider index partitioning');
|
||||
// Could trigger automatic partitioning or compression here
|
||||
if (this.optimizedConfig.compressionEnabled) {
|
||||
this.compressIndex();
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Compress index to reduce memory usage (placeholder)
|
||||
*/
|
||||
compressIndex() {
|
||||
console.log('Index compression not implemented yet');
|
||||
// This would implement vector quantization or other compression techniques
|
||||
}
|
||||
/**
|
||||
* Start automatic parameter tuning
|
||||
*/
|
||||
startParameterTuning() {
|
||||
this.parameterTuningInterval = setInterval(() => {
|
||||
this.tuneParameters();
|
||||
}, 30000); // Tune every 30 seconds
|
||||
}
|
||||
/**
|
||||
* Automatic parameter tuning based on performance metrics
|
||||
*/
|
||||
tuneParameters() {
|
||||
if (this.searchHistory.length < 10) {
|
||||
return; // Not enough data
|
||||
}
|
||||
const recentSearches = this.searchHistory.slice(-20);
|
||||
const averageLatency = recentSearches.reduce((sum, s) => sum + s.latency, 0) / recentSearches.length;
|
||||
// Tune based on performance vs targets
|
||||
const latencyRatio = averageLatency / this.optimizedConfig.targetSearchLatency;
|
||||
const recallRatio = this.performanceMetrics.averageRecall / this.optimizedConfig.targetRecall;
|
||||
// Adjust M (connectivity) for long-term performance
|
||||
if (this.size() > 10000) { // Only tune for larger indices
|
||||
if (recallRatio < 0.95 && latencyRatio < 1.5) {
|
||||
// Recall is low but we have latency budget, increase M
|
||||
this.dynamicParams.M = Math.min(this.dynamicParams.M + 2, 64);
|
||||
}
|
||||
else if (latencyRatio > 1.2 && recallRatio > 1.0) {
|
||||
// Latency is high but recall is good, can reduce M
|
||||
this.dynamicParams.M = Math.max(this.dynamicParams.M - 2, 16);
|
||||
}
|
||||
}
|
||||
console.log(`Parameter tuning: efSearch=${this.dynamicParams.efSearch}, M=${this.dynamicParams.M}, latency=${averageLatency.toFixed(1)}ms`);
|
||||
}
|
||||
/**
|
||||
* Get optimized configuration recommendations for current dataset size
|
||||
*/
|
||||
getOptimizedConfig() {
|
||||
const currentSize = this.size();
|
||||
let recommendedConfig = {};
|
||||
if (currentSize < 10000) {
|
||||
// Small dataset - optimize for speed
|
||||
recommendedConfig = {
|
||||
M: 16,
|
||||
efConstruction: 200,
|
||||
efSearch: 50,
|
||||
ml: 16
|
||||
};
|
||||
}
|
||||
else if (currentSize < 100000) {
|
||||
// Medium dataset - balance speed and recall
|
||||
recommendedConfig = {
|
||||
M: 24,
|
||||
efConstruction: 300,
|
||||
efSearch: 75,
|
||||
ml: 20
|
||||
};
|
||||
}
|
||||
else if (currentSize < 1000000) {
|
||||
// Large dataset - optimize for recall
|
||||
recommendedConfig = {
|
||||
M: 32,
|
||||
efConstruction: 400,
|
||||
efSearch: 100,
|
||||
ml: 24
|
||||
};
|
||||
}
|
||||
else {
|
||||
// Very large dataset - maximum quality
|
||||
recommendedConfig = {
|
||||
M: 48,
|
||||
efConstruction: 500,
|
||||
efSearch: 150,
|
||||
ml: 28
|
||||
};
|
||||
}
|
||||
return {
|
||||
...this.optimizedConfig,
|
||||
...recommendedConfig
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Get current performance metrics
|
||||
*/
|
||||
getPerformanceMetrics() {
|
||||
return {
|
||||
...this.performanceMetrics,
|
||||
currentParams: { ...this.dynamicParams },
|
||||
searchHistorySize: this.searchHistory.length
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Apply optimized bulk insertion strategy
|
||||
*/
|
||||
async bulkInsert(items) {
|
||||
console.log(`Starting optimized bulk insert of ${items.length} items`);
|
||||
// Sort items to optimize insertion order (by vector similarity)
|
||||
const sortedItems = this.optimizeInsertionOrder(items);
|
||||
// Temporarily adjust construction parameters for bulk operations
|
||||
const originalEfConstruction = this.dynamicParams.efConstruction;
|
||||
this.dynamicParams.efConstruction = Math.min(this.dynamicParams.efConstruction * 1.5, 800);
|
||||
const results = [];
|
||||
const batchSize = 100;
|
||||
try {
|
||||
// Process in batches to manage memory
|
||||
for (let i = 0; i < sortedItems.length; i += batchSize) {
|
||||
const batch = sortedItems.slice(i, i + batchSize);
|
||||
for (const item of batch) {
|
||||
const id = await this.addItem(item);
|
||||
results.push(id);
|
||||
}
|
||||
// Periodic memory check
|
||||
if (i % (batchSize * 10) === 0) {
|
||||
this.checkMemoryUsage();
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
// Restore original construction parameters
|
||||
this.dynamicParams.efConstruction = originalEfConstruction;
|
||||
}
|
||||
console.log(`Completed bulk insert of ${results.length} items`);
|
||||
return results;
|
||||
}
|
||||
/**
|
||||
* Optimize insertion order to improve index quality
|
||||
*/
|
||||
optimizeInsertionOrder(items) {
|
||||
if (items.length < 100) {
|
||||
return items; // Not worth optimizing small batches
|
||||
}
|
||||
// Simple clustering-based ordering
|
||||
// In practice, you might use more sophisticated methods
|
||||
return items.sort(() => Math.random() - 0.5); // Shuffle for now
|
||||
}
|
||||
/**
|
||||
* Cleanup resources
|
||||
*/
|
||||
destroy() {
|
||||
if (this.parameterTuningInterval) {
|
||||
clearInterval(this.parameterTuningInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=optimizedHNSWIndex.js.map
|
||||
1
dist/hnsw/optimizedHNSWIndex.js.map
vendored
Normal file
1
dist/hnsw/optimizedHNSWIndex.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
101
dist/hnsw/partitionedHNSWIndex.d.ts
vendored
Normal file
101
dist/hnsw/partitionedHNSWIndex.d.ts
vendored
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
/**
|
||||
* Partitioned HNSW Index for Large-Scale Vector Search
|
||||
* Implements sharding strategies to handle millions of vectors efficiently
|
||||
*/
|
||||
import { DistanceFunction, HNSWConfig, Vector, VectorDocument } from '../coreTypes.js';
|
||||
export interface PartitionConfig {
|
||||
maxNodesPerPartition: number;
|
||||
partitionStrategy: 'semantic' | 'hash';
|
||||
semanticClusters?: number;
|
||||
autoTuneSemanticClusters?: boolean;
|
||||
}
|
||||
export interface PartitionMetadata {
|
||||
id: string;
|
||||
nodeCount: number;
|
||||
bounds?: {
|
||||
centroid: Vector;
|
||||
radius: number;
|
||||
};
|
||||
strategy: string;
|
||||
created: Date;
|
||||
}
|
||||
/**
|
||||
* Partitioned HNSW Index that splits large datasets across multiple smaller indices
|
||||
* This enables efficient search across millions of vectors by reducing memory usage
|
||||
* and parallelizing search operations
|
||||
*/
|
||||
export declare class PartitionedHNSWIndex {
|
||||
private partitions;
|
||||
private partitionMetadata;
|
||||
private config;
|
||||
private hnswConfig;
|
||||
private distanceFunction;
|
||||
private dimension;
|
||||
private nextPartitionId;
|
||||
constructor(partitionConfig?: Partial<PartitionConfig>, hnswConfig?: Partial<HNSWConfig>, distanceFunction?: DistanceFunction);
|
||||
/**
|
||||
* Add a vector to the partitioned index
|
||||
*/
|
||||
addItem(item: VectorDocument): Promise<string>;
|
||||
/**
|
||||
* Search across all partitions for nearest neighbors
|
||||
*/
|
||||
search(queryVector: Vector, k?: number, searchScope?: {
|
||||
partitionIds?: string[];
|
||||
maxPartitions?: number;
|
||||
}): Promise<Array<[string, number]>>;
|
||||
/**
|
||||
* Select the appropriate partition for a new item
|
||||
* Automatically chooses semantic partitioning when beneficial, falls back to hash
|
||||
*/
|
||||
private selectPartition;
|
||||
/**
|
||||
* Hash-based partitioning for even distribution
|
||||
*/
|
||||
private hashPartition;
|
||||
/**
|
||||
* Semantic clustering partitioning
|
||||
*/
|
||||
private semanticPartition;
|
||||
/**
|
||||
* Auto-tune semantic clusters based on dataset size and performance
|
||||
*/
|
||||
private autoTuneSemanticClusters;
|
||||
/**
|
||||
* Select which partitions to search based on query
|
||||
*/
|
||||
private selectSearchPartitions;
|
||||
/**
|
||||
* Update partition bounds for semantic clustering
|
||||
*/
|
||||
private updatePartitionBounds;
|
||||
/**
|
||||
* Split an overgrown partition into smaller partitions
|
||||
*/
|
||||
private splitPartition;
|
||||
/**
|
||||
* Simple hash function for consistent partitioning
|
||||
*/
|
||||
private simpleHash;
|
||||
/**
|
||||
* Get partition statistics
|
||||
*/
|
||||
getPartitionStats(): {
|
||||
totalPartitions: number;
|
||||
totalNodes: number;
|
||||
averageNodesPerPartition: number;
|
||||
partitionDetails: PartitionMetadata[];
|
||||
};
|
||||
/**
|
||||
* Remove an item from the index
|
||||
*/
|
||||
removeItem(id: string): Promise<boolean>;
|
||||
/**
|
||||
* Clear all partitions
|
||||
*/
|
||||
clear(): void;
|
||||
/**
|
||||
* Get total size across all partitions
|
||||
*/
|
||||
size(): number;
|
||||
}
|
||||
304
dist/hnsw/partitionedHNSWIndex.js
vendored
Normal file
304
dist/hnsw/partitionedHNSWIndex.js
vendored
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
/**
|
||||
* Partitioned HNSW Index for Large-Scale Vector Search
|
||||
* Implements sharding strategies to handle millions of vectors efficiently
|
||||
*/
|
||||
import { HNSWIndex } from './hnswIndex.js';
|
||||
import { euclideanDistance } from '../utils/index.js';
|
||||
/**
|
||||
* Partitioned HNSW Index that splits large datasets across multiple smaller indices
|
||||
* This enables efficient search across millions of vectors by reducing memory usage
|
||||
* and parallelizing search operations
|
||||
*/
|
||||
export class PartitionedHNSWIndex {
|
||||
constructor(partitionConfig = {}, hnswConfig = {}, distanceFunction = euclideanDistance) {
|
||||
this.partitions = new Map();
|
||||
this.partitionMetadata = new Map();
|
||||
this.dimension = null;
|
||||
this.nextPartitionId = 0;
|
||||
this.config = {
|
||||
maxNodesPerPartition: 50000, // Optimal size for memory efficiency
|
||||
partitionStrategy: 'semantic', // Default to semantic for better performance
|
||||
semanticClusters: 8, // Auto-tuned based on dataset
|
||||
autoTuneSemanticClusters: true,
|
||||
...partitionConfig
|
||||
};
|
||||
// Optimized HNSW parameters for large scale
|
||||
this.hnswConfig = {
|
||||
M: 32, // Higher connectivity for better recall
|
||||
efConstruction: 400, // Better build quality
|
||||
efSearch: 100, // Balance speed vs accuracy
|
||||
ml: 24, // Deeper hierarchy
|
||||
...hnswConfig
|
||||
};
|
||||
this.distanceFunction = distanceFunction;
|
||||
}
|
||||
/**
|
||||
* Add a vector to the partitioned index
|
||||
*/
|
||||
async addItem(item) {
|
||||
if (this.dimension === null) {
|
||||
this.dimension = item.vector.length;
|
||||
}
|
||||
// Determine which partition this item belongs to
|
||||
const partitionId = await this.selectPartition(item);
|
||||
// Get or create the partition
|
||||
let partition = this.partitions.get(partitionId);
|
||||
if (!partition) {
|
||||
partition = new HNSWIndex(this.hnswConfig, this.distanceFunction, { useParallelization: true });
|
||||
this.partitions.set(partitionId, partition);
|
||||
// Initialize partition metadata
|
||||
this.partitionMetadata.set(partitionId, {
|
||||
id: partitionId,
|
||||
nodeCount: 0,
|
||||
strategy: this.config.partitionStrategy,
|
||||
created: new Date()
|
||||
});
|
||||
}
|
||||
// Add item to the selected partition
|
||||
await partition.addItem(item);
|
||||
// Update partition metadata
|
||||
const metadata = this.partitionMetadata.get(partitionId);
|
||||
metadata.nodeCount = partition.size();
|
||||
// Update bounds for semantic strategy
|
||||
if (this.config.partitionStrategy === 'semantic') {
|
||||
this.updatePartitionBounds(partitionId, item.vector);
|
||||
}
|
||||
// Check if partition is getting too large and needs splitting
|
||||
if (metadata.nodeCount > this.config.maxNodesPerPartition * 1.2) {
|
||||
await this.splitPartition(partitionId);
|
||||
}
|
||||
return item.id;
|
||||
}
|
||||
/**
|
||||
* Search across all partitions for nearest neighbors
|
||||
*/
|
||||
async search(queryVector, k = 10, searchScope) {
|
||||
if (this.partitions.size === 0) {
|
||||
return [];
|
||||
}
|
||||
// Determine which partitions to search
|
||||
const partitionsToSearch = await this.selectSearchPartitions(queryVector, searchScope);
|
||||
// Search partitions in parallel
|
||||
const searchPromises = partitionsToSearch.map(async (partitionId) => {
|
||||
const partition = this.partitions.get(partitionId);
|
||||
if (!partition)
|
||||
return [];
|
||||
// Search with higher k to get better global results
|
||||
const partitionK = Math.min(k * 2, partition.size());
|
||||
return partition.search(queryVector, partitionK);
|
||||
});
|
||||
const partitionResults = await Promise.all(searchPromises);
|
||||
// Merge and sort results from all partitions
|
||||
const allResults = [];
|
||||
for (const results of partitionResults) {
|
||||
allResults.push(...results);
|
||||
}
|
||||
// Sort by distance and return top k
|
||||
allResults.sort((a, b) => a[1] - b[1]);
|
||||
return allResults.slice(0, k);
|
||||
}
|
||||
/**
|
||||
* Select the appropriate partition for a new item
|
||||
* Automatically chooses semantic partitioning when beneficial, falls back to hash
|
||||
*/
|
||||
async selectPartition(item) {
|
||||
// Auto-tune semantic clusters based on current dataset size
|
||||
if (this.config.autoTuneSemanticClusters && this.config.partitionStrategy === 'semantic') {
|
||||
this.autoTuneSemanticClusters();
|
||||
}
|
||||
switch (this.config.partitionStrategy) {
|
||||
case 'semantic':
|
||||
return await this.semanticPartition(item.vector);
|
||||
case 'hash':
|
||||
default:
|
||||
return this.hashPartition(item.id);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Hash-based partitioning for even distribution
|
||||
*/
|
||||
hashPartition(id) {
|
||||
const hash = this.simpleHash(id);
|
||||
const existingPartitions = Array.from(this.partitions.keys());
|
||||
// Find partition with space, or create new one
|
||||
for (const partitionId of existingPartitions) {
|
||||
const metadata = this.partitionMetadata.get(partitionId);
|
||||
if (metadata && metadata.nodeCount < this.config.maxNodesPerPartition) {
|
||||
return partitionId;
|
||||
}
|
||||
}
|
||||
// Create new partition
|
||||
return `partition_${this.nextPartitionId++}`;
|
||||
}
|
||||
/**
|
||||
* Semantic clustering partitioning
|
||||
*/
|
||||
async semanticPartition(vector) {
|
||||
// Find closest partition centroid
|
||||
let closestPartition = '';
|
||||
let minDistance = Infinity;
|
||||
for (const [partitionId, metadata] of this.partitionMetadata.entries()) {
|
||||
if (metadata.bounds?.centroid) {
|
||||
const distance = this.distanceFunction(vector, metadata.bounds.centroid);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closestPartition = partitionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If no suitable partition found or it's full, create new one
|
||||
if (!closestPartition ||
|
||||
this.partitionMetadata.get(closestPartition).nodeCount >= this.config.maxNodesPerPartition) {
|
||||
closestPartition = `semantic_${this.nextPartitionId++}`;
|
||||
}
|
||||
return closestPartition;
|
||||
}
|
||||
/**
|
||||
* Auto-tune semantic clusters based on dataset size and performance
|
||||
*/
|
||||
autoTuneSemanticClusters() {
|
||||
const totalNodes = this.size();
|
||||
const currentPartitions = this.partitions.size;
|
||||
// Optimal clusters based on dataset size
|
||||
let optimalClusters = Math.max(4, Math.min(32, Math.floor(totalNodes / 10000)));
|
||||
// Adjust based on current partition performance
|
||||
if (currentPartitions > 0) {
|
||||
const avgNodesPerPartition = totalNodes / currentPartitions;
|
||||
if (avgNodesPerPartition > this.config.maxNodesPerPartition * 0.8) {
|
||||
// Partitions are getting full, increase clusters
|
||||
optimalClusters = Math.min(32, this.config.semanticClusters + 2);
|
||||
}
|
||||
else if (avgNodesPerPartition < this.config.maxNodesPerPartition * 0.3 && currentPartitions > 4) {
|
||||
// Partitions are underutilized, decrease clusters
|
||||
optimalClusters = Math.max(4, this.config.semanticClusters - 1);
|
||||
}
|
||||
}
|
||||
if (optimalClusters !== this.config.semanticClusters) {
|
||||
console.log(`Auto-tuning semantic clusters: ${this.config.semanticClusters} → ${optimalClusters}`);
|
||||
this.config.semanticClusters = optimalClusters;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Select which partitions to search based on query
|
||||
*/
|
||||
async selectSearchPartitions(queryVector, searchScope) {
|
||||
if (searchScope?.partitionIds) {
|
||||
return searchScope.partitionIds.filter(id => this.partitions.has(id));
|
||||
}
|
||||
const maxPartitions = searchScope?.maxPartitions || Math.min(5, this.partitions.size);
|
||||
if (this.config.partitionStrategy === 'semantic') {
|
||||
// Search partitions with closest centroids
|
||||
const distances = [];
|
||||
for (const [partitionId, metadata] of this.partitionMetadata.entries()) {
|
||||
if (metadata.bounds?.centroid) {
|
||||
const distance = this.distanceFunction(queryVector, metadata.bounds.centroid);
|
||||
distances.push([partitionId, distance]);
|
||||
}
|
||||
}
|
||||
distances.sort((a, b) => a[1] - b[1]);
|
||||
return distances.slice(0, maxPartitions).map(([id]) => id);
|
||||
}
|
||||
// For other strategies, search all partitions or random subset
|
||||
const allPartitionIds = Array.from(this.partitions.keys());
|
||||
if (allPartitionIds.length <= maxPartitions) {
|
||||
return allPartitionIds;
|
||||
}
|
||||
// Return random subset
|
||||
const shuffled = [...allPartitionIds].sort(() => Math.random() - 0.5);
|
||||
return shuffled.slice(0, maxPartitions);
|
||||
}
|
||||
/**
|
||||
* Update partition bounds for semantic clustering
|
||||
*/
|
||||
updatePartitionBounds(partitionId, vector) {
|
||||
const metadata = this.partitionMetadata.get(partitionId);
|
||||
if (!metadata.bounds) {
|
||||
metadata.bounds = {
|
||||
centroid: [...vector],
|
||||
radius: 0
|
||||
};
|
||||
return;
|
||||
}
|
||||
// Update centroid using incremental mean
|
||||
const { centroid } = metadata.bounds;
|
||||
const nodeCount = metadata.nodeCount;
|
||||
for (let i = 0; i < centroid.length; i++) {
|
||||
centroid[i] = (centroid[i] * (nodeCount - 1) + vector[i]) / nodeCount;
|
||||
}
|
||||
// Update radius
|
||||
const distance = this.distanceFunction(vector, centroid);
|
||||
metadata.bounds.radius = Math.max(metadata.bounds.radius, distance);
|
||||
}
|
||||
/**
|
||||
* Split an overgrown partition into smaller partitions
|
||||
*/
|
||||
async splitPartition(partitionId) {
|
||||
const partition = this.partitions.get(partitionId);
|
||||
if (!partition)
|
||||
return;
|
||||
console.log(`Splitting partition ${partitionId} with ${partition.size()} nodes`);
|
||||
// For now, we'll implement a simple strategy
|
||||
// In a full implementation, you'd want to analyze the data distribution
|
||||
// and create more intelligent splits
|
||||
// This is a placeholder - actual implementation would require
|
||||
// accessing the internal nodes of the HNSW index
|
||||
}
|
||||
/**
|
||||
* Simple hash function for consistent partitioning
|
||||
*/
|
||||
simpleHash(str) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash; // Convert to 32-bit integer
|
||||
}
|
||||
return Math.abs(hash);
|
||||
}
|
||||
/**
|
||||
* Get partition statistics
|
||||
*/
|
||||
getPartitionStats() {
|
||||
const partitionDetails = Array.from(this.partitionMetadata.values());
|
||||
const totalNodes = partitionDetails.reduce((sum, p) => sum + p.nodeCount, 0);
|
||||
return {
|
||||
totalPartitions: partitionDetails.length,
|
||||
totalNodes,
|
||||
averageNodesPerPartition: totalNodes / partitionDetails.length || 0,
|
||||
partitionDetails
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Remove an item from the index
|
||||
*/
|
||||
async removeItem(id) {
|
||||
// Find which partition contains this item
|
||||
for (const [partitionId, partition] of this.partitions.entries()) {
|
||||
if (partition.removeItem(id)) {
|
||||
// Update metadata
|
||||
const metadata = this.partitionMetadata.get(partitionId);
|
||||
metadata.nodeCount = partition.size();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Clear all partitions
|
||||
*/
|
||||
clear() {
|
||||
for (const partition of this.partitions.values()) {
|
||||
partition.clear();
|
||||
}
|
||||
this.partitions.clear();
|
||||
this.partitionMetadata.clear();
|
||||
this.nextPartitionId = 0;
|
||||
}
|
||||
/**
|
||||
* Get total size across all partitions
|
||||
*/
|
||||
size() {
|
||||
return Array.from(this.partitions.values()).reduce((sum, partition) => sum + partition.size(), 0);
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=partitionedHNSWIndex.js.map
|
||||
1
dist/hnsw/partitionedHNSWIndex.js.map
vendored
Normal file
1
dist/hnsw/partitionedHNSWIndex.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
142
dist/hnsw/scaledHNSWSystem.d.ts
vendored
Normal file
142
dist/hnsw/scaledHNSWSystem.d.ts
vendored
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
/**
|
||||
* Scaled HNSW System - Integration of All Optimization Strategies
|
||||
* Production-ready system for handling millions of vectors with sub-second search
|
||||
*/
|
||||
import { Vector, VectorDocument } from '../coreTypes.js';
|
||||
import { PartitionConfig } from './partitionedHNSWIndex.js';
|
||||
import { OptimizedHNSWConfig } from './optimizedHNSWIndex.js';
|
||||
import { SearchStrategy } from './distributedSearch.js';
|
||||
export interface ScaledHNSWConfig {
|
||||
expectedDatasetSize?: number;
|
||||
maxMemoryUsage?: number;
|
||||
targetSearchLatency?: number;
|
||||
s3Config?: {
|
||||
bucketName: string;
|
||||
region: string;
|
||||
endpoint?: string;
|
||||
accessKeyId?: string;
|
||||
secretAccessKey?: string;
|
||||
};
|
||||
autoConfigureEnvironment?: boolean;
|
||||
learningEnabled?: boolean;
|
||||
enablePartitioning?: boolean;
|
||||
enableCompression?: boolean;
|
||||
enableDistributedSearch?: boolean;
|
||||
enablePredictiveCaching?: boolean;
|
||||
partitionConfig?: Partial<PartitionConfig>;
|
||||
hnswConfig?: Partial<OptimizedHNSWConfig>;
|
||||
readOnlyMode?: boolean;
|
||||
}
|
||||
/**
|
||||
* High-performance HNSW system with all optimizations integrated
|
||||
* Handles datasets from thousands to millions of vectors
|
||||
*/
|
||||
export declare class ScaledHNSWSystem {
|
||||
private config;
|
||||
private autoConfig;
|
||||
private partitionedIndex?;
|
||||
private distributedSearch?;
|
||||
private cacheManager?;
|
||||
private batchOperations?;
|
||||
private readOnlyOptimizations?;
|
||||
private performanceMetrics;
|
||||
constructor(config?: ScaledHNSWConfig);
|
||||
/**
|
||||
* Initialize the optimized system based on configuration
|
||||
*/
|
||||
private initializeOptimizedSystem;
|
||||
/**
|
||||
* Calculate optimal configuration based on dataset size and constraints
|
||||
*/
|
||||
private calculateOptimalConfiguration;
|
||||
/**
|
||||
* Add vector to the scaled system
|
||||
*/
|
||||
addVector(item: VectorDocument): Promise<string>;
|
||||
/**
|
||||
* Bulk insert vectors with optimizations
|
||||
*/
|
||||
bulkInsert(items: VectorDocument[]): Promise<string[]>;
|
||||
/**
|
||||
* High-performance vector search with all optimizations
|
||||
*/
|
||||
search(queryVector: Vector, k?: number, options?: {
|
||||
strategy?: SearchStrategy;
|
||||
useCache?: boolean;
|
||||
maxPartitions?: number;
|
||||
}): Promise<Array<[string, number]>>;
|
||||
/**
|
||||
* Get system performance metrics
|
||||
*/
|
||||
getPerformanceMetrics(): typeof this.performanceMetrics & {
|
||||
partitionStats?: any;
|
||||
cacheStats?: any;
|
||||
compressionStats?: any;
|
||||
distributedSearchStats?: any;
|
||||
};
|
||||
/**
|
||||
* Optimize insertion order for better index quality
|
||||
*/
|
||||
private optimizeInsertionOrder;
|
||||
/**
|
||||
* Calculate optimal batch size based on system resources
|
||||
*/
|
||||
private calculateOptimalBatchSize;
|
||||
/**
|
||||
* Update search performance metrics
|
||||
*/
|
||||
private updateSearchMetrics;
|
||||
/**
|
||||
* Estimate current memory usage
|
||||
*/
|
||||
private estimateMemoryUsage;
|
||||
/**
|
||||
* Generate performance report
|
||||
*/
|
||||
generatePerformanceReport(): string;
|
||||
/**
|
||||
* Get overall system status
|
||||
*/
|
||||
private getSystemStatus;
|
||||
/**
|
||||
* Check if adaptive learning should be triggered
|
||||
*/
|
||||
private shouldTriggerLearning;
|
||||
/**
|
||||
* Adaptively learn from performance and adjust configuration
|
||||
*/
|
||||
private adaptivelyLearnFromPerformance;
|
||||
/**
|
||||
* Update dataset analysis for better auto-configuration
|
||||
*/
|
||||
updateDatasetAnalysis(vectorCount: number, vectorDimension?: number): Promise<void>;
|
||||
/**
|
||||
* Infer access patterns from current metrics
|
||||
*/
|
||||
private inferAccessPatterns;
|
||||
/**
|
||||
* Cleanup system resources
|
||||
*/
|
||||
cleanup(): void;
|
||||
}
|
||||
/**
|
||||
* Create a fully auto-configured Brainy system - minimal setup required!
|
||||
* Just provide S3 config if you want persistence beyond the current session
|
||||
*/
|
||||
export declare function createAutoBrainy(s3Config?: {
|
||||
bucketName: string;
|
||||
region?: string;
|
||||
accessKeyId?: string;
|
||||
secretAccessKey?: string;
|
||||
}): ScaledHNSWSystem;
|
||||
/**
|
||||
* Create a Brainy system optimized for specific scenarios
|
||||
*/
|
||||
export declare function createQuickBrainy(scenario: 'small' | 'medium' | 'large' | 'enterprise', s3Config?: {
|
||||
bucketName: string;
|
||||
region?: string;
|
||||
}): Promise<ScaledHNSWSystem>;
|
||||
/**
|
||||
* Legacy factory function - still works but consider using createAutoBrainy() instead
|
||||
*/
|
||||
export declare function createScaledHNSWSystem(config?: ScaledHNSWConfig): ScaledHNSWSystem;
|
||||
559
dist/hnsw/scaledHNSWSystem.js
vendored
Normal file
559
dist/hnsw/scaledHNSWSystem.js
vendored
Normal file
|
|
@ -0,0 +1,559 @@
|
|||
/**
|
||||
* Scaled HNSW System - Integration of All Optimization Strategies
|
||||
* Production-ready system for handling millions of vectors with sub-second search
|
||||
*/
|
||||
import { PartitionedHNSWIndex } from './partitionedHNSWIndex.js';
|
||||
import { DistributedSearchSystem, SearchStrategy } from './distributedSearch.js';
|
||||
import { EnhancedCacheManager } from '../storage/enhancedCacheManager.js';
|
||||
import { BatchS3Operations } from '../storage/adapters/batchS3Operations.js';
|
||||
import { ReadOnlyOptimizations } from '../storage/readOnlyOptimizations.js';
|
||||
import { euclideanDistance } from '../utils/index.js';
|
||||
import { AutoConfiguration } from '../utils/autoConfiguration.js';
|
||||
/**
|
||||
* High-performance HNSW system with all optimizations integrated
|
||||
* Handles datasets from thousands to millions of vectors
|
||||
*/
|
||||
export class ScaledHNSWSystem {
|
||||
constructor(config = {}) {
|
||||
// Performance monitoring and learning
|
||||
this.performanceMetrics = {
|
||||
totalSearches: 0,
|
||||
averageSearchTime: 0,
|
||||
cacheHitRate: 0,
|
||||
compressionRatio: 0,
|
||||
memoryUsage: 0,
|
||||
indexSize: 0,
|
||||
lastLearningUpdate: Date.now()
|
||||
};
|
||||
this.autoConfig = AutoConfiguration.getInstance();
|
||||
// Set basic defaults - these will be overridden by auto-configuration
|
||||
this.config = {
|
||||
expectedDatasetSize: 100000,
|
||||
maxMemoryUsage: 4 * 1024 * 1024 * 1024,
|
||||
targetSearchLatency: 150,
|
||||
autoConfigureEnvironment: true,
|
||||
learningEnabled: true,
|
||||
enablePartitioning: true,
|
||||
enableCompression: true,
|
||||
enableDistributedSearch: true,
|
||||
enablePredictiveCaching: true,
|
||||
readOnlyMode: false,
|
||||
...config
|
||||
};
|
||||
this.initializeOptimizedSystem();
|
||||
}
|
||||
/**
|
||||
* Initialize the optimized system based on configuration
|
||||
*/
|
||||
async initializeOptimizedSystem() {
|
||||
console.log('Initializing Scaled HNSW System with auto-configuration...');
|
||||
// Auto-configure if enabled
|
||||
if (this.config.autoConfigureEnvironment) {
|
||||
const autoConfigResult = await this.autoConfig.detectAndConfigure({
|
||||
expectedDataSize: this.config.expectedDatasetSize,
|
||||
s3Available: !!this.config.s3Config,
|
||||
memoryBudget: this.config.maxMemoryUsage
|
||||
});
|
||||
console.log(`Detected environment: ${autoConfigResult.environment}`);
|
||||
console.log(`Available memory: ${(autoConfigResult.availableMemory / 1024 / 1024 / 1024).toFixed(1)}GB`);
|
||||
console.log(`CPU cores: ${autoConfigResult.cpuCores}`);
|
||||
// Override config with auto-detected values
|
||||
this.config = {
|
||||
...this.config,
|
||||
expectedDatasetSize: autoConfigResult.recommendedConfig.expectedDatasetSize,
|
||||
maxMemoryUsage: autoConfigResult.recommendedConfig.maxMemoryUsage,
|
||||
targetSearchLatency: autoConfigResult.recommendedConfig.targetSearchLatency,
|
||||
enablePartitioning: autoConfigResult.recommendedConfig.enablePartitioning,
|
||||
enableCompression: autoConfigResult.recommendedConfig.enableCompression,
|
||||
enableDistributedSearch: autoConfigResult.recommendedConfig.enableDistributedSearch,
|
||||
enablePredictiveCaching: autoConfigResult.recommendedConfig.enablePredictiveCaching
|
||||
};
|
||||
}
|
||||
// Determine optimal configuration
|
||||
const optimizedConfig = this.calculateOptimalConfiguration();
|
||||
// Initialize partitioned index with semantic partitioning as default
|
||||
if (this.config.enablePartitioning) {
|
||||
this.partitionedIndex = new PartitionedHNSWIndex({
|
||||
...optimizedConfig.partitionConfig,
|
||||
partitionStrategy: 'semantic', // Always use semantic for better performance
|
||||
autoTuneSemanticClusters: true // Enable auto-tuning
|
||||
}, optimizedConfig.hnswConfig, euclideanDistance);
|
||||
console.log('✓ Partitioned index initialized with semantic clustering');
|
||||
}
|
||||
// Initialize distributed search system
|
||||
if (this.config.enableDistributedSearch && this.partitionedIndex) {
|
||||
this.distributedSearch = new DistributedSearchSystem({
|
||||
maxConcurrentSearches: optimizedConfig.maxConcurrentSearches,
|
||||
searchTimeout: this.config.targetSearchLatency * 5,
|
||||
adaptivePartitionSelection: true,
|
||||
loadBalancing: true
|
||||
});
|
||||
console.log('✓ Distributed search system initialized');
|
||||
}
|
||||
// Initialize batch S3 operations
|
||||
if (this.config.s3Config) {
|
||||
this.batchOperations = new BatchS3Operations(null, // Would be initialized with actual S3 client
|
||||
this.config.s3Config.bucketName, {
|
||||
maxConcurrency: 50,
|
||||
useS3Select: this.config.expectedDatasetSize > 100000
|
||||
});
|
||||
console.log('✓ Batch S3 operations initialized');
|
||||
}
|
||||
// Initialize enhanced caching
|
||||
if (this.config.enablePredictiveCaching) {
|
||||
this.cacheManager = new EnhancedCacheManager({
|
||||
hotCacheMaxSize: optimizedConfig.hotCacheSize,
|
||||
warmCacheMaxSize: optimizedConfig.warmCacheSize,
|
||||
prefetchEnabled: true,
|
||||
prefetchStrategy: 'hybrid', // Type casting for enum compatibility
|
||||
prefetchBatchSize: 50
|
||||
});
|
||||
if (this.batchOperations) {
|
||||
this.cacheManager.setStorageAdapters(null, this.batchOperations);
|
||||
}
|
||||
console.log('✓ Enhanced cache manager initialized');
|
||||
}
|
||||
// Initialize read-only optimizations
|
||||
if (this.config.readOnlyMode && this.config.enableCompression) {
|
||||
this.readOnlyOptimizations = new ReadOnlyOptimizations({
|
||||
compression: {
|
||||
vectorCompression: 'quantization',
|
||||
metadataCompression: 'gzip',
|
||||
quantizationType: 'scalar',
|
||||
quantizationBits: 8
|
||||
},
|
||||
segmentSize: optimizedConfig.segmentSize,
|
||||
memoryMapped: true,
|
||||
cacheIndexInMemory: optimizedConfig.cacheIndexInMemory
|
||||
});
|
||||
console.log('✓ Read-only optimizations initialized');
|
||||
}
|
||||
console.log('Scaled HNSW System ready for', this.config.expectedDatasetSize, 'vectors');
|
||||
}
|
||||
/**
|
||||
* Calculate optimal configuration based on dataset size and constraints
|
||||
*/
|
||||
calculateOptimalConfiguration() {
|
||||
const size = this.config.expectedDatasetSize;
|
||||
const memoryBudget = this.config.maxMemoryUsage;
|
||||
let config = {};
|
||||
if (size <= 10000) {
|
||||
// Small dataset - optimize for speed
|
||||
config = {
|
||||
partitionConfig: {
|
||||
maxNodesPerPartition: 10000,
|
||||
partitionStrategy: 'hash'
|
||||
},
|
||||
hnswConfig: {
|
||||
M: 16,
|
||||
efConstruction: 200,
|
||||
efSearch: 50,
|
||||
targetSearchLatency: this.config.targetSearchLatency
|
||||
},
|
||||
hotCacheSize: 1000,
|
||||
warmCacheSize: 5000,
|
||||
maxConcurrentSearches: 4,
|
||||
segmentSize: 5000,
|
||||
cacheIndexInMemory: true
|
||||
};
|
||||
}
|
||||
else if (size <= 100000) {
|
||||
// Medium dataset - balance performance and memory
|
||||
config = {
|
||||
partitionConfig: {
|
||||
maxNodesPerPartition: 25000,
|
||||
partitionStrategy: 'semantic',
|
||||
semanticClusters: 8
|
||||
},
|
||||
hnswConfig: {
|
||||
M: 24,
|
||||
efConstruction: 300,
|
||||
efSearch: 75,
|
||||
targetSearchLatency: this.config.targetSearchLatency,
|
||||
dynamicParameterTuning: true
|
||||
},
|
||||
hotCacheSize: 2000,
|
||||
warmCacheSize: 15000,
|
||||
maxConcurrentSearches: 8,
|
||||
segmentSize: 10000,
|
||||
cacheIndexInMemory: memoryBudget > 2 * 1024 * 1024 * 1024 // 2GB
|
||||
};
|
||||
}
|
||||
else if (size <= 1000000) {
|
||||
// Large dataset - optimize for scale
|
||||
config = {
|
||||
partitionConfig: {
|
||||
maxNodesPerPartition: 50000,
|
||||
partitionStrategy: 'semantic',
|
||||
semanticClusters: 16
|
||||
},
|
||||
hnswConfig: {
|
||||
M: 32,
|
||||
efConstruction: 400,
|
||||
efSearch: 100,
|
||||
targetSearchLatency: this.config.targetSearchLatency,
|
||||
dynamicParameterTuning: true,
|
||||
memoryBudget: memoryBudget
|
||||
},
|
||||
hotCacheSize: 5000,
|
||||
warmCacheSize: 25000,
|
||||
maxConcurrentSearches: 12,
|
||||
segmentSize: 20000,
|
||||
cacheIndexInMemory: memoryBudget > 8 * 1024 * 1024 * 1024 // 8GB
|
||||
};
|
||||
}
|
||||
else {
|
||||
// Very large dataset - maximum optimization
|
||||
config = {
|
||||
partitionConfig: {
|
||||
maxNodesPerPartition: 100000,
|
||||
partitionStrategy: 'hybrid',
|
||||
semanticClusters: 32
|
||||
},
|
||||
hnswConfig: {
|
||||
M: 48,
|
||||
efConstruction: 500,
|
||||
efSearch: 150,
|
||||
targetSearchLatency: this.config.targetSearchLatency,
|
||||
dynamicParameterTuning: true,
|
||||
memoryBudget: memoryBudget,
|
||||
diskCacheEnabled: true
|
||||
},
|
||||
hotCacheSize: 10000,
|
||||
warmCacheSize: 50000,
|
||||
maxConcurrentSearches: 20,
|
||||
segmentSize: 50000,
|
||||
cacheIndexInMemory: false // Too large for memory
|
||||
};
|
||||
}
|
||||
return config;
|
||||
}
|
||||
/**
|
||||
* Add vector to the scaled system
|
||||
*/
|
||||
async addVector(item) {
|
||||
if (!this.partitionedIndex) {
|
||||
throw new Error('System not properly initialized');
|
||||
}
|
||||
const startTime = Date.now();
|
||||
const result = await this.partitionedIndex.addItem(item);
|
||||
// Update performance metrics
|
||||
this.performanceMetrics.indexSize = this.partitionedIndex.size();
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Bulk insert vectors with optimizations
|
||||
*/
|
||||
async bulkInsert(items) {
|
||||
if (!this.partitionedIndex) {
|
||||
throw new Error('System not properly initialized');
|
||||
}
|
||||
console.log(`Starting optimized bulk insert of ${items.length} vectors`);
|
||||
const startTime = Date.now();
|
||||
// Sort items for optimal insertion order
|
||||
const sortedItems = this.optimizeInsertionOrder(items);
|
||||
const results = [];
|
||||
const batchSize = this.calculateOptimalBatchSize(items.length);
|
||||
// Process in batches
|
||||
for (let i = 0; i < sortedItems.length; i += batchSize) {
|
||||
const batch = sortedItems.slice(i, i + batchSize);
|
||||
for (const item of batch) {
|
||||
const id = await this.partitionedIndex.addItem(item);
|
||||
results.push(id);
|
||||
}
|
||||
// Progress logging
|
||||
if (i % (batchSize * 10) === 0) {
|
||||
const progress = ((i / sortedItems.length) * 100).toFixed(1);
|
||||
console.log(`Bulk insert progress: ${progress}%`);
|
||||
}
|
||||
}
|
||||
const totalTime = Date.now() - startTime;
|
||||
console.log(`Bulk insert completed: ${results.length} vectors in ${totalTime}ms`);
|
||||
return results;
|
||||
}
|
||||
/**
|
||||
* High-performance vector search with all optimizations
|
||||
*/
|
||||
async search(queryVector, k = 10, options = {}) {
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
let results;
|
||||
if (this.distributedSearch && this.partitionedIndex) {
|
||||
// Use distributed search for optimal performance
|
||||
results = await this.distributedSearch.distributedSearch(this.partitionedIndex, queryVector, k, options.strategy || SearchStrategy.ADAPTIVE);
|
||||
}
|
||||
else if (this.partitionedIndex) {
|
||||
// Fall back to partitioned search
|
||||
results = await this.partitionedIndex.search(queryVector, k, { maxPartitions: options.maxPartitions });
|
||||
}
|
||||
else {
|
||||
throw new Error('No search system available');
|
||||
}
|
||||
// Update performance metrics and learn from performance
|
||||
const searchTime = Date.now() - startTime;
|
||||
this.updateSearchMetrics(searchTime, results.length);
|
||||
// Adaptive learning - adjust configuration based on performance
|
||||
if (this.config.learningEnabled && this.shouldTriggerLearning()) {
|
||||
await this.adaptivelyLearnFromPerformance();
|
||||
}
|
||||
return results;
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Search failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get system performance metrics
|
||||
*/
|
||||
getPerformanceMetrics() {
|
||||
const metrics = { ...this.performanceMetrics };
|
||||
// Add subsystem metrics
|
||||
if (this.partitionedIndex) {
|
||||
metrics.partitionStats = this.partitionedIndex.getPartitionStats();
|
||||
}
|
||||
if (this.cacheManager) {
|
||||
metrics.cacheStats = this.cacheManager.getStats();
|
||||
}
|
||||
if (this.readOnlyOptimizations) {
|
||||
metrics.compressionStats = this.readOnlyOptimizations.getCompressionStats();
|
||||
}
|
||||
if (this.distributedSearch) {
|
||||
metrics.distributedSearchStats = this.distributedSearch.getSearchStats();
|
||||
}
|
||||
return metrics;
|
||||
}
|
||||
/**
|
||||
* Optimize insertion order for better index quality
|
||||
*/
|
||||
optimizeInsertionOrder(items) {
|
||||
if (items.length < 1000) {
|
||||
return items; // Not worth optimizing small batches
|
||||
}
|
||||
// Simple clustering-based approach for better HNSW construction
|
||||
// In production, you might use more sophisticated clustering
|
||||
return items.sort(() => Math.random() - 0.5);
|
||||
}
|
||||
/**
|
||||
* Calculate optimal batch size based on system resources
|
||||
*/
|
||||
calculateOptimalBatchSize(totalItems) {
|
||||
const memoryBudget = this.config.maxMemoryUsage;
|
||||
const estimatedItemSize = 1000; // Rough estimate per item in bytes
|
||||
const maxBatch = Math.floor(memoryBudget * 0.1 / estimatedItemSize);
|
||||
const targetBatch = Math.min(1000, Math.max(100, maxBatch));
|
||||
return Math.min(targetBatch, totalItems);
|
||||
}
|
||||
/**
|
||||
* Update search performance metrics
|
||||
*/
|
||||
updateSearchMetrics(searchTime, resultCount) {
|
||||
this.performanceMetrics.totalSearches++;
|
||||
this.performanceMetrics.averageSearchTime =
|
||||
(this.performanceMetrics.averageSearchTime + searchTime) / 2;
|
||||
// Update other metrics
|
||||
if (this.cacheManager) {
|
||||
const cacheStats = this.cacheManager.getStats();
|
||||
const totalOps = cacheStats.hotCacheHits + cacheStats.hotCacheMisses +
|
||||
cacheStats.warmCacheHits + cacheStats.warmCacheMisses;
|
||||
this.performanceMetrics.cacheHitRate = totalOps > 0 ?
|
||||
(cacheStats.hotCacheHits + cacheStats.warmCacheHits) / totalOps : 0;
|
||||
}
|
||||
if (this.readOnlyOptimizations) {
|
||||
const compressionStats = this.readOnlyOptimizations.getCompressionStats();
|
||||
this.performanceMetrics.compressionRatio = compressionStats.compressionRatio;
|
||||
}
|
||||
// Estimate memory usage
|
||||
this.performanceMetrics.memoryUsage = this.estimateMemoryUsage();
|
||||
}
|
||||
/**
|
||||
* Estimate current memory usage
|
||||
*/
|
||||
estimateMemoryUsage() {
|
||||
let totalMemory = 0;
|
||||
if (this.partitionedIndex) {
|
||||
// Rough estimate: 1KB per vector
|
||||
totalMemory += this.partitionedIndex.size() * 1024;
|
||||
}
|
||||
if (this.cacheManager) {
|
||||
const cacheStats = this.cacheManager.getStats();
|
||||
totalMemory += (cacheStats.hotCacheSize + cacheStats.warmCacheSize) * 1024;
|
||||
}
|
||||
return totalMemory;
|
||||
}
|
||||
/**
|
||||
* Generate performance report
|
||||
*/
|
||||
generatePerformanceReport() {
|
||||
const metrics = this.getPerformanceMetrics();
|
||||
return `
|
||||
=== Scaled HNSW System Performance Report ===
|
||||
|
||||
Dataset Configuration:
|
||||
- Expected Size: ${this.config.expectedDatasetSize.toLocaleString()} vectors
|
||||
- Current Size: ${metrics.indexSize.toLocaleString()} vectors
|
||||
- Memory Budget: ${(this.config.maxMemoryUsage / 1024 / 1024 / 1024).toFixed(1)}GB
|
||||
- Target Latency: ${this.config.targetSearchLatency}ms
|
||||
|
||||
Performance Metrics:
|
||||
- Total Searches: ${metrics.totalSearches.toLocaleString()}
|
||||
- Average Search Time: ${metrics.averageSearchTime.toFixed(1)}ms
|
||||
- Cache Hit Rate: ${(metrics.cacheHitRate * 100).toFixed(1)}%
|
||||
- Memory Usage: ${(metrics.memoryUsage / 1024 / 1024).toFixed(1)}MB
|
||||
- Compression Ratio: ${metrics.compressionRatio ? (metrics.compressionRatio * 100).toFixed(1) + '%' : 'N/A'}
|
||||
|
||||
System Status: ${this.getSystemStatus()}
|
||||
`.trim();
|
||||
}
|
||||
/**
|
||||
* Get overall system status
|
||||
*/
|
||||
getSystemStatus() {
|
||||
const metrics = this.getPerformanceMetrics();
|
||||
if (metrics.averageSearchTime <= this.config.targetSearchLatency) {
|
||||
return '✅ OPTIMAL';
|
||||
}
|
||||
else if (metrics.averageSearchTime <= this.config.targetSearchLatency * 2) {
|
||||
return '⚠️ ACCEPTABLE';
|
||||
}
|
||||
else {
|
||||
return '❌ NEEDS OPTIMIZATION';
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Check if adaptive learning should be triggered
|
||||
*/
|
||||
shouldTriggerLearning() {
|
||||
const timeSinceLastLearning = Date.now() - this.performanceMetrics.lastLearningUpdate;
|
||||
const minLearningInterval = 30000; // 30 seconds
|
||||
const minSearches = 20; // Minimum searches before learning
|
||||
return timeSinceLastLearning > minLearningInterval &&
|
||||
this.performanceMetrics.totalSearches > minSearches &&
|
||||
this.performanceMetrics.totalSearches % 50 === 0; // Learn every 50 searches
|
||||
}
|
||||
/**
|
||||
* Adaptively learn from performance and adjust configuration
|
||||
*/
|
||||
async adaptivelyLearnFromPerformance() {
|
||||
try {
|
||||
const currentMetrics = {
|
||||
averageSearchTime: this.performanceMetrics.averageSearchTime,
|
||||
memoryUsage: this.performanceMetrics.memoryUsage,
|
||||
cacheHitRate: this.performanceMetrics.cacheHitRate,
|
||||
errorRate: 0 // Could be tracked separately
|
||||
};
|
||||
const adjustments = await this.autoConfig.learnFromPerformance(currentMetrics);
|
||||
if (Object.keys(adjustments).length > 0) {
|
||||
console.log('🧠 Adaptive learning: Adjusting configuration based on performance');
|
||||
// Apply learned adjustments
|
||||
let configChanged = false;
|
||||
if (adjustments.enableDistributedSearch !== undefined &&
|
||||
adjustments.enableDistributedSearch !== this.config.enableDistributedSearch) {
|
||||
this.config.enableDistributedSearch = adjustments.enableDistributedSearch;
|
||||
configChanged = true;
|
||||
}
|
||||
if (adjustments.enableCompression !== undefined &&
|
||||
adjustments.enableCompression !== this.config.enableCompression) {
|
||||
this.config.enableCompression = adjustments.enableCompression;
|
||||
configChanged = true;
|
||||
}
|
||||
if (adjustments.enablePredictiveCaching !== undefined &&
|
||||
adjustments.enablePredictiveCaching !== this.config.enablePredictiveCaching) {
|
||||
this.config.enablePredictiveCaching = adjustments.enablePredictiveCaching;
|
||||
configChanged = true;
|
||||
}
|
||||
// Apply partition adjustments
|
||||
if (adjustments.maxNodesPerPartition &&
|
||||
this.partitionedIndex &&
|
||||
adjustments.maxNodesPerPartition !== this.partitionedIndex.getPartitionStats().averageNodesPerPartition) {
|
||||
// This would require rebuilding the index in a real implementation
|
||||
console.log(`Learning suggests partition size: ${adjustments.maxNodesPerPartition}`);
|
||||
}
|
||||
if (configChanged) {
|
||||
console.log('✅ Configuration updated based on performance learning');
|
||||
}
|
||||
}
|
||||
this.performanceMetrics.lastLearningUpdate = Date.now();
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('Adaptive learning failed:', error);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Update dataset analysis for better auto-configuration
|
||||
*/
|
||||
async updateDatasetAnalysis(vectorCount, vectorDimension) {
|
||||
if (this.config.autoConfigureEnvironment) {
|
||||
const analysis = {
|
||||
estimatedSize: vectorCount,
|
||||
vectorDimension,
|
||||
accessPatterns: this.inferAccessPatterns()
|
||||
};
|
||||
await this.autoConfig.adaptToDataset(analysis);
|
||||
console.log(`📊 Dataset analysis updated: ${vectorCount} vectors${vectorDimension ? `, ${vectorDimension}D` : ''}`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Infer access patterns from current metrics
|
||||
*/
|
||||
inferAccessPatterns() {
|
||||
// Simple heuristic - in practice, this would track read/write ratios
|
||||
if (this.performanceMetrics.totalSearches > 100) {
|
||||
return 'read-heavy';
|
||||
}
|
||||
return 'balanced';
|
||||
}
|
||||
/**
|
||||
* Cleanup system resources
|
||||
*/
|
||||
cleanup() {
|
||||
this.distributedSearch?.cleanup();
|
||||
this.cacheManager?.clear();
|
||||
this.readOnlyOptimizations?.cleanup();
|
||||
this.partitionedIndex?.clear();
|
||||
this.autoConfig.resetCache();
|
||||
console.log('Scaled HNSW System cleaned up');
|
||||
}
|
||||
}
|
||||
// Export convenience factory functions
|
||||
/**
|
||||
* Create a fully auto-configured Brainy system - minimal setup required!
|
||||
* Just provide S3 config if you want persistence beyond the current session
|
||||
*/
|
||||
export function createAutoBrainy(s3Config) {
|
||||
return new ScaledHNSWSystem({
|
||||
s3Config: s3Config ? {
|
||||
bucketName: s3Config.bucketName,
|
||||
region: s3Config.region || 'us-east-1',
|
||||
accessKeyId: s3Config.accessKeyId,
|
||||
secretAccessKey: s3Config.secretAccessKey
|
||||
} : undefined,
|
||||
autoConfigureEnvironment: true,
|
||||
learningEnabled: true
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Create a Brainy system optimized for specific scenarios
|
||||
*/
|
||||
export async function createQuickBrainy(scenario, s3Config) {
|
||||
const { getQuickSetup } = await import('../utils/autoConfiguration.js');
|
||||
const quickConfig = await getQuickSetup(scenario);
|
||||
return new ScaledHNSWSystem({
|
||||
...quickConfig,
|
||||
s3Config: s3Config && quickConfig.s3Required ? {
|
||||
bucketName: s3Config.bucketName,
|
||||
region: s3Config.region || 'us-east-1',
|
||||
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
|
||||
} : undefined,
|
||||
autoConfigureEnvironment: true,
|
||||
learningEnabled: true
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Legacy factory function - still works but consider using createAutoBrainy() instead
|
||||
*/
|
||||
export function createScaledHNSWSystem(config = {}) {
|
||||
return new ScaledHNSWSystem(config);
|
||||
}
|
||||
//# sourceMappingURL=scaledHNSWSystem.js.map
|
||||
1
dist/hnsw/scaledHNSWSystem.js.map
vendored
Normal file
1
dist/hnsw/scaledHNSWSystem.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue