Go client SDK for the spada search engine
  • Go 96.1%
  • Shell 3.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Yannick Koechlin 9e725e8c4f
spada-sdk-go 0.1.0 — the Go client, with the semantics in the API
Generated stubs (buf + protoc-gen-go/-go-grpc, managed go_package) for
the vendored spada.v1 surface with pinned provenance, 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, Search with the receipt as a first-class type
(EXACT is a proof; UnitsDropped surfaced), and S-150 typed error details
decoded through the STOCK grpc-go status machinery — which doubles as
live proof the server's RpcStatus mirror is byte-identical to
google.rpc.Status. Classify + Policy give the table-driven retry answer,
with HomeReleased as the one retryable precondition and SealerFailed as
operator-action.

Verified against a live server: quickstart (write, dedup replay, EXACT
receipt, passages) and typederrors (LimitExceeded naming its setting key
as a field) both run clean. go build + go vet clean.

Claude-Session: https://claude.ai/code/session_01JrTbCU5Da5hJ19XUrYYxq9
2026-08-05 23:17:34 +02:00
examples spada-sdk-go 0.1.0 — the Go client, with the semantics in the API 2026-08-05 23:17:34 +02:00
gen/spada/v1 spada-sdk-go 0.1.0 — the Go client, with the semantics in the API 2026-08-05 23:17:34 +02:00
proto spada-sdk-go 0.1.0 — the Go client, with the semantics in the API 2026-08-05 23:17:34 +02:00
buf.gen.yaml spada-sdk-go 0.1.0 — the Go client, with the semantics in the API 2026-08-05 23:17:34 +02:00
client.go spada-sdk-go 0.1.0 — the Go client, with the semantics in the API 2026-08-05 23:17:34 +02:00
errors.go spada-sdk-go 0.1.0 — the Go client, with the semantics in the API 2026-08-05 23:17:34 +02:00
generate.sh spada-sdk-go 0.1.0 — the Go client, with the semantics in the API 2026-08-05 23:17:34 +02:00
go.mod spada-sdk-go 0.1.0 — the Go client, with the semantics in the API 2026-08-05 23:17:34 +02:00
go.sum spada-sdk-go 0.1.0 — the Go client, with the semantics in the API 2026-08-05 23:17:34 +02:00
query.go spada-sdk-go 0.1.0 — the Go client, with the semantics in the API 2026-08-05 23:17:34 +02:00
README.md spada-sdk-go 0.1.0 — the Go client, with the semantics in the API 2026-08-05 23:17:34 +02:00
sync-protos.sh spada-sdk-go 0.1.0 — the Go client, with the semantics in the API 2026-08-05 23:17:34 +02:00
write.go spada-sdk-go 0.1.0 — the Go client, with the semantics in the API 2026-08-05 23:17:34 +02:00

spada-sdk-go — Go client for the spada search engine

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

go get git.teixos.net/yannick/spada-sdk-go

Quickstart

import (
    "context"
    spada "git.teixos.net/yannick/spada-sdk-go"
)

client, err := spada.Connect("127.0.0.1:50051", spada.Options{})
if err != nil { /* … */ }
defer client.Close()
ctx := context.Background()

// Idempotent, flushed write: the ack is a durability barrier.
out, err := client.Write(ctx, "articles", []spada.Doc{
    {DocID: "munich", Text: "# Munich\n\nBayern beats the rain.",
        Fields: map[string]string{"lang": "en"}},
}, spada.WriteOptions{MutationID: "ingest-0001", Flush: true})

// Query — and READ THE RECEIPT: EXACT is a proof, not a mood.
answer, err := client.Search(ctx, "articles", spada.Query{
    Terms: []string{"bayern"}, K: 10,
})
fmt.Println("receipt:", answer.Receipt()) // e.g. "EXACT"
for _, hit := range answer.Hits {
    fmt.Printf("%s %.3f\n", hit.DocId, hit.Score)
}

Runnable versions: examples/quickstart and examples/typederrors — start a dev server with spada serve --data-dir /tmp/demo --listen 127.0.0.1:50051, then go run ./examples/quickstart.

The three contracts

1. Durability: Flush is the barrier, SealTriggered 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 — the response's flag means a seal was triggered). When "this call returning" must mean "on disk", set WriteOptions.Flush. A flush drains the server's seal pool and can block for seconds; the SDK gives flushing calls the longer FlushTimeout (default 120 s) automatically.

Client.Ingest streams a corpus over one WriteStream in batches bounded by both count and bytes, flushing on the last message — its report means durable. Note WriteStream's contract: strictly serial, one response, no cross-message transaction — a mid-stream failure leaves prior messages applied.

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

r := answer.Receipt()
if !r.IsExact() {
    log.Printf("not certified: %s", r) // APPROXIMATE [stages] / UNPROVEN (why)
}
if r.UnitsDropped() > 0 {
    // A referenced segment was unreadable: the answer is PARTIAL.
}

Watch for the stage "query-embed-skipped": a hybrid query silently degraded to lexical-only because no embedding model was available. If your application must never act on an uncertified answer, set Query.ExactnessRequired — the server refuses (typed) instead of degrading.

3. Errors: the detail type is the retry policy

Since spada S-150, structured refusals ride the standard grpc-status-details-bin trailer as a genuine google.rpc.Status — this SDK decodes it through the stock google.golang.org/grpc/status machinery, no spada-specific transport code. Never parse error messages:

if _, err := client.Search(ctx, ns, q); err != nil {
    if e, ok := spada.Classify(err); ok {
        switch e.Policy() {
        case spada.RetryWithBackoff: // transient: back off, retry here
        case spada.RetryElsewhere:   // re-resolve routing, retry there
        case spada.OperatorAction:   // page someone; do NOT spin
        case spada.RetryNever:       // fix the request
        }
        if l, ok := e.Detail.(*pb.LimitExceeded); ok {
            log.Printf("raise %s (limit %d, asked %d)", l.SettingKey, l.Limit, l.Actual)
        }
    }
}

The two details that matter most: HomeReleased — the one FAILED_PRECONDITION that IS retryable (the namespace moved home; re-resolve and retry there) — and SealerFailed — the server fail-stopped writes; retrying 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 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 upstream.

Idempotent writes

Set WriteOptions.MutationID on any write you might retry. A retry with the same id returns the original result verbatim (Deduplicated) and writes nothing. The horizon is finite (write.dedup_history server-side): past it a retry is refused typed, never silently re-executed — bound your retry window well inside it. And cite DocVersions for identity, never DocID (a mutable attribute over a succession of versions).

Beyond the wrapper

client.Raw() exposes the full generated spada.v1 client (Export, QueryStream, MultiQuery, BatchQuery, schema, recall evaluation), with client.WithAuth(ctx) to carry the bearer token. Two rules when you drop down: always read a QueryStream to its terminal receipt frame (stopping early leaves you holding an unverified result), and never set the x-spada-forwarded metadata key (reserved; setting it suppresses routing).

TLS and auth

ca, _ := os.ReadFile("ca.pem")
client, err := spada.Connect("spada.example:50051", spada.Options{
    TLSCAPEM: ca,
    Token:    os.Getenv("SPADA_TOKEN"),
})

Client.Health returns DevModeNoAuth — if true, the server runs without authorization and must not be exposed to a network. Surface it.

Protos, codegen, compatibility

The spada.v1 protos are vendored under proto/ with their source commit pinned in proto/PROVENANCE.md. Regenerate with ./generate.sh (buf + protoc-gen-go/protoc-gen-go-grpc); refresh from a spada checkout with ./sync-protos.sh <path>. The surface is frozen additive-only and machine-enforced upstream. Only the public package is vendored — spada.internal.v1 and spada.model.v1 carry no compatibility promise and a client must never speak them.

License

Apache-2.0.