--- title: Batch Operations slug: guides/batching public: true category: guides template: guide order: 5 description: Eliminate N+1 query patterns with batchGet() and storage-level batch APIs for fast multi-entity reads against filesystem and memory storage. next: - api/reference - guides/find-system --- # Batch Operations API > **Production-Ready** | Zero N+1 Query Patterns ## Overview Brainy provides batch operations at the storage layer to eliminate N+1 query patterns for VFS operations, relationship queries, and entity retrieval. ### Problem Solved The naive pattern of looping and calling `brain.get(id)` once per item issues sequential reads through the storage layer. Batched APIs collapse that into a single read pass. **IMPORTANT:** The batch optimizations apply **ONLY to `getTreeStructure()`** at the VFS layer and the explicit `batchGet()` / `getNounMetadataBatch()` calls — not to `readFile()` or individual `get()` operations. --- ## New Public APIs ### 1. `brain.batchGet(ids, options?)` Batch retrieval of multiple entities (metadata-only by default). ```typescript // Fetch multiple entities in a single batched operation const ids = ['id1', 'id2', 'id3'] const results: Map = await brain.batchGet(ids) // With vectors (falls back to individual gets) const resultsWithVectors = await brain.batchGet(ids, { includeVectors: true }) // Results map results.get('id1') // → Entity or undefined results.size // → 3 (number of found entities) ``` **Performance:** - Memory storage: Instant (parallel reads) - Filesystem storage: Parallel reads, scales with available IOPS **Use Cases:** - Loading multiple entities for display - Bulk data export operations - Relationship traversal (fetch all connected entities) --- ## Storage-Level APIs ### 2. `storage.getNounMetadataBatch(ids)` Batch metadata retrieval with direct O(1) path construction. ```typescript const storage = brain.storage as BaseStorage const ids = ['id1', 'id2', 'id3'] const metadataMap: Map = await storage.getNounMetadataBatch(ids) for (const [id, metadata] of metadataMap) { console.log(metadata.noun) // Type: 'document', 'person', etc. console.log(metadata.data) // Entity data } ``` **Features:** - ✅ Direct O(1) path construction from ID (no type lookup needed!) - ✅ Sharding preservation (all paths include `{shard}/{id}`) - ✅ Write-cache coherent (read-after-write consistency) - ✅ 40x faster than v5.x type-first architecture **Performance:** - ~1ms per 100 entities (consistent, no cache misses!) - Filesystem: parallel reads bounded by IOPS - No type search delays — every ID maps directly to storage path --- ### 3. `storage.getVerbsBySourceBatch(sourceIds, verbType?)` Batch relationship queries by source entity IDs. ```typescript const storage = brain.storage as BaseStorage // Get all relationships from multiple sources const results: Map = await storage.getVerbsBySourceBatch([ 'person1', 'person2' ]) // Filter by verb type const createsResults = await storage.getVerbsBySourceBatch( ['person1', 'person2'], 'creates' ) // Process results for (const [sourceId, verbs] of results) { console.log(`${sourceId} has ${verbs.length} relationships`) verbs.forEach(verb => { console.log(` → ${verb.verb} → ${verb.targetId}`) }) } ``` **Use Cases:** - Social graph traversal (fetch all connections for multiple users) - Knowledge graph queries (find all relationships of specific type) - Bulk export of relationship data **Performance:** - Memory storage: <10ms for 1000 relationships - Filesystem storage: parallel reads through the metadata index --- ## VFS Integration VFS operations automatically use batch APIs for maximum performance. ### Directory Traversal ```typescript // Tree traversal uses batched reads under the hood const tree = await brain.vfs.getTreeStructure('/my-dir') // ✅ PathResolver.getChildren() uses brain.batchGet() internally // ✅ Parallel traversal of directories at the same tree level // ✅ 2-3 batched calls instead of 22 sequential calls ``` **Architecture:** ``` VFS.getTreeStructure() ↓ PARALLEL (breadth-first traversal) → PathResolver.getChildren() [all dirs at level processed in parallel] ↓ BATCHED → brain.batchGet(childIds) [1 call instead of N] ↓ BATCHED → storage.getNounMetadataBatch(ids) [1 call instead of N] ↓ ADAPTER → Filesystem: Promise.all() parallel reads → Memory: Promise.all() parallel reads ``` --- ## Advanced Features Compatibility ### ✅ ID-First Storage Architecture All batch operations use direct ID-first paths - no type lookup needed! **ID-First Path Structure:** ``` entities/nouns/{SHARD}/{ID}/metadata.json entities/verbs/{SHARD}/{ID}/metadata.json ``` **Direct O(1) Path Construction:** ```typescript // Every ID maps directly to exactly ONE path - 40x faster! const id = 'abc-123' const shard = getShardIdFromUuid(id) // → 'ab' (first 2 hex chars) const path = `entities/nouns/${shard}/${id}/metadata.json` // No type cache needed! // No type search needed! // No multi-type fallback needed! // Just pure O(1) lookup! ``` **Benefits:** - **40x faster** path lookups (eliminates 42-type sequential search) - **Simpler code** - removed 500+ lines of type cache complexity - **Scalable** - works at billion-scale without type tracking overhead --- ### ✅ Sharding All batch paths include shard IDs calculated via `getShardIdFromUuid(id)`: ```typescript const id = 'a3c4e5f7-...' const shard = getShardIdFromUuid(id) // → 'a3' (first 2 hex chars) const path = `entities/nouns/${shard}/${id}/metadata.json` ``` **Distribution:** 256 shards (00-ff) for optimal load distribution. --- ### ✅ Generational MVCC (8.0) Batch reads always serve the **live** generation through the fast paths shown above. Point-in-time reads go through the Db API instead: a pinned `Db` (`brain.now()`, `brain.asOf()`) resolves changed ids from immutable generation records and unchanged ids from the same live paths batch reads use — see the [consistency model](concepts/consistency-model.md). ```typescript const db = brain.now() // pinned view const entity = await db.get(id) // correct at the pinned generation const results = await brain.batchGet(ids) // live state, batched await db.release() ``` --- ## Performance Benchmarks ### VFS Operations (12 Files) | Storage | Before optimization | After optimization | Improvement | |---------|---------------------|--------------------|-------------| | **Memory** | 150ms | 50ms | **67% faster** | | **Filesystem** | ~500ms | ~80ms | **84% faster** | ### Entity Batch Retrieval (100 Entities) | Storage | Individual Gets | Batch Get | Improvement | |---------|-----------------|-----------|-------------| | **Memory** | 180ms | 15ms | **92% faster** | | **Filesystem** | ~1.2s | ~120ms | **90% faster** | ### Throughput (Entities/Second) | Storage | Individual | Batch | Improvement | |---------|------------|-------|-------------| | **Memory** | 556 ent/s | 6667 ent/s | **12x** | | **Filesystem** | ~80 ent/s | ~800 ent/s | **10x** | --- ## Error Handling ### Partial Batch Failures Batch operations gracefully handle missing or invalid entities: ```typescript const validId = 'abc-123-...' const invalidIds = [ '11111111-1111-1111-1111-111111111111', '22222222-2222-2222-2222-222222222222' ] const results = await brain.batchGet([validId, ...invalidIds]) results.size // → 1 (only valid entity) results.has(validId) // → true results.has(invalidIds[0]) // → false (silently skipped) ``` **Behavior:** - Invalid UUIDs: Silently skipped (not included in results) - Missing entities: Silently skipped (not included in results) - Storage errors: Logged, entity excluded from results - No exceptions thrown for partial failures ### Empty Batches ```typescript const results = await brain.batchGet([]) results.size // → 0 (empty map) ``` ### Duplicate IDs ```typescript const results = await brain.batchGet(['id1', 'id1', 'id1']) results.size // → 1 (deduplicated automatically) ``` --- ## Migration Guide ### From Individual Gets **Before:** ```typescript const entities = [] for (const id of ids) { const entity = await brain.get(id) if (entity) entities.push(entity) } ``` **After:** ```typescript const results = await brain.batchGet(ids) const entities = Array.from(results.values()) ``` **Performance Gain:** 10-20x faster on filesystem storage. --- ### From Individual Relationship Queries **Before:** ```typescript const allVerbs = [] for (const sourceId of sourceIds) { const verbs = await brain.related({ from: sourceId }) allVerbs.push(...verbs) } ``` **After:** ```typescript const storage = brain.storage as BaseStorage const results = await storage.getVerbsBySourceBatch(sourceIds) const allVerbs = [] for (const verbs of results.values()) { allVerbs.push(...verbs) } ``` **Performance Gain:** 5-10x faster due to batched metadata fetches. --- ## Best Practices ### 1. **Use Batching for Multiple Entity Operations** ```typescript // ✅ GOOD: Batch fetch const results = await brain.batchGet(ids) // ❌ BAD: Individual gets in loop for (const id of ids) { await brain.get(id) } ``` ### 2. **Batch Size Recommendations** | Storage | Optimal Batch Size | Max Batch Size | |---------|--------------------|----------------| | **Memory** | Unlimited | Unlimited | | **Filesystem** | 100-500 | 1000 | **Guideline:** For batches >1000, split into chunks of 500-1000. ### 3. **Metadata-Only by Default** ```typescript // Default: Metadata-only (fast) const results = await brain.batchGet(ids) // No vectors // Only load vectors if needed const withVectors = await brain.batchGet(ids, { includeVectors: true }) ``` ### 4. **Error Handling** ```typescript // Batch operations never throw for missing entities const results = await brain.batchGet(ids) // Check results for (const id of ids) { if (results.has(id)) { // Entity exists const entity = results.get(id) } else { // Entity missing (not an error) console.log(`Entity ${id} not found`) } } ``` --- ## Testing Comprehensive test coverage in `tests/integration/storage-batch-operations.test.ts`: ```bash npx vitest run tests/integration/storage-batch-operations.test.ts ``` **Test Coverage:** - ✅ brain.batchGet() high-level API - ✅ storage.getNounMetadataBatch() with ID-first paths - ✅ COW integration (branch isolation, inheritance) - ✅ storage.getVerbsBySourceBatch() relationship queries - ✅ VFS integration (PathResolver.getChildren()) - ✅ Performance benchmarks (N+1 elimination) - ✅ Error handling (partial failures, empty batches, duplicates) - ✅ ID-first storage verification - ✅ Sharding preservation **Results:** 23 tests passing ✅ --- ## Implementation Details ### Architecture Layers ``` User Code (brain.batchGet) ↓ High-Level API (src/brainy.ts) ↓ Storage Layer (src/storage/baseStorage.ts) ↓ Adapter Layer (readBatchFromAdapter) ↓ Storage Adapter (FileSystemStorage / MemoryStorage) ``` ### Parallel Reads Both shipped adapters fall back to `Promise.all` over individual reads: ```typescript // BaseStorage.readBatchFromAdapter() return await Promise.all(resolvedPaths.map(path => this.read(path))) ``` **Shipped Adapters:** - MemoryStorage - FileSystemStorage --- ## API Summary - `brain.batchGet(ids, options?)` - High-level batch entity retrieval - `storage.getNounMetadataBatch(ids)` - Storage-level metadata batch - `storage.getVerbsBySourceBatch(sourceIds, verbType?)` - Batch relationship queries **Performance Improvements:** - VFS operations: 90%+ faster than the naive per-entity loop - Entity retrieval: 10-20x throughput improvement - Zero N+1 query patterns **Compatibility:** - ✅ ID-first storage - ✅ Sharding (256 shards) - ✅ Generational MVCC — batch reads serve the live generation; pinned `Db` views serve the past - ✅ All indexes respected (vector, metadata, graph adjacency) --- ## Support - **Documentation:** `/docs/BATCHING.md`, `/docs/PERFORMANCE.md` - **Tests:** `/tests/integration/storage-batch-operations.test.ts` - **Issues:** https://github.com/soulcraft/brainy/issues - **Discussions:** https://github.com/soulcraft/brainy/discussions --- **Built with ❤️ for enterprise-scale knowledge graphs**