# corvid documentation — full text (0.3.1) # Source: /docs/v0.3.1/llms-full.txt ================================================================================ # corvid documentation # /docs/v0.3.1/ ================================================================================ **corvid** is an embedded, multi-modal data store for AI applications. One in-process dependency does vector search, full-text search, metadata filtering, and rank fusion — composed into a single fluent call instead of three services glued together in application code. ```rust use corvid::{Db, Metric, Value, field}; let db = Db::open("memory.corvid")?; let docs = db.collection("docs"); let mut doc = std::collections::BTreeMap::new(); doc.insert("category".into(), Value::Text("blog".into())); doc.insert("body".into(), Value::Text("rust embedded database design".into())); doc.insert("embedding".into(), Value::Vector(vec![0.1, 0.9, 0.2])); docs.insert(b"post-1", &Value::Map(doc))?; // Hybrid query: filter + vector + text, fused and reranked, in one call. let rows = docs .query() .filter(field("category").eq(Value::Text("blog".into()))) .vector("embedding", vec![0.1, 0.9, 0.2], 100, Metric::Cosine) .text("body", "rust embedded database", 100) .rerank_mmr(0.7) .limit(10) .run()?; # Ok::<(), corvid::Error>(()) ``` The filter runs *before* ranking, so it is a true predicate — the top-k is computed among matching documents, never a post-hoc trim. ## Where to go next - **New to corvid?** The [tutorial](/tutorial/first-database/) walks from install through your first database to the hybrid query walkthrough. - **Writing queries?** [The corvid language](/language/data-model/) covers the data model, the query builder, filters, aggregations, ordering and pagination. - **Choosing indexes?** The [indexes](/indexes/overview/) section explains every index family — scalar, compound, text, geo, vector (HNSW, quantization, PQ) — and when each serves. - **Coming from C, Node, or another language?** Start at [bindings](/bindings/overview/); the [C ABI](/ffi/overview/) is the frozen contract every binding codes against. - **Operating a deployment?** [Administration](/admin/open-close/) covers backup, dump/load, compaction, bulk load, feature flags and observability. ## What corvid is — and is not corvid is a personal experiment, built in the open: the engine has 1,000+ tests across four feature configurations, >96% line coverage, and a correctness-first design (filters are true predicates, indexes are never stale, writes are transactional). It is pre-1.0: the API and on-disk format change freely until 1.0, with `dump`/`load` as the migration path — never a silent format change. Permanent non-goals: SQL, networking/replication in the engine, distributed transactions, a hosted service. ## About these pages This site is the canonical corvid documentation. Two generated reference pages — the [construct reference](/reference/constructs/) and the [error codes](/reference/error-codes/) — are synced from the engine's conformance manifests at each release; everything else is hand-maintained prose. Versioned snapshots live under `/vX.Y.Z/`; see [About these docs](/about/) for the versioning mechanism. ================================================================================ # Install # /docs/v0.3.1/start/install/ ================================================================================ corvid is a Rust library. Today it is consumed three ways: 1. **From Rust** — a git dependency (crates.io publication is planned; see [bindings roadmap](/bindings/overview/)). 2. **From C or any C-FFI language** — the release artifacts of the engine: the `corvid` cdylib plus the generated `corvid.h`, attached to every [engine release](https://github.com/corvid-db/corvid/releases). 3. **From Node.js** — the [`corvid-node`](https://www.npmjs.com/package/corvid-node) npm package with prebuilt binaries. ## Rust (git dependency) ```toml [dependencies] corvid = { git = "https://github.com/corvid-db/corvid" } ``` Pin a tag if you want reproducible builds (recommended while pre-1.0): ```toml [dependencies] corvid = { git = "https://github.com/corvid-db/corvid", tag = "v0.2.1" } ``` Requires stable Rust, 2024 edition, MSRV **1.88**. The default build has no required features and pulls in only `redb`; the engine is `#![forbid(unsafe_code)]`. ### Optional cargo features Both features are **OFF by default** so the default build stays dependency-minimal (and the WASM size budget stays a contract): | Feature | What it does | Enable with | |---|---|---| | `zstd` | Transparent compression of stored documents at/above 1 KiB. Queries, scans, indexes, dump/load all behave identically — just smaller on disk (~12× on structured text; vector payloads barely compress). | `corvid = { features = ["zstd"] }` | | `tracing` | Structured instrumentation events at the engine's load-bearing points, for any `tracing`-compatible subscriber. | `corvid = { features = ["tracing"] }` | See [feature flags](/admin/features/) for details and caveats (notably: backups are physical copies and not portable across feature builds — use [dump/load](/admin/dump-load/) to move between configurations). ## The C ABI (release artifacts) Every engine release attaches a per-platform FFI archive containing the cdylib, `corvid.h`, and golden fixtures, with sha256 entries in `checksums.txt`: - Linux: `libcorvid.so` - macOS: `libcorvid.dylib` (install name `@rpath/libcorvid.dylib` since v0.2.1) - Windows: `corvid.dll` plus its MSVC import library `corvid.dll.lib` — link the import lib, place the DLL on the loader path The contract these artifacts implement is [the C ABI](/ffi/overview/) specification. [`corvid-c`](/bindings/corvid-c/) shows the full consumption pattern: fetch a pinned release, verify checksums, link, run the golden suite. If you prefer to build from source: ```sh git clone https://github.com/corvid-db/corvid cd corvid cargo build -p corvid-ffi --release # → target/release/libcorvid.{so,dylib} or corvid.dll, plus corvid.h ``` ## Node.js ```sh npm i corvid-node ``` Prebuilt binaries cover `darwin-arm64`, `darwin-x64`, `linux-x64-gnu`, `linux-arm64-gnu`, and `win32-x64-msvc`; other platforms build from source (Rust ≥ 1.88 + a C toolchain). See [corvid-node](/bindings/corvid-node/). ## The MCP sidecar The `corvid-mcp` binary ships on the engine's releases (Linux x86_64/aarch64, macOS Intel/Apple Silicon, Windows x86_64): ```sh # from source: cargo run -p corvid-mcp -- app.corvid # file-backed; omit the path for in-memory ``` See [the MCP sidecar](/admin/mcp/). ## Platform support | Target | Status | |---|---| | Desktop/server (Linux, macOS, Windows) | full support, CI-tested | | WASM (`wasm32-unknown-unknown`) | engine builds; ≈0.2 MB gzipped harness, in-memory use | | Mobile (aarch64 iOS/Android) | engine cross-compiles | WASM persistence (OPFS) and the JS browser binding are planned, not shipped — see the [bindings roadmap](/bindings/overview/). ## Next Open your first database in the [tutorial](/tutorial/first-database/). ================================================================================ # What is corvid? # /docs/v0.3.1/start/what-is-corvid/ ================================================================================ corvid is an **embedded** database: a Rust library linked into your process, not a server. There is no network protocol, no connection string, no daemon. You open a file (or an in-memory instance), get a handle, and share it across threads. What makes it *multi-modal* is that the three things AI applications usually assemble from separate systems — a vector database, a full-text engine, and a metadata store — live behind **one engine and one query builder**, updated in **one transaction**: > Every secondary index reflects the same committed state as the documents, at > the same MVCC version. That invariant is the project's central commitment. It is why a hybrid query in corvid never sees a stale index, and why a filter is a true predicate rather than a post-ranking trim. ## The shape of the API There is no SQL and no JSON on the query path. Documents are typed `Value`s (maps, arrays, scalars, bytes, and first-class dense vectors), and queries are built with chained method calls: ```text filter → vector source → text source → fuse (RRF) → rerank (MMR) → order_by → offset → limit → select → run ``` Each source is a retrieval candidate generator; the builder fuses and reranks them and applies shaping. Zero sources gives a pure filter/scan query. ## What's inside | Capability | Since | |---|---| | Transactional KV storage (redb), atomic multi-op transactions | v0.1.0 | | Typed values + documents (embeddings first-class) | v0.1.0 | | Vector search (cosine / dot / L2), exact baseline + HNSW | v0.1.0 | | Full-text search (BM25) with CJK bigram tokenization | v0.1.0 | | Hybrid fusion (RRF) and diversification (MMR) | v0.1.0 | | Scalar, compound, text, geo, and on-disk indexes | v0.1.0 | | Vector quantization: binary, scalar, product (PQ) — in memory and on disk | v0.1.0 / v0.2.x | | Directed property graph (`link`/`neighbors`/`traverse`) | v0.1.0 | | Geo radius / bbox / k-nearest | v0.1.0 | | TTL, schemas with unique constraints, reactive change feeds | v0.1.0 | | Probabilistic sketches (HLL, Bloom, cuckoo, t-digest, MinHash + LSH) | v0.1.0 / v0.2.x | | Online backup, dump/load migration (format v2), compaction | v0.1.0 | | MCP sidecar (`corvid-mcp`) over stdio | v0.1.0 | | C ABI (`corvid-ffi`): 122-symbol typed cdylib + generated `corvid.h` | v0.2.0 | | Optional zstd compression and tracing instrumentation (cargo features) | v0.2.x | ## What corvid deliberately is not - **No SQL, ever.** The fluent builder is the only entrypoint; a SQL parser would drag in ANSI semantics the design does not want. - **No networking in the engine.** Replication, wire protocols, and servers are permanent non-goals. (The separate `corvid-mcp` sidecar speaks MCP over stdio — a subprocess, not a listener.) - **No embedding models.** You embed in your application (CLIP, sentence transformers, anything); corvid stores and searches the resulting vectors. - **No backward compatibility before 1.0.** A format change is migrated with [`dump`/`load`](/admin/dump-load/) — old files are refused, never silently misread. ## The projects around the engine | Crate / repo | Role | |---|---| | [`corvid`](https://github.com/corvid-db/corvid) | The engine itself (Rust library) | | `corvid-mcp` | MCP sidecar exposing a store to agentic tools | | `corvid-ffi` | The C ABI — `libcorvid.so` / `.dylib` / `corvid.dll` + `corvid.h` | | [`corvid-c`](https://github.com/corvid-db/corvid-c) | Reference C consumer (release-artifact conformance) | | [`corvid-node`](https://github.com/corvid-db/corvid-node) | Node.js binding (native, OOP API) | See [bindings](/bindings/overview/) for the full ecosystem, including planned bindings. Ready? Continue to [install](/start/install/), or jump straight into the [tutorial](/tutorial/first-database/). ================================================================================ # Your first database # /docs/v0.3.1/tutorial/first-database/ ================================================================================ This tutorial walks through opening a database, storing documents, and reading them back. Examples are Rust (the engine's native language); the same shapes exist through [the C ABI](/ffi/overview/) and [bindings](/bindings/overview/) as native classes. ## Open a database ```rust use corvid::Db; let db = Db::open("app.corvid")?; // file-backed, created if absent // let db = Db::open_in_memory()?; // ephemeral, no file # Ok::<(), corvid::Error>(()) ``` A `Db` is one embedded database file. Open it once and share it — it is `Send + Sync`; wrap it in `Arc` to share across threads. A second `Db` handle to the same file fails on the storage engine's exclusive lock, so one process, one handle, is the pattern. ## Collections and documents A **collection** is a named namespace of documents, created lazily on first write. A **document** is a typed [`Value`](/language/values/) — usually a map. A **key** is arbitrary bytes; documents sort by key. ```rust use corvid::{Db, Value}; use std::collections::BTreeMap; let db = Db::open_in_memory()?; let users = db.collection("users"); let mut u = BTreeMap::new(); u.insert("name".into(), Value::Text("ada".into())); u.insert("age".into(), Value::Int(36)); u.insert("loc".into(), Value::Array(vec![ Value::Float(51.5), Value::Float(-0.13), ])); users.insert(b"u1", &Value::Map(u))?; // insert or overwrite let got = users.get(b"u1")?; // Option let n = users.len()?; // O(1) maintained count users.delete(b"u1")?; // returns whether it existed # Ok::<(), corvid::Error>(()) ``` Names starting with `__` are reserved for the engine and rejected; names may not contain a NUL byte or an interior `__` sequence. ## Write modes | Method | Use | |---|---| | `insert(key, &doc)` | insert / full overwrite | | `insert_batch(&[(&[u8], &Value)])` | many docs in one transaction (one fsync) | | `insert_auto(&doc) -> Vec` | append under a generated ordered key | | `patch(key, &partial_map)` | merge top-level fields into an existing doc | | `update(key, \|cur\| -> Option)` | read-modify-write (return `None` to delete) | | `compare_and_set(key, expected, new)` | atomic conditional write / delete / insert-if-absent | | `delete_where(predicate)` | delete every matching doc (index-accelerated) | | `delete_batch(&[&[u8]])` | delete a set of keys | ```rust # use corvid::{Db, Value}; # use std::collections::BTreeMap; # let db = Db::open_in_memory()?; let users = db.collection("users"); # let mut m = BTreeMap::new(); m.insert("age".into(), Value::Int(36)); users.insert(b"u1", &Value::Map(m))?; // Patch: set/add fields without resending the whole document. let mut p = BTreeMap::new(); p.insert("age".into(), Value::Int(37)); users.patch(b"u1", &Value::Map(p))?; // Conditional write: only if absent. let mut v = BTreeMap::new(); v.insert("name".into(), Value::Text("grace".into())); let applied = users.compare_and_set(b"u2", None, Some(Value::Map(v)))?; // true # let _ = applied; # Ok::<(), corvid::Error>(()) ``` See [writes](/language/writes/) for the full semantics of each mode. ## Read it back ```rust # use corvid::{Db, Value}; # let db = Db::open_in_memory()?; # let docs = db.collection("docs"); // Point read let doc = docs.get(b"p1")?; // Full scan, key order (materialized) let all = docs.scan()?; // Streaming scan with early stop (bounded memory) docs.for_each_doc(|key, doc| { // ... true // return false to stop })?; # Ok::<(), corvid::Error>(()) ``` ## Persist and reopen Nothing to flush — every write is durable per-transaction. Drop the handle (or exit); the file reopens with all data, plus every [index](/indexes/overview/) and [schema](/integrity/schema/) definition: ```rust # use corvid::{Db, Value}; # let dir = tempfile::tempdir().unwrap(); # let path = dir.path().join("app.corvid"); { let db = Db::open(&path)?; db.collection("docs").insert(b"p1", &Value::Int(1))?; } // handle dropped let db = Db::open(&path)?; assert_eq!(db.collection("docs").get(b"p1")?, Some(Value::Int(1))); # Ok::<(), corvid::Error>(()) ``` ## Next Continue with the [queries tour](/tutorial/queries-tour/) — filters, vector search, and text search — then the [hybrid walkthrough](/tutorial/hybrid-walkthrough/). ================================================================================ # The hybrid query walkthrough # /docs/v0.3.1/tutorial/hybrid-walkthrough/ ================================================================================ This walkthrough builds a small hybrid-retrieval corpus and queries it the way a RAG application would: metadata filter + embedding similarity + keyword search, fused into one ranked list. It exercises most of the query builder in one continuous example. ## The scenario A notes application stores Markdown documents. Each document carries: - `title`, `body` — text - `tags` — an array of text - `source` — where it came from - `embedding` — a dense vector from your embedding model (corvid does not run models; you embed at the boundary) The retrieval question: *"among my imported notes, which ten are most relevant to a query — by meaning and by keywords — without near-duplicates dominating the page?"* ## Set up the corpus ```rust use corvid::{Db, Metric, Value, field}; use std::collections::BTreeMap; let db = Db::open("notes.corvid")?; let notes = db.collection("notes"); fn note(title: &str, body: &str, source: &str, tags: &[&str], embedding: Vec) -> Value { let mut m = BTreeMap::new(); m.insert("title".into(), Value::Text(title.into())); m.insert("body".into(), Value::Text(body.into())); m.insert("source".into(), Value::Text(source.into())); m.insert("tags".into(), Value::Array( tags.iter().map(|t| Value::Text((*t).into())).collect())); m.insert("embedding".into(), Value::Vector(embedding)); Value::Map(m) } notes.insert(b"n1", ¬e( "HNSW graphs", "Hierarchical navigable small world graphs index vectors for approximate search", "imported", &["vector", "search"], vec![0.1, 0.9, 0.2]))?; notes.insert(b"n2", ¬e( "BM25 in one page", "BM25 ranks documents by term frequency, inverse document frequency, and length", "manual", &["search", "text"], vec![0.9, 0.1, 0.4]))?; // … more documents … # Ok::<(), corvid::Error>(()) ``` ## Add indexes Exact search is the correctness baseline and fine at small scale. Past ~100k documents you want indexes; add them now so the walkthrough is realistic: ```rust # use corvid::{Db, Metric, Quantization}; # let db = Db::open_in_memory()?; let notes = db.collection("notes"); notes.create_scalar_index("source")?; // sub-linear equality filters notes.create_text_index_ondisk("body")?; // BM25 postings, bounded memory notes.create_vector_index_ondisk_quantized( "embedding", Metric::Cosine, Quantization::Scalar)?; // HNSW, ~4x smaller # Ok::<(), corvid::Error>(()) ``` You never change the query to use an index — the builder picks the most selective available index automatically and falls back to a bounded scan when none helps. See [indexes](/indexes/overview/) for choosing. ## The hybrid query ```rust # use corvid::{Db, Metric, Value, field}; # let db = Db::open_in_memory()?; let notes = db.collection("notes"); let query_vec = vec![0.12, 0.85, 0.18]; // embedding of the user's question let query_txt = "vector search index"; let rows = notes .query() .filter(field("source").eq(Value::Text("imported".into()))) .vector("embedding", query_vec, 100, Metric::Cosine) .text("body", query_txt, 100) .fuse_rrf(60.0) .rerank_mmr(0.7) .limit(10) .select(["title", "tags"]) .run()?; # let _ = rows; # Ok::<(), corvid::Error>(()) ``` What each stage does: 1. **`filter`** runs first. It is a true predicate: the candidate set for ranking is exactly the matching documents — the top-k is never computed over documents the filter would reject. Here only the `imported` notes survive (`n2`, the `manual` note, is out before ranking begins). The scalar index on `source` makes this step sub-linear. *Why `source` and not `tags`?* `contains`/`starts_with` are Text-only predicates — on the `tags` **array** they would be `false` for every document (see [filters](/language/filters/)). Filter on a scalar text field, or store tags as a single text field (`"vector search"`), if you need keyword filtering. 2. **`vector`** adds a similarity source: the 100 nearest embeddings by cosine distance among the filtered candidates. 3. **`text`** adds a BM25 source: the 100 best matches for the query terms. Because the filter ran first, BM25's statistics — document frequencies, average length — are computed over the *filtered* corpus, so a score means "relevance within the candidates the filter admits". 4. **`fuse_rrf(60.0)`** merges the two ranked lists with reciprocal-rank fusion: each document scores `Σ 1/(k + rank_i)` across sources. The default constant is `corvid::DEFAULT_RRF_K` = 60. Documents appearing high in *both* lists outrank documents that top only one — the fusion boost. 5. **`rerank_mmr(0.7)`** diversifies: maximal-marginal-relevance reranking trades a little relevance for coverage, using the query vector as the relevance anchor. `λ = 1` is pure relevance (a no-op reorder), `λ = 0` maximizes diversity. Documents without an embedding field survive the rerank (they just don't diversify). 6. **`limit` / `select`** shape the answer: ten rows, each document projected to `title` and `tags` for cheap transport. Ranking still saw the full documents. Each `ResultRow` carries `{ key, score, document }`. `score` is the fused RRF score (`0.0` for pure filter/order queries). ## Variations **No vector model yet?** Drop the `.vector(...)` line — a single text source is a normal BM25 query (served by the text index without a corpus rescan). **No text?** Drop `.text(...)` — single-source vector ranking. **Neither?** The builder degrades to a filtered scan, streamed with bounded memory. **Tighter correctness on the vector side?** Leave `.approx()` off (the default): filtered vector queries run *exact* over the matching set. Add `.approx()` to let a filtered query use the ANN index — over-fetch then filter — which is faster but may return fewer than `limit` rows when the filter is highly selective. **Understand what ran:** ```rust # use corvid::{Db, field, Value}; # let db = Db::open_in_memory()?; let notes = db.collection("notes"); let q = notes.query().filter(field("tags").exists()); println!("{}", q.explain()?); // human-readable plan let shape = q.plan_shape()?; // AnnIndex | TextIndex | IndexedWindow | SortIndex | StreamingTopK | Scan let plan = q.plan()?; // hashable QueryPlan — key a PlanCache on it # let _ = (shape, plan); # Ok::<(), corvid::Error>(()) ``` ## Where to go next - [The query builder](/language/query-builder/) — every knob and its exact semantics. - [Equality semantics](/language/equality/) — the per-construct rules (predicates vs storage equality vs unique constraints). - [Indexes](/indexes/overview/) — which index serves which query shape. - [Performance](/performance/overview/) — the measured numbers behind these defaults. ================================================================================ # A tour of queries # /docs/v0.3.1/tutorial/queries-tour/ ================================================================================ corvid reads through one fluent builder. This tour touches each capability singly; the [hybrid walkthrough](/tutorial/hybrid-walkthrough/) composes them. ## Filters Predicates are built with `field(path)` and evaluate against dotted paths in the document: ```rust 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](/language/filters/) and [equality](/language/equality/) pages. ## Vector search Vectors are first-class document values (`Value::Vector`, dense `f32`). Without an index, search is exact — brute-force with a bounded heap, streamed: ```rust # 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: { 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](/indexes/vector/) switches `vector_search` to HNSW transparently; `Hit.approximate` tells you which path served the answer. ## Text search BM25 ranking over an analyzer that lowercases, drops common English stop words, and applies a conservative plural stemmer (`dog` matches `dogs`): ```rust # 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 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](/fts/overview/). ## The builder, in one shape ```rust # 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 { 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. ## Aggregations Over the filtered set (filters and indexes still apply): ```rust # 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 sales.query().min("amount")?; sales.query().max("amount")?; // Option sales.query().count_distinct("region")?; // usize sales.query().group_count("region")?; // BTreeMap sales.query().group_sum("region", "amount")?; // BTreeMap 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](/language/aggregations/). ## Pagination Keyset (cursor) pagination — no offset rescans: ```rust # use corvid::{Db, field, Value}; # let db = Db::open_in_memory()?; let docs = db.collection("docs"); let mut after: Option> = 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>(()) ``` ## Graph, geo, joins Three more read paths, each with a dedicated section: ```rust # 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](/graph/overview/): link/unlink/neighbors/traverse, cascade semantics. - [Geo](/geo/overview/): radius/bbox/nearest, antimeridian handling. - [Joins](/language/joins/): foreign-key resolution across collections. Next: put it all together in the [hybrid walkthrough](/tutorial/hybrid-walkthrough/). ================================================================================ # Aggregations # /docs/v0.3.1/language/aggregations/ ================================================================================ Aggregations execute against the **filtered set** on one read snapshot. Retrieval sources (`.vector`/`.text`), ranking knobs, and `limit`/`offset`/`select` are ignored — an aggregate measures the filtered collection. Filters and indexes still apply, and indexed vs scan paths give identical answers. ```rust # use corvid::{Db, field, Value}; # let db = Db::open_in_memory()?; let sales = db.collection("sales"); let q = || sales.query().filter(field("region").eq(Value::Text("eu".into()))); q().count()?; // usize — O(1) when unfiltered q().sum("amount")?; // f64 q().avg("amount")?; // Option q().min("amount")?; // Option q().max("amount")?; // Option q().count_distinct("region")?; // usize q().group_count("region")?; // BTreeMap q().group_sum("region", "amount")?; // BTreeMap q().group_avg("region", "amount")?; // BTreeMap # Ok::<(), corvid::Error>(()) ``` Ranking arguments are validated before aggregating (a garbage RRF k or MMR lambda fails with `Error::InvalidArgument` even though ranking is unused). ## Per-aggregate semantics **`count`** — number of matching documents. Without a filter it is the O(1) maintained collection counter. **`sum(field)`** — sums numeric (`Int`/`Float`) values; missing paths and non-numeric values are skipped; all-missing yields `0.0`. NaN members poison the sum; infinities follow IEEE arithmetic. Ints round through f64 beyond 2^53. **`avg(field)`** — mean over the numeric values that were present; `None` when there were none. A NaN member poisons the mean. **`min` / `max(field)`** — smallest/largest *comparable* value (numbers interoperate; text compares lexicographically), returned as the stored `Value`. Incomparable-only fields yield `None`; mixed kinds pin the first comparable kind's winner (numbers beat texts in cross-kind min/max). **`count_distinct(field)`** — distinct scalar values by the canonical [group key](#group-keys): distinct types stay distinct; missing and container values are ignored. **`group_count` / `group_sum` / `group_avg`** — one bucket per distinct value of the group field; counts are exact, sums/averages follow the `sum`/`avg` rules within the bucket. Buckets with no numeric members are absent from `group_sum`/`group_avg`. Grouping respects filters, and — like every aggregate — runs on one snapshot. ## Group keys `group_*` and `count_distinct` key buckets by a canonical type-tagged form so that distinct kinds never collide: | Field value | Group key | |---|---| | `Text("blog")` | `blog` (bare) | | `Int(1)` / `Float(1.5)` / `Bool(true)` | `i:1` / `f:1.5` / `b:true` | | Text that would look tagged (`"i:1"`) | `t:i:1` (escaped) | Consequences visible in results: `1` (Int) and `1.0` (Float) are **distinct** buckets (the tags differ), `0.0` and `-0.0` share a bucket, NaN forms one `f:NaN` bucket, and a text value that happens to look like a tag is escaped, never conflated. The engine's `BTreeMap` gives the iteration order: ascending group-key byte order (the C ABI's group iterator preserves it). ## Approximate distinct `Collection::approx_distinct(field)` estimates a field's distinct count with HyperLogLog — sub-linear memory on large corpora, exact on small counts and duplicate-heavy inputs (see [sketches](/language/sketches/)). Next: [equality semantics](/language/equality/). ================================================================================ # Data model # /docs/v0.3.1/language/data-model/ ================================================================================ corvid's data model is deliberately small: one database, named collections of documents, byte keys, and typed values. Everything else — indexes, graph edges, TTL, schemas — is derived from or attached to that core. ## The `Db` A [`Db`](https://corvid-db.github.io/corvid/api/corvid/struct.Db.html) is one embedded database file (or an in-memory instance): ```rust use corvid::Db; let db = Db::open("app.corvid")?; // file-backed; created if absent let db = Db::open_in_memory()?; // ephemeral ``` - Open it **once** and share it. `Db` is `Send + Sync`; wrap in `Arc` for threads. A second handle to the same file fails on the storage engine's exclusive lock. - Writes are durable per-transaction — there is no explicit flush or close. - The concurrency model is **single writer, concurrent readers** (MVCC): writes serialize database-wide; queries run against point-in-time snapshots. ## Collections A **collection** is a named namespace of documents: ```rust let docs = db.collection("docs"); ``` - Created lazily on first write — `collection()` itself never fails for name reasons; invalid names surface at write time with the exact error. - Names must not start with `__` (engine-reserved, `Error::ReservedCollection`), contain an interior `__` sequence, or contain a NUL byte (`Error::InvalidName`). The empty name is legal. - `db.collections()` lists user collection names (engine namespaces excluded), in name order. A collection that was never written may not appear — creation is lazy. ## Keys A **key** is arbitrary bytes (`&[u8]`) up to any length the storage engine accepts; documents are ordered by key byte order. The empty key is legal and sorts first. `insert_auto` generates zero-padded 20-digit monotonically increasing keys per collection. ## Documents A **document** is a [`Value`](/language/values/) — usually a `Value::Map`. Documents are schemaless by default: different documents in one collection may have different shapes. An optional [schema](/integrity/schema/) enforces field types, required fields, and unique constraints on write. Field paths in filters, index definitions, and `select` are **dotted** and traverse nested maps: `"meta.author"` resolves `doc["meta"]["author"]`. Paths traverse maps only — arrays are never indexed into by path. ## The derived-index invariant The design commitment that shapes everything else: > Every secondary index reflects the same committed state as the documents, > at the same MVCC version. Consequences you can rely on: - **Indexes are never stale at query time.** Index maintenance happens inside the write transaction that changes the documents. A query never sees a document set and an index that disagree. - **Documents are the source of truth.** An index definition is derived state: re-creating an index rebuilds it from the documents, and a corrupt on-disk index errors loudly (`Error::CorruptIndex`) instead of silently serving empty results. - **Query results never depend on which indexes exist.** The builder picks whatever index is most selective, and verifies candidates against the exact predicate. `explain()` tells you which path served a query; the rows are the same either way. ## Where state lives | Kind of state | Where | |---|---| | Documents | user collections | | Index definitions + on-disk index state | engine-reserved `__` namespaces | | Graph edges | reserved edge namespaces (derived adjacency, mirrored) | | TTL expiries | reserved TTL namespaces | | Schemas | reserved schema namespaces | All of it persists across reopen; [dump/load](/admin/dump-load/) carries documents, definitions, edges, TTL, schemas and auto-id counters as one logical, version-stamped stream. Next: the [`Value` type](/language/values/) and [writing documents](/language/writes/). ================================================================================ # Equality semantics # /docs/v0.3.1/language/equality/ ================================================================================ "Equal" means different things in different places, on purpose. The engine pins each rule with conformance tests; this page is the consolidated table. ## The per-construct table | Construct | Equality rule | |---|---| | **Predicates** (`eq`/`ne`, ordered ops, `is_in`, `between`) | Typed total-order comparison: NaN never equals anything (not even NaN); `Int(2)` equals `Float(2.0)` numerically — mixed comparisons convert the integer through f64, exact up to 2^53. | | **`compare_and_set` expected value** | Semantic value equality: NaN == NaN regardless of payload, −0.0 == 0.0, containers element-wise. | | **Unique constraints** | Storage-level semantic equality (NaN == NaN), enforced per field value on write. | | **Joins** | An `Int` foreign key matches a `Text` key via its decimal-string encoding: `Int(7)` joins to the key `"7"`. | | **Group keys** (`group_count`/`group_sum`/`group_avg`, `count_distinct`) | Type-tagged canonical keys: bare for text (`blog`), `i:`/`f:`/`b:` tags for non-text, `t:` escape for text that would look tagged — distinct types stay distinct. | ## The NaN duality Two rules coexist by design: - **Predicate comparisons** — NaN matches nothing, not even NaN. A NaN filter value selects the empty set under `eq` and ordered operators; `ne` selects everything else. This keeps filter semantics a clean total order without an NaN special case. - **Storage equality** — `compare_and_set` expectations and unique constraints treat NaN as equal to NaN (payload-agnostic) and −0.0 equal to 0.0. These constructs answer "is the stored value *this* value?", where rejecting NaN would make NaN-valued fields (common in float pipelines) unusable. ## Worked examples ```rust # use corvid::{Db, Value, field}; # use std::collections::BTreeMap; # let db = Db::open_in_memory()?; let c = db.collection("t"); // field("v") traverses maps, so each doc carries its value under "v" let doc = |v: Value| { let mut m = BTreeMap::new(); m.insert("v".to_string(), v); Value::Map(m) }; // Predicate: NaN selects nothing, ne selects the rest c.insert(b"a", &doc(Value::Float(f64::NAN)))?; c.insert(b"b", &doc(Value::Int(2)))?; c.insert(b"c", &doc(Value::Float(2.0)))?; c.query().filter(field("v").eq(Value::Float(f64::NAN))).count()?; // 0 c.query().filter(field("v").eq(Value::Int(2))).count()?; // 2 — numeric interop c.query().filter(field("v").ne(Value::Int(2))).count()?; // 1 (the NaN doc) // Storage equality: NaN matches NaN in compare_and_set, element-wise // inside containers too — this deletes doc "a" let matched = c.compare_and_set(b"a", Some(&doc(Value::Float(f64::NAN))), None)?; // true # let _ = matched; # Ok::<(), corvid::Error>(()) ``` Unique constraints make NaN==NaN visible too: a second document with NaN in a `unique` Float field is rejected with `Error::SchemaViolation`, whether or not the payloads differ. ## Why joins get a rule of their own Join foreign keys are *document values*; the target is a *byte key*. Text foreign keys compare as bytes; an `Int` foreign key encodes through its decimal string (`Int(7)` → `"7"`) so integer ids join text keys naturally. Unusable shapes (containers, vectors, floats) retain the row with a `None` right side — a lookup join never errors on data shape, it misses. Next: [ordering rules](/language/ordering/). ================================================================================ # Filters # /docs/v0.3.1/language/filters/ ================================================================================ Filters are pure predicates: `Predicate` trees built with the `field(path)` fluent API and evaluated against documents. They compose into [queries](/language/query-builder/), `delete_where`, `page_where`, and the C ABI's predicate family. ## Building predicates ```rust use corvid::{field, Value}; // Comparisons field("category").eq(Value::Text("blog".into())); field("category").ne(Value::Text("draft".into())); field("score").lt(Value::Int(5)); field("score").le(Value::Int(5)); field("score").gt(Value::Int(5)); field("score").ge(Value::Int(5)); // Membership and ranges field("tag").is_in([Value::Text("a".into()), Value::Text("b".into())]); field("score").between(Value::Int(1), Value::Int(10)); // inclusive both ends // Text field("title").starts_with("intro"); field("body").contains("rust"); // Presence field("email").exists(); // Geo (haversine kilometres) field("loc").within_km(51.5, -0.13, 25.0); # let _ = (); ``` ## Composition ```rust # use corvid::{field, Value}; 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)); // Not # let _ = p; ``` `and`/`or`/`not` build the tree; De Morgan identities hold and nesting is arbitrary. Multiple `.filter(...)` calls on one query intersect like `and`. ## Evaluation semantics - **Missing path ⇒ false** for every predicate except `exists()` — including `ne`: "not equal to X" never matches a document that lacks the field (pinned by conformance: *Ne on a MISSING path is FALSE*). Use `exists()` composed with `or` when you want missing values to pass. - **Ordered comparisons across non-comparable kinds ⇒ false.** Numbers compare numerically across `Int`/`Float` (exact to 2^53); text compares lexicographically by UTF-8 bytes; bools, bytes, containers and vectors do not participate in `<`/`>`/`<=`/`>=`. - **`eq` matches per value kind**, with numeric interop: `field("n").eq(Value::Float(2.0))` matches `Int(2)`. - **`ne` is the complement of `eq`** evaluated over a **present** value — it is `true` only for a document that carries the field with a non-matching value (including any value against a NaN filter); the missing-path rule above applies first, so a document without the field still yields `false`. - **NaN matches nothing, not even NaN** — a NaN filter value selects the empty set under `eq` and ordered ops, and every *present* value under `ne` (documents missing the field still miss — see above). (This is the predicate rule; storage equality — CAS, unique constraints — treats NaN as equal to NaN. See [equality semantics](/language/equality/).) - **`is_in`** is an OR over `eq` against each element; an empty list matches nothing. **`between(lo, hi)`** is `lo <= v && v <= hi`, inclusive; a degenerate `lo > hi` range simply matches nothing. - **`starts_with` / `contains`** are byte-level text predicates — false on non-text values and missing paths, case-sensitive. - **`within_km(lat, lon, km)`** resolves the path as a geo point (`[lat, lon]` array or `{lat, lon}` map), false on non-points. ## Index serviceability Filters never *require* an index, but each predicate family has a sub-linear path when one exists: | Predicate | Serviced by | |---|---| | `eq` / `ne`* / `lt` / `le` / `gt` / `ge` | [scalar index](/indexes/scalar/) | | `eq` prefix + trailing range across fields | [compound index](/indexes/scalar/#compound-indexes) | | `is_in` | scalar index (union of windows, capped) | | `between` | scalar index (range window) | | `starts_with` | scalar index (text prefix scan) | | `within_km` | [geo index](/indexes/geo/) | | top-level `or` of index-serviceable disjuncts | index union | *`ne` is not serviced (an anti-scan is not sub-linear) — it verifies on the scan path. The builder probes every serviceable index (each capped) and drives the query on the smallest candidate set, then verifies every candidate against the exact predicate — so results are identical with or without indexes, and unselective predicates fall back to the bounded streaming scan. `explain()` reports which happened (`IndexedWindow` vs `Scan`). Next: the [query builder](/language/query-builder/). ================================================================================ # Joins # /docs/v0.3.1/language/joins/ ================================================================================ `Collection::join(right, fk_field)` is a left-outer lookup join: for every document in the left collection, resolve `fk_field` against the **keys** of the right collection. ```rust # use corvid::{Db, Value}; # let db = Db::open_in_memory()?; # let orders = db.collection("orders"); # let mut o = std::collections::BTreeMap::new(); # o.insert("customer_id".into(), Value::Text("c1".into())); # orders.insert(b"o1", &Value::Map(o))?; let rows = orders.join("customers", "customer_id")?; // Vec # let _ = rows; # Ok::<(), corvid::Error>(()) ``` ## Semantics - **Left-outer**: every left row is retained. A missing foreign-key field, a dangling reference, or an unusable key shape keeps the row with `right = None` — a join never errors on data shape. - Rows follow **left-collection key order**. - The foreign key may be a **dotted path** (`"meta.customer.id"` resolves nested maps). - **Key-kind matching** (see [equality](/language/equality/)): a `Text` foreign key compares against key bytes; a `Bytes` foreign key against raw bytes; an `Int` foreign key joins the text key of its decimal encoding (`Int(7)` → key `"7"`). Containers/floats/vectors are unusable and miss. - Non-map left documents are retained with `right = None`. - **Self-joins** work (`orders.join("orders", "parent_id")`) — the right side is simply the same collection's keys. - An unknown right collection or empty right side yields all rows with `right = None`. - One MVCC snapshot covers the join — both sides read one committed point in time, and later mutations of either side are visible to subsequent joins. ## `JoinRow` ```rust pub struct JoinRow { pub left_key: Vec, pub left: Value, // the left document pub right_key: Option>, pub right: Option, // resolved right document, or None } ``` This is a lookup join, not a relational algebra engine — no join predicates, no multi-field keys, no inner-join-only mode. Filter and project around it with the [query builder](/language/query-builder/) by post-processing rows, or model the relation as [graph edges](/graph/overview/) when you need traversal rather than resolution. Next: [sketches](/language/sketches/). ================================================================================ # Ordering rules # /docs/v0.3.1/language/ordering/ ================================================================================ `order_by(field, descending)` sorts rows by a document field instead of by rank. The contract is a fixed **class order** with a pinned total order inside each class, so mixed-shape fields sort deterministically instead of panicking or interleaving by key. ## The class order ```text 1. comparable values (in value order) 2. incomparable values (bools, containers, vectors, NaN — kind tag first, then key) 3. rows missing the field (stable by key) ``` Ties inside a class break by key. Within the incomparable class, a **kind tag** orders the values first — NaN is a numeric kind, so it precedes the other incomparable kinds (bools, containers, vectors), which then fall to key order among themselves. `descending` reverses the within-class order — kind tag and value together — in **both** the comparable and the incomparable class; the class order (comparable < incomparable < missing) and the key tiebreak are fixed, so incomparable and missing values always sort last. ## The comparable class - **Numbers** — `Int` and `Float` interoperate numerically (i64 converts through f64, exact up to 2^53; larger ints share an f64 encoding and tie, then break by key). NaN is *not* comparable: it sorts with the incomparable class, never among numbers. - **Text** — lexicographic by UTF-8 bytes. - **Across kinds** — numbers sort before texts (a kind tag orders the cross-kind pairs). This closes a historical hole: key-order fallback for cross-kind pairs was not a total order and could construct sort cycles. Bytes, bools, arrays, maps, and vectors are incomparable and occupy class 2 — after every number and text, before the missing rows. ## Interaction with the rest of the builder - `offset`/`limit` apply **after** ordering (a window over the sorted rows). - Filters order only the matching rows. - A filterless `order_by` whose field carries a complete [scalar index](/indexes/scalar/) is served by an index order walk (`PlanShape::SortIndex`): documents are fetched only for the `offset + limit` window; incomparable/missing rows are appended by an on-exhaustion tail scan. Results are identical to the materialize-and-sort path by construction — measured ~9× faster ascending / ~2.7× descending on a 5k corpus with `limit 20`. - With retrieval sources present, `order_by` re-sorts the fused candidates (rank order applies only when `order_by` is absent). ## Example ```rust # use corvid::{Db, Value}; # let db = Db::open_in_memory()?; let c = db.collection("t"); c.insert(b"a", &Value::Map([("v".to_string(), Value::Text("x".into()))].into()))?; c.insert(b"b", &Value::Map([("v".to_string(), Value::Int(10))].into()))?; c.insert(b"c", &Value::Map([("v".to_string(), Value::Int(2))].into()))?; c.insert(b"d", &Value::Map([("v".to_string(), Value::Bool(true))].into()))?; c.insert(b"e", &Value::Int(0))?; // missing "v" entirely // order_by("v", ascending) yields keys: c (2), b (10), a ("x"), d (bool), e (missing) let rows = c.query().order_by("v", false).run()?; # let _ = rows; # Ok::<(), corvid::Error>(()) ``` Descending yields `a` (text), then `b`, `c` (numbers reversed), then `d`, `e` unchanged at the tail — classes keep their order. Next: [pagination](/language/pagination/). ================================================================================ # Pagination # /docs/v0.3.1/language/pagination/ ================================================================================ For walking large result sets, corvid offers **keyset (cursor) pagination**: each page returns rows plus an opaque cursor that resumes strictly after the last key served. No offset rescan, bounded memory, streamed. ## `page` ```rust # use corvid::{Db, Value}; # let db = Db::open_in_memory()?; let c = db.collection("docs"); let mut after: Option> = None; loop { let page = c.page(after.as_deref(), 100)?; for (key, doc) in &page.rows { /* ... */ let _ = (key, doc); } match page.next { Some(cursor) => after = Some(cursor), None => break, // short page = end of collection } } # Ok::<(), corvid::Error>(()) ``` - Rows are in **key order**, from one MVCC snapshot covering the whole chunked walk. - `after = None` (or empty bytes... see below) starts at the beginning. - `Page.next` is `Some(cursor)` iff the page was full — a full page always yields a cursor; a short page means end. - `limit 0` returns an empty page and no cursor. Cursor corner cases, pinned by tests: `after = b""` skips exactly the empty key (the empty key is a legal document key and sorts first). Cursors are opaque byte strings — treat them as tokens (the C ABI returns the buffer for `corvid_free`). ## `page_where` The same walk with a predicate — only matching documents are served, the cursor still advances by key: ```rust # use corvid::{Db, Value, field}; # let db = Db::open_in_memory()?; let c = db.collection("docs"); let page = c.page_where(None, 50, field("category").eq(Value::Text("blog".into())))?; # let _ = page; # Ok::<(), corvid::Error>(()) ``` Filtered pagination composes from `query().filter()` + `offset`/`limit` too; `page_where` gives you the constant-memory cursor form. (The C ABI exposes `page` only in v1 — see the [exclusions](/ffi/stability/).) ## Snapshot semantics Each `page` call opens its **own** read snapshot and runs the entire chunked walk inside it: the returned rows always match some committed point in time, even while writers are active. Successive pages see the then-current state — a walk across concurrent writes is per-page consistent, not walk-consistent. The snapshot-holding cost is space, not latency: freed pages from commits landing during a walk stay in the file until the walk ends (bounded by `limit` rows and 1024-key chunk reads). ## Cursors vs `offset` - `offset` on the query builder is O(offset + limit) — it walks and discards. Fine for shallow pages, dashboards, small windows. - Cursors are O(limit) per page regardless of depth — the shape for exporting a collection, background syncs, and feeding pipelines. - Ranked (multi-source fused) result sets do not have a cursor in v0.2 — fusion materializes the candidate set. Filter-only, order-only, and single-source ranked paths are bounded/streamed. Next: [joins](/language/joins/). ================================================================================ # The query builder # /docs/v0.3.1/language/query-builder/ ================================================================================ One chained call composes filtering, vector search, text search, fusion, and reranking. The builder is the only query entrypoint — there is no SQL and no string-formatted query language. ```rust # 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) .text("body", "rust embedded database", 100) .fuse_rrf(60.0) .rerank_mmr(0.7) .offset(0) .limit(10) .select(["title", "meta.author"]) .run()?; // Vec { key, score, document } # let _ = rows; # Ok::<(), corvid::Error>(()) ``` Execution order is fixed regardless of call order: ```text filter (predicate) → sources (vector/text candidates among matches) → fusion (RRF) → rerank (MMR) → order_by → offset → limit → select ``` ## `filter(predicate)` Adds a [predicate](/language/filters/); multiple calls intersect (`and`). The filter is a **true predicate**: it runs before ranking, so the top-k is computed among matching documents only — never top-k-then-trim. ## `vector(field, query, k, metric)` Adds a vector-similarity source: the `k` nearest values of `field` to `query`. Metrics: `Metric::Cosine`, `Metric::Dot`, `Metric::L2` (squared). Documents missing the field or carrying a different dimension are skipped. With a [vector index](/indexes/vector/) on the field, the source is served by HNSW; otherwise it is an exact bounded scan. Zero-norm vectors are maximally distant under cosine/dot and rank last. ## `text(field, query, k)` Adds a BM25 text source: the `k` best matches for the analyzed query terms. With a [text index](/indexes/text/) the source reads only query-term postings; without one it scores an exact pass. When a filter is present, BM25 statistics are computed over the **filtered** corpus — a score always means "relevance within the candidate set the filter admits". ## `fuse_rrf(k)` Reciprocal-rank fusion across sources: each document scores `Σ_sources 1 / (k + rank)`. Default `corvid::DEFAULT_RRF_K` = 60. `k` must be positive and non-NaN (validated at `run()`, `Error::InvalidArgument`). With one source, fusion is identity; with none, the query is a pure filter/shape query. ## `rerank_mmr(lambda)` Maximal-marginal-relevance diversification over the fused ranking, anchored on the **first vector source's** query (with several vector sources, the earliest `.vector(...)` call supplies the relevance vector). `lambda ∈ [0, 1]` (1 = pure relevance order, 0 = maximal diversity; validated at `run()`). Requires a vector source — without one it is a no-op. Documents without an embedding survive the rerank unchanged. ## `approx()` Opts a **filtered** vector query into the ANN index: fetch the index's top-k first, apply the filter after. Without `.approx()` (default), filtered vector queries run exact over the matching set. Trade: `.approx()` is faster on large collections but a highly selective filter may return fewer than `limit` rows. Unfiltered vector sources always use the index when present. ## `order_by(field, descending)` Sort by a document field **instead of** rank (rank ordering applies when `order_by` is absent). There is **no special `score` field**: `order_by("score", true)` orders by a literal document field named `score` — which documents rarely carry, so every row lands in the missing class, sorted by key. To get rank order, simply omit `order_by`; the fused RRF score rides along on every `ResultRow` if you want to re-sort client-side. Ordering follows the class rules on [ordering](/language/ordering/): comparable values (numbers numerically — numbers before texts across kinds — texts lexically) first; incomparable values after; rows missing the field last; ties by key. `descending` reverses within-class order only. A filterless `order_by` over a complete scalar index is served by an index order walk (`PlanShape::SortIndex`) — documents are fetched only for the `offset + limit` window. ## `offset(n)` / `limit(n)` Windowing applies after ordering, before `select`. `limit 0` yields an empty result. `offset` paginates but is O(offset) — for heavy pagination prefer keyset [cursors](/language/pagination/). ## `select(fields)` Projects each returned document to the listed top-level (dotted) fields. Missing fields are omitted (not null); duplicates collapse; a non-map document passes through unchanged. Ranking and filtering always see the full document — `select` only shapes the output. ## `run()` Executes and returns `Vec` (`{ key, score, document }`). `score` is the fused RRF score (`f32`); `0.0` for pure filter/order queries. One MVCC snapshot covers the whole query — the result set matches one committed point in time. Ranking arguments (RRF k, MMR lambda, BM25 params) are validated here. ## `explain()`, `plan()`, `plan_shape()` ```rust # use corvid::{Db, field}; # let db = Db::open_in_memory()?; let docs = db.collection("docs"); let mut q = docs.query().filter(field("a").exists()); q.explain()?; // String — human-readable plan, pinned to the real decision q.plan_shape()?; // PlanShape (advisory) q.plan()?; // QueryPlan — canonical, identity-hashable # Ok::<(), corvid::Error>(()) ``` `PlanShape` variants: `AnnIndex` (single vector source on an indexed field), `TextIndex` (single text source on an indexed field), `IndexedWindow` (a scalar/compound/geo/or index drives the filter), `SortIndex` (order walk), `StreamingTopK` (bounded ranked pass without an index), `Scan` (streaming filter/shape pass). `QueryPlan` is equal iff the query shape is equal — key a `PlanCache` on it to cache prepared per-shape work (never results). ## Aggregations on the builder `count`, `count_distinct`, `sum`, `avg`, `min`, `max`, `group_count`, `group_sum`, `group_avg` execute against the filtered set on one snapshot and **ignore** sources, ranking, and `limit`/`offset`/`select` — see [aggregations](/language/aggregations/). Next: [aggregations](/language/aggregations/). ================================================================================ # Semantic cache # /docs/v0.3.1/language/semantic-cache/ ================================================================================ The semantic cache answers *"have I already answered a question that is semantically close to this one?"* — a nearest-embedding lookup within a threshold, over an ordinary collection. ```rust # use corvid::{Db, Metric, Value}; # let db = Db::open_in_memory()?; let cache = db.collection("llm_cache") .semantic_cache("embedding", "answer", Metric::Cosine, 0.95); cache.put(b"q1", vec![0.1, 0.9], Value::Text("the answer".into()))?; let hit = cache.get(&[0.1, 0.89])?; // Some(value) — within threshold of q1 let miss = cache.get(&[0.9, 0.1])?; // None — nothing close enough # let _ = (hit, miss); # Ok::<(), corvid::Error>(()) ``` ## How it works `semantic_cache(embedding_field, value_field, metric, threshold)` wraps a collection whose documents carry an embedding under `embedding_field` and the cached payload under `value_field`: - **`put(key, embedding, value)`** stores `{embedding, value}` at `key`. - **`get(query)`** finds the nearest stored embedding by `metric` and returns its value iff the distance is **≤ threshold**; otherwise `None`. Threshold units follow the metric: cosine thresholds live in `[0, 2]` (distance, not similarity — 0.05 means "very close"); L2 thresholds are squared distance. Distances are exact metric distances — `vector_search` reranks ANN hits with exact distances, so a quantized index does not distort threshold comparisons. ## Design notes - The cache is built on the collection's own machinery: create a [vector index](/indexes/vector/) on the embedding field and lookups are ANN-served; without one they are exact scans. Semantics are identical. - It is a cache *pattern*, not a TTL'd store — pair it with [TTL](/integrity/ttl/) on the backing collection when entries should expire. - Tracing subscribers see `semantic_cache_hit` / `semantic_cache_miss` events with the deciding distance when the `tracing` feature is on (see [observability](/admin/observability/)). Next: [TTL and expiry](/integrity/ttl/). ================================================================================ # Probabilistic sketches # /docs/v0.3.1/language/sketches/ ================================================================================ corvid ships six probabilistic data structures with one shared posture: **deterministic, zero dependencies** — std's `DefaultHasher`, no `rand` — so identical inputs produce identical sketches. Use them for cardinality, membership, quantiles, and set similarity at sub-linear memory. ## HyperLogLog — approximate distinct count ```rust use corvid::HyperLogLog; let mut hll = HyperLogLog::new(); // or with_precision(bits) hll.add_bytes(b"user-1"); hll.add_hash(h); // precomputed-hash twin let approx_unique = hll.estimate(); // f64 ``` Precision clamps to a sane range; duplicates are ignored; small counts are near-exact. `Collection::approx_distinct(field)` applies it to a field of a collection in one call. ## BloomFilter — membership, no deletions ```rust use corvid::BloomFilter; let mut bloom = BloomFilter::new(10_000, 0.01); // expected items, fp rate bloom.add_bytes(b"seen"); let maybe = bloom.contains_bytes(b"seen"); // true (never a false negative) ``` No false negatives for admitted items; false positives bounded by the configured rate. ## CuckooFilter — membership **with deletion** ```rust use corvid::CuckooFilter; let mut cuckoo = CuckooFilter::new(10_000, 0.01); cuckoo.add_bytes(b"session-7"); assert!(cuckoo.contains_bytes(b"session-7")); cuckoo.delete_bytes(b"session-7"); // really gone — only delete what you added ``` Deletion is the differentiator vs Bloom. One deliberate divergence from the paper, pinned by conformance tests: when the table exhausts its displacement budget, `add_bytes` returns `false` and the whole eviction chain **rolls back** — a rejected insert leaves the filter byte-identical (the paper's variant silently drops a previously admitted item). `false` therefore means "the filter is full", and older items stay admitted. ## TDigest — streaming quantiles ```rust use corvid::TDigest; let mut td = TDigest::new(100.0); // compression for latency in [12.0, 45.0, 51.0, 80.0] { td.add(latency); } let p99 = td.quantile(0.99); // Option; 0.0/1.0 exact min/max let cdf = td.cdf(50.0); // monotone let merged = TDigest::merge(&[td, other]); // merge algebra ``` NaN and ±infinity observations are rejected. Deterministic given a fixed add/merge history. ## MinHash + LshIndex — set similarity and candidate lookup ```rust use corvid::{MinHash, LshIndex}; let mh = MinHash::new(64); let sig_a = mh.signature(&[b"tag:x", b"tag:y", b"tag:z"]); let sig_b = mh.signature(&[b"tag:y", b"tag:z", b"tag:w"]); let similarity = MinHash::jaccard_estimate(&sig_a, &sig_b); // ~0.5 let mut lsh = LshIndex::new(16, 4); // bands × rows = signature length lsh.insert(b"doc-a", &sig_a); let similar = lsh.candidates(&sig_b); // keys sharing a full band ``` `jaccard_estimate` is exactly `1.0` for identical sets, `0.0` for disjoint ones, `None` on length mismatch. Banding trades recall for precision along the `1 − (1 − J^rows)^bands` curve. ## When to use what | Question | Tool | |---|---| | "How many distinct values?" | `HyperLogLog` / `approx_distinct` | | "Have I ever seen X?" (no deletes) | `BloomFilter` | | "Have I seen X, and can I forget it?" | `CuckooFilter` | | "What's the p99 of this stream?" | `TDigest` | | "Which sets look similar?" | `MinHash` + `LshIndex` | The sketches are host-side structures — they live outside the database file (not persisted), which is why the C ABI excludes them from v1 (see [ABI exclusions](/ffi/stability/)). Next: the [semantic cache](/language/semantic-cache/). ================================================================================ # Values # /docs/v0.3.1/language/values/ ================================================================================ `Value` is the document and field type — the unit of storage, filtering, and encoding: ```text Null · Bool(bool) · Int(i64) · Float(f64) · Text(String) Bytes(Vec) · Array(Vec) · Map(BTreeMap) Vector(Vec) // a dense embedding — first-class ``` Notes on individual kinds: - **`Int`** is a signed 64-bit integer. **`Float`** is IEEE-754 f64: NaN, ±infinity, and −0.0 are preserved bit-exact through storage. - **`Int` and `Float` interoperate numerically** in filters and comparisons: `Int(2)` equals `Float(2.0)`, exact up to 2^53 (beyond that, an i64 may round through f64 — see [equality](/language/equality/)). - **`Text`** must be valid UTF-8 (Rust `String`); **`Bytes`** are opaque. Ordering of text is lexicographic by UTF-8 bytes. - **`Map`** keys are strings; iteration order is sorted by key — construction order never matters for equality or encoding. Documents are usually maps. - **`Vector`** is a dense `f32` embedding. It is a storage/search kind, not a math library: no arithmetic is exposed on it. First-class means filters can test it (`eq` — element-wise), [unique constraints](/integrity/schema/) can police it, and it can serve as a vector-search field. ## Accessors Typed reads return `Option` — a wrong type is `None`, never an error: ```rust # use corvid::{Db, Value}; # let db = Db::open_in_memory()?; # let c = db.collection("docs"); # let mut m = std::collections::BTreeMap::new(); # m.insert("age".into(), Value::Int(36)); # m.insert("name".into(), Value::Text("ada".into())); # c.insert(b"u1", &Value::Map(m))?; let doc = c.get(b"u1")?.unwrap(); let age: Option = doc.as_int(); let name: Option<&str> = doc.as_text(); let flag: Option = doc.as_bool(); let raw: Option<&[u8]> = doc.as_bytes(); let vec: Option<&[f32]> = doc.as_vector(); # let _ = (age, name, flag, raw, vec); # Ok::<(), corvid::Error>(()) ``` `as_float` returns `Some` **only** for a `Float` — unlike comparisons, it does not widen an `Int` (use `as_int` and convert yourself when you need that). ## Dotted paths `Value::get_path` walks nested maps by dotted path: ```rust # use corvid::{Db, Value}; # let db = Db::open_in_memory()?; # let mut inner = std::collections::BTreeMap::new(); # inner.insert("author".into(), Value::Text("ada".into())); # let mut doc = std::collections::BTreeMap::new(); # doc.insert("meta".into(), Value::Map(inner)); # let v = Value::Map(doc); v.get_path("meta.author") // Some(Text("ada")) # ; # Ok::<(), corvid::Error>(()) ``` The same dotted paths work in **filters** (`field("meta.score").gt(...)`), **index definitions** (`create_scalar_index("meta.score")`), and **`select` projection**. Paths traverse maps only; an empty path resolves nothing. ## Encoding `Value::encode` / `Value::decode` implement the deterministic on-disk codec — a tag/length format where tags 0..8 are the value kinds (the C ABI's `corvid_value_type` mirrors them exactly). Nesting is bounded (`value::MAX_NESTING`) and enforced on decode. You rarely touch the codec directly; it is what `dump`/`load` and storage use. Every value variant round-trips byte-exact — including NaN payloads, −0.0, and empty containers. ## Containers as documents Any `Value` can be stored as a document — a bare `Int`, a text blob, an array. Map documents are the common case because dotted paths, `patch`, `select` projection, and schemas are map-shaped operations; non-map documents pass through queries and scans unchanged (`select` returns them as-is). Next: [writes](/language/writes/) — every way to mutate a collection. ================================================================================ # Writes # /docs/v0.3.1/language/writes/ ================================================================================ All writes are atomic per call: the document change, every index maintenance step, unique-constraint checks, TTL changes, and graph-edge cascades commit in one transaction. An error leaves no partial side effects — a failed batch rolls back whole. ## Insert ```rust # use corvid::{Db, Value}; # let db = Db::open_in_memory()?; let c = db.collection("users"); c.insert(b"u1", &Value::Text("ada".into()))?; // insert or full overwrite # Ok::<(), corvid::Error>(()) ``` Overwrite replaces the whole document. The empty key and the empty map are legal. Reserved/invalid collection names are rejected here (lazily, at first write). ## Batch insert ```rust # use corvid::{Db, Value}; # let db = Db::open_in_memory()?; let c = db.collection("users"); let batch: Vec<(&[u8], &Value)> = vec![ (b"u1", &Value::Int(1)), (b"u2", &Value::Int(2)), ]; c.insert_batch(&batch)?; // one transaction, one fsync # Ok::<(), corvid::Error>(()) ``` Duplicates inside a batch follow last-write-wins. A unique or schema violation anywhere rolls back the **whole batch**. ## Auto keys ```rust # use corvid::{Db, Value}; # let db = Db::open_in_memory()?; let c = db.collection("events"); let key: Vec = c.insert_auto(&Value::Int(42))?; // ordered, unique, per-collection # let _ = key; # Ok::<(), corvid::Error>(()) ``` Keys are zero-padded 20-digit monotonically increasing values, so insertion order == key order. The id is reserved inside the insert transaction: a failed insert (schema/unique violation) does not burn an id. ## Patch ```rust # use corvid::{Db, Value}; # let db = Db::open_in_memory()?; let c = db.collection("users"); # let mut m = std::collections::BTreeMap::new(); m.insert("age".into(), Value::Int(36)); # c.insert(b"u1", &Value::Map(m))?; let mut p = std::collections::BTreeMap::new(); p.insert("age".into(), Value::Int(37)); c.patch(b"u1", &Value::Map(p))?; // merge top-level fields # Ok::<(), corvid::Error>(()) ``` Top-level map fields merge; a non-map value under a patched key **replaces** the old value (no deep merge). Patching a key with no document creates it. Either side being a non-map makes the result the patch value. ## Update (read-modify-write) ```rust # use corvid::{Db, Value}; # let db = Db::open_in_memory()?; let c = db.collection("counters"); # c.insert(b"k", &Value::Int(1))?; c.update(b"k", |cur| match cur { Some(Value::Int(n)) => Some(Value::Int(n + 1)), _ => Some(Value::Int(0)), // absent -> create with 0 })?; # Ok::<(), corvid::Error>(()) ``` The closure sees the current document (or `None` when absent — absence is not an error) and returns the replacement, or `None` to delete. `update` is get-then-write and therefore **not linearizable** against concurrent writers to the same key — when that matters, use `compare_and_set`. ## Compare-and-set ```rust # use corvid::{Db, Value}; # let db = Db::open_in_memory()?; let c = db.collection("users"); // insert-if-absent: let applied = c.compare_and_set(b"u9", None, Some(Value::Int(1)))?; // delete-if-present: let removed = c.compare_and_set(b"u9", Some(&Value::Int(1)), None)?; # let _ = (applied, removed); # Ok::<(), corvid::Error>(()) ``` Atomic conditional write / delete / insert-if-absent. The comparison uses the engine's **semantic value equality** — the same rule unique constraints use: `NaN == NaN` regardless of payload, `-0.0 == 0.0`, containers element-wise. A failed compare returns `false` (not an error); nothing is written. ## Deletes ```rust # use corvid::{Db, field, Value}; # let db = Db::open_in_memory()?; let c = db.collection("users"); c.delete(b"u1")?; // bool: existed? c.delete_batch(&[b"u2", b"u3"])?; // usize: how many existed c.delete_where(field("age").lt(Value::Int(18)))?; // usize, index-accelerated # Ok::<(), corvid::Error>(()) ``` Deleting a document **cascades its graph edges** in the same transaction — including edges dangling on a key that never existed as a document. Deleting an absent key is a quiet `false`, still running the edge cascade; no change events fire. ## Events Every write path emits [change events](/integrity/events/) — `Insert` / `Delete` vectors with exact per-path semantics (patch and CAS emit per branch; TTL purges and cascades are silent). Next: [filters](/language/filters/) — building predicates over documents. ================================================================================ # Geo indexes # /docs/v0.3.1/indexes/geo/ ================================================================================ ```rust # use corvid::{Db}; # let db = Db::open_in_memory()?; let c = db.collection("places"); c.create_geo_index("loc")?; # Ok::<(), corvid::Error>(()) ``` The geo index keys documents into **fixed-resolution grid cells (~0.1°)** as order-preserving keys. A radius or bbox query computes the cells its bounding box overlaps, scans only those cells, then verifies exact haversine distance — sub-linear instead of a full-collection scan. - Accelerates `geo_within_radius`, `geo_within_bbox`, `geo_nearest` (which is an expanding-radius exact search), and the builder filter `field("loc").within_km(...)`. - Documents whose indexed field is not a valid point (`[lat, lon]` array or `{lat, lon}` map) are skipped — non-points are simply not in the index. - Moving a document's point (update) or deleting it maintains the index transactionally; indexed and scan paths return byte-identical results. - Very large windows (continental-scale radii, antimeridian-wrapping boxes) exceed the candidate cap and fall back to the bounded scan — correct, just unaccelerated. - On disk, persists across reopen; no rebuild on open. `geo_within_bbox` results are in **key order** on every path (the indexed path used to emit cell order — pinned fixed); `geo_within_radius` and `geo_nearest` are nearest-first with ties by key. See [geo queries](/geo/overview/). Next: [vector indexes](/indexes/vector/). ================================================================================ # Index maintenance # /docs/v0.3.1/indexes/maintenance/ ================================================================================ ## Creation Index creation over an existing collection runs as a **persisted state machine**: the definition is registered as `Building{cursor}`, backfill commits page by page (progress checkpointed), and completion flips the definition to `Complete`. - **Queries never serve a building index** — they use exact/bounded fallbacks, so results are always correct during a backfill. - **Crash or error mid-creation** leaves a resumable `Building` definition — no permanently partial index that queries silently trust. - **The first query after a reopen resumes an interrupted build synchronously**: first-query latency can include the remaining backfill. (Tracing's backfill span-per-page events make the resume visible — see [observability](/admin/observability/).) - On-disk backfill batches commits with a shared node cache for speed. ## Re-creation replaces Calling any `create_*` again — for a parameter change or none — rebuilds the index in one transactional reset: ```rust # use corvid::{Db, Metric, Quantization}; # let db = Db::open_in_memory()?; let c = db.collection("docs"); c.create_vector_index("v", Metric::Cosine)?; // original c.create_vector_index("v", Metric::L2)?; // replaces + rebuilds # Ok::<(), corvid::Error>(()) ``` Same-parameter re-creation does not resume a partial backfill — it resets. Compound indexes recompute their `all_docs_indexed` flag at completion (see [scalar & compound](/indexes/scalar/)). ## Writes maintain everything, transactionally Every mutation path — `insert`, `insert_batch`, `patch`, `update`, `compare_and_set`, `delete`, `delete_where`, `delete_batch`, TTL purges — maintains every index inside the write's transaction. The maintenance contract is conformance-pinned per mutation kind (see the [construct reference](/reference/constructs/)). ## Automatic compaction (on-disk vector) On-disk vector indexes compact automatically once tombstones exceed a third of the index (`dead * 2 > live`), checked on the write path after the commit: - Expect a synchronous rebuild burst (write amplification) when a write crosses the threshold. - Between compactions, the search's over-fetch scales by the tombstone count (`ef_search.max(k) + dead`) so recall does not decay as tombstones accumulate. - Deleting *all* documents leaves index definitions intact (deletes never unregister) — one consequence: a dump of a drained PQ-indexed collection fails on load with `EmptyIndexTraining` (the definition replays but has no training vectors). Re-create the index after such a load, or drop the definition before dumping a drained collection. ## Corruption posture Corrupt persisted index state surfaces loudly: `Error::CorruptIndex` with a context string — never a silently degraded empty result. Recovery is re-creating the index (documents are the source of truth). A corrupt derived *adjacency* row self-heals: the engine rebuilds it from the source edge rows and re-runs the operation. Next: [full-text search](/fts/overview/). ================================================================================ # On-disk vs in-memory indexes # /docs/v0.3.1/indexes/on-disk/ ================================================================================ corvid's index families come in two storage shapes: - **In-RAM** (in-memory HNSW, in-RAM text postings): state lives in memory, definitions persist, state rebuilds lazily on first use after open. Fast, simple, the right default up to ~100k–1M documents. - **On-disk** (`create_vector_index_ondisk*`, `create_text_index_ondisk`, `create_scalar_index`, `create_compound_index`, `create_geo_index`): state lives as storage records. An insert or search touches only the nodes/postings/keys it needs, so **memory is bounded by the operation, not the collection**, and the index persists across reopen with no rebuild. ```rust # use corvid::{Db, Metric}; # let db = Db::open_in_memory()?; let c = db.collection("docs"); c.create_vector_index_ondisk("embedding", Metric::Cosine)?; c.create_text_index_ondisk("body")?; c.create_scalar_index("category")?; c.create_geo_index("loc")?; # Ok::<(), corvid::Error>(()) ``` ## Why it matters: the scaling walls From the engine's measured scaling characteristics (1M documents, file-backed): | Wall at 1M–50M | Removed by | |---|---| | In-memory HNSW build is minutes at 1M; doesn't fit at 50M | on-disk vector indexes | | Unindexed `filter`/`order_by` are O(n) scans | scalar / compound / geo indexes | | Exact (unindexed) search is O(n) time | any vector index past ~100k | Storage, point ops, counts, streamed aggregates, and ordered pagination scale with bounded memory regardless — the on-disk family exists for *search* and *selective filters* at scale. ## Behavior differences to know - **First-use latency**: on-disk indexes are ready immediately after open. In-RAM indexes rebuild on first use (a large collection's first query includes the build; tracing's backfill events make it visible). - **Bulk backfill** of an on-disk index batches commits with a shared node cache — index creation over an existing corpus is checkpointed and resumable (see [maintenance](/indexes/maintenance/)). - **Compaction**: on-disk vector indexes self-compact when tombstones exceed a third of the index; on-disk text/scalar/geo state is maintained incrementally (no periodic compaction needed). - **Recall**: on-disk HNSW corpora pin recall floors (≥0.85 on-disk vs ≥0.9 in-memory against exact-KNN twins on the engine's corpora); over-fetch scales with tombstone count between compactions. Next: [index maintenance](/indexes/maintenance/). ================================================================================ # Indexes: choosing # /docs/v0.3.1/indexes/overview/ ================================================================================ All corvid indexes are **derived**: maintained inside the write transaction, never stale at query time, rebuilt from documents on re-creation, persisted across reopen. You never change a query to use an index — the builder probes every serviceable index and drives on the smallest candidate set, verifying candidates against the exact predicate, falling back to a bounded scan when nothing helps. ## The families | Family | Constructor | Accelerates | Storage | |---|---|---|---| | Scalar | `create_scalar_index(field)` | equality/range filters, counts, `order_by` walks | on disk, persists | | Compound | `create_compound_index(&["a","b"])` | prefix-equality + trailing range across fields | on disk, persists | | Text | `create_text_index(field)` / `create_text_index_ondisk(field)` | BM25 `text_search`, `phrase_search`, builder text sources | in-RAM (rebuilt on open) or on disk | | Geo | `create_geo_index(field)` | radius / bbox / `within_km` / `geo_nearest` | on disk, persists | | Vector | `create_vector_index*(field, metric, ...)` | `vector_search`, builder vector sources | in-RAM HNSW (rebuilt on open) or on disk | Deep dives: [scalar & compound](/indexes/scalar/), [text](/indexes/text/), [geo](/indexes/geo/), [vector](/indexes/vector/), and [quantization](/indexes/quantization/) for the compression paths. ## Choosing Decide by query shape first, scale second: - **Equality/range on one field** (`eq`, `between`, `gt`, `starts_with`) → scalar index on that field. - **Equality on a leading field + range on the next** (e.g. `tenant = X AND ts BETWEEN a AND b`) → compound index `["tenant", "ts"]`. - **Keyword/phrase search** on a body of text → text index; in-RAM up to ~100k–1M docs, on-disk above (bounded memory, persists). - **Radius / bbox / nearest** around points → geo index. - **Vector similarity** past ~100k docs (exact scan is the correct baseline below that) → HNSW. In-memory until it doesn't fit in RAM, on-disk beyond; add [quantization](/indexes/quantization/) when footprint matters more than the last margin of recall. Rules of thumb from the engine's measurements: - The scalar index takes a selective equality filter on 1M docs from a ~662 ms scan to ~3 ms (see [performance](/performance/scaling/)). - An index never changes results — only the plan (`explain()` reports `IndexedWindow` vs `Scan`). Unselective predicates decline to the scan by design (candidate caps). - Indexes cost write amplification: every write maintains every index. Index what you filter on, not what you store. ## Maintenance invariants - **Definitions persist** across reopen; on-disk index state persists with them (no rebuild). In-RAM indexes (in-memory HNSW, in-RAM text) rebuild lazily on first use after open. - **Creation is crash-safe**: an interrupted create resumes on the next query — queries never serve a partially built index (exact fallbacks cover the gap; see [maintenance](/indexes/maintenance/)). - **Re-creating replaces**: calling a `create_*` again (same or different parameters) rebuilds the index in one transactional reset. - Indexes are never listed as collections and never appear in dumps as data — dumps carry the *definitions* and rebuild on load. Next: [scalar and compound indexes](/indexes/scalar/). ================================================================================ # Vector quantization # /docs/v0.3.1/indexes/quantization/ ================================================================================ Quantization trades footprint (and some time) for the last margin of recall. All three modes serve every metric on both the in-memory and on-disk indexes, and all keep the public contract: results are reranked with exact distances. | Mode | Footprint (asymptotic) | Time | Notes | |---|---|---|---| | `Quantization::None` | `dim × 4` bytes/vector | baseline | exact graph, full f32 | | `Quantization::Binary` | 1 bit/dim (sign) | **fastest at volume** | Hamming kernel; ~32× smaller; the throughput lever | | `Quantization::Scalar` | 8-bit/dim + 8-byte header | slower than None at some dims | ~4× smaller; per-eval reconstruction can cost more than it saves at low dim | | PQ (`m, k`) | `m` code bytes/vector | build 2.9×, search 1.9× | trained codebook; 16× at 64d with `m=16, k=256` | ```rust # use corvid::{Db, Metric, Quantization}; # let db = Db::open_in_memory()?; let c = db.collection("docs"); c.create_vector_index_ondisk_quantized("embedding", Metric::Cosine, Quantization::Binary)?; c.create_vector_index_ondisk_pq("embedding", Metric::Cosine, 16, 256)?; # Ok::<(), corvid::Error>(()) ``` ## Product quantization in detail PQ trains a deterministic per-subspace codebook (k-means, parallelized since v0.2) from a bounded sample of existing vectors, stores each vector as `m` code bytes, and persists the codebook in the index's namespace — after a reopen, the lazily rebuilt in-memory graph re-encodes under the **same** codebook. Dump/load round-trips the index as a vector mode carrying `m`/`k`. - L2 scores through the asymmetric-distance computation (ADC — the fast path); cosine and dot score through reconstruction (decode + metric). - Constraints: `dim % m == 0`, `k ∈ 2..=256`, and training requires usable vectors (`Error::EmptyIndexTraining` otherwise). - Time premiums on the pinned 2000×64d corpus (`m=16, k=256`): build 367.9 ms vs 124.9 ms full precision (2.9×); search 35.8 µs vs 19.1 µs (1.9×). Training itself: ~67 ms at 2000×64d, ~356 ms at 10000×128d. ## Recall margins (measured, pinned) | Corpus | Measured recall | Pinned floor | |---|---|---| | Public path (`vector_search`, clustered corpus, over-fetch + exact rerank) | **1.0** | ≥ 0.7 | | Direct HNSW API unit corpus (ef 100/200/400) | 0.56 (identical at all ef) | ≥ 0.55 | The public path's over-fetch plus exact rerank recover the full top-k; the residual gap on the direct API is codebook resolution, not graph reach. ## Guidance 1. **Default to `None`** until footprint or throughput forces a choice. Exact search is OOM-free and correct at any scale; indexes are for speed. 2. **Volume scans → Binary.** On a 2000×768d corpus, the packed-byte Hamming path searches **50.8× faster** than full precision (~32× less traffic and a cheaper kernel) — the measured throughput lever for large corpora. 3. **RAM/disk budget → PQ.** The smallest footprint; accept the 2–3× build and ~2× search premiums and validate recall on *your* corpus (recall depends on data distribution; pinned corpora are the engine's, not yours). 4. **Scalar** when you want a middle compression without a training step — but measure: at 768d its per-evaluation decode can make it *slower* than full precision (306.8 µs vs 239.8 µs on the pinned scan corpus). 5. Whatever the mode, **exact rerank keeps scores trustworthy**; only the candidate set is approximate. If a workload needs the exact top-k under a threshold, keep `None` or validate recall empirically. All numbers above: Apple M1 Max, criterion means, deterministic corpora — single-machine, compare relatively. Full provenance in [performance](/performance/quantization-guidance/). Next: [on-disk vs in-memory](/indexes/on-disk/). ================================================================================ # Scalar and compound indexes # /docs/v0.3.1/indexes/scalar/ ================================================================================ ```rust # use corvid::{Db}; # let db = Db::open_in_memory()?; let c = db.collection("docs"); c.create_scalar_index("category")?; // one field c.create_scalar_index("meta.score")?; // nested paths work c.create_compound_index(&["tenant", "ts"])?; // ordered field list # Ok::<(), corvid::Error>(()) ``` Both store **order-preserving keys** in engine namespaces as ordinary records: selective equality/range filters and counts go sub-linear (an index window instead of a full scan), and the state persists across reopen with no rebuild. ## How the scalar index serves a query - Numbers (Int+Float) share one lane keyed by the IEEE-754 total order of the f64 — the i64→f64 cast is monotonic, so a range scan never excludes a true match. Text has its own lane; a field holding both kinds indexes per-lane. - The index returns a **verified candidate superset**: the builder re-checks every candidate against the exact predicate, so encoding ties cost a few extra checks, never correctness. - `ne` is not serviced — an anti-scan is not sub-linear. - Unselective windows (and `is_in`/`or` unions over the 100,000-key aggregate cap) fall back to the bounded streaming scan. - A filterless `order_by(field)` over a complete scalar index is served by an **index order walk** (`PlanShape::SortIndex`) — documents are fetched only for the `offset + limit` window (see [ordering](/language/ordering/)). Measured shape: on 1M docs, a selective equality drops ~662 ms → ~3 ms; a 100-row range window ~0.5 ms (see [performance](/performance/scaling/)). ## Compound indexes `create_compound_index(&["a", "b"])` covers **equality on a leading prefix + at most one trailing range**: ```rust # use corvid::{Db, field, Value}; # let db = Db::open_in_memory()?; let c = db.collection("events"); # c.create_compound_index(&["tenant", "ts"])?; // servable: equality prefix + trailing range let rows = c.query() .filter(field("tenant").eq(Value::Text("acme".into()))) .filter(field("ts").between(Value::Int(1), Value::Int(99))) .run()?; # let _ = rows; # Ok::<(), corvid::Error>(()) ``` - **Field order matters**: `["tenant","ts"]` serves `tenant = ?` (+ optional `ts` range); it does not serve a bare `ts` range. `["ts","tenant"]` is a different index — both may coexist. - Documents **missing any indexed field are absent from the index.** This has a consequence for prefix-only queries (equality on the leading field with trailing fields unconstrained): a matching document necessarily has the leading field, so it *is* indexed — but only when the index can trust that *every* document has all fields present. Each compound definition persists an `all_docs_indexed` flag, set at backfill completion iff no document ever missed a field, cleared permanently by any write that leaves a field missing/non-encodable, and recomputed by re-creating the index: - flag set → prefix-only queries are served through the window (~6.6× on the 5k benchmark corpus); - flag clear → prefix-only queries decline to the `Scan` path — identical results, just unaccelerated. - Any write that leaves an indexed field missing or non-encodable marks the miss in the write's own transaction, so the flag never lies. ## Maintenance - Re-creating an index (same or different parameters) replaces it — one transactional reset, no stale entries. - Backfills over an existing collection commit per page with progress persistence; creation interrupted by crash/error resumes on first query (see [maintenance](/indexes/maintenance/)). - Scalar indexes also accelerate `delete_where` and unique-constraint enforcement for `unique` schema fields. Next: [text indexes](/indexes/text/). ================================================================================ # Text indexes # /docs/v0.3.1/indexes/text/ ================================================================================ ```rust # use corvid::{Db}; # let db = Db::open_in_memory()?; let c = db.collection("docs"); c.create_text_index("body")?; // in-RAM postings c.create_text_index_ondisk("body")?; // on-disk postings # Ok::<(), corvid::Error>(()) ``` Both are **incremental inverted indexes** storing per-term postings with positional information, updated transactionally with every write. Both back `text_search` (BM25), `phrase_search` (in-order positional matching), and the [query builder](/language/query-builder/)'s `.text(...)` source identically — a query touches only its query terms' postings instead of rescanning the corpus. ## In-RAM vs on-disk | | `create_text_index` | `create_text_index_ondisk` | |---|---|---| | Postings live | in memory | as storage records | | Memory | proportional to corpus | bounded by the operation | | Open cost | rebuilt lazily on first use | ready immediately, no rebuild | | Persists | definition persists; postings rebuild | definition + state persist | | When | up to ~100k–1M docs | beyond, or tight-memory deployments | Non-text values in the indexed field are excluded from postings; text mutations keep search correct on every path (indexed and scan arms are conformance-pinned to match — see the [construct reference](/reference/constructs/)). ## What acceleration looks like On the pinned 2k-doc benchmark corpus, BM25 goes ~8.0 ms (exact scan) → ~0.49 ms (indexed) — the index also serves phrase queries' positional checks. Single-source ranked builder queries are bounded: no corpus materialization. See [performance](/performance/numbers/). ## Analyzer notes The index and the query share one analyzer (lowercase, English stop words, conservative plural stemmer, CJK bigrams) — see [tokenization](/fts/tokenization/). Consequence for upgrades: when the analyzer changes between engine versions (the CJK bigram change is the example), re-create existing text indexes so postings match the new tokenizer. Next: [geo indexes](/indexes/geo/). ================================================================================ # Vector indexes # /docs/v0.3.1/indexes/vector/ ================================================================================ Without an index, vector search is **exact** — a streamed brute-force scan with a bounded heap (the correctness baseline, OOM-free at any size). Creating an HNSW index switches `vector_search` and builder `.vector(...)` sources to the graph transparently; `Hit.approximate` reports which path served the answer. ## The constructors | Constructor | Storage | When | |---|---|---| | `create_vector_index(field, metric)` | in-RAM HNSW | default; fast, rebuilt lazily on open | | `create_vector_index_quantized(field, metric, quant)` | in-RAM, compressed | `Quantization::Binary` (~32×) / `Scalar` (~4×) | | `create_vector_index_pq(field, metric, m, k)` | in-RAM, product-quantized | smallest RAM footprint (`m` code bytes/vector) | | `create_vector_index_ondisk(field, metric)` | on-disk HNSW | bounded memory, persists, no rebuild | | `create_vector_index_ondisk_quantized(field, metric, quant)` | on-disk, compressed | billions of vectors on a laptop | | `create_vector_index_ondisk_pq(field, metric, m, k)` | on-disk, product-quantized | smallest on-disk footprint | ```rust # use corvid::{Db, Metric, Quantization}; # let db = Db::open_in_memory()?; let c = db.collection("docs"); c.create_vector_index_ondisk_quantized("embedding", Metric::Cosine, Quantization::Scalar)?; # Ok::<(), corvid::Error>(()) ``` Compression ratios are asymptotic (less at low dimensions, where the 8-byte scalar header dominates). PQ variants need training vectors: `create_vector_index_pq` fails with `Error::EmptyIndexTraining` when the collection has no usable vectors, when `m` does not divide the field dimension, when `m == 0`, or when `k` is outside `2..=256`. See [quantization](/indexes/quantization/) for choosing a mode. ## Metrics `Metric::Cosine` (1 − cosine similarity, `[0, 2]`; zero-norm vectors are maximally distant), `Metric::Dot` (negated dot product — larger dot sorts first), `Metric::L2` (squared Euclidean). The index is built per metric; `vector_search` with a different metric than the index falls back to the exact path rather than answering with the wrong metric. ## Behavior contract - **Exact distances in results.** Indexed (ANN) hits are reranked with exact metric distances recomputed from the stored documents — quantized internal distances (Hamming counts, reconstruction approximations) never leak into `Hit.score`. Metric-unit thresholds (like the [semantic cache](/language/semantic-cache/)'s) stay meaningful under any index mode. - **Documents are the source of truth.** Dimension mismatches skip documents (and the index falls back to exact for the mismatched query dimension); overwriting a vector with a different dimension tombstones the old node first — ANN results never resurrect stale keys. - **Filtered queries run exact by default.** `.approx()` opts into the over-fetch-then-filter ANN path (see [the query builder](/language/query-builder/)). Unfiltered vector sources use the index whenever present. - **Duplicate re-creation replaces** the index with new parameters in one transactional reset; same-parameter re-creation rebuilds from scratch too (never resumes a stale partial backfill). - **Tombstone hygiene.** On-disk indexes compact automatically when tombstones exceed a third of the index (`dead * 2 > live`) — expect a synchronous rebuild burst on the write that crosses the threshold. Between compactions, over-fetch scales with the tombstone count so recall does not decay. ## HNSW parameters The in-memory graph exposes the direct API (`corvid::Hnsw`) with `DEFAULT_M` / `DEFAULT_EF_CONSTRUCTION` and `with_params` / `with_quant` / `with_pq` constructors for advanced control; collection-level constructors use the defaults, which the pinned corpora validate for recall. ## The direct APIs: `Hnsw` and `Pq` without a `Collection` Everything above manages indexes for you inside a `Collection`. The engine also exposes the primitives directly — reach for them when your vectors do not live in a corvid collection at all, or their lifecycle is shorter than a stored document's. `corvid::Hnsw` is the plain in-memory graph: a build-once, search-many structure over `Vec` you own, with no storage, persistence, or documents attached — e.g. clustering/deduplicating a batch of candidate embeddings at query time, or prototyping recall/latency trade-offs before committing to an index configuration. `corvid::pq::Pq` is the trained product quantizer on its own: encode vectors to `m` bytes and keep the codebook (`to_bytes`/`from_bytes`) wherever you like — shards, files, network messages — scoring codes against a query yourself, with the same compression the collection-level PQ indexes use. ```rust use corvid::{Hnsw, Metric}; use corvid::pq::Pq; use std::sync::Arc; // Hnsw: insert returns a stable id; search returns (id, distance), nearest first. let mut g = Hnsw::new(Metric::Cosine); let ids: Vec = (0..1000).map(|i| g.insert(vec![i as f32 / 1000.0, 1.0])).collect(); let _ = g.search(&[0.5, 1.0], 10, 100); // top-10, ef_search = 100 // Pq: train on a sample, encode each vector to m bytes, score against a query. let sample: Vec> = (0..512).map(|i| vec![i as f32, 1.0]).collect(); let pq = Pq::train(&sample, 1, 16).expect("m divides dim, sample big enough"); let code = pq.encode(&[3.0, 1.0]); // 1 byte per vector let d = pq.distance(Metric::L2, &[3.0, 1.0], &code); // reconstruct-then-distance let _ = Arc::new(pq); // Hnsw::with_pq takes Arc let _ = (ids, d); ``` Notes: `Hnsw::search`'s `ef_search` widens the beam (raised to at least `k` — larger is more accurate, slower); `Hnsw::with_pq(metric, Arc, m, ef_construction)` builds a graph over PQ codes exactly like the collection index (L2 via the asymmetric-distance table, cosine/dot via reconstruction). `Pq::train` is deterministic for a fixed sample and returns `None` on unusable parameters (empty sample, `dim` not divisible by `m`, `k` outside `2..=256`); the distance helpers (`Pq::distance`, `Pq::l2_table`/`adc_l2`) are covered in the [constructs reference](/reference/constructs/). Next: [quantization](/indexes/quantization/). ================================================================================ # Full-text search # /docs/v0.3.1/fts/overview/ ================================================================================ ```rust # use corvid::{Db, Value}; # let db = Db::open_in_memory()?; let c = db.collection("docs"); let hits = c.text_search("body", "rust databases", 10)?; // Vec let phrase = c.phrase_search("body", "embedded database", 10)?; // exact, in order # let _ = (hits, phrase); # Ok::<(), corvid::Error>(()) ``` Text search is **BM25** — term frequency saturated against document length, rare terms outweighing common ones (IDF), scores comparable across a corpus. The same engine serves `text_search` directly and the [query builder](/language/query-builder/)'s `.text(field, query, k)` source. ## Scoring semantics - **Pre-ranking predicates.** A builder text query with a filter ranks the *filtered* candidate set: the predicate runs first (index window or scan), and BM25 statistics — document frequencies, average document length — are computed over exactly those candidates. The same query without a filter scores against full-corpus stats. A score always means "relevance within the candidate set the filter admits". - **Rare terms outrank common ones.** `text_search` for a term appearing in one document outscores a term appearing everywhere, at equal TF/length. Ties break deterministically. - **k semantics**: `k = 0` returns nothing; `k = 1` the best hit; `k` beyond the corpus returns everything that matches. Stop-word-only and empty queries return nothing. - **Missing fields** are skipped, never errors. ## Exact vs indexed Without a [text index](/indexes/text/), search is an exact pass — every document tokenized and scored (correct, streamed, O(n)). With one, a query touches only its query terms' postings. Results are identical either way (conformance-pinned); on the 2k benchmark corpus the indexed path is ~16× faster. Phrase queries store and check positions on both paths; the no-index phrase fallback scores BM25 on the same scale as the indexed paths, so creating or dropping an index does not reorder the same phrase query. ## `TextHit` ```rust pub struct TextHit { pub key: Vec, pub score: f32, pub document: Value } ``` ## Phrase search `phrase_search` matches exact consecutive tokens **in order** (`"quick brown"` matches *quick brown fox*, not *brown quick fox*). Positions are assigned by the shared analyzer — see [phrase search](/fts/phrase-search/) for the stop-word collapse caveat — and CJK bigrams make phrase order work over unspaced scripts (`東京タワー` matches; `タワー東京` does not). ## The analyzer One analyzer feeds index and query on every path: lowercase, English stop words, conservative plural stemming (Harman's S-stemmer: `dogs` → `dog`, `parties` → `party` — but `boxes` → `boxe`, so it does **not** match `box`), and CJK bigram segmentation. Details and boundaries on [tokenization](/fts/tokenization/); the honest limitations: - The S-stemmer is not a full Porter stemmer. Match irregular/s-suffixed pairs by storing a normalized field or querying both forms. - Phrase positions are post-stop-word: `"quick the brown"` matches text containing `"quick brown"` — there is no position gap for a removed stop word. Next: [tokenization](/fts/tokenization/). ================================================================================ # Phrase search # /docs/v0.3.1/fts/phrase-search/ ================================================================================ `phrase_search(field, phrase, k)` matches documents whose field contains the phrase's tokens **consecutively and in order** — a positional check, not a bag-of-words AND: ```rust # use corvid::{Db, Value}; # let db = Db::open_in_memory()?; let c = db.collection("docs"); # let mut m = std::collections::BTreeMap::new(); # m.insert("body".into(), Value::Text("an embedded database for rust".into())); # c.insert(b"d1", &Value::Map(m))?; c.phrase_search("body", "embedded database", 10)?; // matches d1 c.phrase_search("body", "database embedded", 10)?; // order matters — no match c.phrase_search("body", "embedded", 10)?; // single term == term search # Ok::<(), corvid::Error>(()) ``` ## Semantics - **Single-term phrases** equal term search. - **Repeated terms** must actually repeat: `"rust rust"` does not match a single `rust`. - **Non-adjacent tokens do not match**: `"database for"` does not match *database for rust*? It does — `for` may be a stop word (below). But `"database rust"` (skipping a *kept* word) does not. - **k bounds**: `k = 0` yields nothing; beyond the corpus yields everything that matches. An empty phrase yields nothing. - Both index arms (in-RAM and on-disk postings carry positions) and the no-index fallback behave identically; the fallback scores hits with BM25 on the same scale as the indexed paths, so scores are comparable and creating or dropping an index does not reorder the same phrase query. ## The stop-word collapse caveat Token positions are assigned **after stop-word removal**, and there is no position gap for a removed stop word. Therefore phrases match *across* removed stop words: > `"quick the brown"` matches text containing `"quick brown"`. `the` never got a position, so `quick` and `brown` are adjacent in the positional stream. This is documented, pinned behavior — design phrase queries accordingly (avoid stop words inside phrases when adjacency matters against the *raw* text). ## Sentence boundaries Phrase matching does not respect sentence boundaries — positions continue across sentence-internal punctuation in the same field, so a phrase can match across a period if the tokens are adjacent in the stream. Pin your expectations with tests if you rely on boundary behavior. ## CJK phrases Over CJK bigrams, phrase order is order-correct: `東京タワー` matches, `タワー東京` does not (see [tokenization](/fts/tokenization/)). Next: [graph](/graph/overview/). ================================================================================ # Tokenization # /docs/v0.3.1/fts/tokenization/ ================================================================================ One analyzer feeds indexing and querying on every serving path, so token streams always line up. The pipeline: split → lowercase → stop-word removal → conservative plural stemming, with CJK runs segmented as sliding bigrams. ```rust use corvid::text::{tokenize, analyze, Analyzer}; let tokens = tokenize("Dogs chase Cats!"); // raw tokenizer let analyzed = Analyzer::default().analyze("Dogs chase Cats!"); let raw = Analyzer::raw().analyze("Dogs chase Cats!"); // no normalization ``` `Analyzer` is configurable; `tokenize` is the raw Unicode-aware splitter (alphanumeric runs; case and punctuation handled; numbers included). ## Case and stop words - Lowercase folding applies to cased scripts (Latin and friends). - A conservative English stop-word list is removed (`the`, `of`, `and`, ...). - Stop-word removal affects **positions** — see the [phrase search caveat](/fts/phrase-search/). ## The S-stemmer Harman's S-stemmer normalizes common plurals only: | Input | Stem | Matches `box`? | |---|---|---| | `dogs` | `dog` | — | | `parties` | `party` | — | | `boxes` | `boxe` | **no** | | `goes` | `goe` | no | It is deliberately conservative — no full Porter algorithm — so stemming never merges unrelated words. For irregular or s-suffixed pairs that matter to your queries, store a normalized field or query both forms. ## CJK: sliding bigrams Runs of CJK characters tokenize as **sliding bigrams** (a single-character run yields that character) — the standard dictionary-free segmentation fallback for unspaced scripts, with no dictionary data and no dependencies. The CJK set (documented on the tokenizer): - Hiragana + katakana: U+3040–30FF (prolonged sound mark `ー` included) - Han ideographs: U+3400–4DBF, U+4E00–9FFF, U+F900–FAFF, U+20000–323AF Deliberately **outside** the set: - **Hangul** — Korean is space-separated, so whole runs remain its tokens (the Latin behavior). - **Halfwidth katakana** and the iteration marks (々 U+3005, 〆 U+3006, 〱–〵 U+3031–3035): a mark splits the surrounding CJK run and joins the non-CJK whole-token piece. Index and query split identically; NFKC-normalize upstream if you need bigrams across marks. Boundary behavior: - The Han↔kana script transition inside one run does **not** restart the window (`東タ` bigrams continue across the transition). - A CJK↔non-CJK transition splits the run. - **Stemming and case folding never apply to CJK tokens** (`東京` never merges with `東`). - Consequence: `東京タワー` indexes so that phrase search matches in order — and `タワー東京` does not. ## Upgrade note CJK behavior previously indexed whole runs as single tokens. Text indexes created before the bigram change should be **re-created** so their postings carry bigrams for CJK fields (definitions persist; postings rebuild on re-creation). Next: [phrase search](/fts/phrase-search/). ================================================================================ # Graph # /docs/v0.3.1/graph/overview/ ================================================================================ A directed property graph lives over document keys, stored in reserved namespaces and indexed by relation. Edges are **atomic** (forward and reverse in one transaction) and endpoints need not exist as documents. ```rust # use corvid::{Db}; # let db = Db::open_in_memory()?; let g = db.collection("people"); g.link(b"alice", "follows", b"bob")?; // directed edge, weight 1.0 g.link_weighted(b"alice", "rates", b"film", 4.5)?; // with a weight g.neighbors(b"alice", "follows")?; // Vec> — out-edges g.in_neighbors(b"bob", "follows")?; // who follows bob g.neighbors_weighted(b"alice", "rates")?; // (target, weight) pairs g.traverse(b"alice", "follows", 3)?; // BFS up to 3 hops g.unlink(b"alice", "follows", b"bob")?; // bool: existed? # Ok::<(), corvid::Error>(()) ``` ## Semantics - **Directed**: `link(a, r, b)` is an edge *from a to b*. `neighbors(a, r)` follows out-edges; `in_neighbors(b, r)` reads the reverse index. - **Relations are isolated**: a `follows` edge never leaks into `rates` queries; relation names follow the collection name rules (empty and Unicode relations are legal, ordered by bytes). - **Idempotent link**: linking an existing edge is a no-op that re-emits the insert event. A plain `link` **overwrites a prior weighted edge's weight** back to 1.0; `link_weighted` overwrites any prior weight. Float extremes (±inf, NaN) round-trip. - **Self-loops** list self in `neighbors`, but `traverse` excludes the start node. - **Missing endpoints are allowed** — edges to keys with no document are legal and queryable (`link` emits an insert event keyed by the `from`). - **Endpoint keys** may be empty or arbitrary bytes, ordered bytewise. - **`unlink` is directional**: it removes the named edge and its reverse twin in one transaction; the reverse-direction edge (b→a, if separately linked) survives. Unlinking a missing edge is a quiet `false` no-op. - **`neighbors`** returns endpoints in key order; a node with no out-edges or an unknown node yields empty. - **`traverse(start, relation, hops)`** is BFS: reachable nodes up to `hops` hops, excluding `start`, each once, in BFS visit order. `hops 0` yields nothing; `hops 1` equals `neighbors`; cycles terminate (visited set); branching and diamond-convergence orders are pinned by tests. One read snapshot covers the walk. ## Cascade semantics Deleting a document — via `delete`, `delete_batch`, `delete_where`, `compare_and_set`, or a [TTL](/integrity/ttl/) purge — removes **all its edges, both directions, in the same transaction**. Even deleting an *absent* key runs the cascade (cleaning edges dangling on a never-inserted key); purging a stranded TTL entry cascades likewise. `link`/`unlink` emit change events; the delete cascade itself is silent (no per-edge events). ## Storage: adjacency Edges live in reserved edge namespaces; two derived **adjacency** namespaces re-key them endpoint-first for reads and cascades: - Steady-state deletes touch only the deleted key's rows — O(edges of that document), not O(collection edges). (Hub-heavy delete sweeps measured ~5.9× faster; see [performance](/performance/numbers/).) - `link` pays two extra rows per edge (~1.4× on the pure-link microbench) — the ratified trade for O(degree) cascades. - The adjacency builds lazily inside the first edge write's (or first cascade's) transaction on legacy databases, self-heals from source rows if a derived row is corrupt, and never appears in `collections()` or dumps (dump→load replays edges through `link_weighted`, rebuilding it). Next: [geo queries](/geo/overview/). ================================================================================ # Geo queries # /docs/v0.3.1/geo/overview/ ================================================================================ A location field holds a point as `[lat, lon]` (array) or `{lat, lon}` (map). Distances are haversine kilometres (spherical Earth). Documents without a valid point are skipped, never errors. ```rust # use corvid::{Db}; # let db = Db::open_in_memory()?; let c = db.collection("places"); c.geo_within_radius("loc", 51.5, -0.13, 25.0)?; // within 25 km, nearest first c.geo_within_bbox("loc", 51.0, -1.0, 52.0, 1.0)?; // bounding box, key order c.geo_nearest("loc", 51.5, -0.13, 5)?; // k nearest, any distance # Ok::<(), corvid::Error>(()) ``` And as a composable builder filter: ```rust # use corvid::{field}; field("loc").within_km(51.5, -0.13, 25.0); # let _ = (); ``` ## `geo_within_radius(lat, lon, radius_km)` - Inclusive boundary: a point at exactly `radius_km` matches. - Results are **nearest first, ties by key**. - `radius 0` matches the point itself; a "full globe" radius matches every valid point. - No input validation is applied — the query is a mathematical predicate (invalid centers behave per the haversine formula; use [`within_km`'s predicate rules](#validation) when you want checked input). - With a [geo index](/indexes/geo/) the window scans only overlapped cells, then verifies exact haversine. ## `geo_within_bbox(min_lat, min_lon, max_lat, max_lon)` - **Validated at entry** (see [validation](#validation)): latitude in `[-90, 90]`, longitude in `[-180, 180]`, NaN rejected, inverted latitude (`min_lat > max_lat`) rejected with `Error::InvalidArgument`. - **Antimeridian**: `min_lon > max_lon` wraps — the box matches **both** longitude ranges (the two sides of the 180° line). The wrap path is exact but unaccelerated (cap-fallback to a scan). - Results are in **key order, portably** — every path (indexed, scan) emits the same documents in the same order. Key order is the contract. - Degenerate shapes are legal: a point box matches the point; a line box (zero height/width) matches the line; pole and globe boxes work. - `GeoHit.distance_km` is the **0.0 sentinel** for bbox hits — the box query has no center, so no distance is computed. ## `geo_nearest(lat, lon, k)` - The true `k` nearest points regardless of distance (an expanding-radius exact search), nearest first, equidistant ties by key. - `k = 0` yields nothing; fewer than `k` results only when fewer valid points exist; antipodal points are found. ## Validation The two entry points differ deliberately: | Entry point | Validation | |---|---| | `geo_within_bbox` | strict: bounds, NaN, inverted latitude → `Error::InvalidArgument` | | `geo_within_radius` / `geo_nearest` | none — mathematical semantics (documented, pinned) | | `field("loc").within_km(...)` predicate | deep-checked: invalid centers and pole-adjacent degenerates are tested per the conformance suite | If your application feeds user input to radius/nearest, validate bounds yourself or route through the predicate. ## `GeoHit` ```rust pub struct GeoHit { pub key: Vec, pub distance_km: f64, pub document: Value } ``` `haversine_km(a, b)` is the public distance helper — symmetric, exact at poles and antipodes. Next: [TTL and expiry](/integrity/ttl/). ================================================================================ # Change events # /docs/v0.3.1/integrity/events/ ================================================================================ ```rust # use corvid::{Db}; # let db = Db::open_in_memory()?; let id = db.subscribe(|ev| { println!("{:?} {} {:?}", ev.kind, ev.collection, ev.key); }); // ... writes fire the callback ... db.unsubscribe(id); # Ok::<(), corvid::Error>(()) ``` In-process subscriptions piggyback on the write path — there is no separate event log. Every event carries: ```rust pub struct ChangeEvent { pub kind: ChangeKind, // Insert | Delete pub collection: String, pub key: Vec, } pub struct SubscriptionId(/* opaque */); ``` ## Dispatch semantics - **Synchronous, post-commit, in mutation order.** Callbacks run on the writing thread after the transaction commits — when your callback returns, the event is delivered. - **Multiple subscribers all receive identical event vectors**, in order. - **Cross-collection tagging**: each event names its own collection. - Callbacks should be fast and non-blocking; a slow callback stalls the writer. Keep a lock-free hand-off if you process asynchronously. - `unsubscribe` reports whether the subscription existed; ids are distinct. ## Exact event vectors per path | Path | Events | |---|---| | `insert` (new key) | one `Insert` | | `insert` (overwrite) | one `Insert` (the new value) | | `insert_batch` | one `Insert` per applied key, in batch order | | `insert_auto` | one `Insert` keyed by the generated key | | `update` returning `Some` | one `Insert` | | `update` returning `None` (delete) | one `Delete` | | `update` on missing key creating | one `Insert` | | `patch` creating | one `Insert` | | `patch` merging | one `Insert` | | `compare_and_set` applied (write) | one `Insert` | | `compare_and_set` applied (delete) | one `Delete` | | `compare_and_set` compare failed | none | | `delete` / `delete_batch` / `delete_where` | one `Delete` per existing key, in order | | `delete` of absent key (incl. edge cascade) | none | | `link` (new or duplicate) | one `Insert` keyed by the `from` endpoint — including links to missing endpoints | | `unlink` | none | | TTL purge | none (silent cascade) | | stranded TTL purge | none | The "duplicate link re-emits insert" and "TTL purge is silent" rows are the surprising ones — both are pinned by the conformance suite. ## What events are for Cache invalidation, derived-state maintenance, audit trails within the process, live UIs. For cross-process notification, run the [MCP sidecar](/admin/mcp/) or front the engine with your own service — the engine has no networking, by design, and the C ABI excludes subscriptions in v1 (reentrancy across languages; see [ABI exclusions](/ffi/stability/)). Next: [administration](/admin/open-close/). ================================================================================ # Schemas and constraints # /docs/v0.3.1/integrity/schema/ ================================================================================ Schemas are **optional and opt-in**; schemaless collections are unaffected. A declared schema is enforced on **write** — existing documents are never retroactively validated. ```rust use corvid::schema::{Schema, Field, FieldType}; # use corvid::{Db}; # let db = Db::open_in_memory()?; let c = db.collection("users"); let schema = Schema::new() .field(Field::new("name", FieldType::Text).required()) .field(Field::new("email", FieldType::Text).unique()) .field(Field::new("age", FieldType::Int)); c.set_schema(&schema)?; // future writes are validated; violations error # Ok::<(), corvid::Error>(()) ``` `Collection::schema()` reads back the declared fields (`None` when undeclared). ## Field types `FieldType::Any | Bool | Int | Float | Text | Bytes | Vector | Array | Map`. `Any` accepts every value; the others accept their kind exactly. A violation (type mismatch, missing `required` field, duplicate `unique` value) fails the write with `Error::SchemaViolation` — and nothing is stored. ## `required` The field must be present. A `Null` value counts as present (use `Any` + required checks sparingly; there is no `not-null` constraint distinct from presence). ## `unique` No two documents may hold equal values of the field, enforced per write (insert, batch, patch, update, CAS, `insert_auto`): - Equality is **storage-level semantic equality** — `NaN` conflicts with `NaN` regardless of payload, `-0.0` conflicts with `0.0`, containers compare element-wise (see [equality](/language/equality/)). Numeric kinds interop: `Int(7)` and `Float(7.0)` collide. - Works for **non-index-encodable values** (Bytes/Array/Map/Vector) when a scalar index exists on the field — and the check keys on the actual stored values, so numerically equal-but-distinct stored values never falsely reject. - A unique violation **rolls back the whole write** — including an `insert_batch` (the batch is all-or-nothing). - Delete-then-reinsert of the same value is allowed (uniqueness is over live documents). - A scalar index on the unique field makes enforcement index-served and keeps it enforced as values move. ## Where schemas apply | Path | Behavior | |---|---| | `insert` / `insert_batch` / `insert_auto` | validated; violations roll back the write | | `patch` / `update` / `compare_and_set` | the resulting document must satisfy the schema | | `set_schema` replacing an existing schema | subsequent writes validated against the new one | | existing documents | never re-validated | | dump/load | definitions round-trip; load replays writes through validation | ## MCP tools The sidecar exposes `set_schema` (fields array of `{name, type: any|bool|int|float|text|bytes|vector|array|map, required?, unique?}`) and `get_schema` (`{fields: null}` when none declared) — see [the MCP sidecar](/admin/mcp/). Next: [transactions](/integrity/transactions/). ================================================================================ # Transactions # /docs/v0.3.1/integrity/transactions/ ================================================================================ The document API is transactional per call: every write is one atomic transaction covering the document, indexes, constraints, TTL, and edge cascades. For multi-document units, `insert_batch` and `Store`-level transactions give you the same atomicity across many keys. ## The concurrency model - **Single writer, whole database** — writes serialize (the storage engine's model). No savepoints. - **Concurrent readers, MVCC** — every query/scan/page runs against one point-in-time snapshot; readers never block the writer and vice versa. - **Snapshot isolation, not serializability.** A query's result always matches one committed state; omission-only mid-write anomalies are possible within a query, never torn reads. - Single-call atomicity: a failed multi-op write (batch, CAS, transaction) leaves no partial side effects. ## `Db::bulk` — the durability scope ```rust # use corvid::{Db, Value}; # let dir = tempfile::tempdir().unwrap(); # let mut db = Db::open(dir.path().join("app.corvid"))?; db.bulk(|| { let c = db.collection("docs"); for i in 0..100_000u32 { c.insert(&i.to_le_bytes(), &Value::Int(i as i64))?; } Ok(()) })?; // ~N fsyncs -> ~1 # Ok::<(), corvid::Error>(()) ``` Bulk load under relaxed durability: committed data stays consistent, but in-flight writes may be lost on a crash **before the closing flush**. Errors inside the closure still persist writes made before the error. A panic inside the closure unwinds past the flush (the next durable commit makes the writes durable — rebulk or reopen if that matters). See also [bulk loading](/admin/bulk/). ## `Store` — the byte-level surface `Store` is the lower-level KV API the document layer is built on — useful for systems that want raw bytes without document semantics: ```rust # use corvid::{Db}; # let db = Db::open_in_memory()?; let store = db.store(); // Atomic multi-op write transaction store.transaction(|tx| { tx.put(b"c", b"k1", b"v1")?; tx.delete(b"c", b"k0")?; Ok(()) })?; // WriteBatch — staged writes applied atomically use corvid::store::WriteBatch; let mut wb = WriteBatch::new(); wb.put(b"c", b"k2", b"v2"); wb.delete(b"c", b"k1"); store.apply(&wb)?; // ReadBatch — one snapshot across many reads use corvid::store::ReadBatch; let rb = store.read()?; let v = rb.get(b"c", b"k2")?; // Option> let n = rb.count(b"c")?; // usize rb.scan_prefix(b"c", b"k", |k, v| true)?; # let _ = v; # Ok::<(), corvid::Error>(()) ``` The full surface: `put`/`get`/`delete`/`scan`/`scan_from`/`scan_prefix`/ `count`/`for_each`, `collections`, `next_auto_id`, `transaction`, `read`, `backup`, `compact`, `begin_bulk`/`flush`, `set_relaxed_durability`. ### `begin_bulk` / `BulkScope` `Store::begin_bulk()` opens a thread-local, panic-safe relaxed-durability scope (`BulkScope`); `Store::flush()` ends it. This is what `Db::bulk` uses — and unlike the old global switch, concurrent writers on other threads are unaffected. Scopes nest. ### Durability control `Store::set_relaxed_durability(bool)` toggles per-transaction durability for advanced hosts; `flush()` makes everything durable. ## What's not here No savepoints, no nested write transactions, no per-collection write locks (the write lock is database-wide), no async API — the engine is synchronous; wrap it in your executor. See [administration](/admin/open-close/) for the operational side. Next: [change events](/integrity/events/). ================================================================================ # TTL and expiry # /docs/v0.3.1/integrity/ttl/ ================================================================================ The engine keeps **no clock** — you supply "now". Expired records stay visible until a purge; purging is your scheduled job. ```rust # use corvid::{Db, Value}; # let db = Db::open_in_memory()?; let c = db.collection("sessions"); # let doc = Value::Int(1); c.insert_with_ttl(b"s1", &doc, 1_700_000_000)?; // expiry timestamp (your epoch) c.set_ttl(b"s1", 1_700_000_500)?; // change it c.ttl(b"s1")?; // Option let purged = c.purge_expired(1_700_000_600)?; // delete everything due by now # let _ = purged; # Ok::<(), corvid::Error>(()) ``` ## Semantics - **Expiry is `<= now`, inclusive** — a record due exactly at `now` is purged; one nanosecond-conceptually before is not. - **Boundary correctness**: one-before / exactly-at / one-after the expiry are pinned by tests; `purge_expired` is idempotent. - **Expired but not purged records remain visible** to every read (`get`, queries, scans, search). TTL hides nothing — it schedules deletion, not visibility. Purge is when state changes. - **Timestamps accept i64 extremes** and order correctly; the epoch is yours (unix seconds, milliseconds, anything monotonic). - **Plain writes clear expiry**: `insert`/`insert_batch`/`patch` on a key with an expiry remove the expiry; `set_ttl` sets/replaces without rewriting the document; `set_ttl` on a missing key is `Ok` (and purges nothing). - `insert_with_ttl` writes row + expiry in one commit; the round-trip (set on insert, after plain insert, after overwrite) is conformance-pinned. ## What a purge does `purge_expired(now)` deletes each due record through the normal delete path, so it: - removes the document from **every index** (scalar, unique, vector, text) — no stale entries, - **cascades the document's graph edges** in the same transaction (both namespaces), including stranded TTL entries (expiry on a key with no document), - emits **no change events** (the cascade is silent), - re-reads each due key inside one transaction and deletes only if the timestamp still matches — a record rewritten after collection is **not** deleted by a racing purge. ## Scheduling pattern ```rust # use corvid::{Db, Value}; # let db = Db::open_in_memory()?; let c = db.collection("sessions"); # let doc = Value::Int(1); # c.insert_with_ttl(b"s1", &doc, 1_700_000_000)?; loop { // your clock, your cadence let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH)?.as_secs() as i64; c.purge_expired(now)?; # break; // sleep ... } # Ok::<(), corvid::Error>(()) ``` TTL persists across reopen (reserved namespaces); [dump/load](/admin/dump-load/) carries TTL entries with the documents. Next: [schemas and constraints](/integrity/schema/). ================================================================================ # Backup # /docs/v0.3.1/admin/backup/ ================================================================================ ```rust # use corvid::{Db}; # let dir = tempfile::tempdir().unwrap(); # let db = Db::open(dir.path().join("app.corvid"))?; db.backup(dir.path().join("backup.corvid"))?; # Ok::<(), corvid::Error>(()) ``` `Db::backup(path)` writes a **consistent point-in-time physical copy** of the database from one read snapshot — safe to run while writers are active. ## Rules - **The target must not exist.** An existing file fails with `Error::BackupTargetExists` — backups never overwrite. A mid-copy failure removes the partial destination (best-effort) so debris never masquerades as a backup or blocks future attempts. - The result is an **independently openable database** — open it with `Db::open`, run the full test suite against it. - Physical copy = fast, but **feature-configuration-dependent**: a backup written by a `zstd`-feature build is not readable by a default build (clean per-row `Decode` errors), and vice versa matters only in that direction. Use [dump/load](/admin/dump-load/) — a logical, format-stable transfer — to move between feature configurations or engine versions. ## Scheduling pattern ```rust # use corvid::{Db, Value}; # let dir = tempfile::tempdir().unwrap(); # let db = Db::open(dir.path().join("app.corvid"))?; # let stamp = 1; let path = format!("backups/app-{stamp}.corvid"); db.backup(&path)?; # Ok::<(), corvid::Error>(()) ``` Take backups on a cadence; verify by opening them. The MCP `backup` tool exposes the same operation to agentic clients; corvid-c's golden suite runs `backup_reopens_as_a_live_database` against every release artifact. Next: [dump and load](/admin/dump-load/). ================================================================================ # Bulk loading # /docs/v0.3.1/admin/bulk/ ================================================================================ Three tools, fastest-first: ## `Db::bulk` — relaxed-durability scope ```rust # use corvid::{Db, Value}; # let dir = tempfile::tempdir().unwrap(); # let mut db = Db::open(dir.path().join("app.corvid"))?; db.bulk(|| { let c = db.collection("docs"); for i in 0..100_000u32 { c.insert(&i.to_le_bytes(), &Value::Int(i as i64))?; } Ok(()) })?; # Ok::<(), corvid::Error>(()) ``` Writes inside the closure run under non-fsync durability; one flush at the end. **Crash window**: committed data stays consistent, but in-flight writes may be lost on a crash before the flush. Writes made before an erroring closure *do* persist. A panic unwinds past the flush — rebulk or reopen if that matters. Bulk is thread-local (`Store::BulkScope` under the hood): concurrent writers on other threads keep normal durability. Scopes nest. ## `insert_batch` — one transaction, one fsync ```rust # use corvid::{Db, Value}; # let db = Db::open_in_memory()?; let c = db.collection("docs"); let batch: Vec<(&[u8], &Value)> = (0..1_000) .map(|i| (Box::leak(i.to_le_bytes().into_boxed_slice()) as &[u8], &Value::Int(i))) .collect(); c.insert_batch(&batch)?; # Ok::<(), corvid::Error>(()) ``` All-or-nothing: a unique/schema violation rolls back the whole batch. Duplicates inside a batch are last-write-wins. Right choice when the batch must be atomic or when you can't tolerate a crash window. ## Loop of inserts — the slow baseline Correct, durable per write, ~one fsync per insert. Fine for hundreds; use the above for thousands-plus. ## Measured shape Batch insert of 16-dim-embedding documents (the engine's scaling example): ~16 ms at 1k, ~0.6 s at 100k, ~4.9 s at 1M — bounded per-batch memory. See [performance: scaling](/performance/scaling/). ## After the load - In-RAM indexes maintained during bulk rebuild are as fresh as the writes (they're transactional — no stale windows). - Reclaim file space after bulk-delete cycles with [compaction](/admin/compact/). - Ingesting a dump file? That's [`load`](/admin/dump-load/), which streams buffered and rebuilds definitions. Next: [feature flags](/admin/features/). ================================================================================ # Compaction # /docs/v0.3.1/admin/compact/ ================================================================================ Two distinct compactions exist — don't confuse them: 1. **`Db::compact()`** — file-level space reclamation (below). 2. **Automatic on-disk vector-index compaction** — self-maintained; see [index maintenance](/indexes/maintenance/). ## `Db::compact` ```rust # use corvid::{Db}; # let dir = tempfile::tempdir().unwrap(); # let mut db = Db::open(dir.path().join("app.corvid"))?; let moved = db.compact()?; // bool: whether any data moved # let _ = moved; # Ok::<(), corvid::Error>(()) ``` After heavy deletes, freed pages can remain allocated in the file. `compact()` rewrites the database to reclaim that space — data unchanged, double-compact tolerated (the second is a no-op). **Offline maintenance**: `compact` takes `&mut self` — the engine requires exclusive access. In practical terms: close other handles, or compact from a point in your lifecycle where the `Db` isn't shared. Through [the C ABI](/ffi/functions-admin/), `corvid_compact` checks exclusivity via the derived-handle counter and fails with the FFI-only `CORVID_E_BUSY` while collection/query handles are alive — a deterministic answer, never a hang. ## When to compact - After deleting a large fraction of a collection (file size doesn't shrink on delete; compaction makes it shrink). - After TTL purges of big batches. - Not routinely — it's O(file) offline work; the automatic vector-index compaction handles index bloat on its own. Next: [bulk loading](/admin/bulk/). ================================================================================ # Dump and load # /docs/v0.3.1/admin/dump-load/ ================================================================================ `dump`/`load` is the **logical migration path** — the way data crosses format breaks, feature configurations, and the pre-1.0 API churn. A dump is a version-stamped byte stream of documents plus every definition; loading replays it and rebuilds derived state. ```rust # use corvid::{Db, Value}; # let dir = tempfile::tempdir().unwrap(); # let mut db = Db::open(dir.path().join("old.corvid"))?; # db.collection("docs").insert(b"p1", &Value::Int(1))?; let mut bytes = Vec::new(); db.dump(&mut bytes)?; // whole database, one read snapshot let fresh = Db::open(dir.path().join("new.corvid"))?; fresh.load(&bytes[..])?; // documents + defs rebuild # Ok::<(), corvid::Error>(()) ``` ## What a dump carries Documents, all index definitions (rebuilt from the renamed documents on load — nothing to re-create by hand), schemas, TTL entries, graph edges (replayed through `link_weighted`, so adjacency rebuilds by the end of the load), and auto-id counters. Loading into a **non-empty** database merges records and counters with what's there. ## Format v2 (current) The 12-byte magic **is** the version marker: `CORVIDDUMPv1` (legacy) or `CORVIDDUMPv2` (what `dump` writes today). v2 widens every length/count prefix — byte-field lengths, per-definition field counts, PQ `m`/`k` — from u32 to u64, so a single value, key, string, or field count beyond 4 GiB is representable (v1's writer truncated such lengths silently). | | writer emits | reader accepts | |---|---|---| | v0.1 binaries | v1 | v1 | | v0.2+ binaries | **v2** | **v1 and v2** | One-way by design: an unknown magic (a future v3) is `Error::InvalidDump` in older binaries. The migration story is always *dump with the old binary, load with the new*. ## `load_with_renames` — the `a__b` migration Collection names containing an interior `__` were accepted before audit remediation wave 4 and are rejected since (they could forge engine-internal namespaces). Dumps from old databases still carry such names — a plain `load` fails at index/schema replay with `Error::InvalidName`. The rename map migrates them: ```rust # use corvid::{Db}; # let dir = tempfile::tempdir().unwrap(); # let db = Db::open(dir.path().join("new.corvid"))?; # let bytes: &[u8] = &[]; let mut renames = std::collections::BTreeMap::new(); renames.insert("a__b".to_owned(), "a_b".to_owned()); db.load_with_renames(bytes, &renames)?; # Ok::<(), corvid::Error>(()) ``` Every collection-name occurrence in the stream — documents, index/schema definitions, TTL entries, graph edges, auto-id counters — is mapped before replay, so indexes rebuild under the new name automatically. The contract: - each target must be a valid user name (else `Error::InvalidName` naming the offending target, checked before the stream is read); - no two dump names may load into one output name — two sources sharing a target, or a target colliding with an unmapped dump collection, is `Error::InvalidArgument` (merging keyspaces would silently overwrite documents); - reserved dump names are rejected before mapping (a rename cannot launder an engine namespace); - a map entry whose source never occurs is a no-op. ## Operational notes - `dump` streams records without materializing the corpus, from one read snapshot (catalog walk included — a concurrent TTL commit can't be omitted). - `load` streams the file through buffered reads and rejects engine-reserved names on **every** replay path (including the auto-id counter section). - The MCP `dump`/`load` tools work on files; `load` takes the same rename table as an optional `rename` object. Next: [compaction](/admin/compact/). ================================================================================ # Feature flags # /docs/v0.3.1/admin/features/ ================================================================================ The engine has **no required features** — the default build pulls in only `redb` and is `#![forbid(unsafe_code)]`. Two optional cargo features exist, both OFF by default so the dependency-minimal default build and the WASM size budget stay contracts: ```toml [dependencies] corvid = { git = "https://github.com/corvid-db/corvid", features = ["zstd"] } ``` ## `zstd` — transparent document compression With the feature on, values in user collections whose encoding is **≥ 1 KiB** are compressed (zstd level 3) when written and decompressed on every read path — queries, scans, paging, indexes, TTL, edges, dump/load, and backup all behave identically, just smaller on disk. - Stored rows are self-describing (a reserved leading marker byte `0xFF` no value encoding can produce), so databases written by default (feature-off) builds read fine under a feature-on build, and `dump` output is format-stable v2 either way. - Incompressible values are stored raw (never larger). The known tax: a ≥1 KiB random value pays the compression attempt (~0.85 µs/KiB) and is stored raw anyway. - Engine-internal `__` namespaces (indexes, edges, TTL) stay raw. **Measured ratios** (deterministic corpora, exact byte counts): | Document | Stored | Ratio | |---|---|---| | Structured text map, 50,975 B | 4,242 B | **8.3% (12×)** | | f32 vector array (smooth sin values), 16k dims | 59,933 B | 91.4% (~1.1×) | | Random bytes, 64 KiB | raw | — | The honest headline: **vector payloads barely compress** — IEEE-754 mantissas are near-full entropy even for smooth sequences. zstd is a text/document play; the vector footprint levers are [quantization](/indexes/quantization/) (Binary/Scalar/PQ). **Per-op overhead** (8 KiB document, insert/get): ~+4.9 µs/write, ~+2.6 µs/read for compressible text; raw rows read at parity. **Portability caveat**: backups are physical copies — a backup written by an ON build fails per-row `Decode` under an OFF binary. `dump`/`load` carries raw encodings either way and is the migration path between feature builds. ## `tracing` — structured instrumentation ```toml corvid = { features = ["tracing"] } ``` Structured events at the engine's load-bearing points, via the `tracing` facade (trimmed: `span!`/`event!` only) — attach any tracing-compatible subscriber; events carry `target = "corvid"`. No public API change; when the feature is off, every call site compiles to nothing through a private telemetry shim (CI asserts the default and WASM dependency graphs never contain `tracing`). Instrumented at per-operation/per-page granularity (never per-document) — the full inventory is on the [observability](/admin/observability/) page. Next: [observability](/admin/observability/). ================================================================================ # The MCP sidecar # /docs/v0.3.1/admin/mcp/ ================================================================================ `corvid-mcp` exposes a store to agentic tools (Claude Code, Codex, Cursor, VSCode/JetBrains MCP clients) over **MCP — JSON-RPC on stdio**. It embeds the engine; all protocol code lives in the sidecar, never in the engine. ```sh cargo run -p corvid-mcp -- app.corvid # file-backed; omit path for in-memory ``` Release binaries ship for Linux (x86_64 + aarch64), macOS (Intel + Apple Silicon), and Windows (x86_64) — or build from source. Point an MCP client at the binary. ## The tools (29) `store`, `patch`, `compare_and_set`, `get`, `delete`, `delete_where`, `page`, `search`, `phrase_search`, `count`, `geo`, `join`, `link`, `unlink`, `neighbors`, `in_neighbors`, `traverse`, `create_index`, `create_text_index`, `create_scalar_index`, `create_compound_index`, `create_geo_index`, `backup`, `dump`, `load`, `list_collections`, `insert_auto`, `set_schema`, `get_schema`. Each mirrors its engine operation with default result caps (list tools clamp oversized limits to 10,000). ## `search` — the hybrid builder as JSON ```json { "filter": { ... }, "vector": { ... }, "text": { ... }, "mmr": 0.7, "rrf_k": 60, "select": ["title"], "limit": 10 } ``` The [query builder](/language/query-builder/) as one tool call — filter plus vector and/or text sources, RRF and MMR knobs, projection, limit. Ranking arguments are validated exactly as in Rust (`BadParams` on garbage). ## `set_schema` / `get_schema` `set_schema` declares (or replaces) a collection's schema — a fields array of `{name, type (any|bool|int|float|text|bytes|vector|array|map), required?, unique?}` — validated on subsequent stores (type and required violations, duplicate unique values). `get_schema` returns the declared fields, or `{fields: null}` when none is declared; `fields: []` is a declared empty schema (distinct from undeclared). Present-but-non-boolean flags are `BadParams` errors naming the flag. ## Value conversion JSON ↔ engine values convert through explicit wrappers where JSON is ambiguous: `{"$vector": [...]}` for vectors, `{"$bytes": "..."}` for bytes. Int/float distinction survives; u64 beyond i64 is lossy through f64; vector components are f32. Nested wrappers work; malformed wrappers fall back to maps. ## Wire details - Envelope methods: `initialize`, `ping`, `tools/list` (all 29 with JSON schemas), `tools/call`. Notifications produce no response. - Framing: line-delimited JSON-RPC on stdio, blank/CRLF lines ignored, frame size capped (`MAX_FRAME_SIZE`; over-limit frames refused, session survives). - Errors: `UnknownTool`, `BadParams` (bad/missing params), `Engine` (typed engine errors, name-keyed). ## In-process testing The sidecar's whole surface is covered by an in-process duplex-I/O suite (78 tests) — `Server::handle` is transport-agnostic, so the MCP layer is tested without a process boundary. The surface manifests behind it are the source of the [construct reference](/reference/constructs/)'s MCP section. Next: [performance](/performance/overview/). ================================================================================ # Observability # /docs/v0.3.1/admin/observability/ ================================================================================ corvid observability has three layers, none requiring a server: 1. **Query introspection** — `explain()` / `plan_shape()` / `plan()` (always on). 2. **Structured events** — the `tracing` cargo feature (see [feature flags](/admin/features/)). 3. **Counters via subscribers** — aggregate the events; no metrics-export subsystem (deliberate non-goal). ## Query introspection ```rust # use corvid::{Db, field}; # let db = Db::open_in_memory()?; let docs = db.collection("docs"); let mut q = docs.query().filter(field("a").exists()); let plan = q.explain()?; // human-readable, pinned to the real decision let shape = q.plan_shape()?; // PlanShape enum # let _ = (plan, shape); # Ok::<(), corvid::Error>(()) ``` `PlanShape` labels what drove the candidate set: `AnnIndex`, `TextIndex`, `IndexedWindow`, `SortIndex`, `StreamingTopK`, `Scan`. `QueryPlan` (via `plan()`) is identity-hashable — key a `PlanCache` on it to cache prepared per-shape work (never results). ## The `tracing` event inventory With `features = ["tracing"]`, these events fire (target `corvid`): | Event | Carries | |---|---| | Index backfill spans | collection, index family (scalar/compound/text/geo/vector), page size, cursor progress; completion event with page count | | Compactions (in-memory + on-disk) | dead/live trigger math on the actual crossing, rebuild outcome | | Lazy index-build resume / adjacency rebuild | including whether the marker was absent or stale-shaped | | Plan-shape selection (one per query) | which arm drove candidates + candidate count | | Order-index walk tail scan | the on-exhaustion fallback | | Edge-cascade rebuild fallback | corrupt adjacency row recovery | | Semantic cache | hit/miss with the deciding distance | Two deliberate label divergences from `PlanShape`: `indexed_window` events carry no family discriminator (the scalar/compound/geo/or kind is `plan_shape()`'s to report), and `stream_scan` is finer than `PlanShape::Scan` (it splits the bounded streaming filter pass from the materializing fallback). ## Counters A subscriber aggregating `plan_shape` per shape **is** the index-probe counter per shape; `semantic_cache_hit`/`semantic_cache_miss` subscribers are the cache-hit-rate counters. Deferred (with triggers): plan-cache hit counters (`PlanCache` is host-side state — the engine sees no traffic to count) and any metrics-export subsystem. ## What there isn't No `.profile()` (the events above carry what a profiler would; reopens only if a need outgrows them), no metrics export, no server to poll. The engine is a library — your process's observability stack is the stack. Next: [the MCP sidecar](/admin/mcp/). ================================================================================ # Opening and closing # /docs/v0.3.1/admin/open-close/ ================================================================================ ```rust use corvid::Db; let db = Db::open("app.corvid")?; // file-backed; created if absent let db = Db::open_in_memory()?; // isolated ephemeral instance ``` ## `Db::open` - Creates the file (and parent expectation: a missing parent directory is an error, not an implicit mkdir). - Takes the storage engine's **exclusive lock**: a second `Db` handle to the same file in the same or another process fails with `Error::Database`. One file, one handle, one process. - Refuses incompatible files via the on-disk format marker (`Error::IncompatibleFormat`) — an old file is never silently misread; migrate with [dump/load](/admin/dump-load/). ## `Db::open_in_memory` A purely in-memory instance — isolated from every other instance (including other in-memory ones), gone when dropped. Useful for tests, caches, scratch state. ## Closing There is no explicit close: persistence is durable **per transaction**, so dropping the handle (or exiting the process) is safe at any point. `Db` is `Send + Sync` — share it behind an `Arc` across threads; writes serialize on the storage engine's single-writer lock, readers get MVCC snapshots. ## What persists Reopening a file restores everything: - documents, in every user collection, - [index definitions and on-disk index state](/indexes/overview/) (on-disk families are ready with no rebuild; in-RAM families rebuild lazily), - [schemas](/integrity/schema/), [TTL entries](/integrity/ttl/), [graph edges](/graph/overview/) and their adjacency, auto-id counters. ## Listing collections ```rust # use corvid::{Db}; # let db = Db::open_in_memory()?; let names = db.collections()?; // Vec, user collections in name order # let _ = names; # Ok::<(), corvid::Error>(()) ``` Engine-reserved `__` namespaces (edges, TTL, index definitions, adjacency) are excluded — you see exactly your collections. A collection that was never written may not appear (creation is lazy, on first write). ## The MCP and binding surfaces The same lifecycle is exposed everywhere: `corvid_open`/`corvid_open_memory` + `corvid_close` in [the C ABI](/ffi/handles/), `Db.open`/`openMemory` in [corvid-node](/bindings/corvid-node/), the `open_server` MCP helpers. Next: [backup](/admin/backup/). ================================================================================ # FFI crossing cost # /docs/v0.3.1/performance/ffi-crossing/ ================================================================================ The [C ABI](/ffi/overview/) exists so bindings pay "zero parsing, bounded crossing cost". The engine measured that phrase directly: the same four shapes through the ABI — a C consumer compiled at bench time against the committed, drift-gated `corvid.h`, linked against the release cdylib — and natively in-process in Rust, on identical deterministic corpora (2000 docs of `{i: int, txt: 4 tokens, vec: [64 × f32]}`). | Shape (iterations) | FFI (through the ABI) | native Rust | ratio | |---|---|---|---| | put — construct + insert (10k) | 20.8 µs/op | 20.9 µs/op | **1.00×** | | get — point-get + read 1 field (100k) | 1510 ns/op | 1508 ns/op | **1.00×** | | scan — full 2000-row pass (200) | 883 µs/pass | 896 µs/pass | **0.99×** | | hybrid — vector+text RRF query, k=10/source, drained (500) | 3.02 ms/query | 2.97 ms/query | **1.02×** | Confirmation run agreed within ±2% (0.99×/1.00×/0.99×/0.99×). ## Method Medians of 5 rounds after a discarded warmup; the C child does its own setup then N iterations; the driver times the whole child and subtracts a zero-iteration baseline (spawn + setup cancel), so there is no timing code in C and the file stays portable ISO C. The `put` row includes document **construction** on both sides — a binding builds a value per call — so it prices the honest end-to-end path. ## Reading The crossing cost is invisible at every shape: each call is a plain C-ABI jump behind engine work that dominates (an insert's transaction, a scan's decode loop, a hybrid query's candidate fusion). There is no serialization anywhere on the path by construction — typed handles in, borrowed views out. The measured bound on "bounded crossing cost" is **≤ ±2%, i.e. noise**. One consequence: the ABI's exclusion of direct `vector_search`/ `text_search` entry points (see [ABI exclusions](/ffi/stability/)) stays closed on this evidence — the builder path through the ABI is at parity with native, so per-call builder overhead cannot be a workload problem at this scale. Provenance: Apple M1 Max, rustc 1.91.1, clang 17, release cdylib + `-O2` C consumer. Single-machine — the claim is the **ratio** column, not the absolute times. Next: [the C ABI](/ffi/overview/). ================================================================================ # The numbers # /docs/v0.3.1/performance/numbers/ ================================================================================ All numbers: Apple M1 Max, criterion, deterministic corpora — means with 95% CI unless noted. Single-machine: compare relatively. Method and caveats on [the previous page](/performance/overview/). ## Current full-suite baseline (2026-08-31) | Bench | Mean | 95% CI | |---|---|---| | value_encode | 409.44 ns | [408.99, 411.51] | | value_decode | 872.73 ns | [872.36, 876.42] | | hnsw_build_2k_64d | 125.92 ms | [125.73, 126.11] | | hnsw_search_2k_64d | 19.04 µs | [18.97, 19.10] | | hnsw_build_pq_2k_64d | 365.69 ms | [363.03, 369.07] | | hnsw_search_pq_2k_64d | 37.79 µs | [36.85, 39.14] | | pq_train_2k_64d (parallel) | 64.93 ms | suite context | | pq_train_10k_128d (parallel) | 381.68 ms | suite context | | bm25_exact_2k | 8.033 ms | [7.968, 8.143] | | bm25_indexed_2k | 490.10 µs | [483.54, 498.24] | | dot_768d | 81.07 ns | [80.42, 81.68] | | l2_768d | 84.26 ns | [82.48, 84.98] | | cosine_768d | 224.29 ns | [220.86, 231.60] | | edge_link_10k | 392.98 ms | suite context | | edge_delete_sweep_100 | 49.70 ms | suite context | | delete_heavy/delete_half_2p5k | 230.39 ms | suite context | | delete_heavy/insert_unique_scalar_5k | 185.72 ms | suite context | | compound_prefix_scan/eq_leading_only_5k | 251.09 µs | suite context | | selective_window_verify/eq_50_of_5k | 236.20 µs | suite context | | selective_window_verify/eq_500_of_5k | 619.56 µs | suite context | | order_by_indexed_5k/asc_limit20 | 312.61 µs | suite context | | order_by_indexed_5k/desc_limit20 | 1.009 ms | suite context | | create_text_index_ondisk_5k | 230.22 ms | suite context | | create_vector_index_ondisk_2k_8d | 402.82 ms | suite context | Named deltas vs the prior baseline: `bm25_exact_2k` +5.0% (the CJK-aware tokenize pass on a latin corpus — documented residual, inside the guard); the delete/edge family sits above its recorded isolated AFTERs (the suite-vs-isolated context gap, unchanged code, isolated re-probes confirmed); `value_decode` +3.0% and the distance kernels +1–5% are ambient machine drift. **No regression attributable to code; the guard holds.** ## The optimization program's headline deltas Each row's BEFORE is the recorded pre-change baseline (provenance in the engine's BENCHES.md; conventions on the [overview](/performance/overview/)). **Verify-candidates batching** (dense indexed windows verify with one ordered walk instead of per-key point-gets): | Bench | BEFORE | AFTER | Δ | |---|---|---|---| | eq_500_of_5k (10% density) | 745.14 µs | 576.25 µs | **−22.7%** | | eq_50_of_5k (1% density) | 221.21 µs | 229.82 µs | +3.9% (point-gets kept; within guard) | **Sort indexes** (filterless `order_by` over a complete scalar index): | Bench | BEFORE | AFTER | Δ | |---|---|---|---| | asc_limit20 | 2.642 ms | 289.5 µs | **−89.0%** | | desc_limit20 | 2.795 ms | 1.024 ms | **−63.4%** | **Compound prefix-only windows** (the `all_docs_indexed` flag): | Bench | BEFORE | AFTER | Δ | |---|---|---|---| | eq_leading_only_5k | 1.540 ms | 232.41 µs | **−84.9% (6.6×)** | **Edge adjacency** (O(degree) delete cascades via endpoint-first derived namespaces): | Bench | BEFORE | AFTER | Δ | |---|---|---|---| | edge_delete_sweep_100 | 241.0 ms | 40.5 ms | **~5.9× — O(degree) vs O(E)** | | delete_half_2p5k | 566.8 ms | 194.5 ms | **~2.9×** | | edge_link_10k | 273.5 ms | 359.4 ms | **+40% — RATIFIED** | The ratified trade: two extra rows per link buy O(degree) cascades — deletes were the workload-blocking hazard. The alternative shapes (consolidated per-endpoint values, pure-lazy adjacency) were measured and rejected with numbers. **Parallel PQ training** (identical codebooks, bit-for-bit): | Bench | Sequential | Parallel | Δ | |---|---|---|---| | pq_train_2k_64d | 177.2 ms | 67.4 ms | **2.6×** | | pq_train_10k_128d | 1465.0 ms | 356.2 ms | **4.1×** | **Graph reads, endpoint-direct** (parity verdict — kept for the shared layout, not speed): | Bench | BEFORE | AFTER | Δ | |---|---|---|---| | hub_out_knows (313 rows) | 37.08 µs | 37.12 µs | +0.1% | | hub_in_knows | 29.58 µs | 29.96 µs | +1.3% | | traverse_hub_2hops | 570.5 µs | 581.4 µs | +1.9% | Next: [scaling](/performance/scaling/). ================================================================================ # Performance: reading these numbers # /docs/v0.3.1/performance/overview/ ================================================================================ corvid's performance story is a **durable record**, not marketing: every "before/after" number of the optimization programs is committed in the engine repo (docs/BENCHES.md), with provenance, and a standing bench rule: > No existing bench regresses beyond noise (>5%), and no "faster" claim > without a before/after table whose provenance is stated. This section restructures that record as prose and tables. The caveats apply to everything here: ## Machine and method - **Machine:** Apple M1 Max (MacBookPro18,2), Darwin arm64, 32 GiB. - **Toolchain:** rustc 1.91.1; MSRV 1.88. - **Method:** `cargo bench -p corvid --bench engine` (criterion, bench profile, in-memory `Db`, deterministic corpora — seeded index math, no `rand`). - Numbers are criterion **means with 95% CI** unless a table says median. - **Single-machine numbers: compare relatively, not absolutely.** Your hardware, corpus, and dimensions will differ; the *shape* (constant vs linear, indexed vs scan) is what transfers. ## Reading conventions in these tables - "BEFORE provenance" — where the before number came from. Some benches were backported to the pre-change tree in a throwaway worktree (the bench did not exist at the base). - "suite context" vs "isolated" — a bench run inside the full suite measures differently from one run alone (machine state, cache); multi-hundred-ms delete-path benches sit a few percent higher in suite context, documented and re-probed. - "RATIFIED" — a regression accepted as a permanent, deliberate trade by explicit decision (e.g. the link/edge-cascade trade in [numbers](/performance/numbers/)). ## Where to go - [Numbers](/performance/numbers/) — the current full-suite baseline table and the program's before/after deltas. - [Scaling](/performance/scaling/) — how operations behave at 1k / 100k / 1M / 50M, and which wall each index family removes. - [Quantization guidance](/performance/quantization-guidance/) — the compression/time/recall trade with the measured tables. - [FFI crossing cost](/performance/ffi-crossing/) — proof the C ABI adds nothing measurable over native Rust. Next: [the numbers](/performance/numbers/). ================================================================================ # Quantization guidance # /docs/v0.3.1/performance/quantization-guidance/ ================================================================================ The compression/time/recall trade with the engine's measured tables. Method and caveats: [reading these numbers](/performance/overview/). ## The volume lever: Binary quantization Same 2,000×768d corpus, same graph params, cosine, k=10, ef=64: | Storage | search | vs None | |---|---|---| | None (f32) | 239.8 µs | — | | **Binary** (96 packed bytes; Hamming) | 4.72 µs | **50.8× faster** | | Scalar (reconstruct to f32) | 306.8 µs | 1.28× slower | Binary's packed-byte Hamming path is the throughput answer for big corpora (~32× less traffic *and* a cheaper kernel). Scalar pays a per-evaluation decode (with allocation) and lands *slower* than full precision at this dimension — compression is not automatically speed. ## PQ's premiums (2000×64d, m=16, k=256) | | None (f32) | PQ | premium | |---|---|---|---| | hnsw build | 124.9 ms | 367.9 ms | 2.9× slower | | hnsw search | 19.1 µs | 35.8 µs | 1.9× slower | | vector payload | 256 B/doc | 16 B/doc | **16× smaller** | Recall margins (pinned): public path (`vector_search`, over-fetch + exact rerank) measures **1.0** (floor ≥0.7); the direct `Hnsw` API corpus measures 0.56, identical at ef 100/200/400 (floor ≥0.55 — the thin margin is deliberate; the corpus is deterministic so the value cannot drift). The residual gap is codebook resolution, not graph reach. ## Why not faster kernels: the SIMD closure (measured) LLVM already auto-vectorizes `dot`/`l2_squared` 4-wide (verified in release assembly). Hot kernels hold 62–83% of the same-shape read ceiling across 64–3072 dimensions with no small-dim cliff. The measured "faster" shapes: | 768d | time | vs shipped | |---|---|---| | shipped dot (8 lanes) | 78.15 ns | — | | 16 accumulator lanes | 55.36 ns | −29% — **declined** (changes f32 summation order → not bit-identical) | | `mul_add` (fused) | 97.01 ns | +24% slower AND de-vectorized | Every faster shape reassociates `f32` summation, which the bit-exactness oracle (recall floors, reproducible codebooks, twin-build equality) declines. Volume scans are memory-side anyway: beyond cache, scans hold 41–42 GB/s against a 43–58 GB/s streaming band — a 29%-faster kernel cannot lift a DRAM-resident scan past it. **The available throughput lever already ships: Binary quantization, 50.8× at 768d.** ## zstd is not a vector feature | Payload | Ratio | |---|---| | Structured text document | **12×** (8.3% of raw) | | f32 vector payload (even smooth values) | ~1.1× — barely compresses | IEEE-754 mantissas are near-full entropy; if you need smaller vectors, the levers are Binary/Scalar/PQ, not zstd (which is a text/document play — see [feature flags](/admin/features/)). ## Decision summary 1. `None` until footprint/throughput forces a choice. 2. Volume scans → **Binary**. 3. Tight RAM/disk budget → **PQ** (validate recall on your corpus). 4. Scalar only with a measurement on your dimension. 5. Text documents too big → `zstd` feature (not for vectors). Next: [FFI crossing cost](/performance/ffi-crossing/). ================================================================================ # Scaling characteristics # /docs/v0.3.1/performance/scaling/ ================================================================================ From the engine's scaling example (file-backed, 16-dim embeddings), after the streaming/index optimizations. One machine — the point is the *shape*: | Operation | 1k | 100k | 1M | memory | |---|---|---|---|---| | batch insert | ~16 ms | ~0.6 s | ~4.9 s | bounded (per batch) | | `count()` (no filter) | µs | µs | ~12 µs | O(1) — maintained counter | | point `get` | µs | ~15 µs | ~22 µs | O(1) | | filtered `count` / `group_count` | <1 ms | ~55 ms | ~0.55 s | **constant** (streamed) | | `order_by` + `limit` | ~1 ms | ~0.13 s | ~0.58 s | **bounded** (≈ page size) | | `text_search` (indexed) | µs | µs | µs | — (after build) | | HNSW build (in-memory) | — | ~15 s | minutes | in-RAM | ## What scales **Constant or bounded memory, O(1)/O(n) time** — these hold at 50M (slower, but no OOM): - storage and point operations, - `count`/`len` (O(1) maintained counters), - streamed aggregates (constant memory, linear time), - ordered pagination and keyset [cursors](/language/pagination/), - the bounded-heap exact vector search (streamed). ## The walls at 1M–50M — and what addresses each **In-memory index build/rebuild.** The in-memory HNSW and inverted index live in RAM and rebuild on open; at 1M the HNSW build is minutes, at 50M they don't fit. → The [on-disk variants](/indexes/on-disk/) (`create_vector_index_ondisk*`, `create_text_index_ondisk`) store state as storage records: an operation touches only what it needs, memory is bounded by the operation, the index persists with no rebuild. **Unindexed `filter`/`order_by`** are O(n) scans (constant memory, linear time). → The [scalar / compound / geo indexes](/indexes/overview/) make selective queries sub-linear; the builder picks the most selective index and falls back to the bounded scan when none is selective enough. Scalar example at 1M: ~662 ms scan → ~3 ms eq / ~0.5 ms for a 100-row range. **Exact (unindexed) search** is O(n) time — correct and OOM-free (streamed), but you want an index past ~100k. → Any [vector index](/indexes/vector/); quantize ([binary/PQ](/performance/quantization-guidance/)) when volume matters. So: storage, point ops, counts, streamed aggregates, and ordered pagination scale to 50M with bounded memory; large-scale *search* and *selective filters* are what the index families exist for — leaving only the in-memory index variants' RAM build as a deliberate small-scale convenience. Next: [quantization guidance](/performance/quantization-guidance/). ================================================================================ # Errors and NULL discipline # /docs/v0.3.1/ffi/errors/ ================================================================================ ## The status channel - Functions report success/failure with `corvid_status` (`CORVID_OK` / `CORVID_ERR`), or with a NULL return where a handle/buffer was expected. - On failure, the detail is in **thread-local** storage: - `corvid_last_error_code()` — one of the 19 codes (0 = nothing failed on this thread); - `corvid_last_error_message(&len)` — the engine's human-readable text, NUL-terminated for convenience. - **Failure signals are always paired with a freshly recorded last error** — a `CORVID_ERR` status or a failure-NULL sets the thread-local code and message as its first act. - **Message lifetime:** valid until the *next failing* corvid call on the same thread (or thread exit). Copy it if you need it longer. Successful calls do **not** clear the last error — read it immediately after the failure that interests you. - Errors never leave partial side effects Rust would not allow — transactions are atomic per call (a `CORVID_ERR` from `corvid_put_many` means the whole batch rolled back). - The engine never panics on user input; the FFI additionally converts any residual panic to `CORVID_ERR` + message (defensive, not contract). ## Absence is a success "Optional value" results — `corvid_get`, `corvid_schema`, `corvid_query_min`, `corvid_query_max` — use an **out-parameter plus status**: `CORVID_OK` + `*out = value`, or `CORVID_OK` + `*out = NULL` for "no such value" (a missing document, an undeclared schema, no comparable value). Absence is never an error and never signalled by a bare NULL return — only unambiguous handles/buffers (open, run, constructors, auto-key) return NULL for failure. ## The NULL discipline (never UB) - An unexpected NULL — NULL handle, NULL required out-param, NULL data pointer with nonzero length — returns `CORVID_ERR` with `CORVID_E_ARGUMENT`. **Never UB.** - **Non-status functions** (no `corvid_status` return): `corvid_value_type`, `corvid_value_as_bool/as_int/as_float`, `corvid_value_len`, the `_ref` trio, and all five `_next` cursors follow the same discipline through a defined inert value — `0` / `*ok = 0` / `NULL` pointer / `0` (= exhausted) — **and** record `CORVID_E_ARGUMENT` in the thread-local last error. Never UB, never a status return. - Nullable-by-contract pointers carry semantics: `corvid_compare_and_set`'s `expected`/`replacement` (absent / delete), `corvid_page`'s `after` (start), `corvid_update`'s `current`/`*out` (absent / delete), optional out-params (`existed_out`, `removed_out`, `moved_out`, the `len_out`s, `doc_out`). - Empty (pointer, length 0) is distinct from NULL and legal for keys, names, text, bytes, vectors — the engine accepts empty keys and documents. - `corvid_free(NULL)` and every `_free(NULL)` are no-ops. - UTF-8-requiring strings with invalid encoding: `CORVID_E_ARGUMENT` (checked, copied, never UB). ## The error codes Codes 1–18 map 1:1 onto the engine's `corvid::Error` variants; 19 (`CORVID_E_BUSY`) is FFI-only (compact-while-derived-handles-open). The full frozen table with meanings is the generated [error codes reference](/reference/error-codes/). The mapping is pinned by a variant-inventory snapshot test — adding, removing, or renaming an engine variant fails the FFI suite until the mapping is maintained; new variants append code 20+, never fill a gap. Next: [lifecycle & collection functions](/ffi/functions-lifecycle/). ================================================================================ # Functions: admin # /docs/v0.3.1/ffi/functions-admin/ ================================================================================ Path-based administrative operations, all wrapping `corvid::Db` methods. Dump and load open the files themselves and hand them to the engine's generic `Read`/`Write` methods; `corvid_backup` passes the path through to the engine, which opens the backup file itself. Semantics on [dump/load](/admin/dump-load/) and [backup](/admin/backup/). ```c corvid_status corvid_dump_to_path(corvid_db *db, const char *path, size_t path_len); ``` Logical, version-stamped dump of the whole database (documents, index/schema/TTL definitions, graph edges, auto-id counters) to `path`, from one read snapshot. ```c corvid_status corvid_load_from_path(corvid_db *db, const char *path, size_t path_len); corvid_status corvid_load_from_path_with_renames(corvid_db *db, const char *path, size_t path_len, const char *const *old_names, const char *const *new_names, const size_t *old_lens, const size_t *new_lens, size_t count); ``` Replay a dump into this database — the plain form equals `load_with_renames` with an empty map. The rename map is the migration path for legacy `__`-containing collection names: invalid targets fail with `CORVID_E_INVALID_NAME` before reading; two-sources-one-target collisions fail with `CORVID_E_ARGUMENT`. ```c corvid_status corvid_backup(corvid_db *db, const char *path, size_t path_len); ``` Consistent point-in-time physical backup to a **fresh** file (an existing target fails with `CORVID_E_BACKUP_TARGET_EXISTS`); safe while writers are active. Physical means feature-configuration-dependent — use dump/load to move between feature builds. ```c corvid_status corvid_compact(corvid_db *db, int *moved_out); ``` Reclaim file space after heavy deletes (offline maintenance). `*moved_out` (nullable) reports whether any data moved. The engine's `Db::compact` needs `&mut self` — **exclusivity** — so this call requires every handle derived from this `db` (collections, queries, anything holding an engine reference) to be freed first. The gate: an FFI-owned derived-handle counter (incremented on handle creation, decremented on free) at exactly 1 (the db handle itself) **and** sole `Arc` ownership (`Arc::get_mut`). Otherwise the call fails with the FFI-only `CORVID_E_BUSY` — a deterministic answer, never a hang or UB. Consequence for bindings: to compact, reach a quiescent point (drop collection/query handles), call compact, then rebuild handles. Threads still holding handles keep `CORVID_E_BUSY` as the answer. Next: [ownership & transfer rules](/ffi/ownership/). ================================================================================ # Functions: aggregations & mutations # /docs/v0.3.1/ffi/functions-data/ ================================================================================ ## Aggregations (11) Every aggregate **consumes the query** and executes on one read snapshot over the filtered set — sources, ranking, limit/offset/select are ignored. ```c corvid_status corvid_query_count(corvid_query *q, size_t *out); ``` O(1) when unfiltered (maintained counter). ```c corvid_status corvid_query_count_distinct(corvid_query *q, const char *field, size_t field_len, size_t *out); ``` Distinct values by the canonical group key (text bare; int/float/bool type-tagged; missing/containers ignored). ```c corvid_status corvid_query_sum(corvid_query *q, const char *field, size_t field_len, double *out); corvid_status corvid_query_avg(corvid_query *q, const char *field, size_t field_len, double *out, int *has_value); ``` Missing/non-numeric skipped; `avg` sets `*has_value = 0` when no numeric values existed. ```c corvid_status corvid_query_min(corvid_query *q, const char *field, size_t field_len, corvid_value **out); corvid_status corvid_query_max(corvid_query *q, const char *field, size_t field_len, corvid_value **out); ``` Min/max comparable value, as an **OWNED** value handle. Absence is a success: `CORVID_OK` + `*out == NULL` when no comparable value exists. ```c corvid_groupiter* corvid_query_group_count(corvid_query *q, const char *field, size_t field_len); corvid_groupiter* corvid_query_group_sum(corvid_query *q, const char *group_field, size_t group_field_len, const char *value_field, size_t value_field_len); corvid_groupiter* corvid_query_group_avg(corvid_query *q, const char *group_field, size_t group_field_len, const char *value_field, size_t value_field_len); int corvid_groupiter_next(corvid_groupiter *it, const char **key_out, size_t *key_len_out, double *value_out); void corvid_groupiter_free(corvid_groupiter *it); ``` `(group key, value)` pairs in ascending group-key byte order. `group_count`'s value is exact in a `double` up to 2^53. Group keys use the canonical tagged form. ## Mutations (13) All wrap `corvid::Collection` methods; document inputs are CLONED. ```c corvid_status corvid_insert(corvid_coll *c, const uint8_t *key, size_t key_len, const corvid_value *doc); corvid_status corvid_put_many(corvid_coll *c, const corvid_kv *items, size_t count); ``` `put_many` is the bulk fast path — one commit instead of N; whole batch rolls back on schema/unique violation; duplicates follow last-write-wins. ```c uint8_t* corvid_insert_auto(corvid_coll *c, const corvid_value *doc, size_t *key_len_out); ``` Fresh zero-padded 20-digit key; the key bytes are returned — **free with `corvid_free`**. NULL + error on failure (a failed insert does not burn an id). ```c corvid_status corvid_update(corvid_coll *c, const uint8_t *key, size_t key_len, corvid_update_fn fn, void *ctx); ``` Read-modify-write via callback: `fn` receives the current document (borrowed; NULL when absent — not an error) and produces the replacement (owned, consumed) or a deletion (`*out = NULL`). A non-`CORVID_OK` return aborts with `CORVID_E_ARGUMENT` and nothing is written. **Not linearizable** against concurrent writers — use CAS when that matters. Callbacks must not make reentrant corvid calls. ```c corvid_status corvid_patch(corvid_coll *c, const uint8_t *key, size_t key_len, const corvid_value *patch); ``` Merge top-level fields (creating if absent); non-map either side replaces with `patch`. ```c corvid_status corvid_compare_and_set(corvid_coll *c, const uint8_t *key, size_t key_len, const corvid_value *expected, /* nullable */ const corvid_value *replacement, /* nullable */ int *applied_out); ``` Atomic conditional write. Nullability is semantic: `expected == NULL` means "must be absent"; `replacement == NULL` means "delete if it matches". `*applied_out` = 0 on a failed compare is **not an error**. Equality is the engine's semantic value equality (NaN==NaN, −0.0==0.0, element-wise containers). ```c corvid_status corvid_delete(corvid_coll *c, const uint8_t *key, size_t key_len, int *existed_out); /* nullable out */ corvid_status corvid_delete_where(corvid_coll *c, corvid_pred *pred, /* CONSUMED */ size_t *removed_out); corvid_status corvid_delete_batch(corvid_coll *c, const uint8_t *const *keys, const size_t *key_lens, size_t count, size_t *removed_out); ``` Deleting a key cascades its graph edges in the same transaction (including edges dangling on a never-existing key). ```c corvid_status corvid_insert_with_ttl(corvid_coll *c, const uint8_t *key, size_t key_len, const corvid_value *doc, int64_t expires_at); corvid_status corvid_set_ttl(corvid_coll *c, const uint8_t *key, size_t key_len, int64_t expires_at); corvid_status corvid_get_ttl(corvid_coll *c, const uint8_t *key, size_t key_len, int64_t *expires_at_out, int *has_ttl); corvid_status corvid_purge_expired(corvid_coll *c, int64_t now, size_t *purged_out); ``` The engine keeps no clock — `now`/`expires_at` are the caller's epoch. `*has_ttl = 0` is absence (not an error). Expiry is `<= now` inclusive; see [TTL](/integrity/ttl/). Next: [reads & indexes](/ffi/functions-reads/). ================================================================================ # Functions: graph & geo # /docs/v0.3.1/ffi/functions-graph-geo/ ================================================================================ ## Graph (7) Directed property graph over document keys; endpoints need not exist as documents. All wrap `corvid::Collection` methods — semantics on [graph](/graph/overview/). ```c corvid_status corvid_link(corvid_coll *c, const uint8_t *from, size_t from_len, const char *relation, size_t rel_len, const uint8_t *to, size_t to_len); ``` Idempotent directed edge with default weight 1.0 (a plain link overwrites a prior weighted edge's weight). ```c corvid_status corvid_link_weighted(corvid_coll *c, const uint8_t *from, size_t from_len, const char *relation, size_t rel_len, const uint8_t *to, size_t to_len, double weight); corvid_status corvid_unlink(corvid_coll *c, const uint8_t *from, size_t from_len, const char *relation, size_t rel_len, const uint8_t *to, size_t to_len, int *removed_out); ``` `unlink` removes the edge and its reverse atomically; `*removed_out` (nullable) reports whether the forward edge existed — false is not an error. ```c corvid_strs* corvid_neighbors(corvid_coll *c, const uint8_t *from, size_t from_len, const char *relation, size_t rel_len); corvid_strs* corvid_in_neighbors(corvid_coll *c, const uint8_t *to, size_t to_len, const char *relation, size_t rel_len); corvid_geohits* corvid_neighbors_weighted(corvid_coll *c, const uint8_t *from, size_t from_len, const char *relation, size_t rel_len); ``` Out-/in-edge endpoints in key order as a strs cursor. `neighbors_weighted` returns `(target, weight)` pairs through the geohits cursor — `distance_km` carries the edge weight (1.0 for unweighted edges); its `doc_out` is always NULL. ```c corvid_strs* corvid_traverse(corvid_coll *c, const uint8_t *start, size_t start_len, const char *relation, size_t rel_len, size_t hops); ``` BFS up to `hops` hops: reachable nodes excluding `start`, each once, BFS order; `hops == 0` yields nothing; cycles terminate. One read snapshot covers the walk. ## Geo & shared string iterators (7) The three geo queries return a geohits cursor (nearest-first for radius/nearest; key order for bbox). A location field holds `[lat, lon]` or a `lat`/`lon` map; invalid points are skipped. Distances are haversine kilometres. Semantics on [geo](/geo/overview/). ```c corvid_geohits* corvid_geo_within_radius(corvid_coll *c, const char *field, size_t field_len, double lat, double lon, double radius_km); corvid_geohits* corvid_geo_within_bbox(corvid_coll *c, const char *field, size_t field_len, double min_lat, double min_lon, double max_lat, double max_lon); corvid_geohits* corvid_geo_nearest(corvid_coll *c, const char *field, size_t field_len, double lat, double lon, size_t k); ``` `geo_within_bbox` validates bounds at entry (latitude `[-90, 90]`, longitude `[-180, 180]`, NaN rejected, inverted latitude rejected) with `CORVID_E_ARGUMENT`; `min_lon > max_lon` wraps the antimeridian (matches both ranges; exact, unaccelerated). bbox hits carry the **0.0 sentinel** in `distance_km` (no center). `geo_nearest` is exact (expanding radius); `k == 0` yields nothing. ```c int corvid_geohits_next(corvid_geohits *h, corvid_geohit *out, const corvid_value **doc_out); void corvid_geohits_free(corvid_geohits *h); int corvid_strs_next(corvid_strs *s, const char **str_out, size_t *len_out); void corvid_strs_free(corvid_strs *s); ``` `geohits_next`: 1 fetched, 0 exhausted; `out->key` BORROWED until the next call or free; `*doc_out` (nullable pointer) is the likewise-borrowed full document — NULL for `neighbors_weighted` cursors. `strs_next` hands out binary-safe borrowed byte strings (graph keys keep arbitrary bytes). Next: [admin functions](/ffi/functions-admin/). ================================================================================ # Functions: lifecycle & values # /docs/v0.3.1/ffi/functions-lifecycle/ ================================================================================ Conventions used throughout the function pages: `corvid_status` return unless stated; `(const char* s, size_t len)` = borrowed, binary-safe, UTF-8 where the engine takes `&str`; `NULL` pointer with `len > 0` is `CORVID_E_ARGUMENT`, `NULL` with `len == 0` is empty only where marked *nullable*; `const corvid_value*` inputs are **CLONED** — the caller keeps ownership. ## Lifecycle & errors (8) ```c uint32_t corvid_ffi_version(void); ``` Returns `1`. No engine counterpart — pure ABI versioning. Bindings verify this before anything else. ```c corvid_db* corvid_open(const char *path, size_t path_len); ``` Open (creating if absent) a file-backed database. Wraps `Db::open`. Returns the handle, or NULL + `CORVID_E_DATABASE` / `CORVID_E_INCOMPATIBLE_FORMAT` / `CORVID_E_IO` (non-UTF-8 paths answer `CORVID_E_ARGUMENT` — the universal UTF-8 rule). ```c corvid_db* corvid_open_memory(void); ``` In-memory database. Wraps `Db::open_in_memory`. ```c corvid_status corvid_close(corvid_db *db); ``` Releases the handle's reference. Persistence is durable per-transaction — no explicit close/flush exists in the engine either. Freeing the db while rows/iterators from it are live is fine (they own their data). ```c corvid_err corvid_last_error_code(void); const char* corvid_last_error_message(size_t *len_out); ``` Thread-local last error — NULL message when none recorded. See [errors](/ffi/errors/). ```c void corvid_free(void *ptr); ``` **The ONLY buffer deallocator** — for ABI-returned buffers (`corvid_insert_auto` keys, `corvid_page`'s `next_after` cursor). Does NOT free handles or values. `corvid_free(NULL)` is a no-op. ```c corvid_strs* corvid_collections(corvid_db *db); ``` User collection names (engine `__` namespaces excluded), name order, as a string cursor. Listing creates nothing (collections are lazy on first write). ## Collection handles (3) ```c corvid_coll* corvid_collection(corvid_db *db, const char *name, size_t name_len); void corvid_collection_free(corvid_coll *coll); const char* corvid_collection_name(corvid_coll *coll, size_t *len_out); ``` `corvid_collection` returns NULL only on NULL arguments; reserved/invalid names fail **at write time** (lazy validation, as in Rust). The name is BORROWED from the handle until `corvid_collection_free`. ## Value construction (11) All constructors return an OWNED `corvid_value*` or NULL + `CORVID_E_ARGUMENT`. Byte/text/vector inputs are **CLONED** into the value. ```c corvid_value* corvid_value_null(void); corvid_value* corvid_value_bool(int v); /* v != 0 */ corvid_value* corvid_value_int(int64_t v); corvid_value* corvid_value_float(double v); /* NaN/±inf/-0.0 bit-exact */ corvid_value* corvid_value_text(const char *s, size_t len); /* must be UTF-8 */ corvid_value* corvid_value_bytes(const uint8_t *b, size_t len); corvid_value* corvid_value_vector(const float *v, size_t dim); /* dim 0 legal */ corvid_value* corvid_value_array_new(void); corvid_status corvid_value_array_push(corvid_value *arr, corvid_value *item); corvid_value* corvid_value_map_new(void); corvid_status corvid_value_map_put(corvid_value *map, const char *key, size_t key_len, corvid_value *val); ``` - `array_push`/`map_put` **consume** `item`/`val` (ownership moves in; do not free them afterwards). Single-threaded mutation of the container. - A duplicate map key REPLACES the previous entry (last write wins; the replaced child is dropped). Map order in the engine is sorted by key — construction order never matters. - Pushing/putting invalidates previously borrowed children of the container (see [ownership](/ffi/ownership/)). ## Value reads (13) ```c corvid_value_type_t corvid_value_type(const corvid_value *v); /* discriminant */ int corvid_value_as_bool(const corvid_value *v, int *ok); int64_t corvid_value_as_int(const corvid_value *v, int *ok); double corvid_value_as_float(const corvid_value *v, int *ok); ``` Wrong type sets `*ok = 0` and returns 0 — **not an error** (mirrors the Rust `Option` accessors). ```c const char* corvid_value_text_ref(const corvid_value *v, size_t *len_out); const uint8_t* corvid_value_bytes_ref(const corvid_value *v, size_t *len_out); const float* corvid_value_vector_ref(const corvid_value *v, size_t *dim_out); ``` Zero-copy BORROWED views. NULL when the value is a different type (not an error). Valid until the parent value is freed or mutated — writing through these pointers is UB. ```c const corvid_value* corvid_value_array_get(const corvid_value *arr, size_t index); const corvid_value* corvid_value_map_get(const corvid_value *map, const char *key, size_t key_len); ``` BORROWED children; NULL when out of range / absent / wrong container (not an error). Child lifetime rides the parent — freeing a borrowed child is UB. ```c corvid_strs* corvid_value_map_keys(const corvid_value *v); ``` The map's keys as an OWNED string cursor (added in 0.3.0), in ascending key-BYTE order — the engine's `BTreeMap` iteration order. Drive it with `corvid_strs_next` / `corvid_strs_free` (see [graph & geo](/ffi/functions-graph-geo/)); each key is UTF-8 handed out as a binary-safe (pointer, length) pair borrowed until the cursor's next `next` or its free. A non-map `v` yields an EMPTY cursor — inert, not an error (the `as_*` wrong-type convention). NULL `v` answers NULL + `CORVID_E_ARGUMENT`. ```c size_t corvid_value_len(const corvid_value *v); /* items/entries/dims/bytes */ corvid_value* corvid_value_clone(const corvid_value *v); /* deep copy, OWNED */ void corvid_value_free(corvid_value *v); /* OWNED values only */ ``` `corvid_value_clone` is the sanctioned way to keep data observed through a borrowed pointer (e.g. a rows document) beyond the parent's lifetime. `corvid_value_free` on a borrowed child (from `_ref`, `array_get`, `map_get`, `rows_next`, `geohits_next`, callbacks, or already-consumed push/put inputs) is **undefined behavior**. Next: [predicates & queries](/ffi/functions-query/). ================================================================================ # Functions: predicates & queries # /docs/v0.3.1/ffi/functions-query/ ================================================================================ ## Predicates (11) Ten constructors return an OWNED `corvid_pred*` (NULL + `CORVID_E_ARGUMENT` on bad input); the combinators **consume** their children. Rust counterparts are the `corvid::field(path)` fluent builders. Paths are dotted and traverse nested maps; an empty path resolves nothing. ```c corvid_pred* corvid_pred_exists(const char *path, size_t path_len); ``` True when the path resolves. Counterpart: `field(path).exists()`. ```c corvid_pred* corvid_pred_compare(const char *path, size_t path_len, corvid_cmp op, const corvid_value *value); ``` Compare against a constant (CLONED). Counterpart: `field(path).eq/ne/...`. Semantics: missing path ⇒ false; unordered kinds under ordered ops ⇒ false; Int/Float compare numerically across kinds (exact to 2^53); NaN compares false against everything except `NE`. ```c corvid_pred* corvid_pred_in(const char *path, size_t path_len, const corvid_value *const *values, size_t count); corvid_pred* corvid_pred_between(const char *path, size_t path_len, const corvid_value *low, const corvid_value *high); corvid_pred* corvid_pred_starts_with(const char *path, size_t path_len, const char *prefix, size_t prefix_len); corvid_pred* corvid_pred_contains(const char *path, size_t path_len, const char *substr, size_t substr_len); ``` `is_in` (each element CLONED; empty list matches nothing), inclusive `between`, and the two text predicates (false on non-text values and missing paths). ```c corvid_pred* corvid_pred_geo_within(const char *path, size_t path_len, double lat, double lon, double radius_km); ``` Path holds a point within `radius_km` (inclusive, haversine). Counterpart: `field(path).within_km(...)`. ```c corvid_pred* corvid_pred_and(corvid_pred *a, corvid_pred *b); corvid_pred* corvid_pred_or(corvid_pred *a, corvid_pred *b); corvid_pred* corvid_pred_not(corvid_pred *a); void corvid_pred_free(corvid_pred *p); ``` Combinators **consume their argument(s)** and return a new root — after a combine, the children belong to the tree. `corvid_pred_free` frees a **never-consumed root only**: predicates handed to and/or/not, `corvid_query_filter`, or `corvid_delete_where` are consumed and must not be freed (double free = UB). ## Query builder, rows & direct phrase search (16) A query is built on a `corvid_query*` (single-threaded) and executed by `corvid_query_run` or any aggregate, **either of which consumes it** (mirroring the Rust builder taking `self`). ```c corvid_query* corvid_query_new(corvid_coll *coll); /* Collection::query() */ corvid_status corvid_query_filter(corvid_query *q, corvid_pred *pred); ``` Add a filter — **CONSUMES `pred`**. Multiple calls AND together. ```c corvid_status corvid_query_vector(corvid_query *q, const char *field, size_t field_len, const float *query, size_t dim, size_t k, corvid_metric metric); corvid_status corvid_query_text(corvid_query *q, const char *field, size_t field_len, const char *s, size_t s_len, size_t k); ``` Add a vector source (query CLONED) / a BM25 text source. ```c corvid_status corvid_query_fuse_rrf(corvid_query *q, float k); /* default 60 */ corvid_status corvid_query_rerank_mmr(corvid_query *q, float lambda); /* [0,1] */ ``` The setters always succeed; the engine validates at execution (a non-positive/NaN k or out-of-range lambda fails run/aggregates with `CORVID_E_ARGUMENT`). ```c corvid_status corvid_query_approx(corvid_query *q); corvid_status corvid_query_limit(corvid_query *q, size_t n); corvid_status corvid_query_offset(corvid_query *q, size_t n); corvid_status corvid_query_order_by(corvid_query *q, const char *field, size_t field_len, int descending); corvid_status corvid_query_select(corvid_query *q, const char *const *fields, const size_t *field_lens, size_t count); ``` `limit 0` yields empty; `offset` applies after ordering, before limit. The ordering contract is the engine's class rule (comparable → incomparable → missing, ties by key; `descending` reverses within-class order only — see [ordering](/language/ordering/)). `select` projects result documents (missing fields absent; non-map documents pass through; ranking sees the full document). ```c corvid_rows* corvid_query_run(corvid_query *q); ``` Execute — **CONSUMES `q`**. Returns a rows cursor even for an empty result (distinguish failure by `CORVID_ERR`); NULL + error on failure. One MVCC snapshot covers the query; ranking parameters validate here. ```c void corvid_query_free(corvid_query *q); ``` For builders abandoned without running — NOT after run/aggregates. ```c int corvid_rows_next(corvid_rows *rows, const uint8_t **key_out, size_t *key_len_out, const corvid_value **doc_out, float *score_out); void corvid_rows_free(corvid_rows *rows); ``` Advance: 1 and fill out-params, 0 at exhaustion (never errors — the result is materialized). The key and document are **BORROWED from the cursor: valid only until the next `corvid_rows_next` or `corvid_rows_free`** — using or freeing them after is UB; `corvid_value_clone` copies what you keep. `score` is the producing call's ranking: the fused RRF score for `corvid_query_run` (`0.0` for pure filter/order queries), the BM25 phrase score for `corvid_phrase_search`, `0.0` for `corvid_page` rows. ```c corvid_rows* corvid_phrase_search(corvid_coll *c, const char *field, size_t field_len, const char *phrase, size_t phrase_len, size_t k); ``` DIRECT positional text search — no query handle, one call over the collection (added in 0.3.0; wraps `Collection::phrase_search`). Documents whose `field` TEXT contains `phrase` as a consecutive, IN-ORDER run of analyzed tokens, most relevant first, ties by key, up to `k` rows. The engine's analysis applies to the phrase too, and stop words collapse out of adjacency on both sides — `"embedded the database"` matches text containing `"embedded database"`. Documents lacking the field or holding a non-text value there are not part of the corpus. `k == 0` yields an empty cursor (inert — the `geo_nearest`/`page` convention, not an error); larger `k` just caps. Returns an OWNED rows cursor whose `score` is the hit's BM25 relevance (the sum over the phrase's analyzed terms — `TextHit::score`, not the builder's fused RRF scale). One MVCC snapshot covers the search. NULL `c`/`field`/`phrase`, or invalid UTF-8 in either string, answers NULL + `CORVID_E_ARGUMENT`. Next: [aggregations & mutations](/ffi/functions-data/). ================================================================================ # Functions: reads & indexes # /docs/v0.3.1/ffi/functions-reads/ ================================================================================ ## Reads (4) ```c corvid_status corvid_get(corvid_coll *c, const uint8_t *key, size_t key_len, corvid_value **out); ``` Fetch and decode — `*out` receives an OWNED value. Absence is a success: `CORVID_OK` + `*out == NULL` for a missing key. ```c corvid_status corvid_scan(corvid_coll *c, corvid_scan_fn fn, void *ctx); ``` Stream every `(key, document)` in key order to the callback — constant memory. The callback returns 1 to continue, 0 to stop (stopping is not an error); `key`/`doc` are borrowed, valid only inside the callback. Callbacks must not make reentrant corvid calls. ```c corvid_status corvid_page(corvid_coll *c, const uint8_t *after, size_t after_len, size_t limit, corvid_rows **rows_out, uint8_t **next_after_out, size_t *next_after_len_out); ``` Keyset pagination: up to `limit` documents in key order strictly after `after` (NULL/empty starts at the beginning), from one MVCC snapshot. `*rows_out` is an owned rows cursor (score 0.0). `*next_after_out` is the resume cursor — **free it with `corvid_free`** — or NULL with length 0 at the end. `limit == 0` returns empty rows and no cursor. (Filtered pagination `page_where` composes from `query().filter()`; not exposed in v1.) ```c corvid_status corvid_len(corvid_coll *c, size_t *out); ``` Document count, O(1) maintained counter. ## Indexes & schema (15) Every create is create-or-replace (re-creating rebuilds); all validate names (`CORVID_E_RESERVED_COLLECTION` / `CORVID_E_INVALID_NAME`) and persist across reopen. Semantics mirror the engine — see [indexes](/indexes/overview/). ```c corvid_status corvid_create_scalar_index(corvid_coll *c, const char *field, size_t field_len); corvid_status corvid_create_compound_index(corvid_coll *c, const char *const *fields, const size_t *field_lens, size_t count); corvid_status corvid_create_text_index(corvid_coll *c, const char *field, size_t field_len); corvid_status corvid_create_text_index_ondisk(corvid_coll *c, const char *field, size_t field_len); corvid_status corvid_create_geo_index(corvid_coll *c, const char *field, size_t field_len); ``` The six HNSW variants, 1:1 with the engine: ```c corvid_status corvid_create_vector_index(corvid_coll *c, const char *field, size_t field_len, corvid_metric metric); corvid_status corvid_create_vector_index_quantized(corvid_coll *c, const char *field, size_t field_len, corvid_metric metric, corvid_quant quant); corvid_status corvid_create_vector_index_ondisk(corvid_coll *c, const char *field, size_t field_len, corvid_metric metric); corvid_status corvid_create_vector_index_ondisk_quantized(corvid_coll *c, const char *field, size_t field_len, corvid_metric metric, corvid_quant quant); corvid_status corvid_create_vector_index_pq(corvid_coll *c, const char *field, size_t field_len, corvid_metric metric, size_t m, size_t k); corvid_status corvid_create_vector_index_ondisk_pq(corvid_coll *c, const char *field, size_t field_len, corvid_metric metric, size_t m, size_t k); ``` PQ arity is `(field, metric, m, k)` — `m` subspaces × `k` centroids, `dim % m == 0`. PQ creates fail with `CORVID_E_EMPTY_INDEX_TRAINING` when there are no usable training vectors, and (the training domain checks fold into the same error) also for `m == 0`, `k` outside `2..=256`, `dim % m != 0`, zero-dimensional or mixed-dimension training vectors. ```c corvid_status corvid_set_schema(corvid_coll *c, const corvid_field_def *fields, size_t count); corvid_status corvid_schema(corvid_coll *c, corvid_schemaiter **out); int corvid_schemaiter_next(corvid_schemaiter *it, corvid_field_def *out); void corvid_schemaiter_free(corvid_schemaiter *it); ``` `set_schema` declares (or replaces) the schema — enforced on subsequent writes only (existing documents are not retroactively validated). `corvid_schema` absence is a success (`CORVID_OK` + `*out == NULL` when undeclared). The iterator yields fields in declaration order; `out->name` is BORROWED until the next call or free. Next: [graph & geo](/ffi/functions-graph-geo/). ================================================================================ # Handles # /docs/v0.3.1/ffi/handles/ ================================================================================ Every non-trivial object crosses the ABI as an opaque handle. One table rules them all: | Handle | Backed by (Rust) | Thread contract | Created by | Freed by | |---|---|---|---|---| | `corvid_db*` | `Arc` | **thread-safe**: concurrent reads from many threads; writes serialized by the engine | `corvid_open`, `corvid_open_memory` | `corvid_close` | | `corvid_coll*` | `Arc` + collection name | **thread-safe** (shares the `Arc`) | `corvid_collection` | `corvid_collection_free` | | `corvid_value*` | `corvid::Value` | builder handles **single-threaded**; borrowed children ride the parent's lifetime | any `corvid_value_*` constructor, `corvid_get`, `corvid_query_min/max`, `corvid_value_clone` | `corvid_value_free` (owned values only) | | `corvid_pred*` | `Predicate` tree | **single-threaded** construction | the 10 `corvid_pred_*` constructors | `corvid_pred_free` (never-consumed roots only); consumed by and/or/not/filter/delete_where | | `corvid_query*` | owned QueryBuilder state | **single-threaded** build | `corvid_query_new` | `corvid_query_run` and every aggregate (CONSUME); `corvid_query_free` for abandoned builders | | `corvid_rows*` | materialized `Vec` + cursor | read-only cursor; **single-threaded** use | `corvid_query_run`, `corvid_page`, `corvid_phrase_search` | `corvid_rows_free` | | `corvid_strs*` | owned byte-string vector + cursor | read-only cursor; **single-threaded** | `corvid_collections`, `corvid_neighbors`, `corvid_in_neighbors`, `corvid_traverse`, `corvid_value_map_keys` | `corvid_strs_free` | | `corvid_geohits*` | owned hit vector + cursor | read-only cursor; **single-threaded** | the 3 `corvid_geo_*` fns, `corvid_neighbors_weighted` | `corvid_geohits_free` | | `corvid_groupiter*` | owned group list (sorted by group key) + cursor | read-only cursor; **single-threaded** | `corvid_query_group_count/sum/avg` (consume the query) | `corvid_groupiter_free` | | `corvid_schemaiter*` | owned field list + cursor | read-only cursor; **single-threaded** | `corvid_schema` | `corvid_schemaiter_free` | ## Lifecycle rules - `corvid_db` holds the only strong reference after open; every `corvid_coll` clones the `Arc`. `corvid_close` drops the handle's reference — the `Db` (and its file locks) release when the **last derived handle** is gone. Freeing the db while collection handles live is fine (the collection keeps the engine open). - Collections are created lazily on first write (engine `Db::collection` is infallible) — `corvid_collection` never fails for name reasons; reserved/invalid names surface at write time, exactly as in Rust. - **Cross-family frees are forbidden.** Each handle has exactly one destructor. Passing a handle to any function of another family is undefined behavior (C's type system cannot stop it). `_free(NULL)` is a no-op for every family. - `corvid_compact` requires exclusivity (see [admin](/ffi/functions-admin/)) — the counter-plus-`Arc::get_mut` gate answers `CORVID_E_BUSY`. ## Implementation errata (recorded, contract-unchanged) - The derived-handle counter for `corvid_compact` is necessary but not sufficient alone: a query's execute releases its count at entry while its engine `Arc` clone lives through the engine call — the gate is the counter at exactly 1 **and** sole `Arc` ownership. - `corvid_strs*`'s backing is `Vec>`, not `Vec` — graph endpoints are document **keys** (arbitrary bytes), so the cursor preserves bytes and hands out the same binary-safe `(pointer, length)` pairs either way. Next: [errors and NULL discipline](/ffi/errors/). ================================================================================ # The C ABI: overview # /docs/v0.3.1/ffi/overview/ ================================================================================ The C ABI (`corvid-ffi`) is corvid's cross-language contract: the `corvid` cdylib (`libcorvid.so` / `libcorvid.dylib` / `corvid.dll`) plus the generated `corvid.h` — **124 symbols** covering the engine surface, at `FFI_VERSION = 1` (locked; 122 before the additive 0.3.0 expansion). Every binding repo codes against it. ```text corvid_ffi_version → 1 10 opaque handle types, ~15 POD structs/enums 124 functions in 13 families: lifecycle & errors · collections · value construction · value reads predicates · query builder, rows & direct phrase search · aggregations mutations · reads · indexes & schema · graph · geo & iterators · admin ``` ## The locked rulings 1. **No SQL, no JSON, no serialization anywhere in the runtime path.** The ABI is typed C function calls end to end. (The MCP sidecar keeps JSON only because JSON-RPC is the MCP spec; the FFI never touches it.) 2. **Typed calls end to end.** Documents are built and read through `corvid_value` handles; there is no parse step, no string-formatted query, and no byte-blob document interface on the hot path. 3. **Bindings expose idiomatic OOP; FFI symbols never leak into a binding's public API.** Handles become native classes, iterators become the language's native iteration protocol, `CORVID_ERR` becomes native exceptions, handle destructors map to the language's dispose pattern. v1 bindings are synchronous (the engine is sync). ## Calling conventions - All functions use the C ABI (`extern "C"`), the platform's default cdecl/System V convention, and are **synchronous**. All symbols are prefixed `corvid_`. - `corvid_status` (`CORVID_OK`/`CORVID_ERR`) is the standard return; NULL where a handle/buffer was expected; out-params for optional values. See [errors](/ffi/errors/). - Strings and keys cross as **pointer + length**, binary-safe, NOT NUL-terminated; empty is non-NULL pointer + length 0. Engine string parameters (collection names, field paths, relations, disk paths) must be **valid UTF-8** or the call fails with `CORVID_E_ARGUMENT` — never UB. Keys and `Bytes` payloads may be arbitrary bytes. - `size_t` for lengths/counts; `int` for booleans (0/1); `int64_t` for engine `i64`; `double` for `f64`; `float` for `f32`. ## Where functions live on this site | Family (count) | Page | |---|---| | Lifecycle & errors (8), collections (3) | [Lifecycle & collections](/ffi/functions-lifecycle/) | | Value construction (11), value reads (13) | [Lifecycle & collections](/ffi/functions-lifecycle/) | | Predicates (11), query builder, rows & phrase search (16) | [Predicates & queries](/ffi/functions-query/) | | Aggregations (11), mutations (13) | [Aggregations & mutations](/ffi/functions-data/) | | Reads (4), indexes & schema (15) | [Reads & indexes](/ffi/functions-reads/) | | Graph (7), geo & iterators (7) | [Graph & geo](/ffi/functions-graph-geo/) | | Admin (5) | [Admin](/ffi/functions-admin/) | Cross-cutting: [types & enums](/ffi/types/), [handles](/ffi/handles/), [errors & NULL discipline](/ffi/errors/), [ownership & transfer rules](/ffi/ownership/), [threading](/ffi/threading/), [stability & exclusions](/ffi/stability/). ## Enforcement (why you can trust the header) - The generated `corvid.h` is **committed and drift-gated**: a test regenerates it from the crate and diffs — spec, header, and radar can never disagree silently. - A spec-referential radar asserts the header exposes exactly the 124 pinned symbols, and a C smoke suite **drives every one** (124/124), compiled as a cargo test per OS/compiler (gcc, clang, MSVC via `corvid.dll.lib`). - Golden fixtures (267 lines across 8 files: NaN/±inf/−0.0, cursors, map-key enumeration, phrase search, unique violations, geo boundaries, persistence-across-reopen) pin observable behavior. - CI runs a 3-OS release-profile job and an ASan+UBSan+LSan Linux job — **zero leaks is the contract** (every handle family's free path executes inside the fixtures). Release archives attach the cdylib (Windows: plus `corvid.dll.lib`), `corvid.h`, and the golden fixtures, sha256-verified in `checksums.txt`. Note for C authors: the header's value-type typedef/enum tag spells `corvid_value_type_t` (the bare name is the function — see [types](/ffi/types/)); on Windows link the import library and place `corvid.dll` on the loader path. Next: [types and enums](/ffi/types/). ================================================================================ # Ownership & transfer # /docs/v0.3.1/ffi/ownership/ ================================================================================ The ABI's transfer rules, in full: 1. **ABI-returned buffers** (strings, `next_after`, auto-keys) → `corvid_free(ptr)` only. 2. **Handles** → their own `_free`, never cross-family. 3. **`const corvid_value*` inputs are CLONED** — caller keeps ownership. 4. **Predicates consumed** by and/or/not/filter/delete_where. 5. **`run` and aggregations CONSUME the query.** 6. **Owned-vs-borrowed outputs documented per signature** (rows doc + value children are borrowed; freeing them is UB). 7. **NULL discipline per parameter**; unexpected NULL → `CORVID_E_ARGUMENT`, never UB. A function that **consumes** a handle or value consumes it **unconditionally** — even when it later fails (a failed `corvid_query_run` has still consumed the query; a failed `corvid_pred_and` has still consumed both children). Callers must not free consumed handles afterwards. This mirrors Rust's by-value semantics and makes ownership transfer single-shot. ## Per-family transfer table Inputs: C = cloned, K = consumed, B = borrowed-read. Outputs: O = owned-by-caller, B = borrowed. | Family | Inputs | Outputs | |---|---|---| | Lifecycle & errors | path B | db handle O; error message B (thread-local); strs handle O | | Collection | name B | coll handle O; name B (until free) | | Value construction | text/bytes/vector C; `array_push`/`map_put` item K | value O | | Value reads | parent B | `_ref` buffers B; children B; `as_*` by value; `clone` O | | Predicates | path/value C; combinators' children K | pred O | | Query builder | filter pred K; vector/text/select/fields B | query O; `run` → rows O (query K) | | Aggregations | query K; field names B | scalars by value; min/max O; groupiter O | | Mutations | keys/docs B (docs C into the engine); update callback's `*out` K; CAS/pred per rule 4 | auto-key buffer O (corvid_free); counters by value | | Reads | key/after B | `get` value O; scan rows B (callback-scoped); page rows O + next_after O (corvid_free) | | Indexes & schema | field(s) B; field_defs B | schemaiter O; iterated names B | | Graph | keys/relations B | strs/geohits O | | Geo & iterators | field/coords by value | geohits O; hit keys/docs B | | Admin | paths B | by value | ## The UB prohibitions (bold by design) - **Freeing or writing through a `_ref` buffer** (text/bytes/vector views) — borrowed from the parent value, valid until it is freed or mutated. - **`corvid_value_free` on a borrowed child** — from `_ref`, `array_get`, `map_get`, `rows_next`, `geohits_next`, callbacks, or push/put inputs already consumed. - **Using or freeing a rows cursor's key/document after the next `corvid_rows_next` or `corvid_rows_free`.** - **Freeing a consumed predicate or query** (double free). - **Cross-family frees** — each handle has exactly one destructor. - **Concurrent calls on a single-threaded handle** from two threads — documented, not detected (see [threading](/ffi/threading/)). The sanctioned escape hatch for keeping borrowed data: `corvid_value_clone` — a deep copy returning an owned handle. Next: [threading](/ffi/threading/). ================================================================================ # Stability & v1 exclusions # /docs/v0.3.1/ffi/stability/ ================================================================================ ## Naming conventions - Every symbol is prefixed `corvid_`. - Constructors return handles and end in a noun or `_new` (`corvid_value_int`, `corvid_query_new`, the `corvid_pred_*` family). - Destructors end in `_free` — exactly one per handle type, never cross-family. `corvid_free` (no suffix) is reserved for plain buffers. - Cursor advance ends in `_next` and returns `int` (1 row, 0 exhausted). - Zero-copy borrows end in `_ref`. - Fluent query setters are `corvid_query_` — the Rust chain `.filter(...).vector(...)` becomes a sequence of calls. - A function that **consumes** a handle or value says so in this spec and consumes it unconditionally, even on failure. ## Stability policy - `corvid_ffi_version()` returns `FFI_VERSION = 1`. - **Enum values are frozen.** `corvid_status`, `corvid_err` (1–19), `corvid_cmp`, `corvid_metric`, `corvid_quant`, `corvid_value_type_t`, `corvid_field_type` are never renumbered or reordered; new values may only be appended (a new engine `Error` variant appends code 20+, never fills a gap — and the variant-inventory snapshot test fails until it is mapped). - **Pre-1.0 break policy:** breaking ABI changes are allowed but must be loud — bump `FFI_VERSION`, change the artifact names, record the break in the CHANGELOG and the design decision log. Bindings pin exact engine tags, so a break is a coordinated bump PR per binding repo, never a surprise. - **Post-1.0 soname discipline:** the cdylib is `libcorvid.so.1` / `libcorvid.1.dylib` / `corvid.dll` with import-lib versioning; additive changes keep soname `.1` and `FFI_VERSION = 1`; any breaking change bumps `FFI_VERSION` to 2 and the soname to `.2`, shipped alongside a migration note. Struct layouts in `corvid.h` are append-only (new fields at the end, with size checks in the header). - The generated `corvid.h` is committed and drift-gated: a test regenerates it from the crate and diffs — spec, header, and radar can never disagree silently. ## v1 exclusions (deliberate, with reopen triggers) | Exclusion | Why | Reopen trigger | |---|---|---| | Events / subscriptions | reentrancy across languages | demonstrated v2 need (a binding shipping a portable event loop story) | | Direct `vector_search` / `text_search` fns | the query builder covers them (`.vector`/`.text` sources). (`phrase_search` gained a direct fn in 0.3.0 — positional semantics do not compose out of the bag-of-words `.text` source; see [predicates & queries](/ffi/functions-query/)) | a workload proving per-call builder overhead matters — **stays closed on the measured parity** (see [FFI crossing cost](/performance/ffi-crossing/)) | | Sketches (Bloom, Cuckoo, HLL, LshIndex, MinHash, TDigest) | not core to the typed-document story | binding-user demand | | Semantic cache | young API | engine-side stabilization | | `PlanCache` / `explain` / `plan_shape` | advisory/diagnostic, no runtime contract | a binding asks for query introspection | | `Db::bulk` (begin_bulk relaxed durability) | `corvid_put_many` covers the bulk fast path | a dump-ingest bench showing per-commit fsync cost matters | | `Collection::page_where` | filtered keyset pagination composes from `query().filter()` + `offset/limit`; cursor semantics across a moving filter set are subtle | a binding needing constant-memory filtered pagination | | `Store`-level byte API | the ABI is typed-document only by ruling 1 | none foreseen | | Non-UTF-8 filesystem paths | `Db::open` accepts any `AsRef`; the ABI takes `(const char*, len)` and requires UTF-8 — one encoding rule covers every string | a binding on a platform where UTF-8 paths are insufficient (then: a wide-char or OS-native path entry point, additive) | Next: [bindings](/bindings/overview/). ================================================================================ # Threading # /docs/v0.3.1/ffi/threading/ ================================================================================ - **`corvid_db` / `corvid_coll`: thread-safe.** Concurrent reads from any number of threads; writes are serialized by the engine (single writer); queries are MVCC point-in-time. The engine's `Db` is `Sync`. - **Value builders, predicates, queries, and every cursor: single-threaded** construction and use. Concurrent calls on the same handle from two threads are **undefined behavior — documented, not detected**. Bindings enforce this by confining each object to one thread/queue (per-language idiom maps do this naturally; PHP ZTS note: one handle per request/thread). - **Different handles may be used concurrently**, even derived from one db: a query on thread A and an insert on thread B are fine — each sees a consistent snapshot/commit as documented. - **`corvid_last_error_code/message` are thread-local**: each thread sees its own last failure; no locking is needed or provided. - **Freeing a handle while another thread is calling into it is UB** — free after joining/quiescing. - **Callbacks** (scan sink, update closure) run on the caller's thread between engine operations: reads through other handles are memory-safe, but callbacks must not issue further writes to the same database, must not free or mutate borrowed arguments, and should not make other corvid calls at all — the portable contract is "no reentrant corvid calls". ## The compact quiescence rule `corvid_compact` needs exclusive engine access (the derived-handle counter plus sole `Arc` ownership — see [admin](/ffi/functions-admin/)). Concurrent use of other handles is unaffected until the compact call, but a binding that wants to compact must reach a quiescent point (all collection/query handles freed) first; threads still holding handles keep `CORVID_E_BUSY` as the deterministic answer — never a hang, never UB. ## How bindings map this | Engine/ABI reality | Binding idiom | |---|---| | thread-safe `db` handle | shareable native object | | single-threaded builder/cursor | confined to thread/queue/event-loop turn | | `_next` cursors | the language's native iteration protocol | | `CORVID_ERR` + last error | native exceptions carrying the code | | `_free` destructors | dispose/finalizer patterns | Next: [stability & exclusions](/ffi/stability/). ================================================================================ # Types and enums # /docs/v0.3.1/ffi/types/ ================================================================================ ## Opaque handles (10 types) Every handle is an opaque, single-pointer-sized forward-declared struct — see [handles](/ffi/handles/): ```c typedef struct corvid_db corvid_db; typedef struct corvid_coll corvid_coll; typedef struct corvid_value corvid_value; typedef struct corvid_pred corvid_pred; typedef struct corvid_query corvid_query; typedef struct corvid_rows corvid_rows; typedef struct corvid_strs corvid_strs; typedef struct corvid_geohits corvid_geohits; typedef struct corvid_groupiter corvid_groupiter; typedef struct corvid_schemaiter corvid_schemaiter; ``` ## POD structs ```c /* One (key, value) pair for bulk inserts (corvid_put_many). */ typedef struct corvid_kv { const uint8_t *key; /* non-NULL; may point at empty (len 0) */ size_t key_len; /* bytes */ const corvid_value *val; /* non-NULL; CLONED by the call, caller keeps ownership */ } corvid_kv; /* One declared schema field (corvid_set_schema input, schemaiter output). */ typedef struct corvid_field_def { const char *name; /* non-NULL for inputs; BORROWED when filled by schemaiter_next */ corvid_field_type type; int required; /* 0 or 1 */ int unique; /* 0 or 1 */ } corvid_field_def; /* One geospatial / weighted hit (corvid_geohits_next output). */ typedef struct corvid_geohit { const uint8_t *key; /* BORROWED until the next geohits_next or geohits_free */ size_t key_len; double distance_km; /* geo: km from the query point; neighbors_weighted: the edge weight; geo_within_bbox: 0.0 sentinel (no center). */ } corvid_geohit; ``` ## Status and error enums (frozen) ```c typedef enum corvid_status { CORVID_OK = 0, /* success */ CORVID_ERR = 1 /* failure; detail in corvid_last_error_code/message */ } corvid_status; ``` `corvid_err` — the detailed codes returned by `corvid_last_error_code()` — maps 1:1 onto the engine's error variants (codes 1–18), plus one FFI-only code (19). Value 0 means "no error recorded on this thread". **Never renumber**; see the full table on [error codes](/reference/error-codes/) and the error model on [errors](/ffi/errors/). ## Domain enums (frozen, values mirror engine discriminants) ```c typedef enum corvid_cmp { /* mirrors corvid::CmpOp */ CORVID_CMP_EQ = 0, CORVID_CMP_NE = 1, CORVID_CMP_LT = 2, CORVID_CMP_LE = 3, CORVID_CMP_GT = 4, CORVID_CMP_GE = 5 } corvid_cmp; typedef enum corvid_metric { /* mirrors corvid::Metric */ CORVID_METRIC_COSINE = 0, /* 1 - cos_sim, [0,2]; zero-norm = maximally distant */ CORVID_METRIC_DOT = 1, /* negated dot product (larger dot sorts first) */ CORVID_METRIC_L2 = 2 /* squared Euclidean (monotonic with L2) */ } corvid_metric; typedef enum corvid_quant { /* mirrors corvid::Quantization */ CORVID_QUANT_NONE = 0, /* full f32 (dim*4 bytes/vector) */ CORVID_QUANT_BINARY = 1, /* 1 bit/dim (sign), Hamming; ~32x smaller */ CORVID_QUANT_SCALAR = 2 /* 8-bit per-vector min+scale; ~4x smaller */ } corvid_quant; typedef enum corvid_value_type_t { /* tags 0..8, identical to the value codec */ CORVID_TYPE_NULL = 0, CORVID_TYPE_BOOL = 1, CORVID_TYPE_INT = 2, CORVID_TYPE_FLOAT = 3, CORVID_TYPE_TEXT = 4, CORVID_TYPE_BYTES = 5, CORVID_TYPE_ARRAY = 6, CORVID_TYPE_MAP = 7, CORVID_TYPE_VECTOR = 8 } corvid_value_type_t; typedef enum corvid_field_type { /* mirrors schema FieldType (0..8) */ CORVID_FIELD_ANY = 0, CORVID_FIELD_BOOL = 1, CORVID_FIELD_INT = 2, CORVID_FIELD_FLOAT = 3, CORVID_FIELD_TEXT = 4, CORVID_FIELD_BYTES = 5, CORVID_FIELD_VECTOR = 6, CORVID_FIELD_ARRAY = 7, CORVID_FIELD_MAP = 8 } corvid_field_type; ``` ## The `corvid_value_type_t` naming erratum ISO C forbids one identifier from being both a typedef and a function in the same scope — and the locked function `corvid_value_type` (§4.4) collides with the enum type's original spelling. Resolution: the **function** name `corvid_value_type` and the member values (`CORVID_TYPE_NULL..=VECTOR`, frozen 0..=8) are unchanged; in the generated `corvid.h` the type's typedef **and** enum tag are spelled `corvid_value_type_t`. C sources write `corvid_value_type_t` for the type. (Discovered by compiling the C smoke suite — the pre-fix header did not compile as C at all.) ## Strings, keys, lengths - Binary-safe pointer+length; **not** NUL-terminated; empty = non-NULL pointer, length 0. - Names/paths/relations/text must be UTF-8 (`CORVID_E_ARGUMENT` otherwise); keys and `Bytes` payloads arbitrary. - Name rules are the engine's: no NUL byte, no `__` anywhere (`CORVID_E_INVALID_NAME`), no leading `__` (`CORVID_E_RESERVED_COLLECTION`). The empty name is legal. Violations surface on the first write/definition call — handles never validate on creation. Next: [handles](/ffi/handles/). ================================================================================ # corvid-c # /docs/v0.3.1/bindings/corvid-c/ ================================================================================ [`corvid-c`](https://github.com/corvid-db/corvid-c) is the canonical C consumer of corvid. It exists to prove, continuously and outside the engine's repository, that the **published FFI artifacts** — the platform cdylib, `corvid.h`, and the golden fixtures shipped in each release archive — work for a plain C consumer. Its role in the bindings program is **reference consumer**: everything there links the release artifacts exactly the way a third-party binding author would — no engine checkout, no vendored binaries. **When to choose this binding:** you are writing C (or building another binding, a plugin, or an embedded deployment) and want the zero-dependency path — no Rust toolchain, no language runtime, just a C11 compiler, CMake, and the sha256-verified release archive. It is also the reference for how the ABI's ownership rules (cloned inputs, consumed queries and predicates, borrowed row views) are meant to be driven by hand, and the place where published-artifact defects surface first. ## What's inside | Path | What it is | |---|---| | `fetch.sh` / `fetch.ps1` | Download the pinned release archive, verify against the release's `checksums.txt` (sha256), extract into gitignored `deps/` | | `CMakeLists.txt` | Offline-first build consuming `deps/`; builds the demo, the examples tour, and the golden-suite port; installs a `corvid.pc` | | `examples/demo.c` | A small idiomatic consumer: open, insert, query, print (~20 symbols) | | `examples/{quickstart,hybrid,vector_index,text_search,graph,geo}.c` | The examples tour — one runnable program per concept, each a ctest on every CI leg | | `test/golden.c` | The golden-suite port — replays the engine's 267-line fixture suite against the downloaded libcorvid | ## Quick start Requirements: a C11 compiler, CMake ≥ 3.28, `curl` + `shasum`/`sha256sum` (macOS/Linux) or PowerShell 5+ (Windows). ```sh ./fetch.sh # download + verify corvid v0.3.0 into deps/ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release cmake --build build ctest --test-dir build --output-on-failure # golden suite + demo + examples ./build/bin/demo # open → insert → query → print ./build/bin/example_hybrid # the flagship hybrid query ``` Windows: `./fetch.ps1`, then the same CMake steps (`ctest -C Release`). ## The examples Six runnable programs, each also a ctest (and each leak-clean under the CI sanitizer job): **quickstart** (open, insert, kNN, print), **hybrid** (filter + vector + BM25, RRF fusion, MMR rerank, limit), **vector_index** (in-memory / on-disk / binary-quantized HNSW vs the exact scan, plus a close/reopen), **text_search** (BM25, English + CJK, plus the v0.3.0 direct `corvid_phrase_search`), **graph** (neighbors/traverse + delete cascade), and **geo** (radius / bbox / nearest with haversine kilometres). The quickstart and hybrid sources are embedded below — imported from the repo's `examples/` so they cannot drift from what CI executes (`scripts/sync-binding-examples.sh` keeps this page in step; the drift gate reddens CI if they diverge). ### Quickstart ```c static void put_doc(corvid_coll *docs, const char *key, const char *title, const char *kind, const float *v, size_t dim) { corvid_value *doc = corvid_value_map_new(); must("map_put title", corvid_value_map_put( doc, "title", 5, corvid_value_text(title, strlen(title)))); must("map_put kind", corvid_value_map_put( doc, "kind", 4, corvid_value_text(kind, strlen(kind)))); must("map_put v", corvid_value_map_put(doc, "v", 1, corvid_value_vector(v, dim))); must("insert", corvid_insert(docs, (const uint8_t *)key, strlen(key), doc)); corvid_value_free(doc); /* insert CLONES the value; ours is still ours */ } int main(void) { corvid_db *db = corvid_open_memory(); if (!db) { fprintf(stderr, "quickstart: open failed\n"); return 1; } corvid_coll *docs = corvid_collection(db, "docs", 4); if (!docs) { fprintf(stderr, "quickstart: collection failed\n"); return 1; } put_doc(docs, "p1", "rust embedded database", "doc", (const float[]){1.0f, 0.0f}, 2); put_doc(docs, "p2", "python web frameworks", "doc", (const float[]){0.0f, 1.0f}, 2); put_doc(docs, "p3", "rust again database", "doc", (const float[]){0.9f, 0.1f}, 2); /* kNN: the 3 nearest documents to (1, 0) under cosine. */ corvid_query *q = corvid_query_new(docs); if (!q) { fprintf(stderr, "quickstart: query_new failed\n"); return 1; } must("query_vector", corvid_query_vector(q, "v", 1, (const float[]){1.0f, 0.0f}, 2, 3, CORVID_METRIC_COSINE)); corvid_rows *rows = corvid_query_run(q); /* consumes q */ if (!rows) { size_t len = 0; const char *msg = corvid_last_error_message(&len); fprintf(stderr, "quickstart: query_run failed: %.*s\n", (int)len, msg); return 1; } int rank = 0; for (;;) { const uint8_t *key = NULL; size_t key_len = 0; const corvid_value *doc = NULL; float score = 0.0f; if (corvid_rows_next(rows, &key, &key_len, &doc, &score) != 1) break; const corvid_value *title = corvid_value_map_get(doc, "title", 5); size_t title_len = 0; const char *title_p = corvid_value_text_ref(title, &title_len); printf("%d. %-.*s score=%.6f %.*s\n", ++rank, (int)key_len, key, (double)score, (int)title_len, title_p ? title_p : "?"); } corvid_rows_free(rows); corvid_collection_free(docs); must("close", corvid_close(db)); return 0; } ``` ### Hybrid retrieval ```c static void put_doc(corvid_coll *docs, const char *key, const char *kind, const char *body, const float *v) { corvid_value *doc = corvid_value_map_new(); must("map_put kind", corvid_value_map_put( doc, "kind", 4, corvid_value_text(kind, strlen(kind)))); if (body) must("map_put body", corvid_value_map_put( doc, "body", 4, corvid_value_text(body, strlen(body)))); if (v) must("map_put v", corvid_value_map_put(doc, "v", 1, corvid_value_vector(v, 2))); must("insert", corvid_insert(docs, (const uint8_t *)key, strlen(key), doc)); corvid_value_free(doc); } static void print_rows(corvid_rows *rows) { int rank = 0; for (;;) { const uint8_t *key = NULL; size_t key_len = 0; const corvid_value *doc = NULL; float score = 0.0f; if (corvid_rows_next(rows, &key, &key_len, &doc, &score) != 1) break; const corvid_value *body = corvid_value_map_get(doc, "body", 4); size_t body_len = 0; const char *body_p = corvid_value_text_ref(body, &body_len); printf("%d. %-.*s score=%.6f %.*s\n", ++rank, (int)key_len, key, (double)score, (int)body_len, body_p ? body_p : "?"); } corvid_rows_free(rows); } int main(void) { corvid_db *db = corvid_open_memory(); if (!db) { fprintf(stderr, "hybrid: open failed\n"); return 1; } corvid_coll *docs = corvid_collection(db, "docs", 4); if (!docs) { fprintf(stderr, "hybrid: collection failed\n"); return 1; } put_doc(docs, "s1", "doc", "rust embedded database", (const float[]){1.0f, 0.0f}); put_doc(docs, "s2", "doc", "python web frameworks", (const float[]){0.0f, 1.0f}); put_doc(docs, "s3", "doc", "rust again database", (const float[]){0.9f, 0.1f}); put_doc(docs, "m1", "meta", NULL, NULL); /* filtered out below */ /* The flagship query: filter + vector + text, RRF + MMR + limit. */ corvid_query *q = corvid_query_new(docs); if (!q) { fprintf(stderr, "hybrid: query_new failed\n"); return 1; } corvid_value *doc_kind = corvid_value_text("doc", 3); corvid_pred *only_docs = corvid_pred_compare("kind", 4, CORVID_CMP_EQ, doc_kind); corvid_value_free(doc_kind); /* CLONED into the tree (§5 rule 3) */ if (!only_docs) { fprintf(stderr, "hybrid: pred_compare failed\n"); return 1; } must("query_filter", corvid_query_filter(q, only_docs)); /* consumes pred */ must("query_vector", corvid_query_vector(q, "v", 1, (const float[]){1.0f, 0.0f}, 2, 2, CORVID_METRIC_COSINE)); must("query_text", corvid_query_text(q, "body", 4, "rust database", 13, 2)); must("query_fuse_rrf", corvid_query_fuse_rrf(q, 60.0f)); must("query_rerank_mmr", corvid_query_rerank_mmr(q, 1.0f)); must("query_limit", corvid_query_limit(q, 2)); corvid_rows *rows = corvid_query_run(q); /* consumes q */ if (!rows) { size_t len = 0; const char *msg = corvid_last_error_message(&len); fprintf(stderr, "hybrid: query_run failed: %.*s\n", (int)len, msg); return 1; } print_rows(rows); corvid_collection_free(docs); must("close", corvid_close(db)); return 0; } ``` The fused scores are RRF rank sums: `s1` is rank 1 of both sources (1/61 + 1/61 = 2/61 ≈ 0.032787), `s3` rank 2 of both (2/62 ≈ 0.032258). Every construct maps to the [ABI function pages](/ffi/functions-lifecycle/); the ownership flow (cloned document inputs, consumed query and predicate, borrowed row views) follows the [transfer rules](/ffi/ownership/). ## Installing (system use) `cmake --install build` installs `corvid.h`, the library, and a `corvid.pc` pkg-config file: ```sh pkg-config --cflags --libs corvid ``` ## Versioning The engine pin lives in one variable in the fetch scripts (`CORVID_VERSION=v0.3.0`). Artifacts are always taken from that exact tag's GitHub release and sha256-verified; `deps/` is never committed. ## The macOS note (a bindings-program war story) The v0.2.0 darwin dylibs shipped with the release CI runner's absolute path as their install name, so binaries linked against them aborted at launch. corvid-c caught this (finding F1 in its plan); the engine fixed its release pipeline; **every pin since v0.2.1 — the current is v0.3.0 — is clean**: `otool -D` shows `@rpath/libcorvid.dylib`, and the golden suite runs 267/267 with no workarounds. v0.2.1's Linux `.so` also gained its SONAME (finding F2, likewise resolved). This is the reference-consumer role working as designed. Next: [corvid-node](/bindings/corvid-node/). ================================================================================ # corvid-cpp # /docs/v0.3.1/bindings/corvid-cpp/ ================================================================================ [`corvid-cpp`](https://github.com/corvid-db/corvid-cpp) is the C++20 binding: a **header-first RAII library** over the frozen C ABI (one public header, `corvid/corvid.hpp`, plus one implementation TU), linking the **published FFI artifacts** — the platform cdylib, the generated header, and the golden fixtures — downloaded from a pinned engine release (v0.3.0) and sha256-verified. No engine checkout, no Rust toolchain, no dependencies beyond the C++ standard library. **When to choose this binding:** you are writing modern C++ (C++20 floor; CI runs latest-ish GCC, Clang, and MSVC) and want the engine's typed documents, vector/text/hybrid search, graph edges, and geo — with RAII doing the freeing, `std::optional`/`std::span` shaping the reads, and failures arriving as `corvid::Error` carrying the frozen error `code()`. The architecture ruling in one breath: every engine handle becomes a **move-only class** whose destructor calls the ABI's free family (a copied handle would double-free; deep copies are explicit via `Value::clone()`); the fluent `Query` builder mirrors the engine's Rust builder; and **no raw ABI symbol ever appears in the public header** — a CI gate (`scripts/idiom-gate.sh`) scans the header to keep it that way, and `test/raii.cpp` pins move-only-ness at compile time. ## Install **Pending first packaged release** — build from source meanwhile (a C++20 compiler, CMake ≥ 3.28, and `curl` + `shasum`/`sha256sum` or PowerShell): ```sh git clone https://github.com/corvid-db/corvid-cpp && cd corvid-cpp ./fetch.sh # download + sha256-verify corvid v0.3.0 into deps/ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release cmake --build build ctest --test-dir build --output-on-failure ``` Consume it from your own CMake two ways — `FetchContent` of the repo (after its `fetch.sh` populates `deps/`; the build is offline-first) or `find_package` against an installed package: ```cmake find_package(corvid REQUIRED) target_link_libraries(my_app PRIVATE corvid::corvid) ``` ## The examples Six runnable programs in the repo's `examples/` directory, executed on every CI leg with deterministic output (and leak-clean under the sanitizer leg): **quickstart**, **hybrid** (the flagship below), **vector-index** (exact scan vs HNSW vs binary-quantized vs on-disk, plus close/reopen), **text-search** (BM25 incl. CJK and the v0.3.0 phrase API), **graph** (neighbors/traverse + delete cascade), and **geo** (radius / bbox / nearest in haversine kilometres). The quickstart and hybrid sources are embedded below — imported from the repo so they cannot drift from what CI executes (`scripts/sync-binding-examples.sh`; the drift gate reddens docs CI if they diverge). ### Quickstart ```cpp void put_doc(corvid::Collection& docs, std::string_view key, std::string_view title, std::string_view kind, std::span v) { using namespace corvid; docs.insert(key, Value::map({{"title", lit::text(title)}, {"kind", lit::text(kind)}, {"v", lit::vec(v)}})); } int run() { using namespace corvid; const float v1[]{1.0f, 0.0f}, v2[]{0.0f, 1.0f}, v3[]{0.9f, 0.1f}; Db db = Db::open_memory(); Collection docs = db.collection("docs"); put_doc(docs, "p1", "rust embedded database", "doc", v1); put_doc(docs, "p2", "python web frameworks", "doc", v2); put_doc(docs, "p3", "rust again database", "doc", v3); // kNN: the 3 nearest documents to (1, 0) under cosine. const float probe[]{1.0f, 0.0f}; Rows rows = docs.query() .vector("v", probe, 3, Metric::Cosine) .run(); // consumes the builder int rank = 0; for (const Row& r : rows) { auto title = r.doc.get("title").as_text(); std::printf("%d. %.*s score=%.6f %.*s\n", ++rank, static_cast(r.key.size()), r.key.data(), static_cast(r.score), static_cast(title ? title->size() : 1), title ? title->data() : "?"); } return 0; } ``` ### Hybrid retrieval ```cpp void put_doc(corvid::Collection& docs, std::string_view key, std::string_view kind, const char* body, const float* v) { using namespace corvid; Value doc = Value::map({{"kind", lit::text(kind)}}); if (body != nullptr) doc.put("body", lit::text(body)); if (v != nullptr) doc.put("v", lit::vec(std::span(v, 2))); docs.insert(key, doc); } int run() { using namespace corvid; const float v1[]{1.0f, 0.0f}, v2[]{0.0f, 1.0f}, v3[]{0.9f, 0.1f}; Db db = Db::open_memory(); Collection docs = db.collection("docs"); put_doc(docs, "s1", "doc", "rust embedded database", v1); put_doc(docs, "s2", "doc", "python web frameworks", v2); put_doc(docs, "s3", "doc", "rust again database", v3); put_doc(docs, "m1", "meta", nullptr, nullptr); // filtered out below // The flagship query: filter + vector + text, RRF + MMR + limit. const float probe[]{1.0f, 0.0f}; Rows rows = docs.query() .filter(pred::eq("kind", "doc")) .vector("v", probe, 2, Metric::Cosine) .text("body", "rust database", 2) .fuse_rrf(60.0f) .rerank_mmr(1.0f) .limit(2) .run(); // consumes the builder AND the predicate int rank = 0; for (const Row& r : rows) { auto body = r.doc.get("body").as_text(); std::printf("%d. %.*s score=%.6f %.*s\n", ++rank, static_cast(r.key.size()), r.key.data(), static_cast(r.score), static_cast(body ? body->size() : 1), body ? body->data() : "?"); } return 0; } ``` The fused scores are RRF rank sums: `s1` is rank 1 of both sources (1/61 + 1/61 = 2/61 ≈ 0.032787), `s3` rank 2 of both (2/62 ≈ 0.032258). ## What the RAII layer adds over the C ABI - **Values from literals**: `Value::map({{"title", lit::text("…")}, {"v", lit::vec(span)}})` and `Value::array({1, 2.5, "x"})` — a copyable `Lit` borrows its bytes for the full expression; nested composites borrow an owned `Value` (cloned at materialization). - **Borrowed reads**: map/array children and row documents surface as the read-only `ValueView`; typed accessors return `std::optional` and `std::span`. - **Map keys** (the v0.3.0 additive symbol): `Value::map_keys()` — owned keys in ascending key-byte order (the engine's BTreeMap order). - **Phrase search** (the other v0.3.0 symbol): `docs.phrase_search("body", "embedded database", 10)` — direct positional search, consecutive and in order, BM25 phrase scores. - **Errors**: every failing call throws `corvid::Error` with the frozen `code()` (mirroring the ABI's error enum 1:1 — pinned at compile time on both sides by `test/errcodes.cpp`). - **Callbacks**: `scan` and `update` take `std::function`; exceptions thrown inside a callback cross the C frame safely and rethrow. ## The correctness floor Every binding replays the engine's golden fixtures; corvid-cpp ports the C harness itself to C++ (`test/golden.cpp`) and drives the **downloaded** cdylib over the release's fixtures — 267 executable lines at v0.3.0, including the additive map-keys and phrase ops. If the published `.so`/`.dylib`/`.dll`, header, or fixtures disagree, that CI leg reddens where the engine's own suite stayed green. On top of the golden port, `test/raii.cpp` exercises the wrapper's own surface (145 checks), and `docs/SURFACE.tsv` resolves all 327 engine constructs (180 mapped / 147 N/A-with-reason) against a CI gate. Next: the [C ABI reference](/ffi/overview/) underneath every binding. ================================================================================ # corvid-go # /docs/v0.3.1/bindings/corvid-go/ ================================================================================ [`corvid-go`](https://github.com/corvid-db/corvid-go) is the Go binding: it links the engine's **published FFI artifacts** (the platform cdylib and `corvid.h`) over **cgo** and carries an idiomatic Go API on top — `Db`, `Collection`, a fluent `Query` builder, and `Field(...)` predicates. Deliberately different from the node/python bindings (Rust-source builds): Go users expect a system/shared library, not a Rust toolchain — `make deps` fetches and sha256-verifies the pinned release archive, and the requirement stops at "a C compiler", which cgo already needs. **When to choose this binding:** your service is written in Go and you want corvid embedded without CGO-free purism getting in the way — errors are Go errors (`*corvid.CorvidError`, never panics), `Db` and `Collection` are safe for concurrent use, and the engine loads as a shared library your binary links at runtime. ## Install From the pinned release artifacts (default): ```sh make deps # fetch + verify corvid v0.3.0 into deps/current go test ./... # the golden suite (267 fixture lines) ``` Requirements: Go ≥ 1.26, a C compiler (CGO enabled — the default when one is present), `curl` + `shasum`/`sha256sum`. Or, if corvid is installed as a system library, point cgo at it with `CGO_CFLAGS` / `CGO_LDFLAGS` (see the repo README). ## Documents, maps, and phrases Engine v0.3.0 added the map-key iterator (`corvid_value_map_keys`) and the direct positional phrase search (`corvid_phrase_search`) to the C ABI: - **Map decoding is complete, everywhere.** The v0.2.x-era boundary — a candidate-key oracle that failed `Get` with `ErrMapKeyEnumeration` on unknown keys — collapsed into a plain decode through the real iterator: `Get`/`Scan`/`Page`/query rows decode every document the engine can read, on any database, whatever wrote it (UTF-8 and nested keys included). - Retrieval queries still return `Row.Doc == nil` without `Query.Select(...)` — keys and scores by design; read the document explicitly, or use `PhraseSearch` (whose rows always carry documents). - `(*Collection).PhraseSearch(field, phrase, k)` is the DIRECT positional search: consecutive, in-order analyzed tokens, stop words collapsing out of adjacency, rows carrying the BM25 phrase score (the phrase scale, not the builder's fused RRF scale); `k == 0` answers empty — inert, never an error. ## The examples Six runnable programs under the repo's `examples/` directory (`go run ./examples/`), executed on every CI leg with deterministic output: **quickstart**, **hybrid** (the flagship below), **vector-index** (in-memory / on-disk / binary-quantized HNSW vs the exact scan), **text-search** (BM25 incl. CJK bigram segmentation, plus the v0.3.0 direct `PhraseSearch`), **graph** (neighbors/traverse + delete cascade), and **geo** (radius / bbox / nearest). The quickstart and hybrid sources are embedded below — imported from the repo so they cannot drift from what CI executes (`scripts/sync-binding-examples.sh`; the drift gate reddens docs CI if they diverge). ### Quickstart ```go func main() { db, err := corvid.OpenMemory() if err != nil { panic(err) } defer func() { must(db.Close()) }() docs, err := db.Collection("docs") if err != nil { panic(err) } defer docs.Close() must(docs.Insert([]byte("p1"), map[string]any{ "title": "rust embedded database", "kind": "doc", "v": []float32{1.0, 0.0}, })) must(docs.Insert([]byte("p2"), map[string]any{ "title": "python web frameworks", "kind": "doc", "v": []float32{0.0, 1.0}, })) must(docs.Insert([]byte("p3"), map[string]any{ "title": "rust again database", "kind": "doc", "v": []float32{0.9, 0.1}, })) // kNN: the 3 nearest documents to (1, 0) under cosine. Row.Doc is // materialized only under Select — retrieval rows carry keys and // scores, so select the field the printout needs. rows, err := docs.Query(). Vector("v", []float32{1.0, 0.0}, 3, corvid.MetricCosine). Select("title"). Run() if err != nil { panic(err) } for rank, r := range rows { fmt.Printf("%d. %s score=%.6f %v\n", rank+1, r.Key, r.Score, r.Doc) } } ``` ### Hybrid retrieval ```go func main() { db, err := corvid.OpenMemory() if err != nil { panic(err) } defer func() { must(db.Close()) }() docs, err := db.Collection("docs") if err != nil { panic(err) } defer docs.Close() must(docs.Insert([]byte("s1"), map[string]any{ "kind": "doc", "body": "rust embedded database", "v": []float32{1.0, 0.0}, })) must(docs.Insert([]byte("s2"), map[string]any{ "kind": "doc", "body": "python web frameworks", "v": []float32{0.0, 1.0}, })) must(docs.Insert([]byte("s3"), map[string]any{ "kind": "doc", "body": "rust again database", "v": []float32{0.9, 0.1}, })) must(docs.Insert([]byte("m1"), map[string]any{"kind": "meta"})) // filtered out below // The flagship query: filter + vector + text, RRF + MMR + limit. rows, err := docs.Query(). Filter(corvid.Field("kind").Eq("doc")). Vector("v", []float32{1.0, 0.0}, 2, corvid.MetricCosine). Text("body", "rust database", 2). FuseRRF(60). RerankMMR(1.0). Limit(2). Select("body"). Run() if err != nil { panic(err) } for rank, r := range rows { fmt.Printf("%d. %s score=%.6f %v\n", rank+1, r.Key, r.Score, r.Doc) } } ``` The fused scores are RRF rank sums: `s1` is rank 1 of both sources (1/61 + 1/61 = 2/61 ≈ 0.032787), `s3` rank 2 of both (2/62 ≈ 0.032258). ## Value mapping | Go | engine | |---|---| | `nil` / `bool` / `string` | Null / Bool / Text | | `int64` | Int (full i64) | | `float64` | Float — NaN and ±inf cross bit-exactly | | `[]byte` | Bytes | | `[]float32` | Vector | | `[]any` / `map[string]any` | Array / Map | Keys are `[]byte`. Errors are `*corvid.CorvidError` (implements `error` + `Code()`); `Query`/`Predicate` builders are single-goroutine, build-once, consumed-by-the-terminal; `Close` on every handle, with runtime finalizers as backstops only. ## Correctness story The binding replays the engine's **golden suite** — the same 267-line fixture files the C ABI smoke harness runs, vendored byte-identical and verified against each release — through its public API on every CI run (`golden_test.go`), then executes the six-example tour under `go run` (and golangci-lint). The plan (architecture ruling, lifetime mapping, pointer discipline) lives in the repo. Next: the [reference section](/reference/constructs/). ================================================================================ # corvid-js # /docs/v0.3.1/bindings/corvid-js/ ================================================================================ [`corvid-js`](https://github.com/corvid-db/corvid-js) is the JavaScript binding for browsers and Web Workers: the engine compiled to `wasm32-unknown-unknown` (a Rust crate pinned to an exact corvid release tag) behind **wasm-bindgen typed exports**, wrapped as idiomatic **synchronous OOP** — `Db`, `Collection`, a fluent `Query` builder, and `field()` predicates. No SQL, no JSON, no serialization on the data path; values cross the boundary natively. **When to choose this binding:** your application runs in the browser or a Worker (edge runtimes, client-side search, offline-first caches, in-page analytics) and you want an embedded database with vector/text/hybrid search, graph edges, and geo — without a server round-trip. The engine ships as one `.wasm` artifact (~363 KB gzipped at bootstrap, budget-gated at 1 MB in CI); every call is synchronous. **The persistence boundary, stated plainly: a `Db` is in-memory per session.** wasm has no filesystem, so nothing survives a page reload today — OPFS-backed persistence is a *decided, trigger-based* future addition (the repo's plan §5). Everything else the engine does — every index family, schemas, TTL, graph, geo, hybrid queries — works and is pinned by the engine's golden fixtures. ## Install ```sh npm i corvid-js ``` **Pending first publish** — the package is not on npm yet; build from source meanwhile (Rust ≥ 1.88 with the `wasm32-unknown-unknown` target + [wasm-pack](https://rustwasm.github.io/wasm-pack/)): ```sh npm install npm run build ``` ## The examples Six runnable programs in the repo's `examples/` directory, executed on every CI leg with deterministic output: **quickstart**, **hybrid** (the flagship below), **vector-index** (in-memory / on-disk-mode / binary-quantized HNSW vs the exact scan), **text-search** (BM25 incl. CJK bigram segmentation and v0.3.0 phrase search), **graph** (neighbors/traverse + delete cascade), and **geo** (radius / bbox / nearest). The quickstart and hybrid sources are embedded below — imported from the repo so they cannot drift from what CI executes (`scripts/sync-binding-examples.sh`; the drift gate reddens docs CI if they diverge). The files run as Node scripts against the same wasm binary browsers load — in a browser only the loader line differs: ```js import { Db, init } from 'corvid-js'; await init(); // fetch + instantiate the wasm module (the only async part) const db = new Db(); // ...everything below, unchanged ``` ### Quickstart ```js import { Db } from '../node.mjs'; const db = new Db(); const docs = db.collection('docs'); docs.insert('p1', { title: 'rust embedded database', kind: 'doc', v: new Float32Array([1.0, 0.0]), }); docs.insert('p2', { title: 'python web frameworks', kind: 'doc', v: new Float32Array([0.0, 1.0]), }); docs.insert('p3', { title: 'rust again database', kind: 'doc', v: new Float32Array([0.9, 0.1]), }); // kNN: the 3 nearest documents to (1, 0) under cosine. const rows = docs .query() .vector('v', new Float32Array([1.0, 0.0]), 3, 'cosine') .run(); // [{ key, doc, score }] let rank = 0; for (const { key, doc, score } of rows) { console.log(`${++rank}. ${key} score=${score.toFixed(6)} ${doc.title}`); } docs.close(); db.close(); ``` ### Hybrid retrieval ```js import { Db, field } from '../node.mjs'; const db = new Db(); const docs = db.collection('docs'); docs.insert('s1', { kind: 'doc', body: 'rust embedded database', v: new Float32Array([1.0, 0.0]) }); docs.insert('s2', { kind: 'doc', body: 'python web frameworks', v: new Float32Array([0.0, 1.0]) }); docs.insert('s3', { kind: 'doc', body: 'rust again database', v: new Float32Array([0.9, 0.1]) }); docs.insert('m1', { kind: 'meta' }); // filtered out below // The flagship query: filter + vector + text, RRF + MMR + limit. const rows = docs .query() .filter(field('kind').eq('doc')) .vector('v', new Float32Array([1.0, 0.0]), 2, 'cosine') .text('body', 'rust database', 2) .fuseRrf(60) .rerankMmr(1.0) .limit(2) .run(); // [{ key, doc, score }] let rank = 0; for (const { key, doc, score } of rows) { console.log(`${++rank}. ${key} score=${score.toFixed(6)} ${doc.body}`); } docs.close(); db.close(); ``` The fused scores are RRF rank sums: `s1` is rank 1 of both sources (1/61 + 1/61 = 2/61 ≈ 0.032787), `s3` rank 2 of both (2/62 ≈ 0.032258). ## Value mapping | JS | engine | |---|---| | `null`, `boolean`, `string` | Null / Bool / Text | | `number` (integer-valued, ≤ 2^53) | Int — `2` and `2.0` collapse; `CorvidFloat(n)` forces the Float kind | | `number` (`0.5`, `inf`, `NaN`, `-0.0`), `bigint` | Float / Int (full i64) | | `Uint8Array` (Buffer included) | Bytes | | `Float32Array` | Vector | | `Array` / plain object | Array / Map | Reading back: Int → `number` (or `bigint` beyond ±2^53); Float → `number` with f64 bits preserved **except NaN payloads**, which canonicalize across the JS↔wasm Number boundary (`-0.0`, `±inf` are exact; vector elements keep their f32 bits). Keys are strings (UTF-8) or Uint8Arrays. `Object.keys()` of a mapped document enumerates the engine's ascending key-byte order — the JS form of the ABI's v0.3.0 `map_keys` surface. Errors are `CorvidError` with the frozen C-ABI `code` table. ## Correctness story The binding replays the engine's **golden suite** — the same fixture files the C ABI smoke harness runs — against its public API on every CI run: 230/230 executable lines across the six in-memory fixture files, including the v0.3.0 `VMAP_KEYS` and `PHRASE` additions. The two file-backed fixture files (`persist.txt`, `admin.txt`) are not vendored — their scenarios are exactly the deferred persistence boundary; their in-memory-executable contracts (the compact quiescence gate, collections listing, session durability) are pinned by the binding's regression suite. The suite runs under node's wasm runtime against the same binary browsers load. A size gate holds the gzipped wasm under 1 MB (the engine's own reference: 2 MB), and a surface-manifest gate resolves every engine construct at the pinned tag to a binding API or a documented N/A. ## Development ```sh npm install # vitest (Rust + wasm-pack required for the build) npm run build # wasm-pack build --release --target web -> pkg/ npm test # the golden suite (230 lines) + regressions node examples/hybrid.js # the examples tour npm run lint # cargo fmt --check + clippy -D warnings ``` Next: [the FFI reference](/ffi/overview/). ================================================================================ # corvid-node # /docs/v0.3.1/bindings/corvid-node/ ================================================================================ [`corvid-node`](https://github.com/corvid-db/corvid-node) is the Node.js binding: the engine compiled in (a Rust napi crate pinned to an exact corvid release tag), exposed as idiomatic **synchronous OOP** — `Db`, `Collection`, a fluent `Query` builder, and `field()` predicates. No SQL, no JSON, no serialization on the data path; values map natively. **When to choose this binding:** your application is Node.js (servers, CLIs, tooling) and you want an embedded database with vector/text/hybrid search, graph edges, and geo — without running a separate database server. The engine compiles into the process (a prebuilt native binary per platform), calls are synchronous, and JavaScript values cross the boundary natively. For browsers, wait for the planned wasm binding; for a system library you can link from anything, see [corvid-c](/bindings/corvid-c/). ## Install ```sh npm i corvid-node ``` **Pending first publish** — the package is not on npm yet; publishing waits on the platform packages existing first (the repo's plan §5). Until then build from source (Rust ≥ 1.88 + a C toolchain): ```sh npm install npm run build ``` Prebuilt binaries (`optionalDependencies`) will cover `darwin-arm64` / `darwin-x64` / `linux-x64-gnu` / `linux-arm64-gnu` / `win32-x64-msvc`. ## The examples Six runnable programs in the repo's `examples/` directory, executed on every CI leg with deterministic output: **quickstart**, **hybrid** (the flagship below), **vector-index** (in-memory / on-disk / binary-quantized HNSW vs the exact scan), **text-search** (BM25 incl. CJK bigram segmentation, plus the v0.3.0 direct `phraseSearch()`), **graph** (neighbors/traverse + delete cascade), and **geo** (radius / bbox / nearest). The quickstart and hybrid sources are embedded below — imported from the repo so they cannot drift from what CI executes (`scripts/sync-binding-examples.sh`; the drift gate reddens docs CI if they diverge). Run them from a checkout with `npm run build && node examples/hybrid.js` (they `require('..')`; in an application, `require('corvid-node')`). ### Quickstart ```js const { Db } = require('..'); const db = Db.openMemory(); const docs = db.collection('docs'); docs.insert('p1', { title: 'rust embedded database', kind: 'doc', v: new Float32Array([1.0, 0.0]), }); docs.insert('p2', { title: 'python web frameworks', kind: 'doc', v: new Float32Array([0.0, 1.0]), }); docs.insert('p3', { title: 'rust again database', kind: 'doc', v: new Float32Array([0.9, 0.1]), }); // kNN: the 3 nearest documents to (1, 0) under cosine. const rows = docs .query() .vector('v', new Float32Array([1.0, 0.0]), 3, 'cosine') .run(); // [{ key, doc, score }] let rank = 0; for (const { key, doc, score } of rows) { console.log(`${++rank}. ${key} score=${score.toFixed(6)} ${doc.title}`); } docs.close(); db.close(); ``` ### Hybrid retrieval ```js const { Db, field } = require('..'); const db = Db.openMemory(); const docs = db.collection('docs'); docs.insert('s1', { kind: 'doc', body: 'rust embedded database', v: new Float32Array([1.0, 0.0]) }); docs.insert('s2', { kind: 'doc', body: 'python web frameworks', v: new Float32Array([0.0, 1.0]) }); docs.insert('s3', { kind: 'doc', body: 'rust again database', v: new Float32Array([0.9, 0.1]) }); docs.insert('m1', { kind: 'meta' }); // filtered out below // The flagship query: filter + vector + text, RRF + MMR + limit. const rows = docs .query() .filter(field('kind').eq('doc')) .vector('v', new Float32Array([1.0, 0.0]), 2, 'cosine') .text('body', 'rust database', 2) .fuseRrf(60) .rerankMmr(1.0) .limit(2) .run(); // [{ key, doc, score }] let rank = 0; for (const { key, doc, score } of rows) { console.log(`${++rank}. ${key} score=${score.toFixed(6)} ${doc.body}`); } docs.close(); db.close(); ``` The fused scores are RRF rank sums: `s1` is rank 1 of both sources (1/61 + 1/61 = 2/61 ≈ 0.032787), `s3` rank 2 of both (2/62 ≈ 0.032258). ## Value mapping | JS | engine | |---|---| | `null`, `boolean`, `string` | Null / Bool / Text | | `number` (integer-valued, ≤ 2^53) | Int — `2` and `2.0` collapse; `CorvidFloat(n)` forces the Float kind | | `number` (`0.5`, `inf`, `NaN`, `-0.0`), `bigint` | Float / Int (full i64) | | `Buffer` / `Uint8Array` | Bytes | | `Float32Array` | Vector | | `Array` / plain object | Array / Map | Reading back: Int → `number` (or `bigint` beyond ±2^53); Float → `number` with f64 bits preserved **except NaN payloads**, which V8 canonicalizes at the N-API number boundary (`-0.0`, `±inf` are exact; vector elements keep their f32 bits). Keys are strings (UTF-8) or Buffers. ## Correctness story The binding replays the engine's **golden suite** — the same 267-line fixture files the C ABI smoke harness runs — against its public API on every CI run (`test/golden.spec.ts`), then executes the six-example tour. The plan (architecture ruling, OOP surface, value contract, follow-ups) lives in the repo. ## Development ```sh npm install # @napi-rs/cli + vitest npm run build # build the native binary for this platform npm test # the golden suite (267 lines) node examples/hybrid.js # the examples tour npm run lint # cargo fmt --check + clippy -D warnings ``` Next: [corvid-python](/bindings/corvid-python/). ================================================================================ # corvid-python # /docs/v0.3.1/bindings/corvid-python/ ================================================================================ [`corvid-python`](https://github.com/corvid-db/corvid-python) is the Python binding: the engine compiled in (a Rust pyo3 crate pinned to an exact corvid release tag), exposed as idiomatic **synchronous OOP** — `Db`, `Collection`, a fluent `Query` builder, and `field()` predicates. No SQL, no JSON, no serialization on the data path; values map natively (`array('f')` is the vector type; floats round-trip with f64 bits preserved exactly — no JS-style NaN caveats). **When to choose this binding:** your workload lives in Python — data pipelines, notebooks, model-serving sidecars, scripts — and you want an embedded vector + text + graph + geo store with zero servers and zero serialization. Handles are context managers (`with Db(...) as db:`), cursors are Python iterables, and every failure raises a typed `CorvidError` carrying the engine's frozen error `code`. For other languages see the [bindings overview](/bindings/overview/). ## Install **Pending first publish** — the package is not on PyPI yet; everything is prepared (maturin wheel config, one abi3 wheel per platform), and publishing waits on the first release tag. Until then build from source — Python ≥ 3.11, Rust ≥ 1.88, and a C toolchain: ```sh pip install maturin maturin develop --release # into the active venv ``` The wheel is abi3 (cp311), so one wheel per platform covers every Python ≥ 3.11. Planned platform matrix: `linux-x64` / `linux-arm64` / `macos-arm64` / `windows-x64`. ## The examples Six runnable programs in the repo's `examples/` directory, executed on every CI leg with deterministic output: **quickstart**, **hybrid** (the flagship below), **vector-index** (in-memory / on-disk / binary-quantized HNSW vs the exact scan), **text-search** (BM25 incl. CJK bigram segmentation, plus the v0.3.0 direct `phrase_search()`), **graph** (neighbors/traverse + delete cascade), and **geo** (radius / bbox / nearest). The quickstart and hybrid sources are embedded below — imported from the repo so they cannot drift from what CI executes (`scripts/sync-binding-examples.sh`; the drift gate reddens docs CI if they diverge). Run them from a checkout with `maturin develop && python examples/hybrid.py`. ### Quickstart ```python from array import array from corvid import Db with Db.open_memory() as db: docs = db.collection("docs") docs.insert("p1", {"title": "rust embedded database", "kind": "doc", "v": array("f", [1.0, 0.0])}) docs.insert("p2", {"title": "python web frameworks", "kind": "doc", "v": array("f", [0.0, 1.0])}) docs.insert("p3", {"title": "rust again database", "kind": "doc", "v": array("f", [0.9, 0.1])}) # kNN: the 3 nearest documents to (1, 0) under cosine. rows = ( docs.query() .vector("v", array("f", [1.0, 0.0]), 3, "cosine") .run() ) # [Row(key, score, document), ...] for rank, row in enumerate(rows, start=1): print(f"{rank}. {row.key} score={row.score:.6f} " f"{row.document['title']}") docs.close() ``` ### Hybrid retrieval ```python from array import array from corvid import Db, field with Db.open_memory() as db: docs = db.collection("docs") docs.insert("s1", {"kind": "doc", "body": "rust embedded database", "v": array("f", [1.0, 0.0])}) docs.insert("s2", {"kind": "doc", "body": "python web frameworks", "v": array("f", [0.0, 1.0])}) docs.insert("s3", {"kind": "doc", "body": "rust again database", "v": array("f", [0.9, 0.1])}) docs.insert("m1", {"kind": "meta"}) # filtered out below # The flagship query: filter + vector + text, RRF + MMR + limit. rows = ( docs.query() .filter(field("kind").eq("doc")) .vector("v", array("f", [1.0, 0.0]), 2, "cosine") .text("body", "rust database", 2) .fuse_rrf(60) .rerank_mmr(1.0) .limit(2) .run() ) # [Row(key, score, document), ...] for rank, row in enumerate(rows, start=1): print(f"{rank}. {row.key} score={row.score:.6f} " f"{row.document['body']}") docs.close() ``` The fused scores are RRF rank sums: `s1` is rank 1 of both sources (1/61 + 1/61 = 2/61 ≈ 0.032787), `s3` rank 2 of both (2/62 ≈ 0.032258). ## Value mapping | Python | engine | |---|---| | `None`, `bool`, `str` | Null / Bool / Text | | `int` | Int (full i64 — out-of-range ints raise code 12) | | `float` | Float | | `bytes` / `bytearray` | Bytes | | `array('f')` | Vector (other typecodes are rejected) | | `list` / `tuple` | Array | | `dict` (str keys) | Map | Reading back: Int → `int` (arbitrary precision — no ±2^53 boundary, unlike the JS binding's number/BigInt split); Float → `float` with **f64 bits preserved exactly** — NaN payloads, `-0.0`, and `±inf` all round-trip bit-exactly. Vector → `array('f')` (f32-exact both directions), Map → `dict` in the engine's key order. Keys are `str` (UTF-8) or `bytes`. ## Correctness story The binding replays the engine's **golden suite** — the same 267-line fixture files the C ABI smoke harness runs — against its public API on every CI run (`tests/test_golden.py`), then executes the six-example tour. Type stubs ship in-package (`py.typed`); the plan (architecture ruling, OOP surface, value contract) lives in the repo. ## Development ```sh python -m venv .venv && source .venv/bin/activate pip install maturin pytest maturin develop # build the native extension pytest tests # the golden suite (267 fixture lines) python examples/hybrid.py # the examples tour cargo fmt --check # + cargo clippy --all-targets -- -D warnings ``` Next: [corvid-go](/bindings/corvid-go/). ================================================================================ # corvid-zig # /docs/v0.3.1/bindings/corvid-zig/ ================================================================================ [`corvid-zig`](https://github.com/corvid-db/corvid-zig) is the Zig binding: it links the engine's **published FFI artifacts** (the platform cdylib and `corvid.h`) through a single `@cImport` and carries an idiomatic Zig layer on top. Deliberately the corvid-c/corvid-go pattern (a fetched, checksummed shared library), not the node/python one (Rust-source builds): `./fetch.sh` downloads and sha256-verifies the pinned release archive, `zig build` does the rest — no Rust toolchain, no vendored binaries. **When to choose this binding:** your project is Zig and you want corvid embedded with the language's own shape — `defer`-friendly handles, error unions instead of status codes, and the ABI's sharpest UB classes (consumed-then-freed handles, freed borrows) turned into compile errors or safe no-ops by construction. ## The idiom mapping | C ABI | corvid-zig | | --- | --- | | opaque handles (`corvid_db*`, …) | `Db` / `Collection` / `Query` / `Pred` / `Value` structs with `deinit()` — `defer` is the ownership model | | `CORVID_ERR` + thread-local last error | `corvid.Error` — one Zig error per `corvid_err` code; `lastErrorCode()` / `lastErrorMessage()` stay public | | frozen enums | `Metric`, `Quant`, `Cmp`, `FieldType`, `ValueKind` (exact ABI values) | | consumed-by-call args (pred trees, builders) | **moves**: consuming calls take the wrapper by pointer and null its handle — a moved wrapper's `deinit()` is a safe no-op, so the double-free UB class cannot happen | | borrowed views (`_ref` buffers, row docs, `map_get` children) | `ValueView` — a read-only type with **no** `deinit`: freeing a borrow is a *compile error* in Zig, where in C it is undefined behavior | | `corvid_update_fn` / `corvid_scan_fn` | Zig closures (context + function); an `update` callback returning ANY Zig error aborts the read-modify-write through the ABI's §1.6 abort channel, and panics never unwind through C frames (Zig panics abort) | | strings / bytes / vectors | `[]const u8` / `[]const f32` slices — the ABI's binary-safe ptr+len maps 1:1 | The raw ABI stays importable as `corvid.c` — the golden harness drives it exactly the way the engine's own C harness does. Application code should stick to the wrapper types. ## Install From the pinned release artifacts: ```sh ./fetch.sh # fetch + sha256-verify corvid v0.3.0 into deps/current zig build test # wrapper unit tests + the golden suite (267 lines) ``` Requirements: Zig 0.16.0 (the current stable line — the floor rides it; see the repo's `docs/PLAN.md` toolchain-policy note), `curl` + `shasum`/`sha256sum` (or PowerShell on Windows). On Windows, binaries run by hand want `deps/current` on `PATH` so the loader finds `corvid.dll` (the build's run steps handle this for you). ## Documents, maps, and phrases Engine v0.3.0's ABI additions are first-class here: - `Value.mapKeys()` / `ValueView.mapKeys()` — the map's keys in ascending byte order (UTF-8 and nested keys included), over the §4.12 string cursor. Wrong-typed values answer an empty cursor — inert, not an error. - `Collection.phraseSearch(field, phrase, k)` — the DIRECT positional search: consecutive, in-order analyzed tokens, stop words collapsing out of adjacency (`"embedded the database"` matches `"embedded database"`), rows carrying the BM25 phrase score (the phrase scale, not the builder's fused RRF scale); `k == 0` answers an empty cursor — inert, never an error. The `text_search` example demonstrates all of it, CJK bigram phrases included. ## The examples Six runnable programs under the repo's `examples/` directory (`zig build run-`), executed on every CI leg with deterministic output: **quickstart**, **hybrid** (the flagship below), **vector_index** (in-memory / on-disk / binary-quantized HNSW vs the exact scan, plus a close/reopen), **text_search** (BM25 incl. CJK bigram segmentation, plus the v0.3.0 direct `phraseSearch`), **graph** (neighbors/traverse + delete cascade), and **geo** (radius / bbox / nearest with haversine kilometres). The quickstart and hybrid sources are embedded below — imported from the repo so they cannot drift from what CI executes (`scripts/sync-binding-examples.sh`; the drift gate reddens docs CI if they diverge). ### Quickstart ```zig fn putDoc(docs: corvid.Collection, key: []const u8, title: []const u8, kind: []const u8, v: []const f32) !void { var doc = corvid.Value.map(); defer doc.deinit(); // insert CLONES the value; ours is still ours var t = try corvid.Value.text(title); try doc.put("title", &t); // moves t into the map var k = try corvid.Value.text(kind); try doc.put("kind", &k); var vec = corvid.Value.vector(v); try doc.put("v", &vec); try docs.insert(key, doc); } pub fn main(init: std.process.Init) u8 { var db = corvid.Db.openMemory() catch |e| return fail(e, "open"); defer db.deinit(); var docs = db.collection("docs") catch |e| return fail(e, "collection"); defer docs.deinit(); putDoc(docs, "p1", "rust embedded database", "doc", &.{ 1.0, 0.0 }) catch |e| return fail(e, "insert"); putDoc(docs, "p2", "python web frameworks", "doc", &.{ 0.0, 1.0 }) catch |e| return fail(e, "insert"); putDoc(docs, "p3", "rust again database", "doc", &.{ 0.9, 0.1 }) catch |e| return fail(e, "insert"); // kNN: the 3 nearest documents to (1, 0) under cosine. The builder // methods chain; run() consumes the builder. var q = docs.query() catch |e| return fail(e, "query_new"); defer q.deinit(); // no-op after run() var rows = (q .vector("v", &.{ 1.0, 0.0 }, 3, .cosine) catch |e| return fail(e, "query_vector") ).run() catch |e| return fail(e, "query_run"); defer rows.deinit(); var rank: usize = 0; var buf: [256]u8 = undefined; var w = std.Io.File.stdout().writer(init.io, &buf); while (rows.next()) |row| { rank += 1; const title = row.doc.mapGet("title").?.textRef().?; w.interface.print("{d}. {s} score={d:.6} {s}\n", .{ rank, row.key, @as(f64, row.score), title }) catch {}; } w.interface.flush() catch {}; return 0; } ``` ### Hybrid retrieval ```zig fn putDoc(docs: corvid.Collection, key: []const u8, kind: []const u8, body: ?[]const u8, v: ?[]const f32) !void { var doc = corvid.Value.map(); defer doc.deinit(); var k = try corvid.Value.text(kind); try doc.put("kind", &k); if (body) |b| { var t = try corvid.Value.text(b); try doc.put("body", &t); } if (v) |vec| { var val = corvid.Value.vector(vec); try doc.put("v", &val); } try docs.insert(key, doc); } fn printRows(init: std.process.Init, rows: *corvid.Rows) void { var buf: [256]u8 = undefined; var w = std.Io.File.stdout().writer(init.io, &buf); var rank: usize = 0; while (rows.next()) |row| { rank += 1; const body = row.doc.mapGet("body").?.textRef() orelse "?"; w.interface.print("{d}. {s} score={d:.6} {s}\n", .{ rank, row.key, @as(f64, row.score), body }) catch {}; } w.interface.flush() catch {}; } pub fn main(init: std.process.Init) u8 { var db = corvid.Db.openMemory() catch |e| return fail(e, "open"); defer db.deinit(); var docs = db.collection("docs") catch |e| return fail(e, "collection"); defer docs.deinit(); putDoc(docs, "s1", "doc", "rust embedded database", &.{ 1.0, 0.0 }) catch |e| return fail(e, "insert"); putDoc(docs, "s2", "doc", "python web frameworks", &.{ 0.0, 1.0 }) catch |e| return fail(e, "insert"); putDoc(docs, "s3", "doc", "rust again database", &.{ 0.9, 0.1 }) catch |e| return fail(e, "insert"); putDoc(docs, "m1", "meta", null, null) catch |e| return fail(e, "insert"); // filtered out below // The flagship query: filter + vector + text, RRF + MMR + limit. var q = docs.query() catch |e| return fail(e, "query_new"); defer q.deinit(); // no-op after run() var kind = corvid.Value.text("doc") catch |e| return fail(e, "text"); defer kind.deinit(); // pred_compare CLONES it; ours is still ours var only_docs = corvid.Pred.compare("kind", .eq, kind) catch |e| return fail(e, "pred_compare"); defer only_docs.deinit(); // safe no-op after filter moves it _ = q.filter(&only_docs) catch |e| return fail(e, "query_filter"); // moves the pred // The setters mutate the builder and return it, so each step is one // statement with its own named failure; run() consumes the builder. _ = q.vector("v", &.{ 1.0, 0.0 }, 2, .cosine) catch |e| return fail(e, "query_vector"); _ = q.text("body", "rust database", 2) catch |e| return fail(e, "query_text"); _ = q.fuseRrf(60.0) catch |e| return fail(e, "query_fuse_rrf"); _ = q.rerankMmr(1.0) catch |e| return fail(e, "query_rerank_mmr"); _ = q.limit(2) catch |e| return fail(e, "query_limit"); var rows = q.run() catch |e| return fail(e, "query_run"); defer rows.deinit(); printRows(init, &rows); return 0; } ``` ## The correctness floor `zig build test` replays the engine's entire **golden fixture suite** — 267 executable lines across 8 files, including the v0.3.0 `VMAP_KEYS`/`GET_KEYS` (map-key iteration) and `PHRASE`/`PHRASE_K0` (direct positional search) lines — against the **downloaded** cdylib, through a statement-for-statement port of the engine's own C harness (`test/golden.zig`): every counted line must dispatch, the first failure names file:line + OP + expected-vs-got, and every handle is freed on its creation path (the CI sanitizer leg builds the harness with ASan and expects zero reports). The fixtures are vendored in the repo and byte-compared against the release's copies at fetch time, so a bad artifact is a loud fetch failure, never a silent skip. On top sits `docs/SURFACE.tsv` — every construct of the engine's public surface (327 rows at this pin) resolved to the Zig API exposing it plus the golden line that proves it, or `N/A` with the ABI's §9 reason, gated in CI (`scripts/surface-gate.sh`). Next: [corvid-js](/bindings/corvid-js/). ================================================================================ # Bindings ecosystem # /docs/v0.3.1/bindings/overview/ ================================================================================ Bindings sit on [the C ABI](/ffi/overview/) (or embed the engine directly), expose idiomatic OOP per the ABI's ruling 3 — handles become native classes, iterators become the language's native iteration, `CORVID_ERR` becomes native exceptions, destructors map to the dispose pattern — and are **synchronous** in v1 (the engine is sync). No FFI symbols leak into a binding's public API. ## Live | Binding | Language | Status | |---|---|---| | [corvid-c](/bindings/corvid-c/) | C (reference consumer) | live — release-artifact conformance, golden suite port, six-example tour as ctests | | [corvid-node](/bindings/corvid-node/) | Node.js (native, engine compiled in) | live — golden-suite CI + examples tour; npm publish pending first release | | [corvid-python](/bindings/corvid-python/) | Python (native, engine compiled in) | live — golden-suite CI + examples tour; PyPI publish pending first release | | [corvid-go](/bindings/corvid-go/) | Go (cgo over the published cdylib) | live — golden-suite CI + examples tour, no Rust toolchain required | | [corvid-js](/bindings/corvid-js/) | JavaScript (browser/Worker, engine compiled to wasm) | live — golden-suite CI + examples tour + a CI-enforced wasm size budget; in-memory per session (OPFS persistence is a decided, trigger-based deferral); npm publish pending first release | | [corvid-cpp](/bindings/corvid-cpp/) | C++ (RAII over the published cdylib) | live — golden-suite CI + examples tour, header-first RAII library, no Rust toolchain required | | [corvid-zig](/bindings/corvid-zig/) | Zig (@cImport of the published cdylib) | live — golden-suite CI + examples tour (text search exercises the v0.3.0 phrase API), move-safe handles and typed borrows, no Rust toolchain required | | corvid (Rust) | Rust | the engine itself; native API | Every live binding ships the same **examples tour** — six runnable programs per language (quickstart, hybrid RRF+MMR, vector-index families, text search incl. CJK, graph with delete cascade, geo), executed on every CI leg with deterministic output. The quickstart and hybrid sources are imported into each binding page here (`scripts/sync-binding-examples.sh`; CI diffs against the binding repos' master so they cannot drift). ## Planned bindings Each planned binding codes against the same frozen ABI, pins an exact engine tag, and ships the engine's golden fixtures as its correctness floor. One planned-scope line each: | Binding | Planned scope | |---|---| | **corvid-jvm** | JNI bindings for Java/Kotlin with `AutoCloseable` handles; cursor iterators as `java.util.Iterator`; gradle-consumed artifacts per platform. | | **corvid-dart** | Flutter/Dart FFI bindings with `Finalizable` handles — mobile-first (the engine already cross-compiles for aarch64 iOS/Android). | | **corvid-php** | PHP extension (FFI or native) with one handle per request/thread (ZTS posture per the ABI threading rules). | | **corvid-rust** (crates.io) | the engine itself, published to crates.io as `corvid` — replacing the git dependency (see [install](/start/install/)). | Planned means planned: none of the above exist yet. Each will get its own documentation section here when it ships. ## The correctness floor Every binding replays the engine's **golden fixtures** — the 267-line fixture suite the C ABI smoke harness runs — against its public API on every CI run. corvid-c ports the fixtures in C; corvid-node and corvid-python replay them through TypeScript and pytest; corvid-go drives them through `go test`; corvid-js replays the six in-memory fixture files through the wasm binary node's runtime and browsers share (the two file-backed fixture files are the deferred OPFS persistence boundary — their in-memory contracts are pinned by its regression suite); corvid-zig replays them through a statement-for-statement port of the engine's own C harness. On top of the golden suite, every binding's CI executes its examples tour. A binding that cannot pass the golden suite is not shippable; that is the program's bar. Next: [corvid-c](/bindings/corvid-c/). ================================================================================ # Construct reference # /docs/v0.3.1/reference/constructs/ ================================================================================ > **generated — synced from the engine at v0.3.1.** This page lists the > complete writable surface of `corvid` and `corvid-mcp`: every public > construct grouped by statement class (the SQL analogue is a guide, not a > promise), each with the integration tests that pin its > happy/edge/error/corner behavior. Construct paths are canonical Rust > paths; `mcp::tool::` / `mcp::envelope::` are wire syntax. > Human-oriented guides: [the corvid language](/language/data-model/). ### Mutations — 10 construct(s) - `corvid::Collection` — `mutations_smoke_insert_roundtrips` - `corvid::Collection::insert` — `mutations_smoke_insert_roundtrips`, `mutations_insert_roundtrips_every_value_variant`, `mutations_insert_overwrites_and_accepts_empty_key_and_empty_map`, `mutations_insert_rejects_reserved_and_invalid_collection_names` - `corvid::Collection::update` — `mutations_update_rewrites_document_to_every_value_kind`, `mutations_update_on_missing_key_creates_or_stays_absent`, `mutations_update_maintains_scalar_index` - `corvid::Collection::patch` — `mutations_patch_merges_top_level_and_replaces_non_maps` - `corvid::Collection::compare_and_set` — `mutations_compare_and_set_swap_noop_delete_and_semantic_float_equality`, `mutations_compare_and_set_uses_semantic_value_equality`, `mutations_compare_and_set_maintains_scalar_index` - `corvid::Collection::insert_batch` — `mutations_insert_batch_happy_empty_overwrite_and_duplicates`, `mutations_insert_batch_unique_conflict_rolls_back_whole_batch`, `mutations_insert_batch_schema_violation_rolls_back_whole_batch` - `corvid::Collection::insert_auto` — `mutations_smoke_insert_roundtrips`, `mutations_insert_auto_keys_are_unique_zero_padded_and_monotonic_per_collection`, `mutations_insert_auto_failure_does_not_burn_an_id` - `corvid::Collection::delete` *(shared across classes: Mutations, Graph)* — `mutations_smoke_insert_roundtrips`, `mutations_delete_removes_state_from_get_scan_and_count`, `graph_delete_missing_document_still_purges_dangling_edges` - `corvid::Collection::delete_where` *(shared across classes: Mutations, Graph)* — `mutations_delete_where_counts_zero_partial_and_full`, `mutations_delete_where_exact_with_scalar_index_present`, `graph_delete_where_and_delete_batch_cascade_edges` - `corvid::Collection::delete_batch` *(shared across classes: Mutations, Graph)* — `mutations_delete_batch_counts_existing_only_and_accepts_empty`, `graph_delete_where_and_delete_batch_cascade_edges` ### WHERE — 52 construct(s) - `corvid::Value` *(shared across classes: Mutations, WHERE)* — `mutations_smoke_insert_roundtrips`, `mutations_insert_roundtrips_every_value_variant`, `filters_compare_eq_matches_each_value_kind` - `corvid::Value::Null` *(shared across classes: Mutations, WHERE)* — `mutations_insert_roundtrips_every_value_variant`, `filters_compare_eq_matches_each_value_kind` - `corvid::Value::Bool` *(shared across classes: Mutations, WHERE)* — `mutations_insert_roundtrips_every_value_variant`, `filters_compare_eq_matches_each_value_kind`, `filters_unordered_kinds_compare_false_for_ordered_ops` - `corvid::Value::Int` *(shared across classes: Mutations, WHERE)* — `mutations_smoke_insert_roundtrips`, `mutations_insert_roundtrips_every_value_variant`, `filters_compare_eq_matches_each_value_kind`, `filters_int_float_precision_beyond_2_pow_53` - `corvid::Value::Float` *(shared across classes: Geo, Mutations, WHERE)* — `search_geo_smoke_within_radius_and_nearest`, `mutations_insert_roundtrips_every_value_variant`, `filters_compare_eq_matches_each_value_kind`, `filters_nan_comparisons_all_false_except_ne`, `filters_int_float_precision_beyond_2_pow_53` - `corvid::Value::Text` *(shared across classes: Mutations, WHERE)* — `mutations_smoke_insert_roundtrips`, `mutations_insert_roundtrips_every_value_variant`, `filters_compare_eq_matches_each_value_kind`, `filters_text_ordering_lexicographic_utf8` - `corvid::Value::Bytes` *(shared across classes: Mutations, WHERE)* — `mutations_insert_roundtrips_every_value_variant`, `filters_compare_eq_matches_each_value_kind` - `corvid::Value::Array` *(shared across classes: Geo, Mutations, WHERE)* — `search_geo_smoke_within_radius_and_nearest`, `mutations_insert_roundtrips_every_value_variant`, `filters_compare_eq_matches_each_value_kind`, `filters_nested_dotted_paths_traverse_maps_only` - `corvid::Value::Map` *(shared across classes: Mutations, WHERE)* — `mutations_smoke_insert_roundtrips`, `mutations_insert_roundtrips_every_value_variant`, `filters_compare_eq_matches_each_value_kind`, `filters_nested_dotted_paths_traverse_maps_only` - `corvid::Value::Vector` *(shared across classes: Vector search, Mutations, WHERE)* — `search_vector_smoke_ranks_nearest_first_exact`, `mutations_insert_roundtrips_every_value_variant`, `filters_compare_eq_matches_each_value_kind`, `filters_unordered_kinds_compare_false_for_ordered_ops` - `corvid::Value::get` — `filters_value_accessors_read_stored_kinds` - `corvid::Value::get_path` — `filters_value_accessors_read_stored_kinds` - `corvid::Value::as_bool` — `filters_value_accessors_read_stored_kinds` - `corvid::Value::as_int` — `filters_value_accessors_read_stored_kinds` - `corvid::Value::as_float` — `filters_value_accessors_read_stored_kinds` - `corvid::Value::as_text` — `filters_value_accessors_read_stored_kinds` - `corvid::Value::as_bytes` — `filters_value_accessors_read_stored_kinds` - `corvid::Value::as_vector` — `filters_value_accessors_read_stored_kinds` - `corvid::CmpOp` — `filters_smoke_field_eq_selects_matching_rows`, `filters_field_builders_produce_claimed_predicates` - `corvid::CmpOp::Eq` *(shared across classes: WHERE, Mutations)* — `filters_smoke_field_eq_selects_matching_rows`, `mutations_update_maintains_scalar_index`, `filters_compare_eq_matches_each_value_kind` - `corvid::CmpOp::Ne` — `filters_compare_ne_and_missing_path_semantics`, `filters_nan_comparisons_all_false_except_ne` - `corvid::CmpOp::Lt` — `filters_ordered_comparisons_numbers_and_edges`, `filters_text_ordering_lexicographic_utf8` - `corvid::CmpOp::Le` — `filters_ordered_comparisons_numbers_and_edges`, `filters_between_inclusive_and_degenerate_bounds` - `corvid::CmpOp::Gt` — `filters_ordered_comparisons_numbers_and_edges`, `filters_text_ordering_lexicographic_utf8` - `corvid::CmpOp::Ge` *(shared across classes: Mutations, WHERE)* — `mutations_delete_where_counts_zero_partial_and_full`, `filters_ordered_comparisons_numbers_and_edges`, `filters_between_inclusive_and_degenerate_bounds` - `corvid::Predicate` *(shared across classes: WHERE, Mutations)* — `filters_smoke_field_eq_selects_matching_rows`, `mutations_delete_where_counts_zero_partial_and_full`, `filters_and_or_not_nesting_and_de_morgan` - `corvid::Predicate::Compare` — `filters_smoke_field_eq_selects_matching_rows`, `filters_compare_eq_matches_each_value_kind`, `filters_ordered_comparisons_numbers_and_edges` - `corvid::Predicate::Exists` — `filters_exists_presence_semantics` - `corvid::Predicate::In` — `filters_in_membership_matrix` - `corvid::Predicate::Between` — `filters_between_inclusive_and_degenerate_bounds` - `corvid::Predicate::StartsWith` — `filters_starts_with_prefix_semantics` - `corvid::Predicate::Contains` — `filters_contains_substring_semantics` - `corvid::Predicate::And` — `filters_and_or_not_nesting_and_de_morgan`, `filters_predicate_combinators_and_direct_construction` - `corvid::Predicate::Or` — `filters_and_or_not_nesting_and_de_morgan`, `filters_indexed_vs_scan_or_union` - `corvid::Predicate::Not` — `filters_and_or_not_nesting_and_de_morgan`, `filters_predicate_combinators_and_direct_construction` - `corvid::Predicate::and` — `filters_predicate_combinators_and_direct_construction`, `filters_multiple_filter_calls_intersect_like_and` - `corvid::Predicate::or` — `filters_predicate_combinators_and_direct_construction`, `filters_indexed_vs_scan_or_union` - `corvid::Predicate::eval` — `filters_compare_eq_matches_each_value_kind`, `filters_ordered_comparisons_numbers_and_edges`, `filters_starts_with_prefix_semantics` - `corvid::field` *(shared across classes: WHERE, Mutations)* — `filters_smoke_field_eq_selects_matching_rows`, `mutations_delete_where_counts_zero_partial_and_full`, `filters_field_builders_produce_claimed_predicates` - `corvid::filter::FieldRef` — `filters_smoke_field_eq_selects_matching_rows`, `filters_field_builders_produce_claimed_predicates` - `corvid::filter::FieldRef::eq` *(shared across classes: WHERE, Mutations)* — `filters_smoke_field_eq_selects_matching_rows`, `mutations_update_maintains_scalar_index`, `filters_field_builders_produce_claimed_predicates`, `filters_compare_eq_matches_each_value_kind` - `corvid::filter::FieldRef::ne` — `filters_field_builders_produce_claimed_predicates`, `filters_compare_ne_and_missing_path_semantics` - `corvid::filter::FieldRef::lt` — `filters_field_builders_produce_claimed_predicates`, `filters_ordered_comparisons_numbers_and_edges` - `corvid::filter::FieldRef::le` — `filters_field_builders_produce_claimed_predicates`, `filters_ordered_comparisons_numbers_and_edges` - `corvid::filter::FieldRef::gt` — `filters_field_builders_produce_claimed_predicates`, `filters_ordered_comparisons_numbers_and_edges` - `corvid::filter::FieldRef::ge` *(shared across classes: Mutations, WHERE)* — `mutations_delete_where_counts_zero_partial_and_full`, `filters_field_builders_produce_claimed_predicates`, `filters_ordered_comparisons_numbers_and_edges` - `corvid::filter::FieldRef::exists` — `filters_exists_presence_semantics`, `filters_field_builders_produce_claimed_predicates` - `corvid::filter::FieldRef::is_in` — `filters_in_membership_matrix`, `filters_field_builders_produce_claimed_predicates` - `corvid::filter::FieldRef::between` — `filters_between_inclusive_and_degenerate_bounds`, `filters_field_builders_produce_claimed_predicates` - `corvid::filter::FieldRef::starts_with` — `filters_starts_with_prefix_semantics`, `filters_field_builders_produce_claimed_predicates` - `corvid::filter::FieldRef::contains` — `filters_contains_substring_semantics`, `filters_field_builders_produce_claimed_predicates` - `corvid::QueryBuilder::filter` *(shared across classes: WHERE, Mutations)* — `filters_smoke_field_eq_selects_matching_rows`, `mutations_update_maintains_scalar_index` ### SELECT shaping — 16 construct(s) - `corvid::ResultRow` *(shared across classes: WHERE, SELECT shaping)* — `filters_smoke_field_eq_selects_matching_rows`, `queries_result_row_fields_per_query_shape`, `queries_run_select_only_returns_all_in_key_order`, `queries_select_preserves_rank_scores_and_filter_visibility` - `corvid::QueryBuilder` *(shared across classes: WHERE, SELECT shaping)* — `filters_smoke_field_eq_selects_matching_rows`, `queries_run_select_only_returns_all_in_key_order` - `corvid::Collection::query` *(shared across classes: WHERE, SELECT shaping)* — `filters_smoke_field_eq_selects_matching_rows`, `queries_run_select_only_returns_all_in_key_order` - `corvid::QueryBuilder::limit` — `queries_smoke_order_by_limit_select_shapes_rows`, `queries_limit_zero_one_exact_and_over_match_count`, `queries_order_by_limit_offset_window_after_ordering`, `queries_limit_offset_on_empty_collection` - `corvid::QueryBuilder::offset` *(shared across classes: WHERE, SELECT shaping)* — `filters_filter_then_limit_offset_pagination`, `queries_offset_boundaries_and_full_range_pagination_loop`, `queries_order_by_limit_offset_window_after_ordering`, `queries_limit_offset_on_empty_collection` - `corvid::QueryBuilder::order_by` — `queries_smoke_order_by_limit_select_shapes_rows`, `queries_order_by_asc_desc_over_int_float_and_text`, `queries_order_by_class_rule_incomparable_then_missing_last_both_directions`, `queries_order_by_mixed_kind_field_groups_numbers_before_texts`, `queries_order_by_limit_offset_window_after_ordering`, `queries_order_by_with_filters_orders_only_matches`, `queries_order_by_indexed_vs_scan_equivalent` - `corvid::QueryBuilder::select` *(shared across classes: SELECT shaping, Schema (ALTER))* — `queries_smoke_order_by_limit_select_shapes_rows`, `queries_select_single_multiple_and_nested_dotted_paths`, `queries_select_missing_fields_omitted_and_duplicates_collapse`, `queries_select_empty_field_list_yields_empty_map_for_map_docs`, `queries_select_non_map_documents_pass_through_unchanged`, `queries_select_preserves_rank_scores_and_filter_visibility`, `schema_select_empty_field_name_matches_get_path_semantics` - `corvid::QueryBuilder::run` *(shared across classes: WHERE, Mutations, SELECT shaping)* — `filters_smoke_field_eq_selects_matching_rows`, `mutations_update_maintains_scalar_index`, `queries_run_on_empty_collection_returns_empty_vec`, `queries_run_select_only_returns_all_in_key_order` - `corvid::Collection::for_each_doc` — `queries_for_each_doc_visits_key_order_and_early_stops_on_false`, `queries_for_each_doc_on_empty_collection_visits_nothing` - `corvid::Collection::len` *(shared across classes: Mutations, SELECT shaping)* — `mutations_smoke_insert_roundtrips`, `mutations_insert_roundtrips_every_value_variant`, `queries_len_and_is_empty_boundaries` - `corvid::Collection::is_empty` *(shared across classes: Mutations, SELECT shaping)* — `mutations_smoke_insert_roundtrips`, `mutations_insert_roundtrips_every_value_variant`, `queries_len_and_is_empty_boundaries` - `corvid::Collection::get` — `mutations_smoke_insert_roundtrips`, `mutations_insert_roundtrips_every_value_variant` - `corvid::Collection::scan` *(shared across classes: Mutations, SELECT shaping)* — `mutations_delete_removes_state_from_get_scan_and_count`, `mutations_insert_overwrites_and_accepts_empty_key_and_empty_map`, `queries_scan_returns_pairs_in_key_order` - `corvid::Collection::page` — `queries_page_cursor_semantics`, `queries_page_after_empty_bytes_skips_only_the_empty_key`, `queries_page_over_empty_collection_yields_empty_page_with_no_cursor` - `corvid::Collection::page_where` — `queries_page_where_predicate_and_full_walk`, `queries_page_over_empty_collection_yields_empty_page_with_no_cursor` - `corvid::db::Page` — `queries_page_cursor_semantics`, `queries_page_where_predicate_and_full_walk` ### Aggregations — 10 construct(s) - `corvid::QueryBuilder::count` *(shared across classes: Aggregations, Mutations, SELECT shaping)* — `aggregations_smoke_sum_group_count_and_count`, `aggregations_count_matrix_filter_empty_and_after_mutations`, `aggregations_ignore_limit_offset_select_order_by_and_sources`, `aggregations_indexed_vs_scan_equivalent_for_every_aggregate`, `aggregations_validate_ranking_args_before_aggregating`, `mutations_delete_removes_state_from_get_scan_and_count`, `queries_count_with_filter_and_after_mutations` - `corvid::QueryBuilder::group_count` — `aggregations_smoke_sum_group_count_and_count`, `aggregations_count_distinct_and_groups_separate_types_by_tag`, `aggregations_group_count_escapes_every_ambiguous_tag_prefix`, `aggregations_group_count_zero_signed_floats_share_and_nan_groups`, `aggregations_group_count_skips_missing_and_container_fields`, `aggregations_group_count_respects_filters`, `aggregations_group_sum_avg_exact_per_bucket_and_absent_empty_buckets`, `aggregations_ignore_limit_offset_select_order_by_and_sources`, `aggregations_indexed_vs_scan_equivalent_for_every_aggregate`, `aggregations_validate_ranking_args_before_aggregating` - `corvid::QueryBuilder::sum` — `aggregations_smoke_sum_group_count_and_count`, `aggregations_sum_int_float_mixed_and_negative_exact`, `aggregations_sum_skips_missing_and_non_numeric_missing_all_is_zero`, `aggregations_sum_nan_poisons_and_infinities_follow_ieee`, `aggregations_sum_large_ints_round_through_f64_beyond_2_pow_53`, `aggregations_ignore_limit_offset_select_order_by_and_sources`, `aggregations_indexed_vs_scan_equivalent_for_every_aggregate`, `aggregations_validate_ranking_args_before_aggregating` - `corvid::QueryBuilder::avg` — `aggregations_avg_matrix_single_mixed_skipped_and_empty`, `aggregations_avg_nan_member_poisons_the_mean`, `aggregations_ignore_limit_offset_select_order_by_and_sources`, `aggregations_indexed_vs_scan_equivalent_for_every_aggregate`, `aggregations_validate_ranking_args_before_aggregating` - `corvid::QueryBuilder::min` — `aggregations_min_max_numbers_interop_and_text_is_lexicographic`, `aggregations_min_max_incomparable_kinds_yield_none`, `aggregations_min_max_mixed_kinds_pin_first_comparable_kind_wins`, `aggregations_sum_large_ints_round_through_f64_beyond_2_pow_53`, `aggregations_ignore_limit_offset_select_order_by_and_sources`, `aggregations_indexed_vs_scan_equivalent_for_every_aggregate`, `aggregations_validate_ranking_args_before_aggregating` - `corvid::QueryBuilder::max` — `aggregations_min_max_numbers_interop_and_text_is_lexicographic`, `aggregations_min_max_incomparable_kinds_yield_none`, `aggregations_min_max_mixed_kinds_pin_first_comparable_kind_wins`, `aggregations_sum_large_ints_round_through_f64_beyond_2_pow_53`, `aggregations_ignore_limit_offset_select_order_by_and_sources`, `aggregations_indexed_vs_scan_equivalent_for_every_aggregate`, `aggregations_validate_ranking_args_before_aggregating` - `corvid::QueryBuilder::count_distinct` — `aggregations_count_distinct_scalars_duplicates_missing_and_empty`, `aggregations_count_distinct_and_groups_separate_types_by_tag`, `aggregations_ignore_limit_offset_select_order_by_and_sources`, `aggregations_indexed_vs_scan_equivalent_for_every_aggregate`, `aggregations_validate_ranking_args_before_aggregating` - `corvid::QueryBuilder::group_sum` — `aggregations_group_sum_avg_exact_per_bucket_and_absent_empty_buckets`, `aggregations_group_sum_avg_respect_filters`, `aggregations_ignore_limit_offset_select_order_by_and_sources`, `aggregations_indexed_vs_scan_equivalent_for_every_aggregate` - `corvid::QueryBuilder::group_avg` — `aggregations_group_sum_avg_exact_per_bucket_and_absent_empty_buckets`, `aggregations_group_sum_avg_respect_filters`, `aggregations_ignore_limit_offset_select_order_by_and_sources`, `aggregations_indexed_vs_scan_equivalent_for_every_aggregate` - `corvid::Collection::approx_distinct` — `aggregations_approx_distinct_exact_small_counts_and_duplicates`, `aggregations_approx_distinct_distinguishes_encoded_kinds_and_skips_missing`, `aggregations_approx_distinct_bounded_error_on_larger_corpus` ### Vector search — 39 construct(s) - `corvid::Metric` — `search_vector_smoke_ranks_nearest_first_exact`, `vector_metric_quantization_cross_orders_and_exact_distances` - `corvid::Metric::Cosine` — `search_vector_smoke_ranks_nearest_first_exact`, `vector_metric_quantization_cross_orders_and_exact_distances`, `vector_zero_norm_cosine_dot_l2_ranking` - `corvid::Metric::Dot` — `vector_metric_quantization_cross_orders_and_exact_distances`, `vector_exact_path_scores_match_hand_computed_formulas`, `vector_zero_norm_cosine_dot_l2_ranking` - `corvid::Metric::L2` — `vector_metric_quantization_cross_orders_and_exact_distances`, `vector_exact_path_scores_match_hand_computed_formulas`, `vector_hnsw_direct_api_extreme_params_and_determinism` - `corvid::Metric::distance` — `vector_exact_path_scores_match_hand_computed_formulas` - `corvid::distance::dot` — `vector_exact_path_scores_match_hand_computed_formulas` - `corvid::distance::l2_squared` — `vector_exact_path_scores_match_hand_computed_formulas` - `corvid::distance::cosine_distance` — `vector_exact_path_scores_match_hand_computed_formulas`, `vector_zero_norm_cosine_dot_l2_ranking` - `corvid::Quantization` — `vector_metric_quantization_cross_orders_and_exact_distances` - `corvid::Quantization::None` — `vector_metric_quantization_cross_orders_and_exact_distances`, `vector_indexed_none_matches_unindexed_twin_for_all_k` - `corvid::Quantization::Binary` — `vector_metric_quantization_cross_orders_and_exact_distances`, `vector_quantization_k1_binary_diverges_scalar_matches_exact`, `vector_create_index_overloads_inmemory_ondisk_and_pq` - `corvid::Quantization::Scalar` — `vector_metric_quantization_cross_orders_and_exact_distances`, `vector_quantization_k1_binary_diverges_scalar_matches_exact` - `corvid::QueryBuilder::vector` *(shared across classes: Hybrid, Vector search)* — `search_hybrid_smoke_rrf_fuses_vector_and_text`, `vector_builder_approx_prefilters_exact_vs_postfilters_approx`, `vector_k_boundaries_zero_one_n_and_beyond`, `vector_empty_collection_single_doc_and_missing_field`, `vector_builder_select_order_limit_offset_interplay` - `corvid::QueryBuilder::approx` — `vector_builder_approx_prefilters_exact_vs_postfilters_approx` - `corvid::Hit` — `search_vector_smoke_ranks_nearest_first_exact`, `vector_index_dispatch_approximate_flag_and_metric_mismatch_fallback`, `vector_metric_quantization_cross_orders_and_exact_distances` - `corvid::Collection::vector_search` — `search_vector_smoke_ranks_nearest_first_exact`, `vector_metric_quantization_cross_orders_and_exact_distances`, `vector_exact_path_scores_match_hand_computed_formulas`, `vector_indexed_none_matches_unindexed_twin_for_all_k`, `vector_index_dispatch_approximate_flag_and_metric_mismatch_fallback`, `vector_k_boundaries_zero_one_n_and_beyond`, `vector_dimension_mismatch_skips_docs_and_index_falls_back`, `vector_zero_norm_cosine_dot_l2_ranking`, `vector_empty_collection_single_doc_and_missing_field` - `corvid::hnsw::DEFAULT_M` — `vector_hnsw_direct_api_extreme_params_and_determinism` - `corvid::hnsw::DEFAULT_EF_CONSTRUCTION` — `vector_hnsw_direct_api_extreme_params_and_determinism` - `corvid::Hnsw` — `vector_hnsw_direct_api_extreme_params_and_determinism` - `corvid::Hnsw::new` — `vector_hnsw_direct_api_extreme_params_and_determinism` - `corvid::Hnsw::with_params` — `vector_hnsw_direct_api_extreme_params_and_determinism` - `corvid::Hnsw::with_quant` — `vector_hnsw_direct_api_extreme_params_and_determinism` - `corvid::Hnsw::with_pq` — `vector_hnsw_direct_pq_adc_and_reconstruction_paths` - `corvid::Hnsw::len` — `vector_hnsw_direct_api_extreme_params_and_determinism` - `corvid::Hnsw::is_empty` — `vector_hnsw_direct_api_extreme_params_and_determinism` - `corvid::Hnsw::insert` — `vector_hnsw_direct_api_extreme_params_and_determinism` - `corvid::Hnsw::search` — `vector_hnsw_direct_api_extreme_params_and_determinism` - `corvid::pq::Pq` — `pq_train_rejects_unusable_params_and_sample`, `pq_train_is_deterministic_and_codebook_size_math_holds`, `pq_adc_recall_bound_on_fixed_corpus` - `corvid::pq::Pq::code_len` — `pq_train_is_deterministic_and_codebook_size_math_holds`, `pq_encode_is_deterministic_compact_and_dimension_guarded` - `corvid::pq::Pq::dim` — `pq_train_is_deterministic_and_codebook_size_math_holds`, `pq_decode_reconstructs_and_guards_malformed_codes` - `corvid::pq::Pq::params` — `pq_train_is_deterministic_and_codebook_size_math_holds`, `pq_adc_l2_fast_path_matches_reconstruction_and_guards` - `corvid::pq::Pq::train` — `pq_train_rejects_unusable_params_and_sample`, `pq_train_is_deterministic_and_codebook_size_math_holds` - `corvid::pq::Pq::encode` — `pq_encode_is_deterministic_compact_and_dimension_guarded` - `corvid::pq::Pq::decode` — `pq_decode_reconstructs_and_guards_malformed_codes` - `corvid::pq::Pq::distance` — `pq_distance_is_reconstruction_distance_for_every_metric` - `corvid::pq::Pq::l2_table` — `pq_adc_l2_fast_path_matches_reconstruction_and_guards` - `corvid::pq::Pq::adc_l2` — `pq_adc_l2_fast_path_matches_reconstruction_and_guards`, `pq_adc_recall_bound_on_fixed_corpus` - `corvid::pq::Pq::to_bytes` — `pq_train_is_deterministic_and_codebook_size_math_holds`, `pq_codebook_roundtrips_bytes_and_rejects_malformed` - `corvid::pq::Pq::from_bytes` — `pq_codebook_roundtrips_bytes_and_rejects_malformed` ### Text search — 15 construct(s) - `corvid::QueryBuilder::text` *(shared across classes: Hybrid, Text search)* — `search_hybrid_smoke_rrf_fuses_vector_and_text`, `text_builder_text_k_bounds_empty_and_stopword_queries`, `text_builder_text_ranking_scores_select_limit_and_missing_fields`, `text_builder_text_index_arm_matches_scan_arm` - `corvid::TextHit` — `search_text_smoke_ranks_most_relevant_first`, `text_search_bm25_ranking_tf_length_and_ties`, `text_phrase_order_sensitive_match_and_scores` - `corvid::Collection::text_search` — `search_text_smoke_ranks_most_relevant_first`, `text_search_bm25_ranking_tf_length_and_ties`, `text_search_rare_term_outscores_common_via_idf`, `text_search_index_inmemory_ondisk_match_scan_twin`, `text_search_k_boundaries_and_corpus_edges`, `text_phrase_single_term_equals_term_search` - `corvid::Collection::phrase_search` — `text_phrase_order_sensitive_match_and_scores`, `text_phrase_repeated_terms_and_non_adjacent_non_match`, `text_phrase_single_term_equals_term_search`, `text_phrase_stopword_collapse_and_sentence_boundary`, `text_phrase_k_boundaries_empty_phrase_and_index_arms` - `corvid::text::Bm25Params` — `text_bm25_params_new_and_validate_error_variants`, `text_term_score_zero_saturation_length_and_b_zero` - `corvid::text::Bm25Params::new` — `text_bm25_params_new_and_validate_error_variants` - `corvid::text::Bm25Params::validate` — `text_bm25_params_new_and_validate_error_variants` - `corvid::text::tokenize` — `text_tokenize_case_punct_unicode_numbers_and_empties`, `text_tokenize_cjk_bigrams_boundary_and_mixed_strings` - `corvid::text::s_stem` — `text_s_stem_pins_conservative_plural_algorithm` - `corvid::text::Analyzer` — `text_analyzer_default_raw_and_flag_combinations` - `corvid::text::Analyzer::raw` — `text_analyzer_default_raw_and_flag_combinations` - `corvid::text::Analyzer::analyze` — `text_analyzer_default_raw_and_flag_combinations`, `text_analyze_cjk_no_stopwords_no_stemming` - `corvid::text::analyze` — `text_analyzer_default_raw_and_flag_combinations`, `text_analyze_cjk_no_stopwords_no_stemming` - `corvid::text::idf` — `text_idf_values_monotonicity_and_nonnegativity` - `corvid::text::term_score` — `text_term_score_zero_saturation_length_and_b_zero` ### Hybrid — 6 construct(s) - `corvid::Error::InvalidArgument` *(shared across classes: Aggregations, Hybrid, Text search, Geo, Lifecycle)* — `aggregations_validate_ranking_args_before_aggregating`, `hybrid_fuse_rrf_rejects_invalid_k_at_run`, `hybrid_rerank_mmr_rejects_out_of_range_and_nan_at_run`, `text_bm25_params_new_and_validate_error_variants`, `geo_bbox_validation_exact_error_variants`, `lifecycle_load_with_renames_error_contract_invalid_target_collisions_and_noops` - `corvid::QueryBuilder::fuse_rrf` — `hybrid_fuse_rrf_rejects_invalid_k_at_run`, `hybrid_fusion_rrf_boost_beats_single_source` - `corvid::QueryBuilder::rerank_mmr` — `hybrid_rerank_mmr_rejects_out_of_range_and_nan_at_run`, `hybrid_rerank_mmr_noop_without_vector_source`, `hybrid_rerank_mmr_lambda_one_reorders_by_relevance`, `hybrid_rerank_mmr_lambda_zero_diversifies`, `hybrid_rerank_mmr_docs_without_embeddings_survive` - `corvid::DEFAULT_RRF_K` — `search_hybrid_smoke_rrf_fuses_vector_and_text`, `hybrid_rrf_direct_formula_exact_scores_and_edges` - `corvid::reciprocal_rank_fusion` — `search_hybrid_smoke_rrf_fuses_vector_and_text`, `hybrid_rrf_direct_formula_exact_scores_and_edges` - `corvid::mmr` — `hybrid_mmr_direct_lambda_zero_one_diversity_and_k` ### Geo — 7 construct(s) - `corvid::Predicate::GeoWithin` *(shared across classes: WHERE, Geo)* — `filters_geo_within_point_formats_and_boundary`, `filters_indexed_vs_scan_geo_window`, `geo_predicate_deep_dateline_poles_invalid_centers` - `corvid::filter::FieldRef::within_km` *(shared across classes: WHERE, Geo)* — `filters_geo_within_point_formats_and_boundary`, `filters_indexed_vs_scan_geo_window`, `geo_predicate_deep_dateline_poles_invalid_centers` - `corvid::haversine_km` — `search_geo_smoke_within_radius_and_nearest`, `geo_haversine_known_distances_symmetry_poles_antipodal` - `corvid::GeoHit` — `search_geo_smoke_within_radius_and_nearest`, `geo_nearest_k_zero_one_n_beyond_and_hit_fields` - `corvid::Collection::geo_within_radius` — `search_geo_smoke_within_radius_and_nearest`, `geo_within_radius_boundary_inclusive_and_ordering`, `geo_within_radius_zero_tiny_and_full_globe_radii`, `geo_within_radius_no_input_validation_mathematical_semantics` - `corvid::Collection::geo_nearest` — `search_geo_smoke_within_radius_and_nearest`, `geo_nearest_k_zero_one_n_beyond_and_hit_fields`, `geo_nearest_equidistant_ties_break_by_key`, `geo_nearest_skips_non_points_empty_and_finds_antipodal` - `corvid::Collection::geo_within_bbox` — `geo_within_bbox_normal_inclusive_edges_and_key_order`, `geo_within_bbox_degenerate_point_line_pole_and_globe`, `geo_bbox_antimeridian_wrap_matches_both_sides`, `geo_bbox_result_order_is_key_order_on_every_path`, `geo_bbox_validation_exact_error_variants` ### Schema (ALTER) — 36 construct(s) - `corvid::Error::CorruptIndex` — `lifecycle_corrupt_ondisk_index_bytes_on_disk_error_queries_with_exact_variant` - `corvid::Error::ReservedCollection` *(shared across classes: Mutations, Graph, Schema (ALTER))* — `mutations_insert_rejects_reserved_and_invalid_collection_names`, `mutations_write_paths_reject_reserved_and_invalid_collection_names`, `graph_link_unlink_reject_reserved_and_invalid_collection_names`, `schema_index_creation_validates_names_across_families` - `corvid::Error::InvalidName` *(shared across classes: Mutations, Graph, Schema (ALTER), Lifecycle)* — `mutations_insert_rejects_reserved_and_invalid_collection_names`, `mutations_write_paths_reject_reserved_and_invalid_collection_names`, `graph_link_unlink_reject_reserved_and_invalid_collection_names`, `schema_index_creation_validates_names_across_families`, `lifecycle_load_with_renames_error_contract_invalid_target_collisions_and_noops` - `corvid::Error::EmptyIndexTraining` *(shared across classes: Vector search, Schema (ALTER))* — `vector_create_index_overloads_inmemory_ondisk_and_pq`, `schema_vector_pq_creation_training_error_variants_and_success` - `corvid::Error::SchemaViolation` *(shared across classes: Schema (ALTER), Mutations)* — `schema_field_type_matrix_and_fields_accessor`, `schema_unique_insert_conflict_rejects_with_exact_variant_and_stores_nothing`, `schema_unique_update_conflict_rejects_whole_write`, `schema_unique_nan_equals_nan_rejects_second_document`, `schema_unique_containers_bytes_text_vector_and_null_rule`, `schema_unique_delete_then_reinsert_same_value_allowed`, `schema_unique_batch_conflict_rolls_back_whole_batch`, `schema_unique_with_scalar_index_stays_enforced_and_moves_with_values`, `schema_unique_numeric_kind_equality_same_with_and_without_index`, `mutations_insert_batch_schema_violation_rolls_back_whole_batch`, `mutations_insert_batch_unique_conflict_rolls_back_whole_batch`, `mutations_insert_auto_failure_does_not_burn_an_id` - `corvid::Collection::create_geo_index` *(shared across classes: WHERE, SELECT shaping, Geo, Schema (ALTER))* — `filters_indexed_vs_scan_geo_window`, `queries_plan_shape_indexed_window_kinds_and_explain_families`, `geo_index_twins_equivalence_and_live_mutations`, `geo_index_plan_shape_serviceable_and_declined`, `schema_geo_index_duplicate_creation_and_non_point_docs_skipped`, `schema_geo_index_point_move_and_delete_maintained` - `corvid::Collection::create_vector_index` *(shared across classes: SELECT shaping, Vector search, Schema (ALTER))* — `queries_plan_shape_ann_index_for_single_vector_source`, `vector_indexed_none_matches_unindexed_twin_for_all_k`, `vector_index_dispatch_approximate_flag_and_metric_mismatch_fallback`, `vector_dimension_mismatch_skips_docs_and_index_falls_back`, `schema_vector_index_duplicate_creation_replaces_previous_params`, `schema_vector_index_over_empty_field_then_insert_immediately_searchable`, `schema_vector_index_mixed_dimensions_match_scan_twin`, `schema_vector_dimension_change_on_update_leaves_index`, `schema_vector_index_compaction_after_deletes_keeps_results_exact` - `corvid::Collection::create_vector_index_quantized` *(shared across classes: Vector search, Schema (ALTER))* — `vector_metric_quantization_cross_orders_and_exact_distances`, `schema_vector_index_duplicate_creation_replaces_previous_params` - `corvid::Collection::create_vector_index_ondisk` *(shared across classes: Vector search, Schema (ALTER))* — `vector_create_index_overloads_inmemory_ondisk_and_pq`, `schema_vector_index_duplicate_creation_replaces_previous_params`, `schema_vector_index_over_empty_field_then_insert_immediately_searchable`, `schema_vector_index_mixed_dimensions_match_scan_twin`, `schema_vector_dimension_change_on_update_leaves_index`, `schema_vector_index_compaction_after_deletes_keeps_results_exact` - `corvid::Collection::create_vector_index_ondisk_quantized` *(shared across classes: Vector search, Schema (ALTER))* — `vector_create_index_overloads_inmemory_ondisk_and_pq`, `schema_vector_index_duplicate_creation_replaces_previous_params` - `corvid::Collection::create_vector_index_ondisk_pq` *(shared across classes: Vector search, Schema (ALTER))* — `vector_create_index_overloads_inmemory_ondisk_and_pq`, `schema_vector_pq_creation_training_error_variants_and_success` - `corvid::Collection::create_vector_index_pq` — `vector_inmemory_pq_cross_metrics_orders_and_exact_distances`, `vector_inmemory_pq_recall_determinism_and_reopen`, `vector_inmemory_pq_creation_requires_training_documents` - `corvid::Collection::create_text_index` *(shared across classes: SELECT shaping, Text search, Schema (ALTER))* — `queries_plan_shape_text_index_for_single_text_source`, `text_search_index_inmemory_ondisk_match_scan_twin`, `text_builder_text_index_arm_matches_scan_arm`, `text_phrase_k_boundaries_empty_phrase_and_index_arms`, `schema_text_index_duplicate_and_non_text_values_excluded`, `schema_text_index_mutations_keep_search_correct` - `corvid::Collection::create_text_index_ondisk` *(shared across classes: Text search, Schema (ALTER))* — `text_search_index_inmemory_ondisk_match_scan_twin`, `text_phrase_k_boundaries_empty_phrase_and_index_arms`, `schema_text_index_mutations_keep_search_correct` - `corvid::Collection::create_scalar_index` *(shared across classes: Mutations, SELECT shaping, Schema (ALTER))* — `mutations_update_maintains_scalar_index`, `mutations_compare_and_set_maintains_scalar_index`, `mutations_delete_where_exact_with_scalar_index_present`, `mutations_insert_batch_unique_conflict_rolls_back_whole_batch`, `queries_order_by_indexed_vs_scan_equivalent`, `schema_scalar_index_empty_collection_creates_and_serves_later`, `schema_scalar_index_backfill_makes_populated_collection_immediately_queryable`, `schema_scalar_index_duplicate_creation_replaces_without_stale_entries`, `schema_scalar_index_mixed_type_field_lanes_and_missing_docs_match_scan`, `schema_scalar_index_maintenance_contract_under_every_mutation_kind` - `corvid::Collection::create_compound_index` *(shared across classes: WHERE, SELECT shaping, Schema (ALTER))* — `filters_indexed_vs_scan_compound_prefix`, `queries_plan_shape_indexed_window_kinds_and_explain_families`, `schema_compound_index_field_order_determines_serviceability`, `schema_compound_index_single_and_three_field_arities`, `schema_compound_index_duplicate_and_reverse_order_coexist`, `schema_compound_trailing_field_mutations_never_surface_stale_entries` - `corvid::schema::FieldType` — `schema_field_type_matrix_and_fields_accessor` - `corvid::schema::FieldType::Any` — `schema_field_type_matrix_and_fields_accessor` - `corvid::schema::FieldType::Bool` — `schema_field_type_matrix_and_fields_accessor` - `corvid::schema::FieldType::Int` *(shared across classes: Schema (ALTER), Mutations)* — `schema_field_type_matrix_and_fields_accessor`, `mutations_insert_batch_schema_violation_rolls_back_whole_batch` - `corvid::schema::FieldType::Float` — `schema_field_type_matrix_and_fields_accessor`, `schema_unique_nan_equals_nan_rejects_second_document` - `corvid::schema::FieldType::Text` *(shared across classes: Schema (ALTER), Mutations)* — `schema_field_type_matrix_and_fields_accessor`, `mutations_insert_batch_unique_conflict_rolls_back_whole_batch` - `corvid::schema::FieldType::Bytes` — `schema_unique_containers_bytes_text_vector_and_null_rule` - `corvid::schema::FieldType::Vector` — `schema_unique_containers_bytes_text_vector_and_null_rule` - `corvid::schema::FieldType::Array` — `schema_field_type_matrix_and_fields_accessor` - `corvid::schema::FieldType::Map` — `schema_field_type_matrix_and_fields_accessor` - `corvid::schema::Field` *(shared across classes: Schema (ALTER), Mutations)* — `schema_field_type_matrix_and_fields_accessor`, `mutations_insert_batch_unique_conflict_rolls_back_whole_batch` - `corvid::schema::Field::new` *(shared across classes: Schema (ALTER), Mutations)* — `schema_field_type_matrix_and_fields_accessor`, `mutations_insert_batch_unique_conflict_rolls_back_whole_batch` - `corvid::schema::Field::required` — `schema_field_type_matrix_and_fields_accessor` - `corvid::schema::Field::unique` *(shared across classes: Schema (ALTER), Mutations)* — `schema_field_type_matrix_and_fields_accessor`, `schema_unique_insert_conflict_rejects_with_exact_variant_and_stores_nothing`, `mutations_insert_batch_unique_conflict_rolls_back_whole_batch` - `corvid::schema::Schema` *(shared across classes: Schema (ALTER), Mutations)* — `schema_field_type_matrix_and_fields_accessor`, `mutations_insert_batch_unique_conflict_rolls_back_whole_batch` - `corvid::schema::Schema::new` *(shared across classes: Schema (ALTER), Mutations)* — `schema_field_type_matrix_and_fields_accessor`, `mutations_insert_batch_unique_conflict_rolls_back_whole_batch` - `corvid::schema::Schema::field` *(shared across classes: Schema (ALTER), Mutations)* — `schema_field_type_matrix_and_fields_accessor`, `mutations_insert_batch_unique_conflict_rolls_back_whole_batch` - `corvid::schema::Schema::fields` — `schema_field_type_matrix_and_fields_accessor` - `corvid::Collection::set_schema` *(shared across classes: Schema (ALTER), Mutations)* — `schema_field_type_matrix_and_fields_accessor`, `mutations_insert_batch_schema_violation_rolls_back_whole_batch`, `mutations_insert_batch_unique_conflict_rolls_back_whole_batch` - `corvid::Collection::schema` — `schema_getter_roundtrips_declared_fields` ### TTL — 4 construct(s) - `corvid::Collection::insert_with_ttl` *(shared across classes: TTL, Mutations)* — `ttl_smoke_insert_with_ttl_purges_at_boundary`, `mutations_insert_with_ttl_sets_and_purges_expiry`, `ttl_roundtrip_set_on_insert_after_plain_insert_and_overwrite`, `ttl_timestamps_accept_i64_extremes_and_order_correctly`, `ttl_plain_write_paths_clear_expiry`, `ttl_purge_removes_doc_from_scalar_unique_vector_and_text_indexes`, `ttl_purge_cascades_edges_of_expired_document_both_namespaces`, `ttl_write_paths_reject_reserved_and_invalid_collection_names` - `corvid::Collection::set_ttl` — `ttl_roundtrip_set_on_insert_after_plain_insert_and_overwrite`, `ttl_set_ttl_on_missing_doc_is_ok_and_purges_without_counting`, `ttl_plain_write_paths_clear_expiry`, `ttl_write_paths_reject_reserved_and_invalid_collection_names` - `corvid::Collection::ttl` *(shared across classes: TTL, Mutations)* — `ttl_smoke_insert_with_ttl_purges_at_boundary`, `mutations_insert_with_ttl_sets_and_purges_expiry`, `ttl_roundtrip_set_on_insert_after_plain_insert_and_overwrite`, `ttl_on_missing_doc_and_doc_without_expiry_both_none`, `ttl_set_ttl_on_missing_doc_is_ok_and_purges_without_counting` - `corvid::Collection::purge_expired` *(shared across classes: TTL, Mutations, Lifecycle)* — `ttl_smoke_insert_with_ttl_purges_at_boundary`, `mutations_insert_with_ttl_sets_and_purges_expiry`, `ttl_purge_boundary_one_before_exactly_at_one_after_and_idempotence`, `ttl_expired_doc_visible_until_purged_hidden_from_all_reads_after`, `ttl_timestamps_accept_i64_extremes_and_order_correctly`, `ttl_purge_removes_doc_from_scalar_unique_vector_and_text_indexes`, `ttl_set_ttl_on_missing_doc_is_ok_and_purges_without_counting`, `ttl_purge_cascades_edges_of_expired_document_both_namespaces`, `ttl_purge_cascades_edges_of_stranded_entry_without_document`, `events_delete_paths_emit_exact_delete_vectors_in_order`, `events_ttl_purge_cascade_is_silent_and_stranded_purge_emits_nothing` ### Graph — 7 construct(s) - `corvid::Collection::link` *(shared across classes: Graph, Lifecycle)* — `graph_smoke_link_neighbors_traverse_unlink`, `graph_link_new_edge_resolves_neighbors_and_in_neighbors`, `graph_link_duplicate_is_idempotent_and_reemits_insert_event`, `graph_link_self_loop_lists_self_but_traverse_excludes_start`, `graph_link_missing_endpoints_allowed_without_documents`, `graph_link_relation_isolation_empty_unicode_and_byte_prefix`, `graph_link_endpoint_keys_empty_and_unicode_in_byte_order`, `graph_link_unlink_reject_reserved_and_invalid_collection_names`, `events_link_to_missing_endpoints_still_emits_insert_keyed_by_from` - `corvid::Collection::link_weighted` — `graph_link_weighted_roundtrip_and_overwrite_semantics`, `graph_link_weighted_float_extremes_round_trip` - `corvid::Collection::neighbors_weighted` — `graph_link_weighted_roundtrip_and_overwrite_semantics`, `graph_link_weighted_float_extremes_round_trip` - `corvid::Collection::unlink` — `graph_smoke_link_neighbors_traverse_unlink`, `graph_unlink_removes_edge_and_reverse_twin`, `graph_unlink_is_directional_reverse_direction_edge_survives`, `graph_unlink_missing_edge_is_silent_noop_returning_false`, `graph_unlink_removes_only_the_named_relation`, `graph_link_unlink_reject_reserved_and_invalid_collection_names` - `corvid::Collection::neighbors` — `graph_smoke_link_neighbors_traverse_unlink`, `graph_link_relation_isolation_empty_unicode_and_byte_prefix`, `graph_link_endpoint_keys_empty_and_unicode_in_byte_order`, `graph_neighbors_key_order_no_out_edges_and_missing_node` - `corvid::Collection::in_neighbors` — `graph_link_new_edge_resolves_neighbors_and_in_neighbors`, `graph_in_neighbors_mirror_target_only_and_mixed`, `graph_delete_cascades_edges_in_both_namespaces` - `corvid::Collection::traverse` — `graph_smoke_link_neighbors_traverse_unlink`, `graph_traverse_depth_zero_empty_depth_one_equals_neighbors`, `graph_traverse_multi_hop_bfs_order_exact`, `graph_traverse_cycles_terminate_with_deduped_set`, `graph_traverse_branching_and_diamond_convergence_order`, `graph_traverse_relation_isolated_from_other_relations`, `graph_traverse_missing_start_node_is_empty` ### Joins — 2 construct(s) - `corvid::JoinRow` — `joins_smoke_left_outer_resolves_and_misses`, `joins_happy_path_join_row_shape_is_exact`, `joins_missing_fk_field_and_dangling_reference_retain_rows_with_none`, `joins_foreign_key_kinds_text_bytes_int_and_unusable_shapes`, `joins_self_join_references_within_one_collection`, `joins_empty_left_empty_right_and_unknown_right_collection`, `joins_rows_follow_left_collection_key_order`, `joins_non_map_left_documents_retained_with_none` - `corvid::Collection::join` — `joins_smoke_left_outer_resolves_and_misses`, `joins_happy_path_join_row_shape_is_exact`, `joins_dotted_foreign_key_path_resolves_nested_maps`, `joins_missing_fk_field_and_dangling_reference_retain_rows_with_none`, `joins_foreign_key_kinds_text_bytes_int_and_unusable_shapes`, `joins_self_join_references_within_one_collection`, `joins_empty_left_empty_right_and_unknown_right_collection`, `joins_rows_follow_left_collection_key_order`, `joins_track_right_and_left_side_mutations`, `joins_non_map_left_documents_retained_with_none` ### Lifecycle — 123 construct(s) - `corvid::value::MAX_NESTING` — `lifecycle_value_decode_enforces_max_nesting_bound` - `corvid::Value::encode` — `lifecycle_dump_load_roundtrips_every_value_variant_bytes_exact`, `lifecycle_value_decode_enforces_max_nesting_bound` - `corvid::Value::decode` — `lifecycle_dump_load_roundtrips_every_value_variant_bytes_exact`, `lifecycle_value_decode_enforces_max_nesting_bound` - `corvid::Result` — `lifecycle_store_kv_surface_roundtrips_and_unknown_collection_contracts` - `corvid::Error` *(shared across classes: Schema (ALTER), Mutations)* — `schema_unique_insert_conflict_rejects_with_exact_variant_and_stores_nothing`, `mutations_insert_rejects_reserved_and_invalid_collection_names` - `corvid::Error::Database` — `lifecycle_db_open_real_file_persists_across_reopen_and_rejects_missing_parent`, `lifecycle_db_open_second_handle_to_same_file_hits_the_redb_exclusive_lock`, `lifecycle_db_backup_restores_identical_state_and_pins_error_paths` - `corvid::Error::Transaction` — *exempt from strict coverage: redb passthrough: transaction failure requires a redb-internal fault; no public-API path reaches it (fault-injection hooks are forbidden, Ruling 3)* - `corvid::Error::Table` — *exempt from strict coverage: redb passthrough: table-open failure requires corruption inside redb's private table format, which public engine calls cannot produce* - `corvid::Error::Storage` — *exempt from strict coverage: redb passthrough: storage-level I/O errors surface only from redb internals (disk fault mid-operation), not from any public call* - `corvid::Error::Commit` — *exempt from strict coverage: redb passthrough: commit failure is a redb-internal disk fault mid-commit; unreachable without fault injection (Ruling 3)* - `corvid::Error::SetDurability` — *exempt from strict coverage: redb passthrough: the durability-mode switch fails only inside redb's own set_durability, on conditions public inputs cannot produce* - `corvid::Error::Compaction` — *exempt from strict coverage: redb passthrough: compaction failure needs an I/O fault during redb's compaction pass inside `Db::compact` internals* - `corvid::Error::Decode` — `lifecycle_value_decode_enforces_max_nesting_bound` - `corvid::Error::IncompatibleFormat` — *exempt from strict coverage: redb's format-version marker lives in its META pages, not the public byte layer the engine writes; an engine-level version mismatch is refused earlier by the engine's own on-disk format marker* - `corvid::Error::InvalidDump` — `lifecycle_load_rejects_reserved_names_and_malformed_streams` - `corvid::Error::BackupTargetExists` — `lifecycle_db_backup_restores_identical_state_and_pins_error_paths`, `lifecycle_store_backup_copies_to_an_independent_openable_file` - `corvid::Error::Io` — `lifecycle_dump_of_empty_db_loads_empty_and_io_errors_surface` - `corvid::PlanShape` *(shared across classes: WHERE, SELECT shaping)* — `filters_indexed_vs_scan_scalar_predicates`, `queries_plan_shape_indexed_window_kinds_and_explain_families` - `corvid::PlanShape::AnnIndex` — `queries_plan_shape_ann_index_for_single_vector_source` - `corvid::PlanShape::TextIndex` — `queries_plan_shape_text_index_for_single_text_source` - `corvid::PlanShape::IndexedWindow` *(shared across classes: WHERE, SELECT shaping)* — `filters_indexed_vs_scan_scalar_predicates`, `queries_plan_shape_indexed_window_kinds_and_explain_families` - `corvid::PlanShape::SortIndex` — `queries_plan_shape_sort_index_for_order_by_on_indexed_field`, `queries_order_by_index_walk_parity_across_kind_lattice` - `corvid::PlanShape::StreamingTopK` — `queries_plan_shape_streaming_topk_without_index` - `corvid::PlanShape::Scan` *(shared across classes: WHERE, SELECT shaping)* — `filters_indexed_vs_scan_scalar_predicates`, `queries_plan_shape_indexed_window_kinds_and_explain_families` - `corvid::QueryBuilder::plan` — `lifecycle_query_plan_key_is_canonical_for_identical_shapes` - `corvid::QueryBuilder::plan_shape` *(shared across classes: WHERE, SELECT shaping)* — `filters_indexed_vs_scan_scalar_predicates`, `queries_plan_shape_ann_index_for_single_vector_source`, `queries_plan_shape_text_index_for_single_text_source`, `queries_plan_shape_indexed_window_kinds_and_explain_families`, `queries_plan_shape_streaming_topk_without_index`, `queries_order_by_indexed_vs_scan_equivalent` - `corvid::QueryBuilder::explain` — `queries_plan_shape_ann_index_for_single_vector_source`, `queries_plan_shape_text_index_for_single_text_source`, `queries_plan_shape_indexed_window_kinds_and_explain_families`, `queries_plan_shape_streaming_topk_without_index` - `corvid::Db` *(shared across classes: Mutations, Lifecycle)* — `mutations_smoke_insert_roundtrips`, `lifecycle_db_open_real_file_persists_across_reopen_and_rejects_missing_parent` - `corvid::Db::open` — `lifecycle_db_open_real_file_persists_across_reopen_and_rejects_missing_parent`, `lifecycle_db_open_second_handle_to_same_file_hits_the_redb_exclusive_lock` - `corvid::Db::open_in_memory` *(shared across classes: Mutations, Lifecycle)* — `mutations_smoke_insert_roundtrips`, `lifecycle_db_open_in_memory_instances_are_isolated` - `corvid::Db::collection` — `mutations_smoke_insert_roundtrips` - `corvid::Db::backup` — `lifecycle_db_backup_restores_identical_state_and_pins_error_paths` - `corvid::Db::bulk` — `lifecycle_db_bulk_is_a_durability_scope_writes_before_err_persist`, `lifecycle_db_bulk_happy_path_applies_and_survives_reopen` - `corvid::Db::compact` — `lifecycle_db_compact_keeps_data_intact_and_tolerates_double_compact` - `corvid::Db::collections` — `lifecycle_db_collections_filters_graph_ttl_and_index_namespaces` - `corvid::Store` — `lifecycle_store_kv_surface_roundtrips_and_unknown_collection_contracts` - `corvid::Store::open` — `lifecycle_store_begin_bulk_scopes_nest_and_flush_makes_writes_durable`, `lifecycle_store_backup_copies_to_an_independent_openable_file` - `corvid::Store::open_in_memory` — `lifecycle_store_transaction_commit_rollback_and_write_batch_surface` - `corvid::Store::set_relaxed_durability` — `lifecycle_store_set_relaxed_durability_and_flush_keep_data_durable` - `corvid::Store::begin_bulk` — `lifecycle_store_begin_bulk_scopes_nest_and_flush_makes_writes_durable` - `corvid::Store::flush` — `lifecycle_store_begin_bulk_scopes_nest_and_flush_makes_writes_durable`, `lifecycle_store_set_relaxed_durability_and_flush_keep_data_durable` - `corvid::Store::compact` — `lifecycle_db_compact_keeps_data_intact_and_tolerates_double_compact` - `corvid::Store::next_auto_id` — `lifecycle_store_kv_surface_roundtrips_and_unknown_collection_contracts`, `lifecycle_store_transaction_commit_rollback_and_write_batch_surface` - `corvid::Store::backup` — `lifecycle_store_backup_copies_to_an_independent_openable_file` - `corvid::Store::transaction` — `lifecycle_store_transaction_commit_rollback_and_write_batch_surface` - `corvid::Store::read` — `lifecycle_store_read_batch_is_one_snapshot_and_mirrors_standalone_ops` - `corvid::Store::put` — `lifecycle_store_kv_surface_roundtrips_and_unknown_collection_contracts` - `corvid::Store::get` — `lifecycle_store_kv_surface_roundtrips_and_unknown_collection_contracts` - `corvid::Store::delete` — `lifecycle_store_kv_surface_roundtrips_and_unknown_collection_contracts` - `corvid::Store::scan` — `lifecycle_store_kv_surface_roundtrips_and_unknown_collection_contracts` - `corvid::Store::collections` — `lifecycle_db_collections_filters_graph_ttl_and_index_namespaces` - `corvid::Store::scan_from` — `lifecycle_store_kv_surface_roundtrips_and_unknown_collection_contracts` - `corvid::Store::count` — `lifecycle_store_kv_surface_roundtrips_and_unknown_collection_contracts` - `corvid::Store::for_each` — `lifecycle_store_kv_surface_roundtrips_and_unknown_collection_contracts` - `corvid::Store::scan_prefix` — `lifecycle_store_kv_surface_roundtrips_and_unknown_collection_contracts` - `corvid::store::BulkScope` — `lifecycle_store_begin_bulk_scopes_nest_and_flush_makes_writes_durable` - `corvid::store::WriteBatch` — `lifecycle_store_transaction_commit_rollback_and_write_batch_surface` - `corvid::store::WriteBatch::put` — `lifecycle_store_transaction_commit_rollback_and_write_batch_surface` - `corvid::store::WriteBatch::get` — `lifecycle_store_transaction_commit_rollback_and_write_batch_surface` - `corvid::store::WriteBatch::delete` — `lifecycle_store_transaction_commit_rollback_and_write_batch_surface` - `corvid::store::WriteBatch::scan` — `lifecycle_store_transaction_commit_rollback_and_write_batch_surface` - `corvid::store::WriteBatch::scan_from` — `lifecycle_store_transaction_commit_rollback_and_write_batch_surface` - `corvid::store::WriteBatch::next_auto_id` — `lifecycle_store_transaction_commit_rollback_and_write_batch_surface` - `corvid::store::ReadBatch` — `lifecycle_store_read_batch_is_one_snapshot_and_mirrors_standalone_ops` - `corvid::store::ReadBatch::collections` — `lifecycle_store_read_batch_is_one_snapshot_and_mirrors_standalone_ops` - `corvid::store::ReadBatch::auto_ids` — `lifecycle_store_read_batch_is_one_snapshot_and_mirrors_standalone_ops` - `corvid::store::ReadBatch::get` — `lifecycle_store_read_batch_is_one_snapshot_and_mirrors_standalone_ops` - `corvid::store::ReadBatch::scan` — `lifecycle_store_read_batch_is_one_snapshot_and_mirrors_standalone_ops` - `corvid::store::ReadBatch::scan_from` — `lifecycle_store_read_batch_is_one_snapshot_and_mirrors_standalone_ops` - `corvid::store::ReadBatch::scan_prefix` — `lifecycle_store_read_batch_is_one_snapshot_and_mirrors_standalone_ops` - `corvid::store::ReadBatch::for_each` — `lifecycle_store_read_batch_is_one_snapshot_and_mirrors_standalone_ops` - `corvid::ChangeKind` *(shared across classes: Lifecycle, Mutations)* — `events_smoke_subscribe_records_insert_and_delete`, `mutations_emit_change_events_per_mutation_kind`, `events_insert_paths_emit_exact_insert_vectors_in_order`, `events_update_and_patch_emit_exact_vectors_for_both_branches`, `events_compare_and_set_emit_exact_vectors_per_branch`, `events_delete_paths_emit_exact_delete_vectors_in_order` - `corvid::ChangeKind::Insert` *(shared across classes: Lifecycle, Mutations)* — `events_smoke_subscribe_records_insert_and_delete`, `mutations_emit_change_events_per_mutation_kind`, `events_insert_paths_emit_exact_insert_vectors_in_order`, `events_update_and_patch_emit_exact_vectors_for_both_branches`, `events_compare_and_set_emit_exact_vectors_per_branch`, `events_link_to_missing_endpoints_still_emits_insert_keyed_by_from` - `corvid::ChangeKind::Delete` *(shared across classes: Lifecycle, Mutations)* — `events_smoke_subscribe_records_insert_and_delete`, `mutations_emit_change_events_per_mutation_kind`, `events_delete_paths_emit_exact_delete_vectors_in_order`, `events_compare_and_set_emit_exact_vectors_per_branch`, `events_ttl_purge_cascade_is_silent_and_stranded_purge_emits_nothing` - `corvid::ChangeEvent` *(shared across classes: Lifecycle, Mutations)* — `events_smoke_subscribe_records_insert_and_delete`, `mutations_emit_change_events_per_mutation_kind`, `events_multiple_subscribers_all_receive_identical_exact_vectors`, `events_cross_collection_tagging_each_event_names_its_collection`, `events_dispatch_is_synchronous_post_commit_and_in_mutation_order` - `corvid::SubscriptionId` — `events_smoke_subscribe_records_insert_and_delete`, `events_subscribe_returns_distinct_ids_and_unsubscribe_reports_existence` - `corvid::Db::subscribe` *(shared across classes: Lifecycle, Mutations)* — `events_smoke_subscribe_records_insert_and_delete`, `mutations_emit_change_events_per_mutation_kind`, `events_subscribe_returns_distinct_ids_and_unsubscribe_reports_existence`, `events_multiple_subscribers_all_receive_identical_exact_vectors`, `events_cross_collection_tagging_each_event_names_its_collection` - `corvid::Db::unsubscribe` — `events_smoke_subscribe_records_insert_and_delete`, `events_subscribe_returns_distinct_ids_and_unsubscribe_reports_existence` - `corvid::SemanticCache` — `lifecycle_semantic_cache_threshold_and_nearest_entry_semantics_cosine`, `lifecycle_semantic_cache_threshold_units_follow_the_metric_l2` - `corvid::Collection::semantic_cache` — `lifecycle_semantic_cache_threshold_and_nearest_entry_semantics_cosine`, `lifecycle_semantic_cache_threshold_units_follow_the_metric_l2` - `corvid::SemanticCache::put` — `lifecycle_semantic_cache_threshold_and_nearest_entry_semantics_cosine` - `corvid::SemanticCache::get` — `lifecycle_semantic_cache_threshold_and_nearest_entry_semantics_cosine`, `lifecycle_semantic_cache_threshold_units_follow_the_metric_l2` - `corvid::HyperLogLog` — `lifecycle_hyperloglog_precision_clamps_estimates_and_ignores_duplicates` - `corvid::HyperLogLog::new` — `lifecycle_hyperloglog_precision_clamps_estimates_and_ignores_duplicates` - `corvid::HyperLogLog::with_precision` — `lifecycle_hyperloglog_precision_clamps_estimates_and_ignores_duplicates` - `corvid::HyperLogLog::add_bytes` — `lifecycle_hyperloglog_precision_clamps_estimates_and_ignores_duplicates`, `lifecycle_hyperloglog_add_hash_is_the_precomputed_twin_of_add_bytes` - `corvid::HyperLogLog::add_hash` — `lifecycle_hyperloglog_add_hash_is_the_precomputed_twin_of_add_bytes` - `corvid::HyperLogLog::estimate` — `lifecycle_hyperloglog_precision_clamps_estimates_and_ignores_duplicates` - `corvid::BloomFilter` — `lifecycle_bloom_filter_no_false_negatives_and_bounded_fp_rate` - `corvid::BloomFilter::new` — `lifecycle_bloom_filter_no_false_negatives_and_bounded_fp_rate` - `corvid::BloomFilter::add_bytes` — `lifecycle_bloom_filter_no_false_negatives_and_bounded_fp_rate` - `corvid::BloomFilter::contains_bytes` — `lifecycle_bloom_filter_no_false_negatives_and_bounded_fp_rate` - `corvid::CuckooFilter` — `lifecycle_cuckoo_filter_membership_delete_and_bounded_fp`, `lifecycle_cuckoo_filter_overflow_rejects_and_rollback_preserves_admitted` - `corvid::CuckooFilter::new` — `lifecycle_cuckoo_filter_membership_delete_and_bounded_fp`, `lifecycle_cuckoo_filter_overflow_rejects_and_rollback_preserves_admitted` - `corvid::CuckooFilter::add_bytes` — `lifecycle_cuckoo_filter_membership_delete_and_bounded_fp`, `lifecycle_cuckoo_filter_overflow_rejects_and_rollback_preserves_admitted` - `corvid::CuckooFilter::contains_bytes` — `lifecycle_cuckoo_filter_membership_delete_and_bounded_fp`, `lifecycle_cuckoo_filter_overflow_rejects_and_rollback_preserves_admitted` - `corvid::CuckooFilter::delete_bytes` — `lifecycle_cuckoo_filter_membership_delete_and_bounded_fp` - `corvid::TDigest` — `lifecycle_tdigest_exact_boundaries_nan_and_monotone_cdf`, `lifecycle_tdigest_merge_algebra_and_bounded_error` - `corvid::TDigest::new` — `lifecycle_tdigest_exact_boundaries_nan_and_monotone_cdf`, `lifecycle_tdigest_merge_algebra_and_bounded_error` - `corvid::TDigest::add` — `lifecycle_tdigest_exact_boundaries_nan_and_monotone_cdf`, `lifecycle_tdigest_merge_algebra_and_bounded_error` - `corvid::TDigest::merge` — `lifecycle_tdigest_merge_algebra_and_bounded_error` - `corvid::TDigest::quantile` — `lifecycle_tdigest_exact_boundaries_nan_and_monotone_cdf`, `lifecycle_tdigest_merge_algebra_and_bounded_error` - `corvid::TDigest::cdf` — `lifecycle_tdigest_exact_boundaries_nan_and_monotone_cdf` - `corvid::MinHash` — `lifecycle_minhash_signature_invariance_and_jaccard_bounds` - `corvid::MinHash::new` — `lifecycle_minhash_signature_invariance_and_jaccard_bounds` - `corvid::MinHash::signature` — `lifecycle_minhash_signature_invariance_and_jaccard_bounds` - `corvid::MinHash::jaccard_estimate` — `lifecycle_minhash_signature_invariance_and_jaccard_bounds` - `corvid::LshIndex` — `lifecycle_lsh_banding_recall_and_skew_fixed_corpus` - `corvid::LshIndex::new` — `lifecycle_lsh_banding_recall_and_skew_fixed_corpus` - `corvid::LshIndex::insert` — `lifecycle_lsh_banding_recall_and_skew_fixed_corpus` - `corvid::LshIndex::candidates` — `lifecycle_lsh_banding_recall_and_skew_fixed_corpus` - `corvid::Db::dump` — `lifecycle_smoke_dump_load_roundtrips_documents`, `lifecycle_dump_load_roundtrips_every_value_variant_bytes_exact`, `lifecycle_dump_load_roundtrips_every_index_family_ttl_edges_schema_and_autoids`, `lifecycle_dump_load_into_nonempty_db_merges_records_and_counters`, `lifecycle_dump_of_empty_db_loads_empty_and_io_errors_surface` - `corvid::Db::load` — `lifecycle_smoke_dump_load_roundtrips_documents`, `lifecycle_dump_load_roundtrips_every_value_variant_bytes_exact`, `lifecycle_dump_load_roundtrips_every_index_family_ttl_edges_schema_and_autoids`, `lifecycle_dump_load_into_nonempty_db_merges_records_and_counters`, `lifecycle_load_rejects_reserved_names_and_malformed_streams`, `lifecycle_dump_of_empty_db_loads_empty_and_io_errors_surface` - `corvid::Db::load_with_renames` — `lifecycle_load_with_renames_migrates_a_legacy_pre_wave4_dump`, `lifecycle_load_with_renames_error_contract_invalid_target_collisions_and_noops` - `corvid::QueryPlan` — `lifecycle_query_plan_key_is_canonical_for_identical_shapes` - `corvid::QueryPlan::key` — `lifecycle_query_plan_key_is_canonical_for_identical_shapes` - `corvid::PlanCache` — `lifecycle_plan_cache_miss_hit_insert_replace_and_closure_runs_once` - `corvid::PlanCache::new` — `lifecycle_plan_cache_miss_hit_insert_replace_and_closure_runs_once` - `corvid::PlanCache::get` — `lifecycle_plan_cache_miss_hit_insert_replace_and_closure_runs_once` - `corvid::PlanCache::insert` — `lifecycle_plan_cache_miss_hit_insert_replace_and_closure_runs_once` - `corvid::PlanCache::get_or_insert_with` — `lifecycle_plan_cache_miss_hit_insert_replace_and_closure_runs_once` - `corvid::PlanCache::len` — `lifecycle_plan_cache_miss_hit_insert_replace_and_closure_runs_once` - `corvid::PlanCache::is_empty` — `lifecycle_plan_cache_miss_hit_insert_replace_and_closure_runs_once` ### MCP wire — 51 construct(s) The sidecar's whole surface is one class: the JSON-RPC envelopes, every tool name, and the Rust items a client's bytes flow through. Covered by the in-process duplex-I/O suite in `crates/corvid-mcp/tests/tools/`. - `corvid_mcp::Server` — `server_new_wraps_an_engine_db`, `tools_smoke_in_process_wire_roundtrip` - `corvid_mcp::Server::new` — `server_new_wraps_an_engine_db` - `corvid_mcp::Server::open` — `backup_reopens_as_a_live_database`, `open_server_memory_and_file_backed` - `corvid_mcp::Server::open_in_memory` — `envelope_initialize_result_shape`, `tools_smoke_in_process_wire_roundtrip` - `corvid_mcp::Server::handle` — `envelope_error_taxonomy_three_surfaces`, `store_then_get_roundtrips_and_overwrites` - `corvid_mcp::ToolError` — `envelope_error_taxonomy_three_surfaces` - `corvid_mcp::ToolError::UnknownTool` — `envelope_error_taxonomy_three_surfaces` - `corvid_mcp::ToolError::BadParams` — `envelope_error_taxonomy_three_surfaces`, `store_and_get_param_errors` - `corvid_mcp::ToolError::Engine` — `envelope_error_taxonomy_three_surfaces`, `store_engine_name_errors_surface` - `corvid_mcp::convert::json_to_value` — `vector_wrapper_roundtrips_through_the_wire`, `convert_malformed_wrappers_fall_back_to_maps`, `convert_int_float_distinction_survives`, `convert_u64_beyond_i64_is_lossy_float` - `corvid_mcp::convert::value_to_json` — `bytes_wrapper_roundtrips_through_the_wire`, `convert_wrappers_nested_and_multi_key`, `convert_vector_components_are_f32_precision`, `convert_unicode_text_survives` - `corvid_mcp::protocol::PROTOCOL_VERSION` — `envelope_initialize_result_shape`, `tools_smoke_in_process_wire_roundtrip` - `corvid_mcp::protocol::MAX_FRAME_SIZE` — `frame_over_default_max_frame_size_is_refused` - `corvid_mcp::protocol::open_server` — `open_server_memory_and_file_backed` - `corvid_mcp::protocol::run` — `envelope_session_multiple_requests_in_order`, `frame_over_default_max_frame_size_is_refused`, `tools_smoke_in_process_wire_roundtrip` - `corvid_mcp::protocol::run_with_limit` — `frame_size_boundary_exact_and_one_over` - `corvid_mcp::protocol::handle_request` — `envelope_initialize_result_shape`, `envelope_notifications_produce_no_response` - `mcp::envelope::initialize` — `envelope_initialize_result_shape`, `tools_smoke_in_process_wire_roundtrip` - `mcp::envelope::ping` — `envelope_ping_empty_result`, `envelope_blank_and_crlf_frames_are_ignored` - `mcp::envelope::tools/list` — `envelope_tools_list_all_29_with_schemas` - `mcp::envelope::tools/call` — `envelope_tools_call_content_shape`, `envelope_tools_call_malformed_request_is_invalid_params` - `mcp::envelope::error_response` — `envelope_unknown_and_missing_method_codes`, `envelope_malformed_line_is_parse_error_and_loop_survives` - `mcp::tool::store` — `store_then_get_roundtrips_and_overwrites`, `store_accepts_every_json_document_kind`, `store_and_get_param_errors`, `store_engine_name_errors_surface` - `mcp::tool::patch` — `patch_merges_top_level_and_creates_missing` - `mcp::tool::compare_and_set` — `compare_and_set_absent_expected_and_mismatch`, `compare_and_set_new_omitted_deletes` - `mcp::tool::get` — `store_then_get_roundtrips_and_overwrites`, `get_missing_key_and_unknown_collection_are_null` - `mcp::tool::delete` — `delete_reports_outcome_and_param_errors` - `mcp::tool::delete_where` — `delete_where_counts_and_filter_errors` - `mcp::tool::search` — `search_vector_orders_by_similarity`, `search_filter_op_matrix`, `search_limit_validation_matrix`, `search_engine_invalid_argument_mmr_and_rrf` - `mcp::tool::create_index` — `create_index_variants_then_search`, `create_index_param_and_training_errors`, `index_tools_on_disk_flag_type_errors` - `mcp::tool::link` — `link_without_docs_and_duplicate_is_idempotent`, `graph_param_errors` - `mcp::tool::unlink` — `unlink_reports_removed_true_then_false` - `mcp::tool::neighbors` — `neighbors_and_in_neighbors_directions`, `list_tools_clamp_oversized_limit_and_reject_invalid` - `mcp::tool::traverse` — `traverse_hops_cycles_and_empty_starts`, `graph_param_errors` - `mcp::tool::geo` — `geo_radius_nearest_and_limit`, `geo_param_errors` - `mcp::tool::join` — `join_left_outer_rows_and_missing_references`, `join_int_foreign_key_matches_decimal_text_key`, `list_tools_clamp_oversized_limit_and_reject_invalid` - `mcp::tool::in_neighbors` — `neighbors_and_in_neighbors_directions`, `list_tools_clamp_oversized_limit_and_reject_invalid` - `mcp::tool::page` — `page_cursor_walk_default_and_boundaries` - `mcp::tool::phrase_search` — `phrase_search_ordered_tokens_and_k_bounds` - `mcp::tool::create_text_index` — `create_text_index_memory_and_ondisk`, `index_tools_param_and_name_errors`, `index_tools_on_disk_flag_type_errors` - `mcp::tool::create_scalar_index` — `create_scalar_index_exact_under_mutation`, `index_tools_param_and_name_errors` - `mcp::tool::create_geo_index` — `create_geo_index_then_radius_exact`, `index_tools_param_and_name_errors` - `mcp::tool::create_compound_index` — `create_compound_index_and_fields_errors`, `index_tools_param_and_name_errors` - `mcp::tool::backup` — `backup_reopens_as_a_live_database`, `backup_existing_target_and_missing_path_errors` - `mcp::tool::dump` — `dump_then_load_roundtrips_through_the_wire`, `load_missing_and_garbage_file_errors` - `mcp::tool::load` — `dump_then_load_roundtrips_through_the_wire`, `load_missing_and_garbage_file_errors`, `load_rename_param_migrates_collections_through_the_wire` - `mcp::tool::list_collections` — `list_collections_lists_user_names_exactly` - `mcp::tool::count` — `count_exact_with_filter_and_unknown_collection`, `create_scalar_index_exact_under_mutation` - `mcp::tool::insert_auto` — `insert_auto_keys_ordered_and_distinct`, `dump_then_load_roundtrips_through_the_wire` - `mcp::tool::set_schema` — `set_schema_then_get_schema_roundtrips`, `set_schema_unique_enforced_on_stores`, `set_schema_required_and_type_violations`, `set_schema_param_and_name_errors`, `dump_load_preserves_schema_constraints`, `set_schema_flag_type_errors`, `set_schema_declared_empty_vs_undeclared_fields` - `mcp::tool::get_schema` — `set_schema_then_get_schema_roundtrips`, `set_schema_param_and_name_errors`, `dump_load_preserves_schema_constraints`, `set_schema_declared_empty_vs_undeclared_fields` 327 engine construct(s) across 13 statement classes, 51 wire construct(s), 305 distinct covering tests (existence and uniqueness enforced by the radars; the 7 exempt row(s) above are the only uncovered ones, each with its justification). ### Semantics notes Cross-class contracts the conformance program pins; each note names the suite that owns it. ### Pre-ranking predicates and BM25 (Text search) A builder text query with a filter ranks the *filtered* candidate set: the predicate runs first (index window or scan), and the BM25 statistics — document frequencies, average document length — are computed over exactly those candidates, not the whole collection. The same query without a filter scores against full-corpus stats, so a score always means "relevance within the candidate set the filter admits". Owned by `tests/search_text.rs`. ### geo_within_bbox returns key order, portably (Geo) `geo_within_bbox` materializes its result sorted by key on every path: the indexed window and the scan are byte-identical, documents included. Key order is the contract — never cell or insertion order. Owned by `tests/search_geo.rs`. ### NaN duality: comparisons vs storage equality Two rules coexist by design: - *Predicate comparisons* (`eq`/`ne`, every ordered operator, `is_in`, `between`): NaN matches nothing, not even NaN — a NaN filter value selects an empty set (`ne` selects everything else). - *Storage equality* (`compare_and_set` expected values, unique constraints): NaN equals NaN regardless of payload, and `-0.0` equals `0.0` — the shared semantic rule (owned by `tests/mutations.rs` and `tests/schema.rs`). ### Equality is per-construct | Construct | Equality rule | |---|---| | `compare_and_set` expected value | Semantic value equality: NaN==NaN across payloads, -0.0==0.0, containers element-wise | | Predicates (`eq`/`ne`, ordered ops) | Typed total-order comparison: NaN never equals anything; `Int(2)` equals `Float(2.0)` numerically (mixed comparisons convert the integer through f64, exact up to 2^53) | | Unique constraints | Storage-level semantic equality (NaN==NaN), enforced per field value on write | | Joins | An `Int` foreign key matches a `Text` key via its decimal-string encoding: `Int(7)` joins to the key `"7"` | | Group keys (`group_count`/`group_sum`/`group_avg`, `count_distinct`) | Type-tagged canonical keys: bare for text (`blog`), `i:`/`f:`/`b:` tags for non-text, `t:` escape for text that would look tagged — distinct types stay distinct | ================================================================================ # Error codes # /docs/v0.3.1/reference/error-codes/ ================================================================================ > **generated — synced from the engine at v0.3.1.** The detailed codes > returned by `corvid_last_error_code()`. Value 0 means "no error recorded > on this thread". Codes 1–18 map 1:1 onto the engine's > `corvid::Error` variants (pinned by the variant-inventory snapshot > test); code 19 is FFI-only. **Never renumbered; new values only appended > (20+).** The error model itself: [errors & NULL discipline](/ffi/errors/). | Code | Name | Meaning | |---|---|---| | `0` | `CORVID_E_OK` | no error | | `1` | `CORVID_E_DATABASE` | corvid::Error::Database — opening/creating the file failed | | `2` | `CORVID_E_TRANSACTION` | corvid::Error::Transaction — beginning a read/write txn failed | | `3` | `CORVID_E_TABLE` | corvid::Error::Table — opening a storage table failed | | `4` | `CORVID_E_STORAGE` | corvid::Error::Storage — a storage read/write failed | | `5` | `CORVID_E_COMMIT` | corvid::Error::Commit — committing a write txn failed | | `6` | `CORVID_E_SET_DURABILITY` | corvid::Error::SetDurability — changing txn durability failed | | `7` | `CORVID_E_COMPACTION` | corvid::Error::Compaction — compacting the file failed | | `8` | `CORVID_E_DECODE` | corvid::Error::Decode — stored bytes are not a decodable Value | | `9` | `CORVID_E_CORRUPT_INDEX` | corvid::Error::CorruptIndex — persisted index state is corrupt | | `10` | `CORVID_E_RESERVED_COLLECTION` | corvid::Error::ReservedCollection — name uses the `__` prefix | | `11` | `CORVID_E_INVALID_NAME` | corvid::Error::InvalidName — name has a NUL byte or interior `__` | | `12` | `CORVID_E_ARGUMENT` | corvid::Error::InvalidArgument — argument outside its domain (RRF k, MMR lambda, geo bounds) AND the FFI's own NULL/UTF-8 discipline (§7) | | `13` | `CORVID_E_INCOMPATIBLE_FORMAT` | corvid::Error::IncompatibleFormat — file is a foreign format version | | `14` | `CORVID_E_EMPTY_INDEX_TRAINING` | corvid::Error::EmptyIndexTraining — PQ create with no training vectors | | `15` | `CORVID_E_SCHEMA_VIOLATION` | corvid::Error::SchemaViolation — write violates the declared schema | | `16` | `CORVID_E_INVALID_DUMP` | corvid::Error::InvalidDump — malformed / unknown-version dump stream | | `17` | `CORVID_E_BACKUP_TARGET_EXISTS` | corvid::Error::BackupTargetExists — backup path already exists | | `18` | `CORVID_E_IO` | corvid::Error::Io — I/O error (dump/load paths, files) | | `19` | `CORVID_E_BUSY` | FFI-ONLY: corvid_compact while derived handles are still open (engine Db::compact needs &mut self; see §4.13). No engine variant. | In Rust, the same failures surface as the typed `corvid::Error` enum (`thiserror`, `#[non_exhaustive]`); methods return `corvid::Result`. Bindings map the code to native exceptions (corvid-node exports this table as `ErrorCode`). ================================================================================ # Glossary # /docs/v0.3.1/reference/glossary/ ================================================================================ A working glossary of terms used across these docs. **adjacency** — the derived, endpoint-first re-keying of graph edges into private `__adj_out__`/`__adj_in__` namespaces so deletes cascade in O(edges-of-document) and neighbor reads are direct. Invisible: never listed, never dumped. **ANN (approximate nearest neighbor)** — vector search served by an HNSW graph index instead of an exact scan. Candidates are approximate; scores are reranked exact. See `Hit.approximate`. **backfill** — the process of building an index over an existing collection, committed page by page with a persisted cursor (`Building{cursor}` → `Complete`) so an interrupted creation resumes and queries never serve a partial index. **candidate superset** — what an index window returns: a set guaranteed to contain every match (encoding ties may add extras); the builder re-checks each candidate against the exact predicate, so results are always exact. **CAS (compare-and-set)** — an atomic conditional write: apply only if the stored value equals `expected` (or is absent). Uses semantic value equality. **change event** — an `Insert`/`Delete` notification delivered synchronously post-commit to subscribers; the engine has no separate event log. **class order (ordering)** — `order_by`'s fixed row classes: comparable values (numbers, then texts) → incomparable values (kind tag first, so NaN precedes the other incomparable kinds; then key) → rows missing the field; ties by key; `descending` reverses the within-class order — kind tag and value together — in both present classes, never the class order itself. **collection** — a named namespace of documents, created lazily on first write; `__`-prefixed names are engine-reserved. **compound index** — an index over an ordered field list serving prefix-equality plus at most one trailing range; carries the `all_docs_indexed` flag that gates prefix-only acceleration. **corvid** — the engine. Corvid = crows/ravens: eat anything, highly intelligent, cache food across thousands of remembered locations. **cursor** — an opaque byte token resuming a keyset page walk strictly after the last served key. Also the general term for the ABI's `_next`-driven iterators. **derived index** — an index maintained transactionally from the documents (the source of truth). Never stale at query time; re-creatable; corrupt state errors loudly. **dump** — the logical, version-stamped export stream (`CORVIDDUMPv1`/`v2`) carrying documents plus definitions; the migration path across format breaks. `load` (and `load_with_renames`) replay it. **exact baseline** — vector/text search without an index: brute-force streamed scoring. Correct at any scale (OOM-free); the default until you create an index. **fused score** — the reciprocal-rank-fusion score of a row (`Σ 1/(k + rank)` across sources); `0.0` for pure filter/order queries. **golden fixtures** — the 256-line fixture suite pinning ABI-observable behavior (NaN/±inf/−0.0, cursors, unique violations, geo boundaries, persistence); every binding replays it in CI. **HNSW** — Hierarchical Navigable Small World: the graph index behind all corvid vector indexes, in-RAM or on-disk, optionally quantized/PQ. **keyset pagination** — walking a collection by cursor instead of offset: each page returns rows plus the resume point; O(limit) per page regardless of depth. **MVCC** — multi-version concurrency control: readers get point-in-time snapshots and never block (or get blocked by) the single writer. **MMR (maximal marginal relevance)** — a rerank trading relevance for diversity; `lambda ∈ [0,1]` (1 = pure relevance, 0 = maximal diversity). **PQ (product quantization)** — compressing vectors to `m` code bytes via a trained per-subspace codebook; the smallest footprint (e.g. 16× at 64d, m=16), scored via ADC (L2) or reconstruction. **predicate** — a filter tree over dotted field paths; a **true predicate** runs before ranking (the top-k is computed among matches). **purge** — the TTL deletion step (`purge_expired(now)`): expired records stay visible until you call it; the engine keeps no clock. **quantization** — storing vectors lossily-compressed: Binary (1 bit/dim, ~32×, Hamming), Scalar (8-bit + header, ~4×), or PQ. **RRF (reciprocal rank fusion)** — merging ranked lists by `Σ 1/(k + rank)`; corvid's default constant is 60. **scalar index** — order-preserving keys making equality/range filters, counts, and order walks sub-linear. **schema** — an optional per-collection declaration of field types/required/unique constraints, enforced on write. **semantic cache** — a vector-keyed cache: nearest embedding within a threshold answers; distance units follow the metric. **snapshot** — the MVCC point-in-time view behind every query, page, join, traverse, and dump; one query = one committed state. **tombstone** — a delete marker inside an on-disk index; over-fetch scales with tombstone count, and compaction triggers when they exceed a third of the index. **true predicate** — see *predicate*. **wave 4** — the audit-remediation wave that tightened name validation (interior `__`, NUL); pre-wave-4 dumps migrate via `load_with_renames`. ================================================================================ # Changelog highlights # /docs/v0.3.1/about/changelog/ ================================================================================ Highlights per release, inlined below so this page stands alone (condensed at release time from the engine repository's [CHANGELOG](https://github.com/corvid-db/corvid/blob/master/CHANGELOG.md) — provenance only; everything you need to evaluate a release is here). Until 1.0, the API and on-disk format change without backward-compatibility guarantees; format changes migrate via [dump/load](/admin/dump-load/). ## v0.3.1 (2026-09) **Header-only fix: `corvid.h` is portable C11/C++.** The generated header had been wrapping every frozen enum in C23 fixed-underlying-type guards (`#if __STDC_VERSION__ >= 202311L` + `: uint32_t`), whose pre-C23 fallback (`typedef uint32_t corvid_status;` beside the enum tag) is ill-formed C++ — the tag and the typedef share a namespace there. Found by corvid-cpp (the C++ binding), which had to preprocessor-mask the guards in its ABI translation units. The header now emits the plain `typedef enum { ... } corvid_xxx;` the FFI spec has always shown — valid C11, C23, and every C++ standard; verified by compiling a trivial TU both ways against the published artifact's header. Values, signatures, and the 124-symbol surface are unchanged; the Rust-side `#[repr(u32)]` wire-type pin is untouched; `FFI_VERSION` and the soname stay at 1. C++ consumers drop their workarounds at the next pin; existing v0.3.0 pins keep working (the C-level surface did not move). ## v0.3.0 (2026-09) **The C ABI's first additive expansion** — no engine storage or query changes; two new FFI symbols (Appendix A 122 → 124), both inside `FFI_VERSION = 1`: - **`corvid_value_map_keys`** — a map's keys as a string cursor in ascending key-byte order (non-maps answer an empty cursor, inert). Bindings could previously read a map's values only by known key; key enumeration needed a candidate-key oracle. - **`corvid_phrase_search`** — the direct positional text search (consecutive in-order analyzed tokens; stop words collapse out of adjacency; most relevant first, ties by key) returning a rows cursor whose score is the hit's BM25 phrase sum. `k == 0` answers an empty cursor. The query builder's `.text` source stays bag-of-words — phrase semantics had no ABI path before this. - Golden fixtures gained executable lines (map-key enumeration, phrase cases) — bindings re-vendor `golden/` at their pin bump. No signature, enum value, or behavior change; the soname and `FFI_VERSION` stay at 1. ## v0.2.1 (2026-08) **Fixes to the v0.2.0 release artifacts** — no engine/API changes: - darwin dylib install name is `@rpath/libcorvid.dylib` and the Linux `.so` carries its SONAME — the v0.2.0 artifacts were unloadable by external consumers (found by corvid-c; verified in-pipeline). ## v0.2.0 (2026-08) The big one — the C ABI, plus a hardening/roadmap-execution program: - **The C ABI** (`corvid-ffi`): a 122-symbol typed cdylib + generated `corvid.h`, `FFI_VERSION = 1` locked contract, golden fixtures, C smoke suite (122/122 symbols), header drift gate, 3-OS release CI, ASan/UBSan/LSan job (zero leaks), per-platform release archives. Documents through value handles — no parsing anywhere; measured at parity with native Rust (0.99–1.02×). - **CJK text search**: sliding-bigram tokenization for Han + kana runs (dictionary-free), phrase-order-correct (`東京タワー` matches, `タワー東京` doesn't); stemming/folding never apply to CJK. Re-create pre-existing text indexes. - **New sketches**: `CuckooFilter` (deletable membership, rollback-on-full), `TDigest` (streaming quantiles/CDF), `MinHash` + `LshIndex` (set similarity + candidate lookup). - **In-memory PQ** (`create_vector_index_pq`) with the persisted codebook; parallel PQ training (2.6–4.1×, bit-identical codebooks). - **Dump format v2** (u64 length prefixes; loader accepts v1 and v2); `load_with_renames` for legacy `__`-containing names. - **zstd feature** (transparent document compression ≥1 KiB, ~12× on text) and **tracing feature** (structured events: backfill, compaction, plan shapes, semantic cache). - **Durability/consistency program**: single-snapshot queries/aggregations/ traverse/page; O(degree) graph-edge cascades via derived adjacency (ratified link trade); crash-safe index creation with resume; compound prefix-only windows (`all_docs_indexed`); sort indexes (`SortIndex`); density-driven verify batching; exact-rerank ANN hits (`Hit.approximate`); `plan_shape()`/`PlanShape`; semantic CAS equality (NaN/−0.0); name validation (no interior `__`/NUL — breaking); antimeridian bbox + geo validation; execution-time RRF/MMR/BM25 argument validation; order_by class rules (audit C4); auto-compaction of on-disk vector indexes; `Store::begin_bulk` thread-local bulk scopes. ## v0.1.1 (2026-05) - Release workflow builds `corvid-mcp` for all desktop/server platforms (Linux x86_64/aarch64, macOS Intel/Apple Silicon, Windows x86_64). ## v0.1.0 (2026-05) The first release — the embedded engine complete: - Transactional KV over redb; typed `Value` model with deterministic codec; document layer with fluent `Collection` handles, `insert_auto` ordered keys. - Vector search (cosine/dot/L2, exact KNN + persistent HNSW, `.approx()` filtered-ANN path), BM25 full-text with incremental inverted indexes, phrase/positional search, CJK-ready tokenizer with S-stemmer. - The multi-modal query builder: filter + vector + text + RRF + MMR + order_by/offset + select + limit; aggregations global and grouped; keyset pagination; selectivity-driven index choice; identity-hashable `QueryPlan`/`PlanCache`; bounded ranked execution. - Indexes: scalar, compound, text (in-RAM + on-disk), geo, on-disk HNSW (quantized variants), on-disk PQ. - Graph (`link`/`neighbors`/`traverse`), geo (radius/bbox/k-nearest), joins, semantic cache, reactive change feeds, sketches (HLL + Bloom), TTL, schemas with unique constraints. - Operations: online backup, logical dump/load, compaction, bulk load; MCP sidecar over stdio; on-disk format version marker; WASM (wasm32 engine + 0.2 MB harness), iOS/Android cross-compiles. - Autovectorized distance kernels under `#![forbid(unsafe_code)]`. ================================================================================ # About these docs # /docs/v0.3.1/about/ ================================================================================ This site (`corvid-db/docs`) is the canonical documentation for corvid. It is built with [Astro Starlight](https://starlight.astro.build), deployed to GitHub Pages at , and versioned PostgreSQL-style: a **current** site plus frozen **release snapshots**. ## Versioning - **`/docs/` (current)** — built from the repo's default branch; tracks the engine's development. Every page carries a version banner stating what you're reading. - **`/docs/vX.Y.Z/` (snapshots)** — at each engine release, the docs repo's state at that moment is snapshotted into a `releases/vX.Y.Z` branch and built with its own base path (`/docs/vX.Y.Z/`) and banner. A snapshot is frozen: it documents exactly the release it was cut for, forever, and never receives fixes (fixes land on current and future snapshots). The first snapshot is **[v0.2.1](/docs/v0.2.1/)** — identical to the initial site content. The mechanism: 1. On each engine release, maintainers branch `releases/vX.Y.Z` from the docs repo (or tag it), then run the *snapshot* workflow ([`.github/workflows/snapshot.yml`](https://github.com/corvid-db/docs/blob/master/.github/workflows/snapshot.yml)) with the version — it builds that branch with `SITE_VERSION=X.Y.Z SITE_BASE=/docs/vX.Y.Z/` and deploys the output under the `vX.Y.Z/` directory of the published site. 2. The *deploy* workflow rebuilds and republishes the root (`/docs/`) from the default branch on every push, preserving the version directories. Honesty note: a snapshot reflects the **docs repo** at the release moment — the engine's own release notes remain the changelog of record (mirrored as [highlights](/about/changelog/)). ## Generated vs hand-maintained pages Two pages are **generated-class** — synced from the engine, not edited by hand: - [Construct reference](/reference/constructs/) — generated from the engine's `docs/SYNTAX.md` (itself generated from the conformance surface manifests). - [Error codes](/reference/error-codes/) — generated from the engine's `docs/FFI.md` §1.3 frozen code table. Both carry a "generated — synced from the engine at vX.Y.Z" header naming the pinned tag (see [`.engine-pin`](https://github.com/corvid-db/docs/blob/master/.engine-pin)). `scripts/sync-from-engine.sh ` regenerates them; CI ([the sync check](https://github.com/corvid-db/docs/blob/master/.github/workflows/ci.yml)) fails if the committed copies drift from the pinned tag — the same drift-gate pattern the engine uses for its generated files. Everything else on this site is hand-maintained canon. ## AI-friendly endpoints - [`/docs/llms.txt`](/llms.txt) — a curated index of every page with one-line descriptions, generated at build time. - [`/docs/llms-full.txt`](/llms-full.txt) — the full site as one markdown stream. - Every page is reachable as **clean markdown** at `/docs/src/.md` — the build copies the source files next to the rendered HTML, with stable URLs and frontmatter-carrying meta descriptions. ## Contributing - PRs to [`corvid-db/docs`](https://github.com/corvid-db/docs) — prose fixes, new pages, better cross-links are all welcome. - Factual claims about engine behavior should cite or mirror the engine's conformance suites; if docs and engine disagree, that's a bug in one of them — open an issue in the right repo. - Local development: ```sh npm install npm run dev # live preview npm run build # build + generate llms.txt + markdown sources npm run verify-sync # check the generated pages match the pinned tag ``` ## License The docs are MIT-licensed, like the engine.