EdgeStore bundles KV storage, vector search, full-text search, and replication into a single embedded library. Each layer sits cleanly on top of the core without polluting it.
Ordered byte keys with namespace isolation. Every key is prefix-encoded as {ns_len:u16}{ns}{key}, giving you a single flat sorted keyspace with logical separation.
// Basic operations
db.put(b"users", b"alice", b"{"age":30}")?;
let val = db.get(b"users", b"alice")?;
// Range scan within a namespace
for (k, v) in db.range(b"users", b"a", b"z")? {
println!("{} = {}", k, v);
}
// Prefix scan
for (k, v) in db.prefix(b"events", b"2024-01-")? {
// all keys starting with 2024-01-
}
Atomic multi-record batches with single-writer semantics. A transaction accumulates puts and deletes in memory, then commits as a single WAL-append with group fsync.
let mut tx = db.begin();
tx.put(b"accounts", b"alice", b"900", 0, 0)?;
tx.put(b"accounts", b"bob", b"1100", 0, 0)?;
// Both writes land atomically, or neither does
db.commit_transaction(tx)?;
Records with TTL are grouped by their predicted death time into cohorts. When a cohort fully expires, compaction removes it with zero live-data relocation — the key mechanism that drives write amplification toward 1.0.
// Session expires in 60 seconds
db.put_with_ttl(b"sessions", b"sess-42", b"active", 60)?;
// Still readable immediately after TTL expires
// (lazy expiry contract)
std::thread::sleep(Duration::from_secs(61));
let val = db.get(b"sessions", b"sess-42")?; // Some(...)
// After compaction, the cohort is gone
db.compact_once()?;
let val = db.get(b"sessions", b"sess-42")?; // None
Store and search dense vectors with SIMD-accelerated distance computation. Two search modes: flat brute-force scan (exact, fast for <500K vectors) and HNSW graph index (approximate, scales to millions).
// Insert vectors
db.vector_put(b"embeddings", b"doc1", 128, Dtype::F32, &vec_data)?;
// Build HNSW index for fast ANN search
db.build_vector_index(b"embeddings", 128, Dtype::F32, Metric::Cosine)?;
// Search top-10 nearest neighbors
let results = db.vector_search(
b"embeddings", &query_vec, 10, Metric::Cosine
)?;
for r in results {
println!("{} distance={:.4}", r.key, r.distance);
}
Embedded Algolia-like search with BM25 relevance scoring. Single merged inverted index per namespace, incrementally updated on write. Search reads the merged index directly — O(1) single-record deserialize, not O(N) per-document merging. No separate server process.
// Index a document
db.index_text(b"docs", b"doc1", "EdgeStore is fast and reliable")?;
// Search with BM25 ranking
let results = db.search_text(b"docs", "fast database", 5)?;
for hit in results {
println!("{} score={:.3}", hit.key, hit.score);
}
// With facet filtering
let opts = SearchOptions::new()
.with_facet("category", "database");
let filtered = db.search_text(b"docs", "fast", 5)?;
Every read API has a _with_stats variant that returns a QueryStats struct alongside the result. Agent loops and quota-enforcement systems can measure exactly how many bytes and segments each query touched — no guesswork.
QueryStats{segments_scanned, bytes_scanned, items_examined}ScanBudget{max_items, max_bytes} and stop early, returning a BudgetedScan{items, truncated, next_key} with a continuation cursor// Cost-aware point lookup
let (val, stats) = db.get_with_stats(b"docs", b"doc1")?;
println!("bytes={} segs={}", stats.bytes_scanned, stats.segments_scanned);
// Bounded range scan — stop after 500 items or 1 MB
let budget = ScanBudget { max_items: 500, max_bytes: 1024 * 1024 };
let result = db.range_budgeted(b"logs", b"2026-01", b"2026-07", budget)?;
println!("{} items, truncated={}", result.items.len(), result.truncated);
// Continue from where we stopped
if result.truncated {
let page2 = db.range_budgeted(
b"logs", &result.next_key, b"2026-07", budget
)?;
}
// ANN search with cost reporting
let (hits, stats) = db.vector_search_with_stats(
b"vec", &query, 10, Metric::Cosine
)?;
search_text_with_snippets returns SnippetResult values that bundle a document ID, BM25 score, and a list of Snippet objects — each with a context window around every matched term. The tokenizer now records exact character offsets so context windows align precisely with the original text.
{doc_id, score, snippets: Vec<Snippet>}{term, span, context_start, context_end} where span is the matched textspan to an LLM as highlighted evidence without sending the full documentdb.index_text(b"docs", b"doc1",
"The quick brown fox jumps over the lazy dog")?;
let results = db.search_text_with_snippets(
b"docs", "fox", 5
)?;
for r in results {
println!("doc={} score={:.3}", r.doc_id, r.score);
for s in r.snippets {
// span = "fox", context window around it
println!(" term={} span={:?}", s.term, s.span);
}
}
// Output:
// doc=doc1 score=1.847
// term=fox span="fox"
Merkle-based delta sync compares range-level Merkle trees between nodes to identify divergent segments. Only missing segments are transferred — not full copies. Lives in edgestore-repl, not the core crate.
s3 feature)// Requires edgestore-repl crate
use edgestore_repl::{AntiEntropyLoop, S3RemoteStore};
// Primary: export manifest
let segments = primary.export_manifest()?;
// Replica: compare and find missing
let missing = replica.missing_segments(&segments);
// Transfer each missing segment
for seg in missing {
let data = primary.read_segment(&seg)?;
replica.import_segment(&seg, &data)?;
}
// Verify Merkle roots match
let match_result = replica.compare_merkle(&primary_root)?;
When local disk can't hold your full dataset, edgestore-tier wraps a local Engine with a transparent S3 read-through layer. Reads try local first; on a miss they fetch the matching archived segment from S3 ephemerally without permanently importing it.
flush_notify() on AsyncTieredEngine wakes a tiering worker the moment a segment landswith_text_stripping(true) auto-strips text index bytes from archived segments to reclaim local diskwith_vector_stripping(true) (v1.5.0) strips __vec__ records from archived segments. Paired with strip_vector_index(seg_id) on the core engine for manual use.// Requires edgestore-tier crate
use edgestore_tier::TieredEngine;
use edgestore_repl::S3RemoteStore;
let local = Engine::open(config)?;
let remote = S3RemoteStore::new("bucket", None, None)?;
let mut tiered = TieredEngine::new(local, Box::new(remote));
// Writes stay local
tiered.put(b"logs", b"2026-07-12", data)?;
// Reads fall through to S3 on local miss
let val = tiered.get(b"logs", b"2025-01-01")?;
// Archive old segments to S3
let old = tiered.local().list_segment_metas();
tiered.archive_segments(&old)?;
RAII point-in-time reads that pin segments in the manifest. Compaction can proceed on live data while snapshots hold old segments for consistent reads.
// Capture point-in-time view
let snap = db.snapshot()?;
// Modify live data
db.put(b"data", b"key", b"new")?;
// Snapshot still sees old value
let old = snap.get(b"data", b"key")?;
// Release when done — pins are dropped automatically
drop(snap);
Every design decision prioritizes SSD health: append-only writes avoid in-place rewrites, deathtime-cohort compaction aligns invalidation with erase blocks, and FDP hints place related data together on NVMe 2.0 hardware.
| Database | App WA | Device WA |
|---|---|---|
| EdgeStore | ~1x | ~1.2x |
| RocksDB | 10-30x | ~90x |
| LevelDB | 10-30x | ~90x |
| SQLite WAL | 2-4x | ~8x |
For TTL workloads. See Papers for methodology.