Runnable code snippets for every major feature. Copy-paste into your project or run the included examples with cargo run --example <name>.
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
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
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(())
}
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?;
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")
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());
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
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
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.
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
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
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);
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
Walk a key range in pages without loading the full range. Each call reads only the segment blocks that overlap the current window; I/O stops once page_size live items are collected.
use edgestore::{EdgestoreConfig, Engine, RangePage};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut db = Engine::open(EdgestoreConfig::new("/tmp/pagination_demo"))?;
// Write 1000 events with timestamp keys
for i in 0..1000u32 {
let key = format!("2026-08-{:04}", i);
db.put(b"events", key.as_bytes(), b"payload")?;
}
db.flush_to_segments()?;
// Page through all events 100 at a time
let mut cursor: Option<Vec<u8>> = None;
let mut total = 0usize;
loop {
let page = db.range_page(
b"events", b"", b"\xff",
cursor.as_deref(), 100
)?;
total += page.items.len();
cursor = page.next_key;
if cursor.is_none() { break; }
}
println!("total events: {total}"); // 1000
Ok(())
}
Descending scan for newest-first queries. When keys encode timestamps, range_rev_page returns the most recent records first and stops early without reading older segments.
// Fetch the 5 most recent log entries
let page = db.range_rev_page(
b"logs", b"", b"\xff", None, 5
)?;
// page.items[0] is the lexicographically largest key (most recent)
for (key, val) in &page.items {
println!("{}: {}", String::from_utf8_lossy(key), String::from_utf8_lossy(val));
}
// Multi-page reverse walk
let mut cursor: Option<Vec<u8>> = None;
loop {
let page = db.range_rev_page(
b"logs", b"", b"\xff",
cursor.as_deref(), 50
)?;
for (key, val) in &page.items { process_newest_first(key, val); }
cursor = page.next_key; // smallest key in this page
if cursor.is_none() { break; }
}
The same API is available on AsyncEngine. Each page call runs inside spawn_blocking and releases the read lock before the next page.
use edgestore_tokio::{AsyncEngine, RangePage};
use edgestore::EdgestoreConfig;
async fn export_all(engine: &AsyncEngine) -> Vec<(Vec<u8>, Vec<u8>)> {
let mut out = Vec::new();
let mut cursor: Option<Vec<u8>> = None;
loop {
let page = engine
.range_page(b"data", b"", b"\xff", cursor.as_deref(), 200)
.await
.unwrap();
out.extend(page.items);
cursor = page.next_key;
if cursor.is_none() { break; }
tokio::task::yield_now().await; // cooperate with the scheduler
}
out
}