API
Federated query
A federated query runs one query across a set of namespaces.
POST /v2/query is namespace-less: the per-namespace
query endpoint names its namespace in the path, while this
endpoint takes the set in the body, so the namespace is no longer a path
parameter. It has no upstream equivalent — turbopuffer’s multi-query is
single-namespace, so the fan-out and the merge are a Layer composition over
per-namespace reads.
One ranking is spread across every namespace in the set and merged into a single ranked list — “search my whole feed / my whole library.” For several independent rankings instead, query each namespace separately; a federated query always returns one fused list.
Fan-out and fuse
Supply one ranking and a namespaces set. The gateway runs the ranking against
each namespace and returns one merged rows list, each row tagged with the
namespace it came from.
response = await client.query({
"namespaces": ["moment-pod-changelog", "moment-pod-latent-space", "moment-pod-no-priors"],
"rank_by": ["text", "Auto", "evaluating RAG systems"],
"top_k": 12,
"filters": ["published_at", "Gte", 1740000000],
"include_attributes": ["text", "show", "source_url", "start_sec"],
})response, err := client.Query(ctx, &hevlayer.FederatedQueryRequest{
Namespaces: []string{"moment-pod-changelog", "moment-pod-latent-space", "moment-pod-no-priors"},
RankBy: []any{"text", "Auto", "evaluating RAG systems"},
TopK: 12,
Filters: []any{"published_at", "Gte", 1740000000},
IncludeAttributes: []string{"text", "show", "source_url", "start_sec"},
})const response = await client.query({
namespaces: ["moment-pod-changelog", "moment-pod-latent-space", "moment-pod-no-priors"],
rank_by: ["text", "Auto", "evaluating RAG systems"],
top_k: 12,
filters: ["published_at", "Gte", 1740000000],
include_attributes: ["text", "show", "source_url", "start_sec"],
});curl -X POST "$LAYER_GATEWAY_URL/v2/query" \
-H "Authorization: Bearer $LAYER_GATEWAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"namespaces": ["moment-pod-changelog", "moment-pod-latent-space", "moment-pod-no-priors"],
"rank_by": ["text", "Auto", "evaluating RAG systems"],
"top_k": 12,
"filters": ["published_at", "Gte", 1740000000],
"include_attributes": ["text", "show", "source_url", "start_sec"]
}' rank_by accepts the same vocabulary as a single-namespace query, including the
Layer HybridText and
Auto expressions. The query text is identical
across the fan-out, so Auto routes once and the chosen route runs against
every namespace — a single routing block is echoed. filters apply to every
namespace.
Name the explicit set to search, or pass namespaces: ["*"] to expand the
authenticated key’s namespace allowlist at request time. Omitting namespaces
is equivalent to ["*"].
Response
One fused list. Each row carries $namespace (its origin) and $rank (its
position within that namespace’s results — the key the merge orders on). A
merge block names the strategy, and a namespaces block reports each
namespace’s freshness and how many rows it returned to the merge.
{
"rows": [
{
"id": "ep123#7",
"$namespace": "moment-pod-latent-space",
"$rank": 1,
"$score": 11.4,
"show": "Latent Space",
"text": "..."
}
],
"merge": { "method": "rank-interleave", "route": "fused" },
"routing": { "route": "fused", "policy": "v1", "tokens": 3, "executed": true },
"hybrid": { "tokens": ["evaluating", "rag", "systems"], "tokens_dropped": 0, "fuzziness": "auto", "rank_constant": 60, "legs": 4, "per_leg_limit": 60 },
"namespaces": [
{ "namespace": "moment-pod-latent-space", "stable_as_of": 1747300000123, "matched": 14 },
{ "namespace": "moment-pod-changelog", "stable_as_of": 1747299999001, "matched": 9 },
{ "namespace": "moment-pod-no-priors", "stable_as_of": 1747300000050, "matched": 0 }
]
}
| Field | Meaning |
|---|---|
$namespace | The namespace this row came from. |
$rank | The row’s 1-based rank within its namespace. The merge orders on this on a text route; see Merge. |
$score / $dist | The row’s native score within its namespace, carried as provenance. Comparable within a namespace, not across the fused list — do not threshold on it. On a vector route $dist is the merged ordering key and is globally comparable. |
merge | The merge applied. method is "distance" on a vector route, "rank-interleave" on a text route; route is currently always "fused" (reserved for future route-specific provenance). |
namespaces | Per-namespace echo: each reached namespace’s stable_as_of watermark and matched — the rows it returned to the merge (up to per_namespace_limit), not its surviving share of the final top_k. A namespace that matched nothing reports matched: 0. |
routing and hybrid echo exactly as they do for a single-namespace
Auto / HybridText
query, since the route runs once for the whole fan-out.
Fusion options
| Option | Default | Meaning |
|---|---|---|
fusion.per_namespace_limit | clamp(2 × top_k, 10, 100) | How many rows each namespace returns to the merge. Shallower than the single-namespace per_leg_limit: across a wide fan-out most namespaces contribute nothing to the final top_k, so deep per-namespace retrieval is wasted. Integer > 0. |
fusion.rank_constant | 60 | Reserved for route-dependent fusion tuning. It is accepted for forward compatibility and is inert in the current gateway. |
Entitlements
The namespaces set uses the same store-derived authentication path as
individual namespace queries: the upstream key must be able to read each listed
namespace. A namespace read failure is reported under
partial failure unless strict is enabled.
For minted scoped keys, explicitly named namespaces must be inside a
vectorstore.<name> read grant. Naming a namespace outside the grant is a hard
403, not a partial result. namespaces: ["*"] expands by listing upstream
namespaces and filtering them through the key’s vectorstore namespace globs, so
the key is the feed/library set and the client does not enumerate it.
Filters
One filters expression is applied to every namespace in the set, and each
namespace evaluates it independently against its own schema. So a filter only
behaves uniformly when the attributes it references are part of a shared
filterable schema across the set — present in every namespace, with the same
type, and declared filterable. This is the filter analog of the
shared embedding space a
vector merge needs: a federated query is only as coherent as the contract its
namespaces share.
A namespace that cannot evaluate the filter — the attribute is absent, has an
incompatible type, or is not filterable there — is reported as
filter_schema_mismatch, not as matched: 0. This is a per-namespace validity
failure: the namespace did not participate in the filtered ranking because it
could not evaluate the predicate.
In default best-effort mode, the namespace is omitted from rows, listed in
errors, and the response carries x-layer-partial: true. With strict: true,
the whole request fails with 422. This docs contract selects 422 for the
strict-mode failure because the namespace set and filter expression are valid
only if every required namespace can evaluate the filter.
A feed or library assembled from namespaces indexed by one pipeline shares a filterable schema by construction, so this is invisible in the common case. The contract bites when a set spans heterogeneously-indexed namespaces — keep a filter to attributes the whole set declares, or scope the set to namespaces that share them.
Merge
A federated query merges by the quantity the route makes comparable across namespaces:
- Vector route (
ANNover a query vector) — distances are comparable across namespaces that share an embedding space, so the gateway merges by$distand the fused order is the exact global nearest-neighbor ranking.merge.methodis"distance". - Text route (
BM25,HybridText, or a routedfused/hybrid_text) — BM25 and hybrid scores are not comparable across corpora (different term statistics; a hybrid$scoreis already a per-namespace fusion). The gateway merges by rank-interleave: rows are ordered by their$rankwithin their namespace, ties broken byid.merge.methodis"rank-interleave". No single pod dominates the head of the list; each contributes its best matches in rank order. The fused list exposes$rankas the ordering key and carries each row’s native$scoreonly as provenance.
top_k truncates the merged list. Each namespace returns
fusion.per_namespace_limit rows to the merge.
Vector merge requires a matching embedding space
Merging by distance is only meaningful when every namespace in the set embeds into
the same geometry. Declare each namespace’s embedding identity with
spec.embedding on its Index — model,
output dimension, and normalization, alongside the existing
spec.backend.distanceMetric.
The gateway compares those profiles before a vector fan-out uses distance merge:
- All reached namespaces have the same profile →
merge.method: "distance". - A profile is missing or differs → best-effort responses downgrade to
merge.method: "rank-interleave"and includemerge.downgraded_reason("missing_embedding_profile"or"embedding_profile_mismatch"). - With
strict: true, a missing or mismatched profile is a422.
A text fan-out is rank-interleave regardless, so embedding profiles do not affect it.
Consistency
Each namespace has its own stable-read watermark;
there is no single consistent cut across independent namespaces. The namespaces
block reports each namespace’s stable_as_of, and the response header
x-layer-stable-as-of carries the minimum across the reached namespaces — the
most conservative answer to “as of when.” Layer does not manufacture a global
watermark.
Partial failure
Across many namespaces, one may be deleted, time out, error upstream, or fail to
evaluate the filter. Unlike single-namespace
multi-query, the fan-out defaults to
best-effort: it returns the namespaces that succeeded, lists the rest in an
errors block, and sets x-layer-partial: true. A feed search does not blank
because one pod is briefly unavailable.
{
"rows": [ "..." ],
"errors": [
{ "namespace": "moment-pod-no-priors", "error": "Upstream error: namespace not found" }
]
}
Availability failures carry a human-readable upstream message in error, not a
stable machine code — match on namespace, not on the error string.
Filter schema mismatches are the exception: they carry the stable
code: "filter_schema_mismatch" plus a human-readable detail so clients can
separate “namespace unavailable” from “namespace cannot evaluate this filter.”
{
"rows": [
{ "id": "ep123#7", "$namespace": "moment-pod-latent-space", "$rank": 1 }
],
"errors": [
{
"namespace": "moment-pod-no-priors",
"code": "filter_schema_mismatch",
"error": "filter schema mismatch",
"detail": "filter attribute published_at is absent, not filterable, or has an incompatible type"
}
]
}
Set "strict": true to opt into fail-fast: any namespace error fails the whole
request. Reach for it when a missing namespace would make the result misleading
rather than merely thinner.
Limits
| Limit | Value |
|---|---|
| Namespaces per request | 512. Over the cap → 422 naming the excess. |
| Pagination | Not supported. cursor is rejected — a fused cursor across independent namespaces does not form the monotone bands pagination relies on. |
Validation
| Condition | Status |
|---|---|
| A named namespace is outside the minted key’s namespace grant | 403 |
| A namespace read fails upstream | 200 with errors, or 502 with strict: true |
A namespace cannot evaluate filters (attribute absent, wrong type, or not filterable) | 200 with errors[].code: "filter_schema_mismatch" and x-layer-partial: true, or 422 with strict: true |
namespaces is empty | 422 |
namespaces: ["*"] mixed with other names | 422 |
namespaces set exceeds the cap | 422 |
Vector route missing/mismatched embedding profiles with strict: true | 422 |
cursor present | 422 |
rank_by expression invalid | 422 (same checks as a single-namespace query) |