brainy/docs/architecture/index-architecture.md
David Snelling 2f6ab9559a feat: optimize metadata indexing with roaring bitmaps for 90% memory reduction
Replace JavaScript Sets with hardware-accelerated RoaringBitmap32 for metadata indexes.

Key improvements:
- 1.4x average speedup, up to 3.3x on 10K entities
- 90% memory reduction (40 bytes/UUID → 4 bytes/int)
- Hardware-accelerated multi-field intersection via SIMD (AVX2/SSE4.2)
- EntityIdMapper for bidirectional UUID ↔ integer mapping
- Portable serialization format

Benchmark results (1,000 queries):
- 10K entities: 3.74ms → 1.14ms (3.3x faster, 90% memory savings)
- 100K entities: 2.60ms → 1.78ms (1.5x faster, 88% memory savings)

Implementation:
- Add EntityIdMapper class for UUID/int mapping with persistence
- Modify ChunkData to use Map<string, RoaringBitmap32>
- Add getIdsForMultipleFields() for fast bitmap intersection
- Include comprehensive tests (25 tests passing)
- Add performance benchmark comparing Set vs Roaring

Technical details:
- roaring@2.4.0 dependency
- Maintains backward compatibility
- All queries still return UUID strings
- Automatic persistence via storage adapter
2025-10-13 16:39:06 -07:00

877 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Index Architecture
Brainy uses a sophisticated **4-index architecture** that enables "Triple Intelligence" - the unified combination of vector similarity, graph relationships, and metadata filtering. This document provides a comprehensive architectural overview of how these indexes work internally and coordinate with each other.
## Overview: The Four Core Indexes
| Index | Purpose | Data Structure | Complexity | File Location |
|-------|---------|----------------|------------|---------------|
| **MetadataIndex** | Fast metadata filtering | Chunked sparse indices with bloom filters + zone maps | O(1) exact, O(log n) ranges | `src/utils/metadataIndex.ts` |
| **HNSWIndex** | Vector similarity search | Hierarchical graphs | O(log n) search | `src/hnsw/hnswIndex.ts` |
| **GraphAdjacencyIndex** | Relationship traversal | Bidirectional adjacency maps | O(1) per hop | `src/graph/graphAdjacencyIndex.ts` |
| **DeletedItemsIndex** | Soft-delete tracking | Simple Set | O(1) all ops | `src/utils/deletedItemsIndex.ts` |
All four indexes share a **UnifiedCache** for coordinated memory management, ensuring fair resource allocation and preventing any single index from monopolizing memory.
## 1. MetadataIndex - Fast Field Filtering
**Purpose**: Enable O(1) field-value lookups and O(log n) range queries on metadata fields using adaptive chunked sparse indexing.
### Internal Architecture (v3.42.0)
```typescript
class MetadataIndexManager {
// Chunked sparse indices: field → SparseIndex (replaces flat files)
private sparseIndices = new Map<string, SparseIndex>()
// Chunk management
private chunkManager: ChunkManager
private chunkingStrategy: AdaptiveChunkingStrategy
// Lightweight field statistics
private fieldIndexes = new Map<string, FieldIndexData>() // value → count
private fieldStats = new Map<string, FieldStats>() // cardinality tracking
// Type-field affinity for NLP understanding
private typeFieldAffinity = new Map<string, Map<string, number>>()
// Shared memory management
private unifiedCache: UnifiedCache
}
```
### Key Data Structures
#### Chunked Sparse Index (NEW in v3.42.0)
```typescript
// SparseIndex: Directory of chunks for a field
// Example: field="status"
class SparseIndex {
field: string
chunks: ChunkDescriptor[] // Metadata about each chunk
bloomFilters: BloomFilter[] // Fast membership testing
}
// ChunkDescriptor: Metadata about a chunk
interface ChunkDescriptor {
chunkId: number
valueCount: number // How many unique values in this chunk
idCount: number // Total entity IDs
zoneMap: ZoneMap // Min/max for range queries
lastUpdated: number
}
// Actual chunk data stored separately
class ChunkData {
chunkId: number
field: string
entries: Map<value, RoaringBitmap32> // ~50 values per chunk (v3.43.0: roaring bitmaps!)
}
```
**Performance**:
- O(1) exact lookup with bloom filters (1% false positive rate)
- O(log n) range queries with zone maps
- 630x file reduction (560k flat files → 89 chunk files)
#### Roaring Bitmap Optimization (NEW in v3.43.0)
**Problem Solved**: JavaScript `Set<string>` for storing entity IDs was inefficient:
- Memory overhead: ~40 bytes per UUID string (36 chars + overhead)
- Slow intersection: JavaScript array filtering for multi-field queries
- No hardware acceleration
**Solution**: Replace `Set<string>` with `RoaringBitmap32` for 90% memory savings and hardware-accelerated operations.
```typescript
// EntityIdMapper: UUID ↔ Integer mapping
class EntityIdMapper {
private uuidToInt = new Map<string, number>()
private intToUuid = new Map<number, string>()
private nextId = 1
getOrAssign(uuid: string): number {
// O(1) mapping: UUIDs → integers for bitmap storage
let intId = this.uuidToInt.get(uuid)
if (!intId) {
intId = this.nextId++
this.uuidToInt.set(uuid, intId)
this.intToUuid.set(intId, uuid)
}
return intId
}
intsIterableToUuids(ints: Iterable<number>): string[] {
// Convert bitmap results back to UUIDs
const result: string[] = []
for (const intId of ints) {
const uuid = this.intToUuid.get(intId)
if (uuid) result.push(uuid)
}
return result
}
}
// ChunkData now uses RoaringBitmap32 instead of Set<string>
class ChunkData {
chunkId: number
field: string
entries: Map<string, RoaringBitmap32> // value → bitmap of integer IDs
}
```
**Key Benefits**:
- **90% memory savings**: Roaring bitmaps compress much better than UUID strings
- **Hardware-accelerated operations**: SIMD instructions (AVX2/SSE4.2) for ultra-fast bitmap AND/OR
- **Portable serialization**: Cross-platform compatible format (Java/Go/Node.js)
- **Lazy conversion**: UUIDs converted to integers only once, not per query
**Multi-Field Intersection (THE BIG WIN!)**:
```typescript
// Before (v3.42.0): JavaScript array filtering
async getIdsForFilter(filter: {status: 'active', role: 'admin'}): Promise<string[]> {
// 1. Fetch UUID arrays for each field
const statusIds = await this.getIds('status', 'active') // ["uuid1", "uuid2", ...]
const roleIds = await this.getIds('role', 'admin') // ["uuid2", "uuid3", ...]
// 2. JavaScript intersection (SLOW!)
return statusIds.filter(id => roleIds.includes(id)) // O(n*m) array filtering
}
// After (v3.43.0): Roaring bitmap intersection
async getIdsForMultipleFields(pairs: [{field, value}, ...]): Promise<string[]> {
// 1. Fetch roaring bitmaps (integers, not UUIDs)
const bitmaps: RoaringBitmap32[] = []
for (const {field, value} of pairs) {
const bitmap = await this.getBitmapFromChunks(field, value)
if (!bitmap) return [] // Short-circuit if any field has no matches
bitmaps.push(bitmap)
}
// 2. Hardware-accelerated intersection (FAST! AVX2/SSE4.2 SIMD)
const result = RoaringBitmap32.and(...bitmaps) // O(1) hardware operation!
// 3. Convert final bitmap to UUIDs (once, not per-field)
return this.idMapper.intsIterableToUuids(result)
}
```
**Performance Impact**:
- Multi-field intersection: **1.4x average speedup**, up to 3.3x on 10K entities
- Memory usage: **90% reduction** (17.17 MB → 2.01 MB for 100K entities)
- Hardware acceleration: SIMD instructions make bitmap operations nearly free
**Benchmark Results** (1,000 queries on various dataset sizes):
| Dataset Size | Operation | Set Time | Roaring Time | Speedup | Memory Savings |
|--------------|-----------|----------|--------------|---------|----------------|
| 10,000 entities | 3-field intersection | 3.74ms | 1.14ms | **3.3x faster** | 90% |
| 100,000 entities | 3-field intersection | 2.60ms | 1.78ms | **1.5x faster** | 88% |
**Implementation**: See `src/utils/entityIdMapper.ts` and benchmark at `tests/performance/roaring-bitmap-benchmark.ts`
#### Bloom Filter (Probabilistic Membership Testing)
```typescript
class BloomFilter {
bits: Uint8Array // Bit array
size: number // Total bits
hashCount: number // Number of hash functions (FNV-1a, DJB2)
mightContain(value): boolean // ~1% false positive, 0% false negative
}
```
**Use case**: Quickly skip chunks that definitely don't contain a value
#### Zone Map (Range Query Optimization)
```typescript
interface ZoneMap {
min: any | null // Minimum value in chunk
max: any | null // Maximum value in chunk
count: number // Number of entries
hasNulls: boolean // Whether chunk contains null values
}
```
**Use case**: Skip entire chunks during range queries (ClickHouse-inspired)
#### Type-Field Affinity
```typescript
// Tracks which fields are commonly used with which types
// Example:
// typeFieldAffinity.get('character') → {
// 'name': 127, // 127 characters have a 'name' field
// 'age': 89, // 89 characters have an 'age' field
// 'alignment': 45 // 45 characters have an 'alignment' field
// }
```
**Use case**: Enables NLP to understand "find characters named John" → knows 'name' is a character field
### Query Algorithm (v3.42.0)
**Exact Match Query**:
```typescript
async getIds(field: string, value: any): Promise<string[]> {
// 1. Load sparse index for field
const sparseIndex = await this.loadSparseIndex(field)
// 2. Find candidate chunks using bloom filters
const candidateChunks = sparseIndex.findChunksForValue(value)
// → Bloom filter checks all chunks (~1ms)
// → Returns only chunks that *might* contain value
// 3. Load candidate chunks and collect IDs
const results = []
for (const chunkId of candidateChunks) {
const chunk = await this.chunkManager.loadChunk(field, chunkId)
const ids = chunk.entries.get(value)
if (ids) results.push(...ids)
}
return results
}
```
**Range Query**:
```typescript
async getIdsForRange(field: string, min: any, max: any): Promise<string[]> {
// 1. Load sparse index for field
const sparseIndex = await this.loadSparseIndex(field)
// 2. Find candidate chunks using zone maps
const candidateChunks = sparseIndex.findChunksForRange(min, max)
// → Check zoneMap.min and zoneMap.max for each chunk
// → Skip chunks where max < min or min > max
// 3. Load chunks and filter values
const results = []
for (const chunkId of candidateChunks) {
const chunk = await this.chunkManager.loadChunk(field, chunkId)
for (const [value, ids] of chunk.entries) {
if (value >= min && value <= max) {
results.push(...ids)
}
}
}
return results
}
```
**Benefits**:
- Bloom filters: Skip 99% of irrelevant chunks (exact match)
- Zone maps: Skip entire chunks that fall outside range
- Adaptive chunking: ~50 values per chunk optimizes I/O
- Immediate flushing: No need for dirty tracking or batch writes
### Temporal Bucketing (v3.41.0)
**Problem Solved**: High-cardinality timestamp fields created massive file pollution.
- Example: 575 entities with unique timestamps → 358,407 index files (98.7% pollution!)
**Solution**: Automatic bucketing of temporal fields to 1-minute intervals.
```typescript
// In normalizeValue(value, field):
if (field && typeof value === 'number') {
const fieldLower = field.toLowerCase()
const isTemporal = fieldLower.includes('time') ||
fieldLower.includes('date') ||
fieldLower.includes('accessed') ||
fieldLower.includes('modified') ||
fieldLower.includes('created') ||
fieldLower.includes('updated')
if (isTemporal) {
// Bucket to 1-minute intervals
const bucketSize = 60000 // milliseconds
const bucketed = Math.floor(value / bucketSize) * bucketSize
return bucketed.toString()
}
}
```
**Benefits**:
- ✅ Reduces 575 unique timestamps → ~10 buckets
- ✅ File count: 358,407 → ~4,600 (98.7% reduction)
- ✅ Zero configuration - automatic field name detection
- ✅ Still enables range queries (not excluded like before)
- ✅ 1-minute precision sufficient for most use cases
**Field Name Detection**: Automatically buckets fields with these keywords:
- `time`, `date`, `accessed`, `modified`, `created`, `updated`
- Examples: `timestamp`, `createdAt`, `lastModified`, `birthdate`, `eventTime`
### Operations
```typescript
// Add to index (src/brainy.ts:387)
await this.metadataIndex.addToIndex(id, metadata)
// Query exact match
const ids = await this.metadataIndex.getIds('status', 'active')
// Query range
const ids = await this.metadataIndex.getIdsForFilter({
publishDate: { greaterThan: 1640995200000 }
})
// Filter discovery (what values exist for a field)
const values = await this.metadataIndex.getFilterValues('status')
// → ['active', 'archived', 'draft']
// Statistics (O(1))
const totalEntities = this.metadataIndex.getTotalEntityCount()
const typeBreakdown = this.metadataIndex.getAllEntityCounts()
// → Map { 'character': 127, 'item': 89, 'location': 45 }
```
### Excluded Fields
Some fields are excluded from indexing to prevent pollution:
```typescript
const DEFAULT_EXCLUDE_FIELDS = [
'id', // Primary key (redundant to index)
'uuid', // Alternative primary key
'vector', // High-dimensional data
'embedding', // Same as vector
'content', // Large text content
'description', // Large text content
'metadata', // Nested object (too large)
'data' // Generic nested object
]
```
**Note**: Timestamp fields like `modified`, `accessed`, `created` are NO LONGER excluded as of v3.41.0 - they are indexed with automatic bucketing.
## 2. HNSWIndex - Vector Similarity Search
**Purpose**: O(log n) semantic similarity search using vector embeddings.
### Internal Architecture
```typescript
class HNSWIndex {
// Per-noun indexes for efficiency
private nouns: Map<string, HNSWNoun> = new Map()
// Global entry point for search
private entryPointId: string | null = null
private maxLevel = 0
// Shared memory management
private unifiedCache: UnifiedCache
private storage: BaseStorage | null = null
}
// Each noun has its own HNSW graph
class HNSWNoun {
noun: string
nodes: Map<string, HNSWNode>
entryPointId: string | null
maxLevel: number
}
// Each node in the graph
class HNSWNode {
id: string
vector: Vector | null // Lazy-loaded from storage
level: number
connections: Map<number, string[]> // level → neighbor IDs
}
```
### Hierarchical Graph Structure
HNSW builds a multi-layered graph:
```
Layer 2: [entry] ←→ [node1] (sparse, long-range connections)
↓ ↓
Layer 1: [entry] ←→ [node1] ←→ [node2] ←→ [node3] (medium density)
↓ ↓ ↓ ↓
Layer 0: [entry] ←→ [node1] ←→ [node2] ←→ [node3] ←→ [node4] ←→ [node5] (dense, all nodes)
```
**Search Algorithm**:
1. Start at entry point in top layer
2. Greedy search for nearest neighbor in current layer
3. Move down to next layer with found neighbor
4. Repeat until reaching layer 0
5. Return k nearest neighbors
**Complexity**: O(log n) due to hierarchical structure
### Adaptive Vector Loading
Vectors are lazy-loaded on demand based on memory availability:
```typescript
private async getVectorSafe(noun: HNSWNoun): Promise<Vector> {
// Check UnifiedCache first
const cached = this.unifiedCache.get(noun.id)
if (cached) return cached
// Load from storage if memory available
if (this.unifiedCache.canCache()) {
const vector = await this.storage.loadVector(noun.id)
this.unifiedCache.set(noun.id, vector)
return vector
}
// Load transiently if memory pressure
return await this.storage.loadVector(noun.id)
}
```
### Operations
```typescript
// Add entity (src/brainy.ts:add)
await this.index.addEntity(id, vector, noun)
// Search for similar vectors
const results = await this.index.search(queryVector, k, threshold)
// Returns: Array<{id: string, similarity: number}>
// Rebuild from storage
await this.index.rebuild()
```
## 3. GraphAdjacencyIndex - O(1) Relationship Traversal
**Purpose**: Constant-time neighbor lookups regardless of graph size.
### Internal Architecture
```typescript
class GraphAdjacencyIndex {
// O(1) bidirectional lookups
private sourceIndex = new Map<string, Set<string>>() // sourceId → targetIds
private targetIndex = new Map<string, Set<string>>() // targetId → sourceIds
// Full relationship data
private verbIndex = new Map<string, GraphVerb>() // verbId → metadata
// Statistics
private relationshipCountsByType = new Map<string, number>()
// Shared memory
private unifiedCache: UnifiedCache
private storage: BaseStorage
}
```
### Key Innovation: Bidirectional Adjacency
**Core Insight**: Store BOTH directions of each relationship for O(1) lookups.
```typescript
// Example: Alice KNOWS Bob
// verbId = "verb-123"
// Source index: Alice → Bob
sourceIndex.set('alice', Set(['bob']))
// Target index: Bob ← Alice
targetIndex.set('bob', Set(['alice']))
// Full metadata
verbIndex.set('verb-123', {
id: 'verb-123',
verb: 'knows',
source: 'alice',
target: 'bob',
metadata: { since: 2020 }
})
```
**Result**: Finding Alice's friends OR Bob's friends is O(1) - just one Map lookup!
### Operations
```typescript
// Add relationship (src/brainy.ts:relate)
await this.graphIndex.addRelationship(verbId, sourceId, targetId, verb)
// Get neighbors (O(1) per hop)
const outgoing = await this.graphIndex.getNeighbors(id, 'out') // Who does id point to?
const incoming = await this.graphIndex.getNeighbors(id, 'in') // Who points to id?
const both = await this.graphIndex.getNeighbors(id, 'both') // All neighbors
// Get relationships
const verbs = await this.graphIndex.getRelationships(sourceId, targetId)
// Statistics (O(1))
const totalRelationships = this.graphIndex.getTotalRelationshipCount()
const byType = this.graphIndex.getRelationshipCountsByType()
// → Map { 'knows': 45, 'created': 23, 'located_at': 12 }
```
### Graph Traversal
The index supports multi-hop traversal:
```typescript
// Find all entities within 2 hops
const reachable = await this.graphIndex.traverse({
startId: 'alice',
depth: 2,
direction: 'out'
})
// Complexity: O(V + E) breadth-first search, but each neighbor lookup is O(1)
```
## 4. DeletedItemsIndex - Soft-Delete Tracking
**Purpose**: O(1) tracking of soft-deleted items without removing data.
### Internal Architecture
```typescript
class DeletedItemsIndex {
private deletedIds: Set<string> = new Set()
private deletedCount: number = 0
private storage: BaseStorage
}
```
**Simplicity is key**: Just a Set of deleted IDs. No complex logic needed.
### Operations
```typescript
// Mark as deleted
this.deletedItemsIndex.markDeleted(id) // O(1)
// Check if deleted
const isDeleted = this.deletedItemsIndex.isDeleted(id) // O(1)
// Filter out deleted items
const active = this.deletedItemsIndex.filterDeleted(results) // O(n)
// Restore
this.deletedItemsIndex.markRestored(id) // O(1)
// Get all deleted
const deleted = this.deletedItemsIndex.getAllDeleted() // O(1) - returns Set
```
### Integration
All query results are filtered through the deleted items index:
```typescript
// In brainy.find() (src/brainy.ts:1026+)
let results = await this.performSearch(query)
// Filter out deleted items before returning
results = results.filter(r => !this.deletedItemsIndex.isDeleted(r.id))
```
## Shared Memory Management: UnifiedCache
All four indexes share a single **UnifiedCache** instance for coordinated memory management.
### Architecture
```typescript
class UnifiedCache {
private cache: Map<string, CachedItem> = new Map()
private maxSize: number
private currentSize: number = 0
private evictionPolicy: 'LRU' | 'LFU' = 'LRU'
}
// Each index gets the same cache instance
const unifiedCache = new UnifiedCache({ maxSize: 1000 })
this.metadataIndex = new MetadataIndexManager(storage, { unifiedCache })
this.hnswIndex = new HNSWIndex(storage, { unifiedCache })
this.graphIndex = new GraphAdjacencyIndex(storage, { unifiedCache })
```
### Benefits
1. **Fair Resource Allocation**: All indexes compete for the same memory pool
2. **Prevents Monopolization**: No single index can starve others of memory
3. **Coordinated Eviction**: LRU eviction across all cached items system-wide
4. **Memory Pressure Handling**: Automatic cache shrinking when memory is tight
5. **Adaptive Loading**: Indexes load data transiently under memory pressure
### Cache Key Patterns
Each index uses different key prefixes:
```typescript
// Metadata index
cache.set(`meta:${field}:${value}`, indexEntry)
// HNSW index
cache.set(`vector:${id}`, vectorData)
// Graph index
cache.set(`graph:${sourceId}`, neighbors)
// Deleted items (no caching needed - uses Set)
```
## How Indexes Work Together
### 1. Entity Creation (`brainy.add()`)
```typescript
// src/brainy.ts:add()
async add(params: AddParams): Promise<string> {
const id = generateId()
const vector = await this.embedder(params.content)
// Add to metadata index (field filtering)
await this.metadataIndex.addToIndex(id, params.metadata)
// Add to HNSW index (vector search)
await this.index.addEntity(id, vector, params.noun)
// Relationships added via separate relate() calls
return id
}
```
### 2. Entity Search (`brainy.find()`)
```typescript
// src/brainy.ts:find()
async find(query: FindQuery): Promise<Result[]> {
let results: Result[] = []
// Step 1: Metadata filtering (fast pre-filter)
if (query.where) {
const filteredIds = await this.metadataIndex.getIdsForFilter(query.where)
results = await this.getEntitiesByIds(filteredIds)
}
// Step 2: Vector similarity search (semantic ranking)
if (query.like) {
const queryVector = await this.embedder(query.like)
const vectorResults = await this.index.search(queryVector, query.limit)
// Intersect or union with metadata results
results = this.combineResults(results, vectorResults)
}
// Step 3: Graph traversal (relationship filtering)
if (query.connected) {
const connectedIds = await this.graphIndex.traverse(query.connected)
results = results.filter(r => connectedIds.includes(r.id))
}
// Step 4: Filter deleted items
results = results.filter(r => !this.deletedItemsIndex.isDeleted(r.id))
return results
}
```
### 3. Entity Update (`brainy.update()`)
```typescript
// src/brainy.ts:update()
async update(params: UpdateParams): Promise<void> {
const existing = await this.get(params.id)
// Update metadata index (remove old, add new)
await this.metadataIndex.removeFromIndex(params.id, existing.metadata)
await this.metadataIndex.addToIndex(params.id, params.metadata)
// Update HNSW index (re-embed if content changed)
if (params.content) {
const newVector = await this.embedder(params.content)
await this.index.updateEntity(params.id, newVector)
}
// Graph relationships unchanged (managed separately)
}
```
### 4. Statistics (`brainy.stats()`)
All indexes provide O(1) statistics:
```typescript
// src/brainy.ts:stats()
async stats(): Promise<Statistics> {
return {
// From metadata index
entities: this.metadataIndex.getTotalEntityCount(),
entityTypes: this.metadataIndex.getAllEntityCounts(),
// From graph index
relationships: this.graphIndex.getTotalRelationshipCount(),
relationshipTypes: this.graphIndex.getRelationshipCountsByType(),
// From deleted items index
deletedItems: this.deletedItemsIndex.getDeletedCount(),
// From HNSW index
vectorIndexSize: this.index.getSize()
}
}
```
### 5. Index Rebuilding
All indexes rebuilt in parallel on initialization:
```typescript
// src/brainy.ts:init()
async init(): Promise<void> {
// Check if indexes are empty
const metadataEmpty = await this.metadataIndex.isEmpty()
const hnswEmpty = await this.index.isEmpty()
const graphEmpty = await this.graphIndex.isEmpty()
if (metadataEmpty || hnswEmpty || graphEmpty) {
// Rebuild all indexes in parallel
await Promise.all([
metadataEmpty ? this.metadataIndex.rebuild() : Promise.resolve(),
hnswEmpty ? this.index.rebuild() : Promise.resolve(),
graphEmpty ? this.graphIndex.rebuild() : Promise.resolve()
])
}
}
```
## Triple Intelligence Integration
The **TripleIntelligenceSystem** (`src/cortex/tripleIntelligence.ts`) combines all three core indexes:
```typescript
class TripleIntelligenceSystem {
constructor(
private metadataIndex: MetadataIndexManager,
private hnswIndex: HNSWIndex,
private graphIndex: GraphAdjacencyIndex,
private embedder: EmbedderFunction,
private storage: BaseStorage
) {}
async query(nlpQuery: string): Promise<Result[]> {
// Parse natural language
const parsed = await this.parseQuery(nlpQuery)
// Execute across all three indexes
const [metadataResults, vectorResults, graphResults] = await Promise.all([
this.metadataIndex.getIdsForFilter(parsed.filters),
this.hnswIndex.search(parsed.vector, parsed.limit),
this.graphIndex.traverse(parsed.graphConstraints)
])
// Fuse results with weighted scoring
return this.fuseResults(metadataResults, vectorResults, graphResults)
}
}
```
## Performance Characteristics
### Operation Complexity by Index
| Operation | MetadataIndex | HNSWIndex | GraphAdjacencyIndex | DeletedItemsIndex |
|-----------|---------------|-----------|---------------------|-------------------|
| **Add** | O(1) per field | O(log n) | O(1) | O(1) |
| **Remove** | O(1) per field | O(log n) | O(1) | O(1) |
| **Exact lookup** | O(1) | N/A | O(1) | O(1) |
| **Range query** | O(log n) + O(k) | N/A | N/A | N/A |
| **Similarity search** | N/A | O(log n) | N/A | N/A |
| **Neighbor lookup** | N/A | N/A | O(1) | N/A |
| **Statistics** | O(1) | O(1) | O(1) | O(1) |
Where:
- n = total number of entities
- k = number of matching results
### Memory Footprint
| Index | Per-Entity Memory | Notes |
|-------|-------------------|-------|
| **MetadataIndex** | ~100 bytes | Depends on field count and cardinality |
| **HNSWIndex** | ~1.5 KB | Vector (384 dims × 4 bytes) + graph connections |
| **GraphAdjacencyIndex** | ~50 bytes per relationship | Bidirectional references + metadata |
| **DeletedItemsIndex** | ~40 bytes per deleted ID | Just Set storage |
**Total overhead**: ~1.6 KB per entity + ~50 bytes per relationship
### Scalability
All indexes scale gracefully:
| Database Size | Metadata Filter | Vector Search | Graph Hop | Combined Query |
|---------------|----------------|---------------|-----------|----------------|
| **1K entities** | 0.3ms | 0.8ms | 0.05ms | 1.1ms |
| **10K entities** | 0.5ms | 1.2ms | 0.08ms | 1.5ms |
| **100K entities** | 0.8ms | 1.8ms | 0.1ms | 2.1ms |
| **1M entities** | 1.2ms | 2.5ms | 0.1ms | 2.8ms |
**Key observations**:
- Graph queries stay O(1) regardless of scale
- Metadata filtering scales sub-linearly
- Vector search degrades gracefully due to HNSW
- Combined queries remain fast even at scale
## Best Practices
### When to Use Each Index
**MetadataIndex**:
- Filtering by exact field values (status, type, category)
- Range queries on numeric/temporal fields (dates, prices, counts)
- Field discovery (what filters are available)
- Type-based querying (find all characters, all items)
**HNSWIndex**:
- Semantic similarity search ("find similar documents")
- Content-based retrieval ("find posts about AI")
- Fuzzy matching (when exact matches aren't required)
- Recommendation systems (find related items)
**GraphAdjacencyIndex**:
- Relationship queries ("who knows whom")
- Path finding ("how are these entities connected")
- Network analysis ("find communities")
- Multi-hop traversal ("friends of friends")
**DeletedItemsIndex**:
- Soft deletes (preserve data but hide from queries)
- Audit trails (track what was deleted when)
- Restoration workflows (undo deletions)
### Query Optimization
1. **Start with metadata filters** - They're fastest and most selective
2. **Use graph constraints** - O(1) lookups significantly reduce search space
3. **Vector search last** - Most expensive, best used on pre-filtered set
4. **Leverage temporal bucketing** - Timestamp range queries work efficiently
5. **Monitor statistics** - Use O(1) stats methods for cardinality estimation
### Memory Management
1. **Configure UnifiedCache appropriately** - Balance between speed and memory
2. **Use lazy loading** - HNSW loads vectors on-demand
3. **Monitor cache hit rates** - Adjust cache size if hit rate is low
4. **Consider storage adapter** - Memory storage = fastest, S3 = most scalable
## Related Documentation
- [Find System](../FIND_SYSTEM.md) - Query-centric view of index usage
- [Triple Intelligence](./triple-intelligence.md) - Advanced query system
- [Storage Architecture](./storage-architecture.md) - Storage layer details
- [Performance Guide](../PERFORMANCE.md) - Performance tuning
- [Overview](./overview.md) - High-level architecture
## Version History
- **v3.42.0** (October 2025): Replaced flat file indexing with adaptive chunked sparse indexing. Bloom filters + zone maps for O(1) exact match and O(log n) range queries. 630x file reduction (560k → 89 files). Removed dual code paths.
- **v3.41.0** (October 2025): Added automatic temporal bucketing to MetadataIndex
- **v3.40.0** (October 2025): Enhanced batch processing for imports
- **v3.0.0** (September 2025): Introduced 4-index architecture with UnifiedCache