API Overview

EdgeStore's public API surface is intentionally small. The core type is Engine — everything else is configuration, traits, or return types. For full rustdoc, see docs.rs/edgestore.

Engine Config KV API Cost Accounting Pagination Vector Text Replication Tiered Types

Engine

The single-writer KV engine. All mutations go through one Engine instance per database directory.

Engine::open(config: EdgestoreConfig) -> Result<Engine, EdgestoreError>

Open or create a database at the path specified in config. Acquires a file lock to enforce single-writer semantics.

engine.put(ns, key, val) -> Result<Lsn, EdgestoreError>

Write a key-value pair in the given namespace. Appends to WAL, then memtable. Returns the assigned LSN.

engine.put_with_ttl(ns, key, val, ttl_seconds) -> Result<Lsn, EdgestoreError>

Write with a time-to-live. Expired via deathtime-cohort compaction, not per-read checks.

engine.get(ns, key) -> Result<Option<Vec<u8>>, EdgestoreError>

Point lookup. Checks memtable first, then segments in LSN order (newest wins).

engine.delete(ns, key) -> Result<Lsn, EdgestoreError>

Write a delete tombstone. Visible to subsequent reads; removed at compaction.

engine.range(ns, start, end) -> Result<Vec<(Vec<u8>, Vec<u8>)>, EdgestoreError>

Range scan with exclusive end bound. Returns latest LSN values across memtable and all segments.

engine.prefix(ns, prefix) -> Result<Vec<(Vec<u8>, Vec<u8>)>, EdgestoreError>

Prefix scan. Efficient for time-series keys, hierarchical data, etc.

engine.flush() -> Result<(), EdgestoreError>

Force WAL fsync. Does not flush memtable to segments.

engine.flush_to_segments() -> Result<SegmentMeta, EdgestoreError>

Flush memtable to an immutable segment on disk. Creates .dat, .idx, .xf, and .meta files. On AsyncEngine, uses the 3-phase lock-free protocol — the write lock is released during disk I/O so reads are not blocked.

engine.begin_flush() -> Option<FlushWork>

v2.2.0 Phase 1 of the 3-phase flush. Snapshots memtable entries without clearing them. Returns None if empty. The caller must call finalize_flush with the returned FlushWork::flushed_max_lsn.

engine.finalize_flush(meta, flushed_max_lsn) -> Result<(), EdgestoreError>

v2.2.0 Phase 3 of the 3-phase flush. Registers the completed segment, rebuilds the memtable (retaining only entries written after the snapshot), and retires old WAL files.

engine.compact_once() -> Result<CompactionStats, EdgestoreError>

Run one deathtime-cohort compaction cycle. Budget-bounded; stops after first partial cohort if write budget exhausted.

engine.snapshot() -> Result<Snapshot, EdgestoreError>

Capture a point-in-time view of current segments. RAII release on Drop.

engine.metrics() -> MetricsSnapshot

Return current engine metrics: segment count, memtable entries, WAL files, total bytes.

Configuration

use edgestore::EdgestoreConfig;

let config = EdgestoreConfig::new("/tmp/mydb")
    .with_wal_max_bytes(64 * 1024 * 1024)      // 64 MB WAL rotation
    .with_wal_max_seconds(60)                      // or 60 seconds
    .with_segment_target_size(16 * 1024 * 1024)    // 16 MB segments
    .with_cohort_window_secs(3600)                  // 1 hour cohorts
    .with_compression_wal(edgestore::Compression::Lz4)
    .with_compression_segment(edgestore::Compression::Zstd { level: 1 });

Transaction API

engine.begin() -> Transaction

Start a new multi-record transaction batch.

tx.put(ns, key, val, ttl, timestamp) -> Result<(), EdgestoreError>

Add a put to the transaction. Accumulated in memory until commit.

engine.commit_transaction(tx) -> Result<Lsn, EdgestoreError>

Atomically commit all transaction records to WAL with a single fsync.

engine.rollback_transaction(tx)

Discard the transaction without writing anything.

Cost Accounting & Bounded Scans v1.5.0

Every read has a _with_stats twin that returns a QueryStats alongside the result. Bounded variants stop early when a ScanBudget is exceeded.

engine.get_with_stats(ns, key) -> Result<(Option<Vec<u8>>, QueryStats), EdgestoreError>

Point lookup with cost reporting. Returns zero stats on cache miss, 1 item + byte count on hit.

engine.range_with_stats(ns, start, end) -> Result<(Vec<(Vec<u8>, Vec<u8>)>, QueryStats), EdgestoreError>

Range scan that accumulates bytes_scanned and items_examined as it iterates.

engine.prefix_with_stats(ns, prefix) -> Result<(Vec<(Vec<u8>, Vec<u8>)>, QueryStats), EdgestoreError>

Prefix scan with cost accounting.

engine.range_budgeted(ns, start, end, budget: ScanBudget) -> Result<BudgetedScan, EdgestoreError>

Range scan that stops when max_items or max_bytes is reached. BudgetedScan.truncated is true when cut short. For cursor-based pagination use range_page instead.

engine.prefix_budgeted(ns, prefix, budget: ScanBudget) -> Result<BudgetedScan, EdgestoreError>

Prefix scan with early termination on budget exhaustion.

engine.vector_search_with_stats(ns, query, k, metric) -> Result<(Vec<VectorSearchResult>, QueryStats), EdgestoreError>

ANN / flat-scan search with byte and segment accounting.

text.search(&engine, ns, query, results_limit) -> Result<Vec<TextSearchResult>, EdgestoreError>

BM25 text search via edgestore-text::TextIndex (text must be let mut). For cost accounting on the text path, combine with engine-level stats (segments scanned, bytes read) from surrounding KV operations.

Cursor Pagination v1.7.0

Bounded page-by-page iteration over a key range. Each call reads only the segment blocks that overlap the current page window and stops at the I/O level once page_size live items are collected. Pass next_key from the result as cursor on the next call; next_key = None means the scan is exhausted.

engine.range_page(ns, start, end, cursor: Option<&[u8]>, page_size) -> Result<RangePage, EdgestoreError>

Forward cursor scan. Items returned in ascending key order. First call: cursor = None. Subsequent calls: cursor = page.next_key.as_deref().

engine.range_rev_page(ns, start, end, cursor: Option<&[u8]>, page_size) -> Result<RangePage, EdgestoreError>

Reverse cursor scan. Items returned in descending key order. next_key is the smallest key on the current page; pass it as cursor to continue scanning leftward. Efficient for latest-N queries when keys encode time.

RangePage

RangePage.items: Vec<(Vec<u8>, Vec<u8>)>

Decoded (key, value) pairs. Ascending for range_page, descending for range_rev_page.

RangePage.next_key: Option<Vec<u8>>

Cursor for the next call. None when all items in the range have been returned.

// Forward pagination — ascending, page_size=100
let mut cursor: Option<Vec<u8>> = None;
loop {
    let page = db.range_page(b"events", b"", b"\xff", cursor.as_deref(), 100)?;
    for (key, val) in &page.items { process(key, val); }
    cursor = page.next_key;
    if cursor.is_none() { break; }
}

// Reverse pagination — latest-N, descending
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;
    if cursor.is_none() { break; }
}

Vector API

VectorEngine::vector_put(ns, key, dims, dtype, data) -> Result<(), EdgestoreError>

Store a typed vector. Uses a synthetic __vec__{ns} namespace underneath.

VectorEngine::vector_get(ns, key) -> Result<Option<VectorRecord>, EdgestoreError>

Retrieve a vector by key.

VectorEngine::vector_delete(ns, key) -> Result<(), EdgestoreError>

Remove a vector from the collection.

engine.vector_search(ns, query, k, metric) -> Result<Vec<VectorSearchResult>, EdgestoreError> v1.8.0: &self

Search top-k nearest neighbors. Routes to HNSW if an index exists, otherwise flat scan. Takes &self — concurrent reads do not block each other. The HNSW cache uses interior RwLock for lazy loading so the outer engine lock stays shared.

engine.try_hnsw_search(ns, query, k) -> Result<Option<Vec<VectorSearchResult>>, EdgestoreError> v1.8.0

Attempt the HNSW path only. Returns None if no fresh index is available, Some(results) otherwise. Used by AsyncEngine to try the fast path under a read lock before falling back to a paged flat scan.

engine.build_vector_index(ns) -> Result<(), EdgestoreError>

Build an HNSW approximate index for the collection. Persists to a sidecar file. Takes &mut self — this is a write operation.

engine.preload_vector_index(ns) -> Result<bool, EdgestoreError> v1.8.0: &self

Load the HNSW index from disk into the interior cache. Returns true if loaded, false if no index exists. Now takes &self — safe to call from a shared reference.

engine.vector_count(ns) -> Option<u64> v1.6.0

Returns the number of vectors in the HNSW cache for the namespace if the index is loaded, None otherwise. Never triggers a disk scan. Call preload_vector_index first if you need a guaranteed count. Takes &self.

engine.strip_vector_index(segment_id: u64) -> Result<SegmentMeta, EdgestoreError> v1.5.0

Remove all __vec__ namespace records from the named segment, returning a new segment (with a new ID) that contains only KV data. The original segment is deleted. Idempotent when called on the returned segment ID.

Vector Types

Dtype

F32, F16, I8 — the vector element type. All distances computed in f32 (widening for f16/i8).

Metric

Cosine, L2 (Euclidean), DotProduct — three supported distance metrics.

Text Search API edgestore-text v2.0

Full-text search lives in the edgestore-text crate. Add edgestore-text = "2.1" to your Cargo.toml. Declare TextIndex as let mut text = TextIndex::new() — all search methods take &mut self to cache WAL-reconstructed indices on first use. persist() writes a sidecar for faster cold-start reads; WAL reconstruction provides crash safety even without it.

TextIndex::new() -> TextIndex

Create a new in-memory text index (English tokenizer). Use TextIndex::with_language(lang) for other Snowball-supported languages.

text.index_document(&mut engine, ns, key, text, facets) -> Result<u64, EdgestoreError>

Tokenize and index a document. Stores the inverted index sidecar in the __text__{ns} namespace. Pass HashMap::new() for no facets.

text.search(&engine, ns, query, results_limit) -> Result<Vec<TextSearchResult>, EdgestoreError>

BM25-ranked search; takes &mut self (caches WAL-reconstructed index on first call). Returns up to results_limit results with doc_id: Vec<u8> and score: f32. Tokenizes the query, intersects posting lists, scores with BM25(k1=1.2, b=0.75).

text.search_with_stats(&engine, ns, query, results_limit) -> Result<(Vec<TextSearchResult>, TextSearchStats), EdgestoreError>

BM25 search with scan statistics: total_docs_indexed, docs_examined (matched before cutoff), bytes_scanned. Useful for diagnosing empty results — if docs_examined == 0 and total_docs_indexed > 0, the query terms are simply absent from the corpus.

text.search_with_options(&engine, ns, query, &SearchOptions) -> Result<Vec<TextSearchResult>, EdgestoreError>

Full-control search. SearchOptions fields: limit: usize (max results), facet_filters: Vec<FacetFilter>, typo_tolerance: bool (edit-distance ≤ 1 expansion). Example: SearchOptions { limit: 10, typo_tolerance: true, facet_filters: vec![] }.

text.search_with_snippets(&engine, ns, query, results_limit, context_chars) -> Result<Vec<SnippetResult>, EdgestoreError>

BM25 search with context windows around matched terms; takes &mut self. Each SnippetResult contains doc_id, score, and snippets: Vec<Snippet>. Each Snippet has text (context window), byte_start, byte_end (match offsets within text).

text.delete_document(&mut engine, ns, key) -> Result<u64, EdgestoreError>

Remove a document from the text index. Writes a tombstone to the WAL immediately; the sidecar is updated on the next persist() call. WAL reconstruction skips tombstoned documents, so the deletion is crash-safe without an explicit persist().

text.persist(&mut engine) -> Result<(), EdgestoreError>

Optional: write the inverted-index sidecar to the engine WAL for faster cold-start reads. Call once after a batch of index_document calls. No-op when nothing changed. Without persist(), the index is rebuilt from raw WAL records on next cold start — correct but slightly slower (one engine.prefix() scan per namespace).

SearchOptions { limit, facet_filters, typo_tolerance }

Passed to search_with_options. All fields: limit: usize (max results to return), facet_filters: Vec<FacetFilter> (narrow by facet value), typo_tolerance: bool (match within edit distance 1). Use ..Default::default() to fill unset fields.

Posting { doc_id, term_freq, doc_len_tokens, facets, positions }

A posting in the inverted index. doc_len_tokens is the token count of the document (not bytes) — used by BM25 for length normalization. Exported for custom scoring via bm25_score() or score_document().

Replication API

engine.export_manifest() -> Result<Vec<SegmentRef>, EdgestoreError>

Export the current live segment list with BLAKE3 hashes. Send this to a replica to compare.

engine.missing_segments(peer_segments) -> Vec<[u8; 32]>

Compare local segments against a peer's manifest. Returns BLAKE3 hashes of missing segments.

engine.import_segment(seg_ref, data) -> Result<ImportResult, EdgestoreError>

Import a segment from a peer. Applies LWW conflict resolution by wall-clock timestamp. Verifies BLAKE3 hash.

engine.range_merkle_root() -> Result<[u8; 32], EdgestoreError>

Compute the root hash of the range-level Merkle tree over all segments.

engine.compare_merkle(other_root) -> Result<bool, EdgestoreError>

Compare local Merkle root with a peer's. Returns true if identical.

engine.confirmed_lsn() -> u64 v1.9.0

Highest LSN whose segment is durably flushed. Replicas use this to know what is safe to serve. Returns 0 when no segments exist yet.

engine.write_token() -> u64 v1.9.0

Current write-fencing token. Monotonically increasing; persisted to WTOKEN. A replica promoted to primary increments this token so anti-entropy loops detect the failover.

engine.set_write_token(token: u64) -> Result<(), EdgestoreError> v1.9.0

Persist a new write token atomically. Use via ReplicatedEngine::promote_to_primary which enforces the monotonic invariant.

GET /watermark → WatermarkResponse { confirmed_lsn, wtoken } v1.9.0

New HTTP endpoint on HttpReplicationServer. Returns the current commit watermark and write token. Polled by AntiEntropyLoop each cycle.

ReplicatedEngine::promote_to_primary(config, bind_addr, old_primary_url) v1.9.0

Fence the old primary, increment write token, start the replication server. Pass None for old_primary_url when the old primary is already down.

remote_store.upload_if_absent(hash, data) -> Result<bool, EdgestoreError> v1.9.0

Idempotent-create on RemoteStore. Returns true on first write, false when already present. Filesystem: O_CREAT|O_EXCL. S3: If-None-Match: * (HTTP 412 → false).

ReplicationProtocol::watermark() -> Result<WatermarkResponse, EdgestoreError> v1.9.0

New method on the ReplicationProtocol trait. Default returns Err(InvalidOperation) for backward compatibility. HttpReplicationClient implements it against GET /watermark.

Tiered Storage API edgestore-tier

TieredEngine::new(local: Engine, remote: Box<dyn RemoteStore>) -> TieredEngine

Wrap a local engine with a remote store. Reads fall through to remote on local miss. Writes always go to local only.

tiered.get(ns, key) -> Result<Option<Vec<u8>>, EdgestoreError>

Local-first read. On miss, downloads the matching archived segment ephemerally from S3 and serves from it.

tiered.archive_segments(metas: &[SegmentMeta]) -> Result<(), EdgestoreError>

Upload the named segments to the remote store. Does not delete local copies — caller decides when to prune.

tiered.local() -> &Engine / tiered.local_mut() -> &mut Engine

Direct access to the underlying local engine for operations that don't need remote fallback.

tiered.get_needs_archived_fetch(ns, key) -> bool

Returns true if answering this get would require fetching an archived segment. Used by AsyncTieredEngine to decide whether to escalate from read lock to write lock.

AsyncTieredEngine::flush_notify() -> Arc<tokio::sync::Notify>

Returns a Notify handle wired to the engine's flush callback. Race this against a polling interval to trigger archiving immediately after a segment is written.

TieredEngine::with_vector_stripping(self) -> TieredEngine v1.5.0

Builder method that wires strip_vector_index into the archive_segments path. When enabled, every segment uploaded to the remote store automatically has its __vec__ records removed from the local copy, reclaiming disk proportional to vector data size.

Key Types

EdgestoreError

All errors: Io, KeyNotFound, Corruption, CompactionError, InvalidArgument, etc.

Snapshot / SnapshotRegistry

Point-in-time reads with RAII segment pinning. Registry tracks active snapshots for compaction exclusion.

CompactionStats

cohorts_collected, segments_removed, segments_written, bytes_written, live_records_relocated.

StorageBackend / DefaultStorageBackend / MemoryStorageBackend

Pluggable I/O trait. Default uses pread/pwrite. Memory backend for tests.

HnswIndex

Standalone HNSW graph. Can be serialized/deserialized for persistence. Used by Engine for vector_search routing.

RangeMerkleTree

Merkle tree over key ranges for anti-entropy probes and delta sync.

QueryStats v1.5.0

segments_scanned: u64, bytes_scanned: u64, items_examined: u64. Returned by all _with_stats read variants. Zero-valued on a miss that short-circuited before touching segments.

ScanBudget v1.5.0

max_items: usize, max_bytes: u64. Passed to range_budgeted / prefix_budgeted. The scan stops when either limit is reached, whichever comes first.

BudgetedScan v1.5.0

items: Vec<(Vec<u8>, Vec<u8>)>, truncated: bool, next_key: Vec<u8>. When truncated is true, pass next_key as the start of the next call to continue pagination.

SnippetResult / Snippet v1.5.0

SnippetResult{doc_id, score, snippets: Vec<Snippet>}. Each Snippet{term, span, context_start, context_end} holds the matched text and its character-accurate context window in the original document.

Async Wrapper (edgestore-tokio)

use edgestore_tokio::AsyncEngine;

let engine = AsyncEngine::open(EdgestoreConfig::new("/tmp/async_db")).await?;
engine.put(b"default", b"key", b"val").await?;
let val = engine.get(b"default", b"key").await?;

All Engine methods are wrapped with tokio::task::spawn_blocking. No async code in the core library — the wrapper is thin and optional.