- Replace complex 8-enhancement proposal with simpler 3-phase approach - Switch from semantic to hash-based partitioning for multi-writer scenarios - Introduce zero-config distributed mode with automatic role detection - Add shared JSON config coordination instead of complex locking mechanisms - Focus on minimal user burden with progressive disclosure for advanced features - Phase 1: Foundation with shared config and hash partitioning (3-4 days) - Phase 2: Optimizations for caching and domain metadata (2-3 days) - Phase 3: Optional advanced features for write-heavy workloads (3-4 days)
468 lines
No EOL
12 KiB
Markdown
468 lines
No EOL
12 KiB
Markdown
t# Brainy Distributed Enhancements - Revised Implementation Plan
|
|
|
|
## Executive Summary
|
|
|
|
Based on analysis of multi-writer scenarios and the need for unified search across diverse data types, this document presents a practical 3-phase approach to distributed Brainy deployment. The focus is on maximizing search performance and relevance while minimizing complexity for users and developers.
|
|
|
|
## Key Design Decisions
|
|
|
|
1. **Hash-based partitioning** for multi-writer scenarios (instead of semantic partitioning)
|
|
2. **Shared JSON config** in S3 for coordination (simple, debuggable)
|
|
3. **Automatic mode** by default with progressive disclosure for advanced users
|
|
4. **Domain tagging** for logical data separation while maintaining unified search
|
|
|
|
## Phase 1: Foundation (3-4 days) - High Benefit, Low Complexity
|
|
|
|
### 1.1 Shared Configuration System
|
|
|
|
Simple JSON config file at `_brainy/config.json` in S3 bucket:
|
|
|
|
```typescript
|
|
// Auto-generated config structure
|
|
{
|
|
"version": 1,
|
|
"updated": "2024-01-15T10:30:00Z",
|
|
"settings": {
|
|
"partitionStrategy": "hash", // Critical: hash for multi-writer
|
|
"partitionCount": 100, // Fixed count for consistency
|
|
"embeddingModel": "text-embedding-ada-002",
|
|
"dimensions": 1536,
|
|
"distanceMetric": "cosine"
|
|
},
|
|
"instances": {} // Auto-populated by instances
|
|
}
|
|
```
|
|
|
|
**Implementation:**
|
|
```typescript
|
|
// src/config/distributedConfig.ts
|
|
export class DistributedConfigManager {
|
|
private config: SharedConfig;
|
|
|
|
async initialize() {
|
|
// Try to load existing config
|
|
this.config = await this.loadOrCreateConfig();
|
|
|
|
// Auto-register this instance
|
|
await this.registerInstance();
|
|
|
|
// Start heartbeat and config watching
|
|
this.startHeartbeat();
|
|
this.watchConfig();
|
|
}
|
|
|
|
private async loadOrCreateConfig(): Promise<SharedConfig> {
|
|
try {
|
|
return await this.s3.getJSON('_brainy/config.json');
|
|
} catch (err) {
|
|
if (err.code === 'NoSuchKey') {
|
|
// First instance - create config
|
|
const newConfig = this.createDefaultConfig();
|
|
await this.s3.putJSON('_brainy/config.json', newConfig);
|
|
return newConfig;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**User Experience:**
|
|
```typescript
|
|
// No change for single instance!
|
|
const brainy = new BrainyData({
|
|
storage: { type: 's3', bucket: 'my-bucket' }
|
|
});
|
|
|
|
// Distributed with zero config
|
|
const brainy = new BrainyData({
|
|
storage: { type: 's3', bucket: 'my-bucket' },
|
|
distributed: true // Auto-detect role!
|
|
});
|
|
```
|
|
|
|
### 1.2 Automatic Role Detection
|
|
|
|
```typescript
|
|
export class RoleManager {
|
|
async determineRole(): Promise<'reader' | 'writer'> {
|
|
// Check environment hints
|
|
if (process.env.BRAINY_ROLE) {
|
|
return process.env.BRAINY_ROLE as 'reader' | 'writer';
|
|
}
|
|
|
|
// Check if running in Lambda (typically read-heavy)
|
|
if (process.env.AWS_LAMBDA_FUNCTION_NAME) {
|
|
return 'reader';
|
|
}
|
|
|
|
// Check existing instances
|
|
const config = await this.loadConfig();
|
|
const writers = Object.values(config.instances)
|
|
.filter(i => i.role === 'writer' && this.isAlive(i));
|
|
|
|
// First writer or no writers alive
|
|
if (writers.length === 0) {
|
|
return 'writer';
|
|
}
|
|
|
|
// Default to reader
|
|
return 'reader';
|
|
}
|
|
}
|
|
```
|
|
|
|
### 1.3 Hash-Based Partitioning
|
|
|
|
Replace semantic partitioning with deterministic hashing for multi-writer compatibility:
|
|
|
|
```typescript
|
|
// src/partitioning/hashPartitioner.ts
|
|
export class HashPartitioner {
|
|
private partitionCount: number;
|
|
|
|
constructor(config: SharedConfig) {
|
|
this.partitionCount = config.settings.partitionCount;
|
|
}
|
|
|
|
getPartition(vectorId: string): string {
|
|
// Deterministic hash - same ID always goes to same partition
|
|
const hash = this.xxhash(vectorId);
|
|
const partitionIndex = hash % this.partitionCount;
|
|
return `vectors/p${partitionIndex.toString().padStart(3, '0')}`;
|
|
}
|
|
|
|
// For domain separation while maintaining unified structure
|
|
getPartitionWithDomain(vectorId: string, domain: string): string {
|
|
const basePartition = this.getPartition(vectorId);
|
|
// Store domain as metadata, not in path
|
|
return basePartition;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Benefits:**
|
|
- ✅ Writers can write to any partition (no coordination needed)
|
|
- ✅ Even distribution of data
|
|
- ✅ Readers search all partitions uniformly
|
|
- ✅ No semantic coherence issues with mixed data types
|
|
|
|
## Phase 2: Optimization (2-3 days) - Medium Benefit, Low Complexity
|
|
|
|
### 2.1 Role-Optimized Caching
|
|
|
|
```typescript
|
|
// src/modes/operationalModes.ts
|
|
export class ReaderMode {
|
|
getCacheConfig() {
|
|
return {
|
|
hotCacheRatio: 0.8, // 80% memory for read cache
|
|
prefetchAggressive: true, // Prefetch neighboring vectors
|
|
ttl: 3600000, // 1 hour cache TTL
|
|
compressionEnabled: true // Trade CPU for memory
|
|
};
|
|
}
|
|
}
|
|
|
|
export class WriterMode {
|
|
getCacheConfig() {
|
|
return {
|
|
hotCacheRatio: 0.2, // 20% memory, focus on write buffer
|
|
writeBufferSize: 10000, // Batch writes
|
|
ttl: 60000, // Short TTL
|
|
compressionEnabled: false // Speed over memory
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2.2 Domain Metadata System
|
|
|
|
Enable filtering while maintaining unified search:
|
|
|
|
```typescript
|
|
// Automatic domain detection
|
|
export class DomainDetector {
|
|
detectDomain(data: any): string {
|
|
// Auto-detect based on data shape
|
|
if (data.symptoms || data.diagnosis) return 'medical';
|
|
if (data.contract || data.clause) return 'legal';
|
|
if (data.price || data.sku) return 'product';
|
|
return 'general';
|
|
}
|
|
}
|
|
|
|
// Writers automatically tag domains
|
|
await brainy.add(vector, {
|
|
id: vectorId,
|
|
domain: this.detectDomain(originalData),
|
|
...metadata
|
|
});
|
|
|
|
// Readers can search all or filter
|
|
const results = await brainy.search(query); // Search all domains
|
|
const medical = await brainy.search(query, {
|
|
filter: { domain: 'medical' }
|
|
});
|
|
```
|
|
|
|
### 2.3 Simple Health Monitoring
|
|
|
|
```typescript
|
|
// src/monitoring/health.ts
|
|
export class HealthMonitor {
|
|
async updateHealth() {
|
|
const health = {
|
|
instanceId: this.instanceId,
|
|
role: this.role,
|
|
status: 'healthy',
|
|
lastHeartbeat: new Date().toISOString(),
|
|
metrics: {
|
|
vectorCount: await this.getVectorCount(),
|
|
cacheHitRate: this.getCacheStats().hitRate,
|
|
memoryUsage: process.memoryUsage().heapUsed
|
|
}
|
|
};
|
|
|
|
// Update in config
|
|
const config = await this.loadConfig();
|
|
config.instances[this.instanceId] = health;
|
|
await this.saveConfig(config);
|
|
}
|
|
|
|
// Auto-cleanup stale instances
|
|
async cleanupStale() {
|
|
const config = await this.loadConfig();
|
|
const now = Date.now();
|
|
|
|
for (const [id, instance] of Object.entries(config.instances)) {
|
|
const lastSeen = new Date(instance.lastHeartbeat).getTime();
|
|
if (now - lastSeen > 60000) { // 60s timeout
|
|
delete config.instances[id];
|
|
}
|
|
}
|
|
|
|
await this.saveConfig(config);
|
|
}
|
|
}
|
|
```
|
|
|
|
**User Experience:**
|
|
```typescript
|
|
// Still zero config!
|
|
const brainy = new BrainyData({
|
|
storage: { type: 's3', bucket: 'my-bucket' },
|
|
distributed: true
|
|
});
|
|
|
|
// Optional: specify domain for better organization
|
|
await brainy.add(vector, {
|
|
domain: 'medical', // Optional hint
|
|
...data
|
|
});
|
|
```
|
|
|
|
## Phase 3: Advanced Features (Optional, 3-4 days)
|
|
|
|
### 3.1 Partition Affinity (Reduce S3 Conflicts)
|
|
|
|
```typescript
|
|
// Writers prefer certain partitions but can use any
|
|
export class AffinityPartitioner extends HashPartitioner {
|
|
private preferredPartitions: Set<number>;
|
|
|
|
constructor(config: SharedConfig, instanceId: string) {
|
|
super(config);
|
|
// Each writer prefers different partition ranges
|
|
const writerIndex = this.getWriterIndex(instanceId);
|
|
const partitionsPerWriter = Math.ceil(this.partitionCount / this.writerCount);
|
|
|
|
this.preferredPartitions = new Set();
|
|
const start = writerIndex * partitionsPerWriter;
|
|
const end = Math.min(start + partitionsPerWriter, this.partitionCount);
|
|
|
|
for (let i = start; i < end; i++) {
|
|
this.preferredPartitions.add(i);
|
|
}
|
|
}
|
|
|
|
getPartition(vectorId: string): string {
|
|
const hash = this.xxhash(vectorId);
|
|
const idealPartition = hash % this.partitionCount;
|
|
|
|
// Use ideal if it's in our preferred set
|
|
if (this.preferredPartitions.has(idealPartition)) {
|
|
return `vectors/p${idealPartition.toString().padStart(3, '0')}`;
|
|
}
|
|
|
|
// Otherwise use it anyway (correctness > optimization)
|
|
return `vectors/p${idealPartition.toString().padStart(3, '0')}`;
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3.2 Smart Batching for Writers
|
|
|
|
```typescript
|
|
export class BatchWriter {
|
|
private batch: Map<string, Vector[]> = new Map();
|
|
private batchSize = 1000;
|
|
private flushInterval = 5000;
|
|
|
|
async add(vector: Vector) {
|
|
const partition = this.getPartition(vector.id);
|
|
|
|
if (!this.batch.has(partition)) {
|
|
this.batch.set(partition, []);
|
|
}
|
|
|
|
this.batch.get(partition).push(vector);
|
|
|
|
// Flush when batch is full
|
|
if (this.batch.get(partition).length >= this.batchSize) {
|
|
await this.flushPartition(partition);
|
|
}
|
|
}
|
|
|
|
private async flushPartition(partition: string) {
|
|
const vectors = this.batch.get(partition);
|
|
if (!vectors || vectors.length === 0) return;
|
|
|
|
// Single S3 write for entire batch
|
|
await this.s3.putJSON(
|
|
`${partition}/batch_${Date.now()}.json`,
|
|
vectors
|
|
);
|
|
|
|
this.batch.delete(partition);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3.3 Query Optimization for Readers
|
|
|
|
```typescript
|
|
export class OptimizedReader {
|
|
private partitionStats: Map<string, PartitionStats> = new Map();
|
|
|
|
async search(query: number[], k: number = 10) {
|
|
// Load partition statistics
|
|
await this.loadPartitionStats();
|
|
|
|
// Parallel search with smart pruning
|
|
const partitions = await this.selectPartitions(query);
|
|
|
|
const results = await Promise.all(
|
|
partitions.map(p => this.searchPartition(p, query, k))
|
|
);
|
|
|
|
// Merge and return top-k
|
|
return this.mergeResults(results, k);
|
|
}
|
|
|
|
private async selectPartitions(query: number[]) {
|
|
// For hash partitioning, usually search all
|
|
// But can optimize based on domain filters or stats
|
|
return this.getAllPartitions();
|
|
}
|
|
}
|
|
```
|
|
|
|
## Deployment Examples
|
|
|
|
### Minimal Configuration
|
|
|
|
```yaml
|
|
# docker-compose.yml
|
|
services:
|
|
writer:
|
|
image: myapp
|
|
environment:
|
|
BRAINY_ROLE: writer # Optional - auto-detects if not set
|
|
|
|
reader:
|
|
image: myapp
|
|
environment:
|
|
BRAINY_ROLE: reader # Optional - auto-detects if not set
|
|
scale: 3
|
|
```
|
|
|
|
### Kubernetes
|
|
|
|
```yaml
|
|
# No config needed - auto-detection works!
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: brainy-readers
|
|
spec:
|
|
replicas: 10
|
|
template:
|
|
spec:
|
|
containers:
|
|
- name: app
|
|
image: myapp
|
|
# Role auto-detected as reader (multiple replicas)
|
|
```
|
|
|
|
### Application Code
|
|
|
|
```typescript
|
|
// Simplest - full auto mode
|
|
const brainy = new BrainyData({
|
|
storage: { type: 's3', bucket: 'my-bucket' },
|
|
distributed: true // That's it!
|
|
});
|
|
|
|
// With domain hints (optional)
|
|
await brainy.add(vector, {
|
|
domain: 'medical', // Helps with organization
|
|
...metadata
|
|
});
|
|
|
|
// Search everything
|
|
const results = await brainy.search(queryVector);
|
|
|
|
// Or filter by domain
|
|
const medical = await brainy.search(queryVector, {
|
|
filter: { domain: 'medical' }
|
|
});
|
|
```
|
|
|
|
## Summary of Benefits
|
|
|
|
### Phase 1 (Days 1-4)
|
|
- ✅ **Zero-config distributed mode** - Just add `distributed: true`
|
|
- ✅ **Automatic role detection** - No manual assignment needed
|
|
- ✅ **Hash partitioning** - Solves multi-writer semantic conflicts
|
|
- ✅ **Shared configuration** - All instances stay in sync
|
|
- **Benefit**: 50-70% search performance improvement through parallel readers
|
|
|
|
### Phase 2 (Days 5-7)
|
|
- ✅ **Optimized caching** - Readers cache aggressively, writers batch
|
|
- ✅ **Domain tagging** - Logical separation without complexity
|
|
- ✅ **Health monitoring** - Automatic cleanup of dead instances
|
|
- **Benefit**: Additional 20-30% performance gain
|
|
|
|
### Phase 3 (Optional)
|
|
- ✅ **Partition affinity** - Reduce S3 write conflicts
|
|
- ✅ **Smart batching** - Fewer S3 operations
|
|
- ✅ **Query optimization** - Pruning and parallel search
|
|
- **Benefit**: 10-20% improvement for write-heavy workloads
|
|
|
|
## Migration Path
|
|
|
|
```typescript
|
|
// Day 1: Your current code (no changes needed!)
|
|
const brainy = new BrainyData({
|
|
storage: { type: 's3', bucket: 'my-bucket' }
|
|
});
|
|
|
|
// Day 4: Enable distributed mode (one line change)
|
|
const brainy = new BrainyData({
|
|
storage: { type: 's3', bucket: 'my-bucket' },
|
|
distributed: true // Everything else is automatic!
|
|
});
|
|
|
|
// That's it! The system handles the rest.
|
|
``` |