Skip to content

v0.2.1 ‐ v0.2.5 changes and sweet sweet Federation

treedinteractive edited this page Jul 6, 2026 · 1 revision

What's new

Area Change
Federation First-class engine module — open a directory of .sfg stores and search across them with automatic routing. Exposed on all three surfaces: Python, C FFI, and the Rust wrapper.
Routing modes best (single best-matched store), top (best n stores), all (exhaustive fan-out). The result top_k is whatever the query asks for — it is not capped by the mode.
Parallel fan-out Multi-store searches run concurrently across a work-stealing pool and merge by score.
Enterprise (Gov) Per-operator identity is now folded into each record's tamper-evident chain rather than carried only as metadata. See Enterprise: Compartmented Stores.
Version Engine, enterprise, and wrapper crates move in lockstep to 0.2.1. Wheels: seraph-db (GPU/candle) and seraph-db-onnx.

Stores written by earlier 0.2.x engines open unchanged — this is an additive release. No store migration is required.


Federation in one minute

A federation is a set of independent single-file stores that share the same embedding model and dimension. You point the engine at a directory; it loads every store, builds a tiny routing summary per store, and routes each query to the store(s) most likely to answer it before searching.

Directory convention

my-federation/
├── biology.sfg
├── history.sfg
├── geography.sfg
└── physics.sfg

open_dir loads every *.sfg in the directory. Each store is named by its file stem (biology.sfgbiology). Files beginning with _ or router are skipped, so you can keep scratch or index files alongside without them being treated as members.

Python

from seraph import Federation, Encoder

fed = Federation.open_dir("my-federation")
print(len(fed), fed.member_names())   # 4  ['biology', 'geography', 'history', 'physics']

# A query is an embedding from the SAME model the stores were built with.
enc = Encoder.from_pretrained("BAAI/bge-small-en-v1.5")
q = enc.encode("how do cells regulate division")

# mode: "best" | "top" (with n) | "all"
hits = fed.search(q, top_k=10, tau=0.60, mode="best")
for h in hits:
    print(h.store, h.score, h.snippet)

# Top-3 stores, still 10 results merged across them:
hits = fed.search(q, top_k=10, tau=0.60, mode="top", n=3)

# Exhaustive fan-out across every store:
hits = fed.search(q, top_k=10, tau=0.60, mode="all")

Rust (wrapper crate)

use seraph::{Federation, RoutingMode};

let fed = Federation::open_dir("my-federation")?;
let hits = fed.search(&query_embedding, 10, 0.60, RoutingMode::BestMatch)?;
//  RoutingMode::Top(3)  |  RoutingMode::All
for h in &hits {
    println!("{} {:.3} {}", h.store, h.score, h.frame_id);
}

C FFI

The same surface is exported as seraph_federation_open_dir, _add_path, _route, _search, _refresh, _len, _member_names, and _free. Calls return JSON strings; free every returned string with seraph_string_free and the federation handle with seraph_federation_free.


Routing modes

Mode Routes to Use when
best The single best-matched store You expect the answer to live in one domain. Lowest latency.
top (n) The best n stores The query may straddle a few domains; n = 3 is a good default.
all Every store You want exhaustive recall and accept fan-out cost. Skips routing entirely.

In every mode each routed store returns up to top_k hits; results are merged by score and truncated to top_k. The mode controls which stores are searched, never how many results you get back.


Refresh — stores stay live

Federation tracks its member stores by event, not by polling or disk sync. When a store promotes new structure, it is flagged stale; calling refresh() re-pulls the routing summaries of exactly the stores that changed and leaves the rest untouched:

fed.refresh()   # cheap; touches only stores that changed since the last call

You do not need to reopen the federation to pick up writes to a member store. You do need to call refresh() if you want routing to account for structure added since the federation was opened.


Gotchas

  • One model, one dimension. Every store in a federation must share the same embedding model and dimension. Adding a mismatched store is rejected at load time — this is deliberate (cross-model embeddings can't be compared). Build each domain store with the same encoder.

  • The query is an embedding, not text. search and route take a query vector. Encode the query with the same model the stores were built with before passing it in. A query embedded by a different model will route and score meaninglessly.

  • tau is model-dependent — calibrate it per encoder. The useful threshold band is a property of your embedding model's similarity distribution, not a universal constant. For bge-class models (what the benchmarks below use) the useful band is roughly ~0.58–0.65; a different encoder will land elsewhere. Sweep a handful of values against known-good queries on your own corpus and pick the knee — much lower returns loose, low-relevance hits; much higher starves results.

  • Routing is a hint, not retrieval. best/top trade a little recall for a lot of latency. If a query genuinely spans domains and you can't afford to miss anything, use all. Routing accuracy is high but not 1.0 — don't use best for exhaustive-recall workloads.

  • Call refresh() after writing to members. A long-lived federation won't see new structure in routing decisions until you refresh. Reads of unchanged stores still work; only the routing summary is what goes stale.

  • Latency scales per-store, not per-federation. Search cost is bounded by the size of the routed store(s), not the total corpus across the federation. Many medium stores beat one giant store for federated search. A practical sweet spot is ~50–100k frames per store.

  • Skipped filenames. _*.sfg and router*.sfg are intentionally ignored by open_dir. If a store isn't showing up in member_names(), check its filename prefix.

  • Free what you allocate (C FFI only). Every JSON string returned by a seraph_federation_* call must be released with seraph_string_free, and the handle with seraph_federation_free. The Python and Rust surfaces manage this for you.


Upgrading

Drop-in for existing 0.2.x users:

  • Python: pip install -U seraph-db (or seraph-db-onnx for the ONNX encoder backend).
  • FFI / wrapper: replace the 0.2.1 shared library (seraph.dll / libseraph.so) and headers.

Existing stores open without migration. Federation is opt-in — nothing changes for single-store usage unless you call into the federation surface.


Benchmarks

Real numbers, not projections. Setup: 14 DBpedia domain stores (~60k frames each, ~840k total — album, animal, artist, athlete, building, company, film, village, …), 98 queries drawn from real frame embeddings, top_k = 10, p50 latency over the runs. Parallel is the shipped rayon fan-out; sequential searches the same store set one at a time, shown for comparison. CPU search path. Corpus embedded with bge-small (384-dim) — the tau values below reflect that encoder; recalibrate for a different model (see the tau gotcha above).

tau Mode Stores searched Sequential p50 Parallel p50 Speed-up
0.58 best 1 0.674 ms 0.658 ms 1.0×
0.58 top (3) 3 1.288 ms 0.797 ms 1.6×
0.58 all 14 3.196 ms 1.013 ms 3.2×
0.60 best 1 0.480 ms 0.475 ms 1.0×
0.60 top (3) 3 0.986 ms 0.612 ms 1.6×
0.60 all 14 2.858 ms 0.815 ms 3.5×
0.65 best 1 0.284 ms 0.292 ms 1.0×
0.65 top (3) 3 0.752 ms 0.395 ms 1.9×
0.65 all 14 2.638 ms 0.604 ms 4.4×

What the numbers say:

  • Single-best routing is sub-millisecond and flat. best mode pays only for the one routed store (0.28–0.67 ms), regardless of how many stores are in the federation. Add a hundred more domains and this number does not move.
  • Parallel fan-out earns its keep as you widen. With one store there's nothing to parallelize (≈1.0×); across all 14 it's 3.2–4.4× faster than searching them sequentially — exhaustive recall over the whole federation in ~0.6–1.0 ms.
  • Higher tau is faster. Fewer candidates clear the threshold, so latency drops as you tighten from 0.58 → 0.65.

Time it on your own stores

No repo required — this runs against the published wheel and your own directory of .sfg stores:

import time
from seraph import Federation, Encoder

fed = Federation.open_dir("my-federation")            # your directory of .sfg stores
enc = Encoder.from_pretrained("BAAI/bge-small-en-v1.5")
queries = [enc.encode(q) for q in ("first query", "second query", "third query")]

for mode in ("best", "top", "all"):
    samples = []
    for q in queries * 30:                            # warm + repeat for a stable p50
        t = time.perf_counter()
        fed.search(q, top_k=10, tau=0.60, mode=mode, n=3)
        samples.append((time.perf_counter() - t) * 1e3)
    samples.sort()
    print(f"{mode:5} p50 {samples[len(samples)//2]:6.3f} ms")

Your absolute numbers will differ with hardware, store sizes, and encoder — but the shape holds: best flat and sub-millisecond, all a few × that and bounded by your largest routed store.

Clone this wiki locally