Getting started
nidus is built for development and small-scale use. You add it as a dependency, open a store in a directory of your choosing, and call methods. There is nothing to install, no daemon to run, and no network.
There are two ways in. This page takes the vector-store path: you bring the embeddings and nidus stores and searches them. If you’d rather hand nidus plain text and let it embed for you — remember natural language, recall the relevant bits — see Remember & recall; it’s the same store with an embedding step wrapped around it.
Add the dependency
Section titled “Add the dependency”[dependencies]nidus = "0.28"anyhow = "1" # nidus returns anyhow::Resultnidus requires Rust 1.96+ (edition 2024). It pulls in only popular, mostly
pure-Rust crates — the local store and search path are pure Rust, and the bundled
S3/GCS backends add only a small TLS compile (ring), never a bundled C++ tree — so
the whole build is seconds, not minutes.
Open a store
Section titled “Open a store”A store is a single directory. The location is always your choice — nidus contributes no path defaults, env vars, or hidden directories. The embedding dimension is pinned at creation and checked on every reopen.
use nidus::{Nidus, Config};
// Open (or create) a store with a pinned 768-dimensional embedding space.let mut db = Nidus::open(Config::new("/path/to/store", 768))?;db.create_collection("code")?;# anyhow::Ok(())Shorthand constructors exist for the common cases:
use nidus::Nidus;
// Same as Config::new(dir, dim) with all defaults.let db = Nidus::open_dir("/path/to/store", 768)?;
// A throwaway store with no files — handy for tests.let db = Nidus::open_in_memory(768)?;# anyhow::Ok(())Index records
Section titled “Index records”A Record is a caller-supplied id, its vector (length must equal the store
dimension), and an open map of typed attrs. Upserts are idempotent by id
within a collection — re-upserting the same id replaces it.
use std::collections::BTreeMap;use nidus::{Record, Value};
let mut attrs = BTreeMap::new();attrs.insert("path".into(), Value::Str("src/auth/login.rs".into()));attrs.insert("lines".into(), Value::Int(42));
db.upsert("code", &[Record::new("a", vec![/* 768 f32s */], attrs)])?;# anyhow::Ok(())Pass a slice to upsert a whole batch in one durable, all-or-nothing call. A record
may also carry no embedding — Record::text_only(id, attrs) — for a document indexed
purely by full-text search.
Search
Section titled “Search”search takes a Scope, a query vector, and
SearchOpts. It returns ranked Hits — each
carrying its source collection, id, cosine score, and the matched record’s
attrs.
use nidus::SearchOpts;
let hits = db.search("code", &query, &SearchOpts { top_k: 5, ..Default::default()})?;
for h in &hits { println!("{:.3} [{}] {}", h.score, h.collection, h.id);}# anyhow::Ok(())Search the whole store at once, with a metadata filter and a score floor:
use nidus::{Scope, SearchOpts, Filter, Predicate};
let opts = SearchOpts { top_k: 10, filter: Filter(vec![Predicate::Glob("path".into(), "src/auth/*".into())]), min_score: Some(0.5),};let hits = db.search(Scope::All, &query, &opts)?;# anyhow::Ok(())Scoping the whole store is sound because every collection shares one embedding space — see Search & filters.
Run the example
Section titled “Run the example”The repository ships an end-to-end demo:
cargo run --example demoWhere to next
Section titled “Where to next”- How it works — the storage model and search path.
- Storage & durability — the on-disk format and crash safety.
- Configuration — every knob on
Config. - API reference — the full surface.