Skip to content

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.

Cargo.toml
[dependencies]
nidus = "0.28"
anyhow = "1" # nidus returns anyhow::Result

nidus 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.

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(())

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 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.

The repository ships an end-to-end demo:

Terminal window
cargo run --example demo