sombra 0.3.2

High-performance graph database with ACID transactions, single-file storage, and bindings for Rust, TypeScript, and Python
Documentation
# Lookup Optimization API Guide

## Overview

The Phase 1 optimizations are fully transparent to existing code - no API changes are required. However, understanding the new behavior can help optimize your usage patterns.

## What Changed (Internally)

### 1. Label-Based Queries Are Now O(1)

**Before**: `get_nodes_by_label()` scanned all nodes  
**After**: Direct index lookup

```rust
// This is now extremely fast (microseconds) even with millions of nodes
let person_nodes = db.get_nodes_by_label("Person")?;
```

**Best Practices**:
- ✅ Use labels liberally for categorization
- ✅ Query by label instead of scanning all nodes
- ✅ Use multiple labels per node when appropriate

### 2. Repeated Node Reads Are Cached

**Before**: Every `get_node()` required disk I/O  
**After**: LRU cache serves repeated reads

```rust
// First read: from disk
let node1 = db.get_node(node_id)?;

// Subsequent reads: from cache (nanoseconds)
let node2 = db.get_node(node_id)?;
let node3 = db.get_node(node_id)?;
```

**Cache Behavior**:
- Cache size: Configurable via `Config.page_cache_size` (default: 1000 entries)
- Eviction: LRU (Least Recently Used)
- Invalidation: Automatic on node/edge modifications

### 3. Cache Invalidation on Mutations

The cache is automatically invalidated when nodes change:

```rust
// Node cached during read
let node = db.get_node(node_id)?;

// This invalidates the cache entry
let mut tx = db.begin_transaction()?;
tx.delete_node(node_id)?;
tx.commit()?;

// Next read will fetch fresh data from disk
let node = db.get_node(node_id)?; // Cache miss, reload from disk
```

**Edge modifications also invalidate caches**:
```rust
// Add edge invalidates both source and target node caches
let mut tx = db.begin_transaction()?;
tx.add_edge(Edge::new(0, source_id, target_id, "KNOWS"))?;
tx.commit()?;
// Caches for source_id and target_id are now cleared
```

## Configuration Options

### Adjust Cache Size

```rust
use sombra::{Config, GraphDB, SyncMode};

let config = Config {
    wal_sync_mode: SyncMode::Normal,
    sync_interval: 100,
    checkpoint_threshold: 5000,
    page_cache_size: 5000,  // ← Increase cache size
    group_commit_timeout_ms: 10,
};

let db = GraphDB::open_with_config("mydb.db", config)?;
```

**Guidelines**:
- Small workload (< 10K nodes): `page_cache_size: 1000` (default)
- Medium workload (10K-100K nodes): `page_cache_size: 5000`
- Large workload (> 100K nodes): `page_cache_size: 10000+`

**Memory Impact**: ~100-500 bytes per cached node

## Performance Characteristics

### Label Queries: `get_nodes_by_label(label)`

| Dataset Size | Time Complexity | Typical Duration |
|--------------|----------------|------------------|
| 1K nodes | O(1) | ~2-5 µs |
| 10K nodes | O(1) | ~2-5 µs |
| 100K nodes | O(1) | ~2-5 µs |
| 1M nodes | O(1) | ~2-5 µs |

*Note: Duration is constant regardless of dataset size*

### Node Reads: `get_node(node_id)`

| Scenario | Time Complexity | Typical Duration |
|----------|----------------|------------------|
| Cache hit | O(1) | ~45 ns |
| Cache miss | O(1) + disk I/O | ~1-10 µs |

### Worst-Case Scenarios

**Cache Thrashing**: If you read more unique nodes than cache size in sequence:
```rust
// BAD: Reading 10,000 unique nodes with cache_size=1000
for node_id in 0..10000 {
    db.get_node(node_id)?; // Cache thrashing
}
```

**Solution**: Increase cache size or batch operations

## Migration Guide

**No migration required!** All existing code continues to work unchanged.

However, you can now optimize patterns like:

### Before (Inefficient)
```rust
// Scanning all nodes to find by label
let mut person_nodes = Vec::new();
for node_id in all_node_ids {
    let node = db.get_node(node_id)?;
    if node.labels.contains(&"Person".to_string()) {
        person_nodes.push(node_id);
    }
}
```

### After (Optimized)
```rust
// Direct index lookup
let person_nodes = db.get_nodes_by_label("Person")?;
```

## Monitoring & Debugging

### Check Index State (Debug Mode)

The label index is rebuilt on database open, ensuring consistency:
```rust
let db = GraphDB::open("mydb.db")?;
// Label index is automatically populated
```

### Test Cache Effectiveness

```rust
use std::time::Instant;

let start = Instant::now();
for _ in 0..1000 {
    db.get_node(node_id)?;
}
let elapsed = start.elapsed();
println!("1000 reads: {:?}, avg: {:?}", elapsed, elapsed / 1000);
// Should show ~45 ns per read if cache is working
```

## Limitations

1. **Cache is in-memory only**: Not persisted across database restarts
2. **Single-threaded**: Cache is not thread-safe (matches existing design)
3. **Label index is in-memory**: Rebuilt on database open

## Future Enhancements

Planned optimizations not yet implemented:
- B-tree primary index (better than HashMap for large datasets)
- Adjacency indexing (faster graph traversals)
- Property-based indexes (query by property values)
- Persistent caching (cache survives restarts)

---

**Version**: Phase 1 Complete  
**Last Updated**: October 18, 2025