Examples

Runnable code snippets for every major feature. Copy-paste into your project or run the included examples with cargo run --example <name>.

Basic KV Operations

Open a database, write keys in different namespaces, read them back, and scan ranges.

use edgestore::{EdgestoreConfig, Engine};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = EdgestoreConfig::new("/tmp/kv_demo");
    let mut db = Engine::open(config)?;

    // Write in different namespaces
    db.put(b"users", b"alice", b"{"name":"Alice"}")?;
    db.put(b"users", b"bob", b"{"name":"Bob"}")?;
    db.put(b"products", b"p100", b"Laptop")?;

    // Point lookup
    let alice = db.get(b"users", b"alice")?;
    assert_eq!(alice, Some(b"{"name":"Alice"}".to_vec()));

    // Range scan (exclusive end)
    for (k, v) in db.range(b"users", b"a", b"z")? {
        println!("{} = {}",
            std::str::from_utf8(&k)?,
            std::str::from_utf8(&v)?);
    }

    // Prefix scan
    for (k, v) in db.prefix(b"products", b"p")? {
        println!("product: {}", std::str::from_utf8(&v)?);
    }

    db.flush()?;
    Ok(())
}

Run: cargo run --example basic_kv

TTL and Compaction

Write time-to-live records, observe lazy expiry, and trigger compaction to reclaim space.

use std::time::Duration;

// Write a session that expires in 2 seconds
db.put_with_ttl(b"sessions", b"sess-1", b"active", 2)?;

// Still readable immediately (lazy expiry)
std::thread::sleep(Duration::from_secs(3));
let val = db.get(b"sessions", b"sess-1")?;
assert!(val.is_some(), "lazy expiry: still readable");

// Flush to segments, then compact
db.flush_to_segments()?;
let stats = db.compact_once()?;

// Now it's gone — the cohort fully expired, so zero bytes were relocated
let val = db.get(b"sessions", b"sess-1")?;
assert!(val.is_none());

println!("Compacted {} cohorts, {} bytes written",
    stats.cohorts_collected, stats.bytes_written);
// bytes_written == 0 for fully-expired cohorts

Vector Search (Flat + HNSW)

Store embeddings and search by cosine similarity. Build an HNSW index for approximate search on large collections. build_vector_index infers dims, dtype, and metric from the stored records.

use edgestore::{EdgestoreConfig, Engine, VectorEngine};
use edgestore::vector::types::{Dtype, VectorRecord};
use edgestore::vector::distance::Metric;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut db = Engine::open(EdgestoreConfig::new("/tmp/vec_demo"))?;

    let dims: u16 = 128;
    let ns = b"embeddings";

    // Insert 1000 f32 vectors (4 bytes per element)
    for i in 0u32..1000 {
        let val = (i as f32).to_le_bytes();
        let bytes: Vec<u8> = (0..dims).flat_map(|_| val).collect();
        db.vector_put(ns, format!("vec-{i}").as_bytes(), dims, Dtype::F32, &bytes)?;
    }
    db.flush_to_segments()?;

    // Build HNSW index — dims/dtype/metric inferred from stored records
    db.build_vector_index(ns)?;
    println!("vectors loaded: {:?}", db.vector_count(ns));

    // Search top-5 nearest neighbors
    let q_bytes: Vec<u8> = (0..dims).flat_map(|_| 0.5f32.to_le_bytes()).collect();
    let query = VectorRecord { dims, dtype: Dtype::F32, data: q_bytes };

    // vector_search takes &self — no mut needed after build
    let results = db.vector_search(ns, &query, 5, Metric::L2)?;
    for (i, r) in results.iter().enumerate() {
        println!("#{} {}  distance={:.4}",
            i + 1, std::str::from_utf8(&r.key)?, r.distance);
    }
    Ok(())
}

Concurrent Vector Search v1.8.0

vector_search takes &self — multiple threads can search concurrently with no external lock. The HNSW cache handles lazy loading internally.

// Build once (needs &mut self)
let mut db = Engine::open(config)?;
// ... insert vectors ...
db.build_vector_index(b"products")?;

// Search from N threads — no Mutex, no serialization
let query = VectorRecord { dims: 128, dtype: Dtype::F32, data: q_bytes };
std::thread::scope(|s| {
    for _ in 0..16 {
        s.spawn(|| {
            let hits = db.vector_search(
                b"products", &query, 10, Metric::Cosine
            ).unwrap();
            handle(hits);
        });
    }
});

// Same in async — both HNSW and flat-scan use read lock
let hits = async_engine
    .vector_search(b"products", &query, 10, Metric::Cosine)
    .await?;

Full-Text Search

Index documents, search with BM25 relevance, and filter by facets.

use edgestore::{EdgestoreConfig, Engine};
use edgestore_text::TextIndex;
use std::collections::HashMap;

let mut db = Engine::open(EdgestoreConfig::new("/tmp/text_demo"))?;
let mut text = TextIndex::new();
let ns = b"articles";

// Index some documents
text.index_document(&mut db, ns, b"doc1", "EdgeStore is a fast embedded database", HashMap::new())?;
text.index_document(&mut db, ns, b"doc2", "RocksDB is an LSM tree database", HashMap::new())?;
text.index_document(&mut db, ns, b"doc3", "SQLite is a lightweight SQL database", HashMap::new())?;
text.persist(&mut db)?;

// Search for "fast database"
let hits = text.search(&db, ns, "fast database", 5)?;
for hit in hits {
    println!("{}  score={:.3}",
        std::str::from_utf8(&hit.doc_id)?, hit.score);
}
// Output: doc1 score=2.341  (matches both "fast" and "database")

Atomic Transactions

Batch multiple writes into a single atomic commit.

// Begin a transaction
let mut tx = db.begin();

// Add multiple operations
tx.put(b"accounts", b"alice", b"900", 0, 0)?;
tx.put(b"accounts", b"bob", b"1100", 0, 0)?;
tx.put(b"accounts", b"charlie", b"500", 0, 0)?;

// Commit atomically
db.commit_transaction(tx)?;

// Or roll back if something goes wrong
// db.rollback_transaction(tx);

// Verify all three are present
assert!(db.get(b"accounts", b"alice")?.is_some());
assert!(db.get(b"accounts", b"bob")?.is_some());
assert!(db.get(b"accounts", b"charlie")?.is_some());

Merkle Replication

Synchronize two engines using content-addressed segments and Merkle tree comparison.

// Setup primary and replica
let primary = Engine::open(EdgestoreConfig::new("/tmp/repl_primary"))?;
let mut replica = Engine::open(EdgestoreConfig::new("/tmp/repl_replica"))?;

// Write on primary, flush
primary.put(b"data", b"key1", b"value1")?;
primary.flush_to_segments()?;

// Export manifest from primary
let seg_refs = primary.export_manifest()?;

// Find missing segments on replica
let missing = replica.missing_segments(&seg_refs);

// Transfer each missing segment
for seg in &missing {
    let data = primary.read_segment(seg)?;
    let result = replica.import_segment(seg, &data)?;
    println!("Imported: {:?}", result);
}

// Verify Merkle roots match
let primary_root = primary.range_merkle_root()?;
let match_ok = replica.compare_merkle(&primary_root)?;
assert!(match_ok);

Run: cargo run --example replication

Primary Failover with Write-Token Fencing v1.9.0

Safely promote a replica to primary. The write token is incremented so anti-entropy loops on other replicas detect the topology change. Inspired by BtrLog §4.2 (Kuschewski et al., VLDB 2026).

use edgestore_repl::ReplicatedEngine;
use edgestore::EdgestoreConfig;

// Normal startup: open as primary
let primary = ReplicatedEngine::open_primary(
    EdgestoreConfig::new("/var/db/node-a"),
    "0.0.0.0:8900",
)?;

// --- Later: node-a goes down, promote node-b ---

// promote_to_primary fetches old primary's wtoken via GET /watermark,
// advances it by 1, persists to WTOKEN, then starts the HTTP server.
let new_primary = ReplicatedEngine::promote_to_primary(
    EdgestoreConfig::new("/var/db/node-b"),
    "0.0.0.0:8900",
    Some("http://node-a:8900"),  // None if old primary is already down
)?;

// Inspect the watermark
let eng = new_primary.engine().lock().unwrap();
println!("confirmed_lsn={} wtoken={}",
    eng.confirmed_lsn(), eng.write_token());
// Anti-entropy loops on other replicas observe wtoken jump → log failover warning

Idempotent Remote Upload v1.9.0

Use upload_if_absent in retry loops to avoid re-uploading segments already present on S3 or local replica stores.

use edgestore_repl::{FilesystemRemoteStore, S3RemoteStore};
use edgestore::RemoteStore;

// Filesystem: O_CREAT|O_EXCL — atomically creates or signals already-exists
let fs_store = FilesystemRemoteStore::new("/data/segments".into())?;
let wrote = fs_store.upload_if_absent(&hash, &segment_bytes)?;
// wrote == true  → freshly written
// wrote == false → already present, no write performed

// S3: PutObject with If-None-Match: * — HTTP 412 → false
let s3_store = S3RemoteStore::new("my-bucket", None, None)?;
let wrote = s3_store.upload_if_absent(&hash, &segment_bytes)?;

// TieredEngine and AntiEntropyLoop use upload_if_absent automatically.
// No manual check needed — retry your outer loop without fear of double-writes.

Command-Line Interface

The edgestore-cli binary provides administrative and data-access commands without writing code.

# Install the CLI
$ cargo install --path edgestore-cli

# Create a database
$ edgestore-cli create --path ./mydb

# Put and get values
$ edgestore-cli put --path ./mydb --key hello --value world
$ edgestore-cli get --path ./mydb --key hello
world

# Range scan
$ edgestore-cli range --path ./mydb --start a --end z

# Store a vector (128-dim f32, hex-encoded)
$ edgestore-cli vector-put --path ./mydb --namespace vec \
    --key doc1 --dims 128 --dtype f32 --data "3f800000..."

# Search vectors
$ edgestore-cli vector-search --path ./mydb --namespace vec \
    --query "3f800000..." --k 5 --metric cosine

# Compact expired data
$ edgestore-cli compact --path ./mydb

# Export to JSON
$ edgestore-cli export --path ./mydb --output backup.json

Cost-Aware Reads

Every read has a _with_stats variant that returns a QueryStats struct alongside the result. Ideal for agent loops that need to track token- or byte-budgets per operation.

use edgestore::{EdgestoreConfig, Engine};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut db = Engine::open(EdgestoreConfig::new("/tmp/cost_demo"))?;

    db.put(b"docs", b"doc1", b"hello world")?;
    db.flush_to_segments()?;

    // Point lookup with cost reporting
    let (val, stats) = db.get_with_stats(b"docs", b"doc1")?;
    println!("value={:?}", val);
    println!("bytes={} segs={} items={}",
        stats.bytes_scanned,
        stats.segments_scanned,
        stats.items_examined);

    // Range scan with accounting
    let (rows, stats) = db.range_with_stats(b"docs", b"a", b"z")?;
    println!("{} rows, {} bytes", rows.len(), stats.bytes_scanned);

    // Vector search with cost reporting
    let (hits, stats) = db.vector_search_with_stats(
        b"vec", &query_vec, 10, Metric::Cosine
    )?;
    println!("{} hits, {} bytes scanned", hits.len(), stats.bytes_scanned);

    Ok(())
}

Run: cargo run --example cost_accounting

Bounded Scans with Pagination

Stop a scan early when a byte or item budget is hit. Use next_key as the start of the next page.

use edgestore::ScanBudget;

let budget = ScanBudget { max_items: 100, max_bytes: 512 * 1024 };
let mut start = b"2026-01-01".to_vec();
let end = b"2026-12-31";

loop {
    let page = db.range_budgeted(b"logs", &start, end, budget)?;

    for (k, v) in &page.items {
        process(k, v);
    }

    if !page.truncated {
        break;
    }
    start = page.next_key;
}

// Same pattern works for prefix scans
let result = db.prefix_budgeted(b"events", b"click:", budget)?;
println!("got {} items, truncated={}", result.items.len(), result.truncated);

Search Snippets for RAG

Return context windows around matched terms. Pass snippets to an LLM as evidence without sending the full document.

use edgestore_text::TextIndex;
use std::collections::HashMap;

let mut db = Engine::open(EdgestoreConfig::new("/tmp/snippets_demo"))?;
let mut text = TextIndex::new();
let ns = b"articles";

text.index_document(&mut db, ns, b"doc1",
    "The quick brown fox jumps over the lazy dog near the river bank", HashMap::new())?;
text.index_document(&mut db, ns, b"doc2",
    "A fox and a hound became unlikely friends in the forest", HashMap::new())?;
text.persist(&mut db)?;

let results = text.search_with_snippets(&db, ns, "fox", 5)?;

for r in &results {
    println!("doc={} score={:.3}", r.doc_id, r.score);
    for s in &r.snippets {
        println!("  matched={:?}  context={:?}", s.span,
            &original_text[s.context_start..s.context_end]);
    }
}

// Output (approximate):
//   doc=doc1  score=1.847
//     matched="fox"  context="quick brown fox jumps over"
//   doc=doc2  score=1.521
//     matched="fox"  context="A fox and a hound became"

Run: cargo run --example search_snippets