Skip to content
Current documentation — tracks the engine's development branch (v0.4.0 at the last sync).v0.2.1v0.3.0v0.3.1v0.3.2v0.4.0v0.4.1About these docs

A tour of queries

corvid reads through one fluent builder. This tour touches each capability singly; the hybrid walkthrough composes them.

Predicates are built with field(path) and evaluate against dotted paths in the document:

use corvid::{field, Value};
field("category").eq(Value::Text("blog".into()));
field("score").gt(Value::Int(5));
field("score").between(Value::Int(1), Value::Int(10)); // inclusive
field("tag").is_in([Value::Text("a".into()), Value::Text("b".into())]);
field("title").starts_with("intro");
field("body").contains("rust");
field("loc").within_km(51.5, -0.13, 25.0); // geo
field("email").exists();
// Combine with and/or/not:
let p = field("category").eq(Value::Text("blog".into()))
.and(field("score").ge(Value::Int(3)))
.or(field("pinned").eq(Value::Bool(true)));
let p = !field("draft").eq(Value::Bool(true)); // negation
# let _ = p;

Comparisons on a missing path are false; ordered comparisons across non-comparable types are false. Full semantics — including the NaN rules — are on the filters and equality pages.

Vectors are first-class document values (Value::Vector, dense f32). Without an index, search is exact — brute-force with a bounded heap, streamed:

# use corvid::{Db, Metric};
# let db = Db::open_in_memory()?; let docs = db.collection("docs");
let hits = docs.vector_search("embedding", &[0.1, 0.9], 10, Metric::Cosine)?;
// Vec<Hit>: { key, score, approximate, document }
# let _ = hits;
# Ok::<(), corvid::Error>(())

Metrics: Metric::Cosine (1 − cos similarity), Metric::Dot (negated dot — larger dot sorts first), Metric::L2 (squared Euclidean). Creating a vector index switches vector_search to HNSW transparently; Hit.approximate tells you which path served the answer.

BM25 ranking over an analyzer that lowercases, drops common English stop words, and applies a conservative plural stemmer (dog matches dogs):

# use corvid::{Db, Value};
# let db = Db::open_in_memory()?; let docs = db.collection("docs");
let hits = docs.text_search("body", "rust databases", 10)?; // Vec<TextHit>
let phrase = docs.phrase_search("body", "embedded database", 10)?; // exact, in order
# let _ = (hits, phrase);
# Ok::<(), corvid::Error>(())

Text containing CJK (Han, hiragana, katakana) tokenizes as sliding bigrams — 東京タワー phrase-matches in order, タワー東京 does not. See full-text search.

# use corvid::{Db, Metric, Value, field};
# let db = Db::open_in_memory()?; let docs = db.collection("docs");
let rows = docs.query()
.filter(field("category").eq(Value::Text("blog".into())))
.vector("embedding", vec![0.1, 0.9], 100, Metric::Cosine) // a retrieval source
.text("body", "rust embedded database", 100) // another source
.fuse_rrf(60.0) // reciprocal-rank-fusion constant (optional)
.rerank_mmr(0.7) // diversify (optional; needs a vector source)
.offset(0)
.limit(10)
.select(["title", "meta.author"]) // project returned docs (optional)
.run()?; // -> Vec<ResultRow> { key, score, document }
# let _ = rows;
# Ok::<(), corvid::Error>(())

Notes:

  • Zero sources → a pure filter/scan query (streamed, bounded memory).
  • One source → ranked by that source. Multiple → fused with RRF.
  • Rank order is what you get above; order_by(field, desc) replaces it with a sort on a literal document field (there is no special score field — to keep rank order, omit order_by; the fused score rides on each row).
  • Filtering happens before ranking, so the top-k is computed among matching documents.
  • .approx() lets a filtered vector query use the ANN index (over-fetch then filter); without it, filtered vector queries run exact.
  • .explain() returns a human-readable plan string; .plan() returns a hashable QueryPlan you can key a PlanCache on.

Over the filtered set (filters and indexes still apply):

# use corvid::{Db, field, Value};
# let db = Db::open_in_memory()?; let sales = db.collection("sales");
sales.query().count()?; // usize
sales.query().filter(field("region").eq(Value::Text("eu".into()))).count()?;
sales.query().sum("amount")?; // f64
sales.query().avg("amount")?; // Option<f64>
sales.query().min("amount")?; sales.query().max("amount")?; // Option<Value>
sales.query().count_distinct("region")?; // usize
sales.query().group_count("region")?; // BTreeMap<String, usize>
sales.query().group_sum("region", "amount")?; // BTreeMap<String, f64>
sales.query().group_avg("region", "amount")?;
# Ok::<(), corvid::Error>(())

Retrieval sources, ranking, and limit/offset/select are ignored by aggregates — they measure the filtered set. Details in aggregations.

Keyset (cursor) pagination — no offset rescans:

# use corvid::{Db, field, Value};
# let db = Db::open_in_memory()?; let docs = db.collection("docs");
let mut after: Option<Vec<u8>> = None;
loop {
let page = docs.page(after.as_deref(), 100)?; // or page_where(after, n, predicate)
for (key, doc) in &page.rows { /* ... */ let _ = (key, doc); }
match page.next { Some(cursor) => after = Some(cursor), None => break }
}
# Ok::<(), corvid::Error>(())

Three more read paths, each with a dedicated section:

# use corvid::{Db, field};
# let db = Db::open_in_memory()?; let people = db.collection("people");
people.link(b"alice", "follows", b"bob")?; // graph edges
people.traverse(b"alice", "follows", 3)?; // BFS up to 3 hops
# let places = db.collection("places");
places.geo_nearest("loc", 51.5, -0.13, 5)?; // k nearest
# let orders = db.collection("orders");
orders.join("customers", "customer_id")?; // left-outer lookup join
# Ok::<(), corvid::Error>(())
  • Graph: link/unlink/neighbors/traverse, cascade semantics.
  • Geo: radius/bbox/nearest, antimeridian handling.
  • Joins: foreign-key resolution across collections.

Next: put it all together in the hybrid walkthrough.