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 }
  ]
}
FieldMeaning
$namespaceThe namespace this row came from.
$rankThe row’s 1-based rank within its namespace. The merge orders on this on a text route; see Merge.
$score / $distThe 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.
mergeThe 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).
namespacesPer-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

OptionDefaultMeaning
fusion.per_namespace_limitclamp(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_constant60Reserved 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 (ANN over a query vector) — distances are comparable across namespaces that share an embedding space, so the gateway merges by $dist and the fused order is the exact global nearest-neighbor ranking. merge.method is "distance".
  • Text route (BM25, HybridText, or a routed fused/hybrid_text) — BM25 and hybrid scores are not comparable across corpora (different term statistics; a hybrid $score is already a per-namespace fusion). The gateway merges by rank-interleave: rows are ordered by their $rank within their namespace, ties broken by id. merge.method is "rank-interleave". No single pod dominates the head of the list; each contributes its best matches in rank order. The fused list exposes $rank as the ordering key and carries each row’s native $score only 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 include merge.downgraded_reason ("missing_embedding_profile" or "embedding_profile_mismatch").
  • With strict: true, a missing or mismatched profile is a 422.

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

LimitValue
Namespaces per request512. Over the cap → 422 naming the excess.
PaginationNot supported. cursor is rejected — a fused cursor across independent namespaces does not form the monotone bands pagination relies on.

Validation

ConditionStatus
A named namespace is outside the minted key’s namespace grant403
A namespace read fails upstream200 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 empty422
namespaces: ["*"] mixed with other names422
namespaces set exceeds the cap422
Vector route missing/mismatched embedding profiles with strict: true422
cursor present422
rank_by expression invalid422 (same checks as a single-namespace query)
esc