Open source vector database with HNSW indexing, graph relationships, and metadata facets. Features CLI with professional augmentation registry integration for discovering extensions and capabilities.
520 lines
No EOL
18 KiB
JavaScript
520 lines
No EOL
18 KiB
JavaScript
/**
|
|
* Enhanced Multi-Level Cache Manager with Predictive Prefetching
|
|
* Optimized for HNSW search patterns and large-scale vector operations
|
|
*/
|
|
// Prefetch prediction strategies
|
|
var PrefetchStrategy;
|
|
(function (PrefetchStrategy) {
|
|
PrefetchStrategy["GRAPH_CONNECTIVITY"] = "connectivity";
|
|
PrefetchStrategy["VECTOR_SIMILARITY"] = "similarity";
|
|
PrefetchStrategy["ACCESS_PATTERN"] = "pattern";
|
|
PrefetchStrategy["HYBRID"] = "hybrid";
|
|
})(PrefetchStrategy || (PrefetchStrategy = {}));
|
|
/**
|
|
* Enhanced cache manager with intelligent prefetching for HNSW operations
|
|
* Provides multi-level caching optimized for vector search workloads
|
|
*/
|
|
export class EnhancedCacheManager {
|
|
constructor(config = {}) {
|
|
this.hotCache = new Map();
|
|
this.warmCache = new Map();
|
|
this.prefetchQueue = new Set();
|
|
this.accessPatterns = new Map(); // Track access times
|
|
this.vectorIndex = new Map(); // For similarity calculations
|
|
this.prefetchInProgress = false;
|
|
// Statistics and monitoring
|
|
this.stats = {
|
|
hotCacheHits: 0,
|
|
hotCacheMisses: 0,
|
|
warmCacheHits: 0,
|
|
warmCacheMisses: 0,
|
|
prefetchHits: 0,
|
|
prefetchMisses: 0,
|
|
totalPrefetched: 0,
|
|
predictionAccuracy: 0,
|
|
backgroundOptimizations: 0
|
|
};
|
|
this.config = {
|
|
hotCacheMaxSize: 1000,
|
|
hotCacheEvictionThreshold: 0.8,
|
|
warmCacheMaxSize: 10000,
|
|
warmCacheTTL: 300000, // 5 minutes
|
|
prefetchEnabled: true,
|
|
prefetchStrategy: PrefetchStrategy.HYBRID,
|
|
prefetchBatchSize: 50,
|
|
predictionLookahead: 3,
|
|
similarityThreshold: 0.8,
|
|
maxSimilarityDistance: 2.0,
|
|
backgroundOptimization: true,
|
|
statisticsCollection: true,
|
|
...config
|
|
};
|
|
// Start background optimization if enabled
|
|
if (this.config.backgroundOptimization) {
|
|
this.startBackgroundOptimization();
|
|
}
|
|
}
|
|
/**
|
|
* Set storage adapters for warm/cold storage operations
|
|
*/
|
|
setStorageAdapters(storageAdapter, batchOperations) {
|
|
this.storageAdapter = storageAdapter;
|
|
this.batchOperations = batchOperations;
|
|
}
|
|
/**
|
|
* Get item with intelligent prefetching
|
|
*/
|
|
async get(id) {
|
|
const startTime = Date.now();
|
|
// Update access pattern
|
|
this.recordAccess(id, startTime);
|
|
// Check hot cache first
|
|
let entry = this.hotCache.get(id);
|
|
if (entry && !this.isExpired(entry)) {
|
|
entry.lastAccessed = startTime;
|
|
entry.accessCount++;
|
|
this.stats.hotCacheHits++;
|
|
// Trigger predictive prefetch
|
|
if (this.config.prefetchEnabled) {
|
|
this.schedulePrefetch(id, entry.data);
|
|
}
|
|
return entry.data;
|
|
}
|
|
this.stats.hotCacheMisses++;
|
|
// Check warm cache
|
|
entry = this.warmCache.get(id);
|
|
if (entry && !this.isExpired(entry)) {
|
|
entry.lastAccessed = startTime;
|
|
entry.accessCount++;
|
|
this.stats.warmCacheHits++;
|
|
// Promote to hot cache if frequently accessed
|
|
if (entry.accessCount > 3) {
|
|
this.promoteToHotCache(id, entry);
|
|
}
|
|
return entry.data;
|
|
}
|
|
this.stats.warmCacheMisses++;
|
|
// Load from storage
|
|
const item = await this.loadFromStorage(id);
|
|
if (item) {
|
|
// Cache the item
|
|
await this.set(id, item);
|
|
// Trigger predictive prefetch
|
|
if (this.config.prefetchEnabled) {
|
|
this.schedulePrefetch(id, item);
|
|
}
|
|
}
|
|
return item;
|
|
}
|
|
/**
|
|
* Get multiple items efficiently with batch operations
|
|
*/
|
|
async getMany(ids) {
|
|
const result = new Map();
|
|
const uncachedIds = [];
|
|
// Check caches first
|
|
for (const id of ids) {
|
|
const cached = await this.get(id);
|
|
if (cached) {
|
|
result.set(id, cached);
|
|
}
|
|
else {
|
|
uncachedIds.push(id);
|
|
}
|
|
}
|
|
// Batch load uncached items
|
|
if (uncachedIds.length > 0 && this.batchOperations) {
|
|
const batchResult = await this.batchOperations.batchGetNodes(uncachedIds);
|
|
// Cache loaded items
|
|
for (const [id, item] of batchResult.items) {
|
|
await this.set(id, item);
|
|
result.set(id, item);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
/**
|
|
* Set item in cache with metadata
|
|
*/
|
|
async set(id, item) {
|
|
const now = Date.now();
|
|
const entry = {
|
|
data: item,
|
|
lastAccessed: now,
|
|
accessCount: 1,
|
|
expiresAt: now + this.config.warmCacheTTL,
|
|
connectedNodes: this.extractConnectedNodes(item),
|
|
predictionScore: 0
|
|
};
|
|
// Store vector for similarity calculations
|
|
if ('vector' in item && item.vector) {
|
|
this.vectorIndex.set(id, item.vector);
|
|
entry.vectorSimilarity = 0;
|
|
}
|
|
// Add to warm cache initially
|
|
this.warmCache.set(id, entry);
|
|
// Clean up if needed
|
|
if (this.warmCache.size > this.config.warmCacheMaxSize) {
|
|
this.evictFromWarmCache();
|
|
}
|
|
// Update statistics
|
|
this.stats.warmCacheHits++; // Count as a potential future hit
|
|
}
|
|
/**
|
|
* Intelligent prefetch based on access patterns and graph structure
|
|
*/
|
|
async schedulePrefetch(currentId, currentItem) {
|
|
if (this.prefetchInProgress || !this.config.prefetchEnabled) {
|
|
return;
|
|
}
|
|
// Use different strategies based on configuration
|
|
let candidateIds = [];
|
|
switch (this.config.prefetchStrategy) {
|
|
case PrefetchStrategy.GRAPH_CONNECTIVITY:
|
|
candidateIds = this.predictByConnectivity(currentId, currentItem);
|
|
break;
|
|
case PrefetchStrategy.VECTOR_SIMILARITY:
|
|
candidateIds = await this.predictBySimilarity(currentId, currentItem);
|
|
break;
|
|
case PrefetchStrategy.ACCESS_PATTERN:
|
|
candidateIds = this.predictByAccessPattern(currentId);
|
|
break;
|
|
case PrefetchStrategy.HYBRID:
|
|
candidateIds = await this.hybridPrediction(currentId, currentItem);
|
|
break;
|
|
}
|
|
// Filter out already cached items
|
|
const uncachedIds = candidateIds.filter(id => !this.hotCache.has(id) && !this.warmCache.has(id)).slice(0, this.config.prefetchBatchSize);
|
|
if (uncachedIds.length > 0) {
|
|
this.executePrefetch(uncachedIds);
|
|
}
|
|
}
|
|
/**
|
|
* Predict next nodes based on graph connectivity
|
|
*/
|
|
predictByConnectivity(currentId, currentItem) {
|
|
const candidates = [];
|
|
if ('connections' in currentItem && currentItem.connections) {
|
|
const connections = currentItem.connections;
|
|
// Add immediate neighbors with higher priority for lower levels
|
|
for (const [level, nodeIds] of connections.entries()) {
|
|
const priority = Math.max(1, 5 - level); // Higher priority for level 0
|
|
for (const nodeId of nodeIds) {
|
|
// Add based on priority
|
|
for (let i = 0; i < priority; i++) {
|
|
candidates.push(nodeId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Shuffle and deduplicate
|
|
const shuffled = candidates.sort(() => Math.random() - 0.5);
|
|
return [...new Set(shuffled)];
|
|
}
|
|
/**
|
|
* Predict next nodes based on vector similarity
|
|
*/
|
|
async predictBySimilarity(currentId, currentItem) {
|
|
if (!('vector' in currentItem) || !currentItem.vector) {
|
|
return [];
|
|
}
|
|
const currentVector = currentItem.vector;
|
|
const similarities = [];
|
|
// Calculate similarities with vectors in cache
|
|
for (const [id, vector] of this.vectorIndex.entries()) {
|
|
if (id === currentId)
|
|
continue;
|
|
const similarity = this.cosineSimilarity(currentVector, vector);
|
|
if (similarity > this.config.similarityThreshold) {
|
|
similarities.push([id, similarity]);
|
|
}
|
|
}
|
|
// Sort by similarity and return top candidates
|
|
similarities.sort((a, b) => b[1] - a[1]);
|
|
return similarities.slice(0, this.config.prefetchBatchSize).map(([id]) => id);
|
|
}
|
|
/**
|
|
* Predict based on historical access patterns
|
|
*/
|
|
predictByAccessPattern(currentId) {
|
|
const currentPattern = this.accessPatterns.get(currentId);
|
|
if (!currentPattern || currentPattern.length < 2) {
|
|
return [];
|
|
}
|
|
// Find similar access patterns
|
|
const candidates = [];
|
|
for (const [id, pattern] of this.accessPatterns.entries()) {
|
|
if (id === currentId || pattern.length < 2)
|
|
continue;
|
|
const similarity = this.patternSimilarity(currentPattern, pattern);
|
|
if (similarity > 0.5) {
|
|
candidates.push([id, similarity]);
|
|
}
|
|
}
|
|
candidates.sort((a, b) => b[1] - a[1]);
|
|
return candidates.slice(0, this.config.prefetchBatchSize).map(([id]) => id);
|
|
}
|
|
/**
|
|
* Hybrid prediction combining multiple strategies
|
|
*/
|
|
async hybridPrediction(currentId, currentItem) {
|
|
const connectivityCandidates = this.predictByConnectivity(currentId, currentItem);
|
|
const similarityCandidates = await this.predictBySimilarity(currentId, currentItem);
|
|
const patternCandidates = this.predictByAccessPattern(currentId);
|
|
// Weighted combination
|
|
const candidateScores = new Map();
|
|
// Connectivity gets highest weight (40%)
|
|
connectivityCandidates.forEach((id, index) => {
|
|
const score = (connectivityCandidates.length - index) / connectivityCandidates.length * 0.4;
|
|
candidateScores.set(id, (candidateScores.get(id) || 0) + score);
|
|
});
|
|
// Similarity gets medium weight (35%)
|
|
similarityCandidates.forEach((id, index) => {
|
|
const score = (similarityCandidates.length - index) / similarityCandidates.length * 0.35;
|
|
candidateScores.set(id, (candidateScores.get(id) || 0) + score);
|
|
});
|
|
// Pattern gets lower weight (25%)
|
|
patternCandidates.forEach((id, index) => {
|
|
const score = (patternCandidates.length - index) / patternCandidates.length * 0.25;
|
|
candidateScores.set(id, (candidateScores.get(id) || 0) + score);
|
|
});
|
|
// Sort by combined score
|
|
const sortedCandidates = Array.from(candidateScores.entries())
|
|
.sort((a, b) => b[1] - a[1])
|
|
.map(([id]) => id);
|
|
return sortedCandidates.slice(0, this.config.prefetchBatchSize);
|
|
}
|
|
/**
|
|
* Execute prefetch operation in background
|
|
*/
|
|
async executePrefetch(ids) {
|
|
if (this.prefetchInProgress || !this.batchOperations) {
|
|
return;
|
|
}
|
|
this.prefetchInProgress = true;
|
|
try {
|
|
const batchResult = await this.batchOperations.batchGetNodes(ids);
|
|
// Cache prefetched items
|
|
for (const [id, item] of batchResult.items) {
|
|
const entry = {
|
|
data: item,
|
|
lastAccessed: Date.now(),
|
|
accessCount: 0, // Prefetched items start with 0 access count
|
|
expiresAt: Date.now() + this.config.warmCacheTTL,
|
|
connectedNodes: this.extractConnectedNodes(item),
|
|
predictionScore: 1 // Mark as prefetched
|
|
};
|
|
this.warmCache.set(id, entry);
|
|
}
|
|
this.stats.totalPrefetched += batchResult.items.size;
|
|
}
|
|
catch (error) {
|
|
console.warn('Prefetch operation failed:', error);
|
|
}
|
|
finally {
|
|
this.prefetchInProgress = false;
|
|
}
|
|
}
|
|
/**
|
|
* Load item from storage adapter
|
|
*/
|
|
async loadFromStorage(id) {
|
|
if (!this.storageAdapter) {
|
|
return null;
|
|
}
|
|
try {
|
|
return await this.storageAdapter.get(id);
|
|
}
|
|
catch (error) {
|
|
console.warn(`Failed to load ${id} from storage:`, error);
|
|
return null;
|
|
}
|
|
}
|
|
/**
|
|
* Promote frequently accessed item to hot cache
|
|
*/
|
|
promoteToHotCache(id, entry) {
|
|
// Remove from warm cache
|
|
this.warmCache.delete(id);
|
|
// Add to hot cache
|
|
this.hotCache.set(id, entry);
|
|
// Evict if necessary
|
|
if (this.hotCache.size > this.config.hotCacheMaxSize) {
|
|
this.evictFromHotCache();
|
|
}
|
|
}
|
|
/**
|
|
* Evict least recently used items from hot cache
|
|
*/
|
|
evictFromHotCache() {
|
|
const threshold = Math.floor(this.config.hotCacheMaxSize * this.config.hotCacheEvictionThreshold);
|
|
if (this.hotCache.size <= threshold) {
|
|
return;
|
|
}
|
|
// Sort by last accessed time and access count
|
|
const entries = Array.from(this.hotCache.entries())
|
|
.sort((a, b) => {
|
|
const scoreA = a[1].accessCount * 0.7 + (Date.now() - a[1].lastAccessed) * -0.3;
|
|
const scoreB = b[1].accessCount * 0.7 + (Date.now() - b[1].lastAccessed) * -0.3;
|
|
return scoreA - scoreB;
|
|
});
|
|
// Remove least valuable entries
|
|
const toRemove = entries.slice(0, this.hotCache.size - threshold);
|
|
for (const [id] of toRemove) {
|
|
this.hotCache.delete(id);
|
|
}
|
|
}
|
|
/**
|
|
* Evict expired items from warm cache
|
|
*/
|
|
evictFromWarmCache() {
|
|
const now = Date.now();
|
|
const toRemove = [];
|
|
for (const [id, entry] of this.warmCache.entries()) {
|
|
if (this.isExpired(entry)) {
|
|
toRemove.push(id);
|
|
}
|
|
}
|
|
// Remove expired items
|
|
for (const id of toRemove) {
|
|
this.warmCache.delete(id);
|
|
this.vectorIndex.delete(id);
|
|
}
|
|
// If still over limit, remove LRU items
|
|
if (this.warmCache.size > this.config.warmCacheMaxSize) {
|
|
const entries = Array.from(this.warmCache.entries())
|
|
.sort((a, b) => a[1].lastAccessed - b[1].lastAccessed);
|
|
const excess = this.warmCache.size - this.config.warmCacheMaxSize;
|
|
for (let i = 0; i < excess; i++) {
|
|
const [id] = entries[i];
|
|
this.warmCache.delete(id);
|
|
this.vectorIndex.delete(id);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Record access pattern for prediction
|
|
*/
|
|
recordAccess(id, timestamp) {
|
|
if (!this.config.statisticsCollection) {
|
|
return;
|
|
}
|
|
let pattern = this.accessPatterns.get(id);
|
|
if (!pattern) {
|
|
pattern = [];
|
|
this.accessPatterns.set(id, pattern);
|
|
}
|
|
pattern.push(timestamp);
|
|
// Keep only recent accesses (last 10)
|
|
if (pattern.length > 10) {
|
|
pattern.shift();
|
|
}
|
|
}
|
|
/**
|
|
* Extract connected node IDs from HNSW item
|
|
*/
|
|
extractConnectedNodes(item) {
|
|
const connected = new Set();
|
|
if ('connections' in item && item.connections) {
|
|
const connections = item.connections;
|
|
for (const nodeIds of connections.values()) {
|
|
nodeIds.forEach(id => connected.add(id));
|
|
}
|
|
}
|
|
return connected;
|
|
}
|
|
/**
|
|
* Check if cache entry is expired
|
|
*/
|
|
isExpired(entry) {
|
|
return entry.expiresAt !== null && Date.now() > entry.expiresAt;
|
|
}
|
|
/**
|
|
* Calculate cosine similarity between vectors
|
|
*/
|
|
cosineSimilarity(a, b) {
|
|
if (a.length !== b.length)
|
|
return 0;
|
|
let dotProduct = 0;
|
|
let normA = 0;
|
|
let normB = 0;
|
|
for (let i = 0; i < a.length; i++) {
|
|
dotProduct += a[i] * b[i];
|
|
normA += a[i] * a[i];
|
|
normB += b[i] * b[i];
|
|
}
|
|
const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
|
|
return magnitude === 0 ? 0 : dotProduct / magnitude;
|
|
}
|
|
/**
|
|
* Calculate pattern similarity between access patterns
|
|
*/
|
|
patternSimilarity(pattern1, pattern2) {
|
|
const minLength = Math.min(pattern1.length, pattern2.length);
|
|
if (minLength < 2)
|
|
return 0;
|
|
// Calculate intervals between accesses
|
|
const intervals1 = pattern1.slice(1).map((t, i) => t - pattern1[i]);
|
|
const intervals2 = pattern2.slice(1).map((t, i) => t - pattern2[i]);
|
|
// Compare interval patterns
|
|
let similarity = 0;
|
|
const compareLength = Math.min(intervals1.length, intervals2.length);
|
|
for (let i = 0; i < compareLength; i++) {
|
|
const diff = Math.abs(intervals1[i] - intervals2[i]);
|
|
const maxInterval = Math.max(intervals1[i], intervals2[i]);
|
|
similarity += maxInterval === 0 ? 1 : 1 - (diff / maxInterval);
|
|
}
|
|
return compareLength === 0 ? 0 : similarity / compareLength;
|
|
}
|
|
/**
|
|
* Start background optimization process
|
|
*/
|
|
startBackgroundOptimization() {
|
|
setInterval(() => {
|
|
this.runBackgroundOptimization();
|
|
}, 60000); // Run every minute
|
|
}
|
|
/**
|
|
* Run background optimization tasks
|
|
*/
|
|
runBackgroundOptimization() {
|
|
// Clean up expired entries
|
|
this.evictFromWarmCache();
|
|
this.evictFromHotCache();
|
|
// Clean up old access patterns
|
|
const cutoff = Date.now() - 3600000; // 1 hour
|
|
for (const [id, pattern] of this.accessPatterns.entries()) {
|
|
const recentAccesses = pattern.filter(t => t > cutoff);
|
|
if (recentAccesses.length === 0) {
|
|
this.accessPatterns.delete(id);
|
|
}
|
|
else {
|
|
this.accessPatterns.set(id, recentAccesses);
|
|
}
|
|
}
|
|
this.stats.backgroundOptimizations++;
|
|
}
|
|
/**
|
|
* Get cache statistics
|
|
*/
|
|
getStats() {
|
|
return {
|
|
...this.stats,
|
|
hotCacheSize: this.hotCache.size,
|
|
warmCacheSize: this.warmCache.size,
|
|
prefetchQueueSize: this.prefetchQueue.size,
|
|
accessPatternsTracked: this.accessPatterns.size
|
|
};
|
|
}
|
|
/**
|
|
* Clear all caches
|
|
*/
|
|
clear() {
|
|
this.hotCache.clear();
|
|
this.warmCache.clear();
|
|
this.prefetchQueue.clear();
|
|
this.accessPatterns.clear();
|
|
this.vectorIndex.clear();
|
|
}
|
|
}
|
|
//# sourceMappingURL=enhancedCacheManager.js.map
|