searchnos-db
August 21, 2026 ยท View on GitHub
searchnos-db is a local Nostr event store and query tool. Its storage layer is implemented directly in this repository as an internal Rust module instead of being used as an external crate or path dependency.
Events are stored as ndb_note payloads. Recent writes go to a hot append-only event file, and older data is compacted into per-day partition files with sidecar search and visibility indexes. Queries use NIP-01 filters plus NIP-50-style search terms.
The crate can be used as both a CLI utility and a library.
Highlights
- Local storage layer: stores event packets in
hot.events, per-day partition files, search sidecars, and a visibility LMDB used by the storage layer. - NIP-01 query support: filters by ids, authors, kinds, time ranges, limits, and single-letter generic tags.
- NIP-50 search support: normalizes searchable text and uses partition search sidecars when available.
- Deletion and replaceable visibility: stores raw events append-only, while query visibility hides deleted events and superseded replaceable/addressable events.
- Dump/load tooling: exports and imports raw
ndb_notepayloads using a simple length-prefixed binary stream.
Stability notice: searchnos-db is under active development. Public interfaces and on-disk storage formats may change without prior notice.
Storage Layout
Given --db-path ./data, searchnos-db manages files under that directory:
./data/
hot.events
partitions/
<unix-day>.events
<unix-day>.search
visibility/
data.mdb
lock.mdb
hot.events contains newly appended event packets. When the hot file exceeds the configured size, storage rotates and compacts it into per-day partition files under partitions/. The .search sidecars support search queries, and visibility/ stores deletion and replaceable-event visibility metadata.
The .search files are derived data that can be rebuilt from their matching .events files. Opening storage runs a non-forced reindex, which rebuilds missing, stale, or unreadable sidecars while leaving current sidecars intact. Search queries also repair a missing or unreadable partition sidecar on demand and use the rebuilt sidecar in the same query when repair succeeds. Reindexing writes rebuilt sidecars through a temporary file followed by an atomic rename.
This repository contains the storage source directly under src/storage/. Keep storage changes local to that module unless the project intentionally moves back to an external crate.
Current Scope
Included:
- Event append and query through the local storage layer.
- Search sidecar creation during compaction.
- Reindex implementation inside
src/storage. - Storage-level negentropy item collection:
(created_at, event_id)values for a Unix day.
Not currently implemented by the CLI:
- Relay negentropy reconciliation loop.
- CLI flags such as
--negentropy-relayor--negentropy-days. - Automatic purge policy behavior from the older LMDB-backed implementation.
Getting Started
cargo build
mkdir -p data
Every CLI subcommand accepts --db-path to target an alternate storage directory. The default is ./data.
CLI Usage
All commands can be run through cargo run --.
Help
cargo run -- --help
Show statistics
cargo run -- --db-path ./data stat
Prints the number of currently query-visible events and the total bytes of their ndb_note payloads. This is not an LMDB page-level report.
Compact hot events
cargo run -- --db-path ./data compact
Moves the current hot.events contents into per-day partition files immediately, even when the hot file has not reached the automatic compaction size.
Rebuild indexes
cargo run -- --db-path ./data reindex
cargo run -- --db-path ./data reindex --force
Rebuilds missing, stale, or unreadable partition sidecars. Use --force to rebuild every partition sidecar.
Import events
cargo run -- --db-path ./data import ./events-a.jsonl ./events-b.jsonl
Reads newline-delimited event JSON and appends valid events with a progress bar. Multiple files are processed sequentially, and blank lines are skipped.
Dump events
cargo run -- --db-path ./data dump ./events.dump
Writes query-visible ndb_note payloads to a binary dump file with a progress bar. The dump format is a repeated sequence of:
- 4-byte unsigned payload length encoded as big-endian
u32 - Raw
ndb_notepayload bytes of that length
Load events
cargo run -- --db-path ./data load ./events.dump
Reads the binary dump format produced by dump, verifies each event, and appends valid payloads to storage. Invalid records are skipped with a warning.
Query events
cargo run -- --db-path ./data query '{"authors": ["<hex-pubkey>"], "kinds": [1]}'
Provide a JSON object for one filter or a JSON array for multiple filters. Matching events are printed as JSON on stdout, and execution timing is logged to stderr.
Library Usage
The crate exposes SearchnosDB as the high-level API. It accepts event JSON,
verifies events before inserting them, returns query results as event JSON, and
provides dump/load helpers.
use searchnos_db::{InsertOptions, SearchnosDB};
let db = SearchnosDB::open("./data")?;
let raw_event = r#"{"id":"...","pubkey":"...","kind":1,"content":"hello","tags":[],"created_at":0,"sig":"..."}"#;
db.insert_event_json(raw_event, InsertOptions::default())?;
Configure subscription snapshot and automatic compaction workers with
SearchnosDBOptions when opening a database:
use std::num::NonZeroUsize;
use searchnos_db::{SearchnosDB, SearchnosDBOptions};
let db = SearchnosDB::open_with_options(
"./data",
SearchnosDBOptions {
snapshot_workers: NonZeroUsize::new(4).unwrap(),
compact_workers: NonZeroUsize::new(4),
..SearchnosDBOptions::default()
},
)?;
Maintenance operations are also available from Rust:
db.compact()?;
db.reindex()?;
db.reindex_all()?;
Query from Rust
use searchnos_db::SearchnosDB;
let db = SearchnosDB::open("./data")?;
let events = db.query(r#"{"limit":100}"#)?;
Use stream_query when the caller wants to process matching events without materializing the full result vector:
db.stream_query(r#"{"limit":100}"#, |event_json| {
// Process event JSON here.
true
})?;
Return false from the callback to stop delivery early.
Subscriptions
Initial snapshots created by subscribe start immediately when a snapshot
worker is idle. When every worker is busy, pending subscriptions are grouped
into balanced batches and assigned as workers become available. Each worker
scans a daily search index once for its whole batch, including subscriptions
with different search terms. Results retain each subscription's filter order,
per-filter limits, event deduplication, and newest-first ordering. After EOSE,
matching events continue through the live subscription path.
Development Workflow
Run the following checks before sending changes:
cargo fmt
cargo check
cargo clippy
cargo test
License
See LICENSE.