Skip to content

Getting started

nidus is a vector store and full-text search engine 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.

A store holds records: a vector, an id, and an open map of typed metadata. You can search it three ways, and they share one set of results, filters, and scoping:

This page takes the vector path, because it exercises the most of the store. If you only want keyword search, full-text search stands alone and needs no embeddings at all.

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

nidus requires Rust 1.98+ (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.

If you only want the storage and search core, cargo add nidus --no-default-features gives you the four core crates plus the storage backends: no HTTP server, no async runtime, no embed, summarize, or rerank providers.

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),
..Default::default()
};
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