The 3.0 native engine ships as @soulcraft/cor (brainy 8.x <-> cor 3.x are a version-matched pair); the old @soulcraft/cortex package stays on the 2.x/7.x line. Public docs and .d.ts-visible comments now name the correct package. Historical 2.x contract references (e.g. the 2.3.1 read-side fallback) keep the old name deliberately.
239 lines
8.2 KiB
Markdown
239 lines
8.2 KiB
Markdown
# Brainy Scaling Guide
|
||
|
||
> **One Line Summary**: Single-node by design — Brainy scales by getting the most out of one machine plus operator-layer backup.
|
||
|
||
## Table of Contents
|
||
- [Quick Start](#quick-start)
|
||
- [How Brainy Scales](#how-brainy-scales)
|
||
- [Storage Configurations](#storage-configurations)
|
||
- [Scaling Patterns](#scaling-patterns)
|
||
- [Real World Examples](#real-world-examples)
|
||
|
||
## Quick Start
|
||
|
||
### In-Memory
|
||
```typescript
|
||
import Brainy from '@soulcraft/brainy'
|
||
const brain = new Brainy({ storage: { type: 'memory' } })
|
||
```
|
||
|
||
### On-Disk (Default for Node)
|
||
```typescript
|
||
const brain = new Brainy({
|
||
storage: { type: 'filesystem', path: './brainy-data' }
|
||
})
|
||
```
|
||
|
||
## How Brainy Scales
|
||
|
||
Brainy 8.0 is a **single-node library**. There is no cluster, no peer discovery, no S3 coordination. Scaling means:
|
||
|
||
- **Up**: give the process more RAM, CPU, and IOPS
|
||
- **Out**: stand up multiple independent Brainy instances behind your own service layer
|
||
- **Cold storage**: snapshot the on-disk artifact off-site so you can rehydrate elsewhere
|
||
|
||
The three knobs that matter most:
|
||
|
||
1. **`config.vector.recall`** — `'fast'`, `'balanced'`, or `'accurate'` (default `'balanced'`)
|
||
2. **`config.vector.persistMode`** — `'immediate'` for durability, `'deferred'` for throughput
|
||
|
||
The native vector provider (via the optional `@soulcraft/cor` package) extends this with a higher-performing index — and its own at-scale acceleration such as on-disk compressed indexing — when installed.
|
||
|
||
## Measured Performance
|
||
|
||
Numbers below are **measured** by `tests/benchmarks/find-composition-scale.js` (a single
|
||
Node 22 process, in-memory storage, 384-dim vectors, `balanced` recall). They are the
|
||
open-core (pure-TypeScript) path — what you get from `@soulcraft/brainy` with no native
|
||
provider installed. Run it yourself: `node --max-old-space-size=8192 tests/benchmarks/find-composition-scale.js 100000`.
|
||
|
||
`find()` query latency, p50 / p95 (200 queries each):
|
||
|
||
| Query | 5,000 entities | 100,000 entities |
|
||
|---|---|---|
|
||
| Vector similarity (`{ vector }`) | 0.8 / 1.3 ms | 1.4 / 4.7 ms |
|
||
| Graph 1-hop (`{ connected }`) | 0.5 / 0.7 ms | 0.7 / 0.8 ms |
|
||
| Metadata filter (`{ where }`, low-selectivity) | 0.7 / 1.2 ms | 23.5 / 30.1 ms |
|
||
| Vector + metadata | 7.7 / 8.3 ms | 78.8 / 93.8 ms |
|
||
|
||
What the shape tells you:
|
||
|
||
- **Vector and graph lookups scale ~logarithmically** — they barely move from 5k to 100k,
|
||
because HNSW search is ~O(ef·log n) and graph adjacency is O(degree).
|
||
- **Metadata-filtered paths scale with the size of the match set, not the database.** The
|
||
benchmark's `category` filter matches ~10% of rows (10,000 at 100k); the cost is
|
||
materializing that candidate set and running the vector search *inside* it (`find()` does
|
||
metadata-first hard filtering, then ranks within the candidates — see
|
||
[How find works](./FIND_SYSTEM.md)). A **high-selectivity** filter (few matches) is far
|
||
cheaper; a 10%-of-everything filter is the worst case. This candidate-restricted search is
|
||
precisely the path the native provider accelerates (Rust roaring-bitmap candidate
|
||
intersection).
|
||
- **Composition is correct, not lossy.** Combining vector + metadata + graph returns exactly
|
||
the entities satisfying all constraints — verified by
|
||
`tests/integration/find-triple-composition.test.ts`.
|
||
|
||
Memory: ~62 KB resident per entity at 100k (6.2 GB RSS for 100k × 384-dim including the HNSW
|
||
graph, metadata index, and 100k edges).
|
||
|
||
**Scale ceiling (open-core).** The pure-JS HNSW *build* cost (~100 inserts/s at 384-dim on
|
||
one core) makes the in-process open-core path most appropriate up to ~10⁵–10⁶ entities.
|
||
*Query* latency stays low well beyond that, but for the 10⁸–10¹⁰ regime install the native
|
||
provider (`@soulcraft/cor`, on-disk DiskANN) — same API, no code change. _Projected from
|
||
the two measured points, vector p50 at 1M is ~2 ms; metadata-heavy composition grows with
|
||
match-set size and is the path to move onto the native provider first._
|
||
|
||
## Storage Configurations
|
||
|
||
### Filesystem (Recommended for Production)
|
||
```typescript
|
||
const brain = new Brainy({
|
||
storage: {
|
||
type: 'filesystem',
|
||
path: '/var/lib/brainy'
|
||
}
|
||
})
|
||
```
|
||
- Stores everything in a sharded JSON tree under `path`
|
||
- Atomic writes via rename
|
||
- Survives process restarts
|
||
- Snapshot it off-site with `gsutil rsync`, `aws s3 sync`, `rclone`, or `tar` from your scheduler
|
||
|
||
### Memory
|
||
```typescript
|
||
const brain = new Brainy({ storage: { type: 'memory' } })
|
||
```
|
||
- Zero I/O, fastest possible
|
||
- No persistence — process exit discards everything
|
||
- Use for tests and ephemeral caches
|
||
|
||
### Auto
|
||
```typescript
|
||
const brain = new Brainy({
|
||
storage: { type: 'auto', path: './data' }
|
||
})
|
||
```
|
||
- Picks `filesystem` when running on Node with a writable `path`
|
||
- Falls back to `memory` otherwise
|
||
|
||
## Scaling Patterns
|
||
|
||
### Stage 1: Prototype (Memory)
|
||
```typescript
|
||
const brain = new Brainy({ storage: { type: 'memory' } })
|
||
// Development, tests, <100K items
|
||
```
|
||
|
||
### Stage 2: Production (Filesystem)
|
||
```typescript
|
||
const brain = new Brainy({
|
||
storage: { type: 'filesystem', path: '/var/lib/brainy' }
|
||
})
|
||
// Most production workloads up to ~10M entities on a single host
|
||
```
|
||
|
||
### Stage 3: Higher Throughput (Tune the Vector Index)
|
||
```typescript
|
||
const brain = new Brainy({
|
||
storage: { type: 'filesystem', path: '/var/lib/brainy' },
|
||
vector: {
|
||
recall: 'fast', // Trade recall for latency
|
||
persistMode: 'deferred' // Batch persistence
|
||
}
|
||
})
|
||
```
|
||
|
||
### Stage 4: Multi-Instance (Operator-Layer)
|
||
Run multiple Brainy processes behind your own routing/service layer. Each process owns its own `path`. Sync each artifact off-site independently. Brainy itself does not coordinate between processes.
|
||
|
||
## Real World Examples
|
||
|
||
### Example 1: Single-Node App With Backup
|
||
```typescript
|
||
const brain = new Brainy({
|
||
storage: { type: 'filesystem', path: '/var/lib/brainy' }
|
||
})
|
||
```
|
||
Schedule (cron / systemd timer):
|
||
```bash
|
||
*/15 * * * * rclone sync /var/lib/brainy remote:brainy-backup
|
||
```
|
||
|
||
### Example 2: Tests
|
||
```typescript
|
||
const brain = new Brainy({ storage: { type: 'memory' } })
|
||
// Fast, no cleanup needed between runs
|
||
```
|
||
|
||
### Example 3: Multi-Tenant Service
|
||
Spin up one Brainy instance per tenant, each in its own directory:
|
||
```typescript
|
||
function brainForTenant(tenantId: string) {
|
||
return new Brainy({
|
||
storage: {
|
||
type: 'filesystem',
|
||
path: `/var/lib/brainy/${tenantId}`
|
||
}
|
||
})
|
||
}
|
||
```
|
||
Your service layer handles routing and isolation; Brainy stays simple.
|
||
|
||
### Example 4: Higher Recall at Scale
|
||
```typescript
|
||
const brain = new Brainy({
|
||
storage: { type: 'filesystem', path: '/var/lib/brainy' },
|
||
vector: {
|
||
recall: 'accurate'
|
||
}
|
||
})
|
||
```
|
||
|
||
## Tuning Knobs Summary
|
||
|
||
| Setting | Values | When to change |
|
||
|---------|--------|----------------|
|
||
| `vector.recall` | `'fast'` / `'balanced'` / `'accurate'` | Trade recall for latency |
|
||
| `vector.persistMode` | `'immediate'` / `'deferred'` | Throughput vs. durability |
|
||
| `storage.cache.maxSize` | integer | Hot-path read cache size |
|
||
| `storage.cache.ttl` | ms | Cache freshness |
|
||
|
||
## Monitoring & Observability
|
||
|
||
```typescript
|
||
const stats = await brain.stats()
|
||
// {
|
||
// nounCount: 50000,
|
||
// verbCount: 80000,
|
||
// vectorIndex: { ... },
|
||
// storage: { used: '45GB' }
|
||
// }
|
||
```
|
||
|
||
## Troubleshooting
|
||
|
||
### Issue: Slow queries
|
||
1. Switch to `vector.recall: 'fast'`
|
||
2. Increase the read cache (`storage.cache.maxSize`)
|
||
3. Consider the optional native vector provider via `@soulcraft/cor`
|
||
|
||
### Issue: Memory pressure
|
||
1. Reduce `storage.cache.maxSize`
|
||
2. Move to `vector.persistMode: 'deferred'` to batch writes
|
||
3. Consider the optional native vector provider via `@soulcraft/cor` for at-scale index acceleration
|
||
|
||
### Issue: Slow startup after a crash
|
||
1. Use `vector.persistMode: 'immediate'` so the index file stays in sync with storage
|
||
2. Verify backup integrity periodically
|
||
|
||
## Best Practices
|
||
|
||
1. **One process = one `path`** — never share a directory between processes
|
||
2. **Snapshot from your scheduler** — Brainy doesn't ship cloud SDKs; use `rclone` / `aws s3 sync` / `gsutil`
|
||
3. **Profile before tuning** — `recall: 'balanced'` is right for most workloads
|
||
4. **Install the native vector provider only when measured profiling shows it pays off**
|
||
|
||
## Summary
|
||
|
||
- Brainy 8.0 is a **library**, not a cluster
|
||
- Storage adapters: `filesystem`, `memory`, `auto`
|
||
- Vector tuning: `recall`, `persistMode`
|
||
- Backup is an operator-layer concern — snapshot `path`
|