quiver
Documentation menu

Embedding in Rust

The server is a thin layer over the quiver-db crate. Link the crate and you get the same collections, indexes, filters, storage engines and snapshots inside your own process, with no network in between.

[dependencies]
quiver-db = { git = "https://github.com/HilthonTT/quiver" }   # features = ["rocksdb"] for RocksDB
serde_json = "1"

Quickstart

The repository ships this as an example: cargo run -p quiver-db --example quickstart.

use quiver_db::{CollectionConfig, Database, Filter, Metric, Point, SearchOptions};
use serde_json::json;
 
fn main() -> quiver_db::Result<()> {
    let dir = std::env::temp_dir().join("quiver-quickstart");
    let _ = std::fs::remove_dir_all(&dir);
    let db = Database::open(&dir)?;
 
    let books = db.create_collection("books", CollectionConfig::new(3).metric(Metric::Cosine))?;
    books.upsert([
        Point::new("dune", vec![0.9, 0.1, 0.0]).with_metadata(json!({"genre": "sci-fi", "year": 1965})),
        Point::new("foundation", vec![0.8, 0.2, 0.1]).with_metadata(json!({"genre": "sci-fi", "year": 1951})),
        Point::new("hobbit", vec![0.1, 0.9, 0.2]).with_metadata(json!({"genre": "fantasy", "year": 1937})),
        Point::new("emma", vec![0.0, 0.2, 0.9]).with_metadata(json!({"genre": "romance", "year": 1815})),
    ])?;
 
    println!("Nearest to [1, 0, 0]:");
    for hit in books.search(&[1.0, 0.0, 0.0], 3)? {
        println!("  {:<12} distance={:.4}", hit.id, hit.distance);
    }
 
    let filter = Filter::parse(&json!({"year": {"$lt": 1960}}))?;
    println!("\nNearest published before 1960:");
    for hit in books.search_with(&[1.0, 0.0, 0.0], 3, &SearchOptions::default().filter(filter))? {
        println!("  {:<12} {}", hit.id, hit.metadata.unwrap_or_default());
    }
 
    db.flush()?;
    println!("\nPersisted to {}", dir.display());
    Ok(())
}
Nearest to [1, 0, 0]:
  dune         distance=0.0061
  foundation   distance=0.0369
  hobbit       distance=0.8922
 
Nearest published before 1960:
  foundation   {"genre":"sci-fi","year":1951}
  hobbit       {"genre":"fantasy","year":1937}
  emma         {"genre":"romance","year":1815}
 
Persisted to /tmp/quiver-quickstart

Opening a database

ConstructorGives
Database::open(path)the file engine in path, with default options
Database::open_with(path, DbOptions { .. })a durable database with your options
Database::in_memory()nothing on disk, no snapshots
Database::new(Option<PathBuf>, DbOptions)either; what the server calls

DbOptions holds storage (StorageKind::File, RocksDb or Memory), fsync (default false), wal_checkpoint_bytes (64 MiB), snapshot_dir (default <path>/snapshots) and snapshot_keep (5). A database directory is locked while it is open: a second process, or a second Database on the same path, gets an error.

Opening replays each collection's write-ahead log on top of its last checkpoint; see storage & durability.

Collections

db.create_collection(name, config), db.get_or_create_collection(name, config) (an error if the existing one has a different configuration), db.collection(name) (a name or an alias), db.collections(), db.list_collections() and db.drop_collection(name). db.update_collection(name, &update) or collection.update(&CollectionUpdate) changes settings, db.rename_collection(from, to) returns the renamed collection (handles to the old one are closed), and db.set_alias(alias, collection), db.delete_alias(alias) and db.aliases() manage aliases. Collections are Arc<Collection>, and Database and Collection are Send + Sync: share an Arc<Database> between threads. Searches take a read lock, writes a write lock, per collection.

CollectionConfig::new(dimension) builds a config, with .metric(Metric::…) and .index(IndexConfig::…) to change the defaults (cosine, HNSW), .quantization(…), .on_disk(true), .with_vector(name, VectorConfig::new(dim)) for a named vector and .with_sparse_vector(name, SparseVectorConfig::default()) for a sparse one, and .expiry(ExpiryConfig::new(field)) (with .default_ttl(seconds)) to let points expire. collection.config() returns a copy of the current settings.

The server sweeps expired points away; embedded, call collection.purge_expired() yourself, or start an ExpirySweeper::start(db, interval) (stopped when dropped). Reads never see expired points either way.

Points and searches

MethodDoes
upsert(points)insert or replace; returns the count
insert(vector, metadata)insert with a generated UUID; returns it
get(id), get_many(&ids)fetch points
delete(&ids), delete_where(&filter)delete; return how many existed
count(Option<&Filter>)count points
facet(field, Option<&Filter>, limit)the most common values of a field, as FacetCounts
update_metadata(&PointSelector, value, MetadataMode)merge or replace metadata
scroll(cursor, limit)a ScrollPage in id order
scroll_with(&ScrollOptions)with a filter, or ordered by a field
search(&vector, k)nearest neighbours, with metadata
search_with(&vector, k, &SearchOptions)with a filter, ef, max_distance, vectors
search_by_id(id, k, &opts), recommend(&pos, &neg, k, &opts)by a stored point; by examples
search_sparse(name, &SparseVector, k, &opts)sparse search
search_groups(&SearchTarget, &GroupBy, &opts)grouped search; HitGroups
discover(Option<&Example>, &[ContextPair], k, &opts)discovery
query(&HybridQuery, k)hybrid search; ScoredHits
info(), len(), flush(), compact(), create_snapshot()housekeeping

SearchOptions::default() includes metadata and leaves vectors out; chain .using(name) for a named vector, .filter(f), .ef(n), .max_distance(d), .include_vectors(true), .include_metadata(false) and .offset(n). Points carry named vectors with Point::with_vector(name, v) and sparse ones with .with_sparse_vector(name, SparseVector::new(indices, values)). A HybridQuery::new(vec![Prefetch::new(QueryVector::Dense { vector, using: None }, 50), …]) fuses several searches; set its filter, fusion (Fusion::Rrf { k }, Fusion::RelativeScore or Fusion::Dbsf), rescore, offset and include_* fields as needed. ScrollOptions::new(limit) chains .cursor(c), .filter(f) and .order_by(OrderBy::desc("year")). Filters are built with Filter::parse(&json), from the filter syntax.

use quiver_db::{CollectionConfig, Database, IndexConfig, MetadataMode, Metric, Point, PointSelector, SearchOptions};
use serde_json::json;
 
fn main() -> quiver_db::Result<()> {
    let db = Database::in_memory();
    let places = db.create_collection(
        "places",
        CollectionConfig::new(2).metric(Metric::Euclidean).index(IndexConfig::Flat),
    )?;
    places.upsert([
        Point::new("brussels", vec![4.35, 50.85]).with_metadata(json!({"country": "BE"})),
        Point::new("antwerp", vec![4.40, 51.22]).with_metadata(json!({"country": "BE"})),
        Point::new("lille", vec![3.06, 50.63]).with_metadata(json!({"country": "FR"})),
    ])?;
 
    let opts = SearchOptions::default().max_distance(1.0).include_metadata(false);
    let near: Vec<_> = places.search_by_id("brussels", 5, &opts)?.into_iter().map(|h| h.id).collect();
    println!("within 1.0 of brussels: {near:?}");
 
    let selector = PointSelector::Ids(vec!["lille".into()]);
    places.update_metadata(&selector, json!({"region": "Hauts-de-France"}), MetadataMode::Merge)?;
    println!("{}", places.get("lille").unwrap().metadata.unwrap());
 
    let page = places.scroll(None, 2);
    println!("{:?} then {:?}", page.points.iter().map(|p| p.id.as_str()).collect::<Vec<_>>(), page.next_cursor);
    Ok(())
}
within 1.0 of brussels: ["antwerp"]
{"country":"FR","region":"Hauts-de-France"}
["antwerp", "brussels"] then Some("brussels")

Snapshots

With a snapshot directory (any durable database has one), collection.create_snapshot() or db.create_snapshot(name) writes an archive, and db.list_snapshots(Some(name)), db.restore_snapshot(id, RestoreOptions { name, replace }) and db.delete_snapshot(id) manage them. SnapshotScheduler::start(Arc::clone(&db), interval) runs scheduled snapshots on a background thread until it is dropped or stop()ped.

Errors

Everything returns quiver_db::Result<T>, whose Error has a variant per failure: DimensionMismatch, CollectionNotFound, CollectionExists, AliasNotFound, AliasExists, SnapshotNotFound, PointNotFound, InvalidArgument, InvalidFilter, Unsupported, Corrupt, Storage, Io and Serde. Bad input is an error, never a panic. Failing I/O on an on-disk vector file is different: it panics in the calling thread, and a write that panics is already in the write-ahead log, so a restart replays it.