Rust client SDK for the spada search engine
  • Rust 98.3%
  • Shell 1.7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Yannick Koechlin f88e4c9139
spada-sdk 0.1.0 — the Rust client, with the semantics as types
Generated tonic stubs for the vendored spada.v1 surface (provenance
pinned), wrapped: connect with TLS+token, idempotent writes with flush
as the visible durability barrier (and its own longer deadline), a
byte-and-count-bounded ingest stream that flushes last, a query builder
over the common surface with the raw client as the escape hatch,
receipts as types (EXACT is a proof; units_dropped surfaced), and S-150
typed error details decoded into a table-driven RetryPolicy — including
HomeReleased as the one retryable precondition and SealerFailed as the
one where even waiting is wrong.

Verified against a live server: quickstart (write, dedup replay, EXACT
receipt, passages) and typed_errors (LimitExceeded decoding its setting
key as a field) both run clean. 4 unit tests + 1 doc test.

Claude-Session: https://claude.ai/code/session_01JrTbCU5Da5hJ19XUrYYxq9
2026-08-05 23:14:19 +02:00
examples spada-sdk 0.1.0 — the Rust client, with the semantics as types 2026-08-05 23:14:19 +02:00
proto spada-sdk 0.1.0 — the Rust client, with the semantics as types 2026-08-05 23:14:19 +02:00
scripts spada-sdk 0.1.0 — the Rust client, with the semantics as types 2026-08-05 23:14:19 +02:00
src spada-sdk 0.1.0 — the Rust client, with the semantics as types 2026-08-05 23:14:19 +02:00
.gitignore spada-sdk 0.1.0 — the Rust client, with the semantics as types 2026-08-05 23:14:19 +02:00
build.rs spada-sdk 0.1.0 — the Rust client, with the semantics as types 2026-08-05 23:14:19 +02:00
Cargo.toml spada-sdk 0.1.0 — the Rust client, with the semantics as types 2026-08-05 23:14:19 +02:00
README.md spada-sdk 0.1.0 — the Rust client, with the semantics as types 2026-08-05 23:14:19 +02:00

spada-sdk — Rust client for the spada search engine

The official Rust SDK for spada's public spada.v1 gRPC surface: an idiomatic wrapper over generated tonic/prost stubs, with the semantics the wire cannot express encoded as types — durability, coverage receipts, and table-driven retry policy.

[dependencies]
spada-sdk = { git = "https://git.teixos.net/yannick/spada-sdk-rs" }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

Quickstart

use spada_sdk::{Client, Doc, QueryOptions};

#[tokio::main]
async fn main() -> Result<(), spada_sdk::Error> {
    let mut client = Client::connect("http://127.0.0.1:50051").await?;

    client
        .write("articles")
        .doc(Doc::new("munich", "# Munich\n\nBayern beats the rain.").field("lang", "en"))
        .mutation_id("ingest-0001") // idempotent: a retry replays, never re-writes
        .flush()                    // the ack becomes a durability barrier
        .send()
        .await?;

    let answer = client
        .query("articles", QueryOptions::terms(["bayern"]).k(10))
        .await?;

    println!("receipt: {}", answer.receipt()); // e.g. "EXACT"
    for hit in &answer.hits {
        println!("{} {:.3}", hit.doc_id, hit.score);
    }
    Ok(())
}

Runnable versions: examples/quickstart.rs and examples/typed_errors.rs — start a dev server with spada serve --data-dir /tmp/demo --listen 127.0.0.1:50051 and cargo run --example quickstart.

The three contracts

These are the parts of spada's semantics that a generated stub alone would let you get wrong. The SDK makes each one hard to miss.

1. Durability: flush is the barrier, sealed_segment is not

A non-flush write's ack means accepted and query-visible. The durable checkpoint completes in the background (spada seals through a pipelined worker pool — its sealed_segment response flag means a seal was triggered). When you need "this call returning means it is on disk", use .flush():

client.write("ns").doc(d).flush().send().await?;

A flush drains the server's seal pool and can legitimately block for seconds; the SDK automatically gives flushing writes the longer flush_timeout (default 120 s) instead of the ordinary call deadline.

Client::ingest streams a whole corpus in batches bounded by both document count and bytes (a count-only bound eventually dies on limits.max_batch_bytes), and flushes on the final message — so its report means durable, not merely accepted.

2. Receipts: EXACT is a proof, everything else is a disclosure

Every answer carries a coverage receipt:

let r = answer.receipt();
if !r.is_exact() {
    // APPROXIMATE names its stages; UNPROVEN names its reason.
    eprintln!("not certified: {r}");
}
if r.units_dropped() > 0 {
    // A referenced segment was unreadable: the answer is PARTIAL.
}

Two receipt stages deserve special handling in applications: "query-embed-skipped" means your hybrid (semantic + lexical) query silently degraded to lexical-only because no embedding model was available; units_dropped > 0 means part of the index could not be read. If your application must never act on an uncertified answer, set QueryOptions::exactness_required() — the server then refuses (typed) instead of degrading.

3. Errors: the detail type is the retry policy

Since spada S-150, structured refusals carry a typed detail on the standard grpc-status-details-bin trailer. Never parse error messages — ask the error:

use spada_sdk::{ErrorDetail, RetryPolicy};

match client.query("ns", opts).await {
    Ok(a) => { /* … */ }
    Err(e) => match e.retry_policy() {
        RetryPolicy::RetryWithBackoff => { /* transient: back off, retry here */ }
        RetryPolicy::RetryElsewhere   => { /* re-resolve routing, retry there */ }
        RetryPolicy::OperatorAction   => { /* page someone; do NOT spin */ }
        RetryPolicy::Never            => { /* fix the request */ }
    },
}

The payloads are fields, not prose: ErrorDetail::LimitExceeded names the exact setting key (limits.max_top_k, …) and both numbers; DedupHorizonExceeded names the mutation id and the knob to raise; NotLeader carries the last-observed leader hint. Two details matter most:

  • HomeReleased — the one FAILED_PRECONDITION that IS retryable (the namespace moved home; re-resolve and retry there).
  • SealerFailed — the server fail-stopped writes; a retry can never succeed and even backing off is wrong. Surface it to an operator. Reads keep working.

A status with no detail is normal (the gRPC code alone yields the safe policy), and an unknown detail type from a newer server degrades to that same safe reading — spada.v1 is additive-only, machine-enforced by the server repository's CI.

Idempotent writes

Give any write you might retry a mutation_id you generate. A retry with the same id returns the original result verbatim (deduplicated = true) and writes nothing. The horizon is finite (write.dedup_history server-side): a retry arriving after its id was evicted is refused typed — never silently re-executed — so bound your retry window well inside it.

Beyond the wrapper

The wrapper covers the common surface. Everything else — Export (streaming scans), QueryStream, MultiQuery/BatchQuery, schema management, recall evaluation — is available through the raw generated client:

let raw: &mut spada_sdk::pb::data_service_client::DataServiceClient<_> = client.raw();

One rule when you drop down: if you consume QueryStream, always read to the terminal receipt frame — stopping early leaves you holding an unverified result. And never set the x-spada-forwarded metadata key; it is reserved for server-to-server forwarding and setting it suppresses routing.

TLS and auth

use spada_sdk::{Client, ClientOptions};
use std::time::Duration;

let client = Client::connect_with("https://spada.example:50051", ClientOptions {
    tls_ca_pem: Some(std::fs::read("ca.pem")?),
    token: Some(std::env::var("SPADA_TOKEN")?),
    call_timeout: Duration::from_secs(30),
    flush_timeout: Duration::from_secs(120),
}).await?;

Client::health returns dev_mode_no_auth — if it is true, the server is running without authorization and must not be exposed to a network. Surface it.

Protos and compatibility

The spada.v1 protos are vendored under proto/ with their source commit recorded in proto/PROVENANCE.md; refresh with scripts/sync-protos.sh <spada-checkout>. The surface is frozen additive-only and machine-enforced upstream, so a newer server never breaks an older SDK — new fields and details simply refine what this SDK already handles safely.

Only the public package is vendored: spada.internal.v1 (cluster control plane) and spada.model.v1 (model sidecars) carry no compatibility promise, and a client must never speak them.

License

Apache-2.0.