Skip to content

nori-sstable

Immutable sorted string tables with blocks, index, bloom filters, and compression.

Get Started API Reference View on GitHub



What is nori-sstable?

nori-sstable provides a production-ready implementation of SSTables (Sorted String Tables) for building LSM-tree storage engines. SSTables are immutable, on-disk data structures that store sorted key-value pairs with efficient lookups and range scans.

This crate is a core building block of the NoriKV distributed key-value store, but can be used standalone in any Rust project that needs persistent sorted storage.


Key Features

Storage & Format

  • Block-based storage: 4KB blocks with prefix compression for space efficiency
  • Two-level index: Sparse block index for fast key lookups with minimal I/O
  • Bloom filters: Probabilistic membership testing (~0.9% false positive rate) to avoid unnecessary disk reads
  • CRC32C checksums: Data integrity validation at block level

Performance & Optimization

  • Block compression: Optional LZ4 (fast, 3.9 GB/s decompress) or Zstd (higher ratio) compression πŸ†•
  • LRU block cache: Configurable in-memory cache for hot data blocks (default 64MB) πŸ†•
  • Async I/O: Built on Tokio for high-performance asynchronous operations
  • ~67ns bloom filter checks: Ultra-fast negative lookups

Developer Experience

  • Tombstones: Explicit delete markers preserved through the storage layer
  • Observability: Vendor-neutral metrics via nori-observe::Meter trait
  • Thread-safe: Concurrent reads supported via Arc<SSTableReader>
  • 100% Safe Rust: No unsafe code in the public API
  • 108 tests passing: Comprehensive test suite including compression and property tests

{: .new }

Recent Updates: nori-sstable now includes production-ready LZ4/Zstd compression and an LRU block cache that delivers 18x performance improvements for hot key workloads!


Quick Example

use nori_sstable::{Compression, Entry, SSTableBuilder, SSTableConfig, SSTableReader};
use std::path::PathBuf;
use std::sync::Arc;

#[tokio::main]
async fn main() -> nori_sstable::Result<()> {
    // Build an SSTable with LZ4 compression
    let config = SSTableConfig {
        path: PathBuf::from("data.sst"),
        estimated_entries: 1000,
        compression: Compression::Lz4,  // Enable compression
        block_cache_mb: 64,              // 64MB cache (default)
        ..Default::default()
    };

    let mut builder = SSTableBuilder::new(config).await?;

    // Add entries in sorted order
    builder.add(&Entry::put("key1", "value1")).await?;
    builder.add(&Entry::put("key2", "value2")).await?;
    builder.add(&Entry::delete("key3")).await?; // Tombstone

    let metadata = builder.finish().await?;
    println!("Created SSTable with {} entries", metadata.entry_count);

    // Read data back
    let reader = Arc::new(SSTableReader::open(PathBuf::from("data.sst")).await?);

    // Point lookup (uses bloom filter + index + cache)
    if let Some(entry) = reader.get(b"key1").await? {
        println!("Found: {:?}", String::from_utf8_lossy(&entry.value));
    }

    // Range scan
    let mut iter = reader.iter();
    while let Some(entry) = iter.try_next().await? {
        println!("{:?} = {:?}", entry.key, entry.value);
    }

    Ok(())
}

Performance Highlights

Benchmarked on Apple M1 (release build):

Operation Performance Notes
Bloom filter check ~67ns In-memory, ultra-fast
Point lookup (hit) ~5Β΅s With cache enabled
Point lookup (miss) <20Β΅s Bloom filter skip
Sequential scan ~1M entries/sec Streaming iteration
Hot key workload 18x faster (14ms β†’ 777Β΅s) With 64MB cache
Compression ratio (LZ4) 2-3x typical 14x on highly compressible data

See detailed benchmarks β†’


Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        Write Path                            β”‚
β”‚  User β†’ SSTableBuilder β†’ BlockBuilder β†’ Compress β†’ Writer   β”‚
β”‚           β”‚                  β”‚              β”‚          β”‚     β”‚
β”‚           β”œβ”€ BloomFilter     β”œβ”€ Prefix      β”‚          └─ File I/O
β”‚           β”‚  (add keys)      β”‚   Compressionβ”‚                β”‚
β”‚           β”‚                  β”‚   (shared len)                β”‚
β”‚           └─ Index           └─ Restart                      β”‚
β”‚              (track blocks)     Points                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         Read Path                            β”‚
β”‚  User β†’ SSTableReader β†’ [Cache?] β†’ [Bloom?] β†’ Index β†’ Block β”‚
β”‚                             ↓          ↓         ↓        ↓  β”‚
β”‚                          Hit/Miss  Contains?  Find    Binary β”‚
β”‚                          (~18x)    (~67ns)    Block   Search β”‚
β”‚                                                (log B)  (log E)β”‚
β”‚         SSTableIterator β†’ load blocks on demand              β”‚
β”‚                            ↓                                 β”‚
β”‚                         BlockIterator                        β”‚
β”‚                          (decompress + decode)               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

File Layout

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Data Block 0   β”‚ ← 4KB blocks (compressed with LZ4/Zstd)
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Data Block 1   β”‚   Entry: shared_len | unshared_len |
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€          value_len | key | value
β”‚     ...        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€   Restart points every 16 entries
β”‚ Data Block N   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Index          β”‚ first_key | block_offset | block_size (per block)
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Bloom Filter   β”‚ xxhash64 with double hashing
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ 10 bits/key β†’ ~0.9% FP rate
β”‚ Footer (64B)   β”‚ Magic | Index offset/size | Bloom offset/size |
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ Compression type | CRC32C checksum

Learn more about the architecture β†’


When to Use nori-sstable

Great Fit

  • Building LSM-tree storage engines (like RocksDB, LevelDB)
  • Time-series databases with immutable time-ordered data
  • Log storage with sorted logs and range queries
  • Snapshot storage for point-in-time database snapshots
  • Need fast reads with bloom filter optimization
  • Hot key workloads where caching provides 10x+ speedups
  • Workloads where compression saves significant storage costs

Not the Right Tool

  • Need mutable data structures (use in-memory B-trees instead)
  • Random writes (SSTables are write-once, immutable)
  • Ultra-low latency < 1Β΅s (use in-memory stores)
  • Very small datasets < 1MB (overhead not worth it)

Documentation Sections

Getting Started

Learn how to install and use nori-sstable in your project.

Getting Started β†’

Core Concepts

Understand the fundamental concepts behind SSTables: immutability, block-based storage, bloom filters, and when to use them.

Core Concepts β†’

Compression πŸ†•

Deep dive into LZ4 and Zstd compression: when to use each, performance tradeoffs, and configuration.

Compression Guide β†’

Caching πŸ†•

Learn how the LRU block cache works, how to tune it for hot workloads, and achieve 18x speedups.

Caching Guide β†’

How It Works

Detailed internals: file format, block format, bloom filters, index structure, compression, and cache implementation.

How It Works β†’

API Reference

Complete API documentation for builders, readers, configuration, and iterators.

API Reference β†’

Performance

Benchmarks, tuning guides, compression ratio analysis, and profiling.

Performance β†’

Design Decisions

Rationale behind key design choices: block-based organization, immutability, compression strategy, and more.

Design Decisions β†’

Recipes

Common patterns and use cases with code examples.

Recipes β†’

Internals

Deep implementation details for contributors and advanced users.

Internals β†’


Installation

Add to your Cargo.toml:

[dependencies]
nori-sstable = "0.1"
tokio = { version = "1", features = ["full"] }

Then import in your code:

use nori_sstable::{
    SSTableBuilder, SSTableReader, SSTableConfig,
    Entry, Compression,
};

Project Status

Aspect Status
Core functionality Production-ready
Tests 108 tests passing
Compression LZ4 + Zstd supported
Caching LRU cache implemented
Documentation Complete
Benchmarks Comprehensive
Published 🚧 Preparing for crates.io

Contributing

nori-sstable is part of the NoriKV project and welcomes contributions!


License

MIT License - see LICENSE for details.


Next Steps

New to SSTables? Start with Getting Started to build your first SSTable in 5 minutes.

Want to optimize performance? Check out Compression and Caching to learn about our newest performance features.

Need the API? Jump to API Reference for complete method documentation.

Curious about internals? Dive into How It Works for implementation details.