feat(distributed): add distributed mode with multi-instance coordination
Implements Phase 1 and Phase 2 of distributed enhancements for horizontal scaling: Phase 1 - Zero-Config Distributed Mode: - Add DistributedConfigManager for shared S3 configuration coordination - Implement explicit role configuration (reader/writer/hybrid) for safety - Add instance registration with heartbeat and health monitoring - Create hash-based partitioner for deterministic data distribution Phase 2 - Intelligent Data Management: - Add DomainDetector for automatic data categorization (medical, legal, product, etc.) - Implement domain-aware search filtering for improved relevance - Create role-based operational modes with specific optimizations - Add HealthMonitor for comprehensive metrics tracking Key Features: - Multi-writer support with consistent hash partitioning - Reader instances optimize for 80% cache utilization - Writer instances optimize for batched writes - Automatic domain detection and tagging - Real-time health monitoring across all instances - Cross-platform crypto utilities for browser compatibility Safety Improvements: - Require explicit role configuration (no automatic assignment) - Validate role compatibility on startup - Track instance health and performance metrics Testing: - Add comprehensive test suite for distributed features - All 25 distributed tests passing - Fixed domain filtering in search functionality Documentation: - Update README with distributed mode highlights - Add examples showing reader/writer setup - Document new capabilities and benefits 🤖 Generated with Claude Code https://claude.ai/code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
29625cc1af
commit
26e9c26852
12 changed files with 3301 additions and 3 deletions
|
|
@ -47,6 +47,14 @@ import {
|
|||
prepareJsonForVectorization,
|
||||
extractFieldFromJson
|
||||
} from './utils/jsonProcessing.js'
|
||||
import { DistributedConfig } from './types/distributedTypes.js'
|
||||
import {
|
||||
DistributedConfigManager,
|
||||
HashPartitioner,
|
||||
OperationalModeFactory,
|
||||
DomainDetector,
|
||||
HealthMonitor
|
||||
} from './distributed/index.js'
|
||||
|
||||
export interface BrainyDataConfig {
|
||||
/**
|
||||
|
|
@ -260,6 +268,12 @@ export interface BrainyDataConfig {
|
|||
updateIndex?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Distributed mode configuration
|
||||
* Enables coordination across multiple Brainy instances
|
||||
*/
|
||||
distributed?: DistributedConfig | boolean
|
||||
|
||||
/**
|
||||
* Cache configuration for optimizing search performance
|
||||
* Controls how the system caches data for faster access
|
||||
|
|
@ -378,6 +392,14 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
private remoteServerConfig: BrainyDataConfig['remoteServer'] | null = null
|
||||
private serverSearchConduit: ServerSearchConduitAugmentation | null = null
|
||||
private serverConnection: WebSocketConnection | null = null
|
||||
|
||||
// Distributed mode properties
|
||||
private distributedConfig: DistributedConfig | null = null
|
||||
private configManager: DistributedConfigManager | null = null
|
||||
private partitioner: HashPartitioner | null = null
|
||||
private operationalMode: any = null
|
||||
private domainDetector: DomainDetector | null = null
|
||||
private healthMonitor: HealthMonitor | null = null
|
||||
|
||||
/**
|
||||
* Get the vector dimensions
|
||||
|
|
@ -513,6 +535,19 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
...config.cache
|
||||
}
|
||||
}
|
||||
|
||||
// Store distributed configuration
|
||||
if (config.distributed) {
|
||||
if (typeof config.distributed === 'boolean') {
|
||||
// Auto-mode enabled
|
||||
this.distributedConfig = {
|
||||
enabled: true
|
||||
}
|
||||
} else {
|
||||
// Explicit configuration
|
||||
this.distributedConfig = config.distributed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1005,6 +1040,11 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
|
||||
// Initialize storage
|
||||
await this.storage!.init()
|
||||
|
||||
// Initialize distributed mode if configured
|
||||
if (this.distributedConfig) {
|
||||
await this.initializeDistributedMode()
|
||||
}
|
||||
|
||||
// If using optimized index, set the storage adapter
|
||||
if (this.useOptimizedIndex && this.index instanceof HNSWIndexOptimized) {
|
||||
|
|
@ -1076,6 +1116,113 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize distributed mode
|
||||
* Sets up configuration management, partitioning, and operational modes
|
||||
*/
|
||||
private async initializeDistributedMode(): Promise<void> {
|
||||
if (!this.storage) {
|
||||
throw new Error('Storage must be initialized before distributed mode')
|
||||
}
|
||||
|
||||
// Create configuration manager with mode hints
|
||||
this.configManager = new DistributedConfigManager(
|
||||
this.storage,
|
||||
this.distributedConfig || undefined,
|
||||
{ readOnly: this.readOnly, writeOnly: this.writeOnly }
|
||||
)
|
||||
|
||||
// Initialize configuration
|
||||
const sharedConfig = await this.configManager.initialize()
|
||||
|
||||
// Create partitioner based on strategy
|
||||
if (sharedConfig.settings.partitionStrategy === 'hash') {
|
||||
this.partitioner = new HashPartitioner(sharedConfig)
|
||||
} else {
|
||||
// Default to hash partitioner for now
|
||||
this.partitioner = new HashPartitioner(sharedConfig)
|
||||
}
|
||||
|
||||
// Create operational mode based on role
|
||||
const role = this.configManager.getRole()
|
||||
this.operationalMode = OperationalModeFactory.createMode(role)
|
||||
|
||||
// Validate that role matches the configured mode
|
||||
// Don't override explicitly set readOnly/writeOnly
|
||||
if (role === 'reader' && !this.readOnly) {
|
||||
console.warn('Distributed role is "reader" but readOnly is not set. Setting readOnly=true for consistency.')
|
||||
this.readOnly = true
|
||||
this.writeOnly = false
|
||||
} else if (role === 'writer' && !this.writeOnly) {
|
||||
console.warn('Distributed role is "writer" but writeOnly is not set. Setting writeOnly=true for consistency.')
|
||||
this.readOnly = false
|
||||
this.writeOnly = true
|
||||
} else if (role === 'hybrid' && (this.readOnly || this.writeOnly)) {
|
||||
console.warn('Distributed role is "hybrid" but readOnly or writeOnly is set. Clearing both for hybrid mode.')
|
||||
this.readOnly = false
|
||||
this.writeOnly = false
|
||||
}
|
||||
|
||||
// Apply cache configuration from operational mode
|
||||
const modeCache = this.operationalMode.cacheStrategy
|
||||
if (modeCache) {
|
||||
this.cacheConfig = {
|
||||
...this.cacheConfig,
|
||||
hotCacheMaxSize: modeCache.hotCacheRatio * 1000000, // Convert ratio to size
|
||||
hotCacheEvictionThreshold: modeCache.hotCacheRatio,
|
||||
warmCacheTTL: modeCache.ttl,
|
||||
batchSize: modeCache.writeBufferSize || 100
|
||||
}
|
||||
|
||||
// Update storage cache config if it supports it
|
||||
if (this.storage && 'updateCacheConfig' in this.storage) {
|
||||
(this.storage as any).updateCacheConfig(this.cacheConfig)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize domain detector
|
||||
this.domainDetector = new DomainDetector()
|
||||
|
||||
// Initialize health monitor
|
||||
this.healthMonitor = new HealthMonitor(this.configManager)
|
||||
this.healthMonitor.start()
|
||||
|
||||
// Set up config update listener
|
||||
this.configManager.setOnConfigUpdate((config) => {
|
||||
this.handleDistributedConfigUpdate(config)
|
||||
})
|
||||
|
||||
if (this.loggingConfig?.verbose) {
|
||||
console.log(`Distributed mode initialized as ${role} with ${sharedConfig.settings.partitionStrategy} partitioning`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle distributed configuration updates
|
||||
*/
|
||||
private handleDistributedConfigUpdate(config: any): void {
|
||||
// Update partitioner if needed
|
||||
if (this.partitioner && config.settings) {
|
||||
this.partitioner = new HashPartitioner(config)
|
||||
}
|
||||
|
||||
// Log configuration update
|
||||
if (this.loggingConfig?.verbose) {
|
||||
console.log('Distributed configuration updated:', config.version)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get distributed health status
|
||||
* @returns Health status if distributed mode is enabled
|
||||
*/
|
||||
public getHealthStatus(): any {
|
||||
if (this.healthMonitor) {
|
||||
return this.healthMonitor.getHealthEndpointData()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to a remote Brainy server for search operations
|
||||
* @param serverUrl WebSocket URL of the remote Brainy server
|
||||
|
|
@ -1338,6 +1485,34 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
if (metadata && typeof metadata === 'object') {
|
||||
// Always make a copy without adding the ID
|
||||
metadataToSave = { ...metadata }
|
||||
|
||||
// Add domain metadata if distributed mode is enabled
|
||||
if (this.domainDetector) {
|
||||
// First check if domain is already in metadata
|
||||
if ((metadataToSave as any).domain) {
|
||||
// Domain already specified, keep it
|
||||
const domainInfo = this.domainDetector.detectDomain(metadataToSave)
|
||||
if (domainInfo.domainMetadata) {
|
||||
(metadataToSave as any).domainMetadata = domainInfo.domainMetadata
|
||||
}
|
||||
} else {
|
||||
// Try to detect domain from the data
|
||||
const dataToAnalyze = Array.isArray(vectorOrData) ? metadata : vectorOrData
|
||||
const domainInfo = this.domainDetector.detectDomain(dataToAnalyze)
|
||||
if (domainInfo.domain) {
|
||||
(metadataToSave as any).domain = domainInfo.domain
|
||||
if (domainInfo.domainMetadata) {
|
||||
(metadataToSave as any).domainMetadata = domainInfo.domainMetadata
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add partition information if distributed mode is enabled
|
||||
if (this.partitioner) {
|
||||
const partition = this.partitioner.getPartition(id)
|
||||
;(metadataToSave as any).partition = partition
|
||||
}
|
||||
}
|
||||
|
||||
await this.storage!.saveMetadata(id, metadataToSave)
|
||||
|
|
@ -1350,6 +1525,12 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
|
||||
// Update HNSW index size (excluding verbs)
|
||||
await this.storage!.updateHnswIndexSize(await this.getNounCount())
|
||||
|
||||
// Update health metrics if in distributed mode
|
||||
if (this.healthMonitor) {
|
||||
const vectorCount = await this.getNounCount()
|
||||
this.healthMonitor.updateVectorCount(vectorCount)
|
||||
}
|
||||
|
||||
// If addToRemote is true and we're connected to a remote server, add to remote as well
|
||||
if (options.addToRemote && this.isConnectedToRemoteServer()) {
|
||||
|
|
@ -1365,6 +1546,12 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
return id
|
||||
} catch (error) {
|
||||
console.error('Failed to add vector:', error)
|
||||
|
||||
// Track error in health monitor
|
||||
if (this.healthMonitor) {
|
||||
this.healthMonitor.recordRequest(0, true)
|
||||
}
|
||||
|
||||
throw new Error(`Failed to add vector: ${error}`)
|
||||
}
|
||||
}
|
||||
|
|
@ -1865,8 +2052,10 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
verbDirection?: 'outgoing' | 'incoming' | 'both' // Direction of verbs to consider when searching connected nouns
|
||||
service?: string // Filter results by the service that created the data
|
||||
searchField?: string // Optional specific field to search within JSON documents
|
||||
filter?: { domain?: string } // Filter results by domain
|
||||
} = {}
|
||||
): Promise<SearchResult<T>[]> {
|
||||
const startTime = Date.now()
|
||||
// Validate input is not null or undefined
|
||||
if (queryVectorOrData === null || queryVectorOrData === undefined) {
|
||||
throw new Error('Query cannot be null or undefined')
|
||||
|
|
@ -1925,7 +2114,25 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
}
|
||||
|
||||
// Default behavior (backward compatible): search locally
|
||||
return this.searchLocal(queryVectorOrData, k, options)
|
||||
try {
|
||||
const results = await this.searchLocal(queryVectorOrData, k, options)
|
||||
|
||||
// Track successful search in health monitor
|
||||
if (this.healthMonitor) {
|
||||
const latency = Date.now() - startTime
|
||||
this.healthMonitor.recordRequest(latency, false)
|
||||
this.healthMonitor.recordCacheAccess(results.length > 0)
|
||||
}
|
||||
|
||||
return results
|
||||
} catch (error) {
|
||||
// Track error in health monitor
|
||||
if (this.healthMonitor) {
|
||||
const latency = Date.now() - startTime
|
||||
this.healthMonitor.recordRequest(latency, true)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1945,6 +2152,7 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
service?: string // Filter results by the service that created the data
|
||||
searchField?: string // Optional specific field to search within JSON documents
|
||||
priorityFields?: string[] // Fields to prioritize when searching JSON documents
|
||||
filter?: { domain?: string } // Filter results by domain
|
||||
} = {}
|
||||
): Promise<SearchResult<T>[]> {
|
||||
if (!this.isInitialized) {
|
||||
|
|
@ -2024,7 +2232,16 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
if (result.metadata && typeof result.metadata === 'object') {
|
||||
const metadata = result.metadata as Record<string, any>
|
||||
// Exclude placeholder nouns from search results
|
||||
return !metadata.isPlaceholder
|
||||
if (metadata.isPlaceholder) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Apply domain filter if specified
|
||||
if (options.filter?.domain) {
|
||||
if (metadata.domain !== options.filter.domain) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
|
@ -4859,6 +5076,30 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
// Sort by score and limit to k results
|
||||
return allResults.sort((a, b) => b.score - a.score).slice(0, k)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup distributed resources
|
||||
* Should be called when shutting down the instance
|
||||
*/
|
||||
public async cleanup(): Promise<void> {
|
||||
// Stop real-time updates
|
||||
if (this.updateTimerId) {
|
||||
clearInterval(this.updateTimerId)
|
||||
this.updateTimerId = null
|
||||
}
|
||||
|
||||
// Clean up distributed mode resources
|
||||
if (this.healthMonitor) {
|
||||
this.healthMonitor.stop()
|
||||
}
|
||||
|
||||
if (this.configManager) {
|
||||
await this.configManager.cleanup()
|
||||
}
|
||||
|
||||
// Clean up worker pools
|
||||
await cleanupWorkerPools()
|
||||
}
|
||||
}
|
||||
|
||||
// Export distance functions for convenience
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue