API

Backend

Search

POST /v2/namespaces/{namespace}/search takes a query string and returns ranked, scored rows. In one request, inside one namespace, the gateway:

  1. Embeds query from the schema’s embed: attribute, through that attribute’s serving preference.
  2. Expands text legs over every full_text_search attribute: one BM25 leg per attribute, then fuzzy legs per token, under a 16-leg budget.
  3. Adds the ANN leg over the derived vector column.
  4. Scatters every leg across the namespace’s shards under one stable-read cut, with filters replicated to every leg.
  5. Fuses with reciprocal rank fusion (rank_constant 60), deduplicating by id.
  6. Cuts the fused list to pool candidates (the L1 stage).
  7. Reranks the pool with Jev: one yes/no relevance question per document, a calibrated probability back.
  8. Prunes below rerank.threshold and returns top_k rows with the probability as score.

/query is unchanged and stays the turbopuffer-shaped wire. /search is Layer-native: text in, scored rows out. The namespace must declare an embed: attribute and at least one full_text_search attribute, and the gateway must hold a reranker key (see Provider key).

response = await client.search_namespace("papers", {
    "query": "does vitamin D improve bone density in older adults",
    "top_k": 10,
})
response, err := client.SearchNamespace(ctx, "papers", &hevlayer.SearchRequest{
    Query: "does vitamin D improve bone density in older adults",
    TopK:  10,
})
const response = await client.searchNamespace("papers", {
  query: "does vitamin D improve bone density in older adults",
  top_k: 10,
});
curl -X POST "$LAYER_GATEWAY_URL/v2/namespaces/papers/search" \
  -H "Authorization: Bearer $LAYER_GATEWAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "does vitamin D improve bone density in older adults", "top_k": 10}'

Request

Only query is required. Unknown fields are rejected with 422.

{
  "query": "does vitamin D improve bone density in older adults",
  "top_k": 10,
  "filters": ["year", "Gte", 2015],
  "include_attributes": ["title", "text", "year"],
  "pool": 50,
  "embed": { "attribute": "text" },
  "text": { "fuzziness": "auto", "stopwords": "en" },
  "rerank": {
    "provider": "jev",
    "threshold": 0.0,
    "attributes": ["title", "text"],
    "docs_per_call": 30,
    "max_chars": 2000,
    "question": "generic-1",
    "required": false
  },
  "explain": false
}
FieldDefaultMeaning
queryrequiredThe search text. Embedded for the ANN leg, ranked as-is by the BM25 legs, tokenized for the fuzzy legs.
top_k10Rows returned. 1..100, and at most pool.
filtersnoneFilter expression in the query grammar. A hard filter, replicated to every leg.
include_attributestruetrue, false, or a list of attribute names to return under each row’s attributes. Reserved _hevlayer_* attributes are returned only when named; vector columns are never returned.
pool50Candidates handed to the reranker after the L1 cut. 50..200. Also sets per-leg depth: clamp(5 × pool, 50, 200).
embed.attributethe only oneWhich embed: attribute to embed query with. Required when the schema declares several.
text.fuzziness"auto""auto" or 0..2, as in HybridText.
text.stopwords"en""en", false, or a list. Suppresses fuzzy legs for stop words; BM25 legs are unaffected.
rerankonfalse disables the stage. Otherwise an object; every key is optional.
rerank.provider"jev"The only provider today.
rerank.threshold0.0Drop rows whose probability is below this, before top_k. 0.1 is the recommended prune: on BEIR shortlists rows under 0.1 were judged relevant about 0.5% of the time.
rerank.attributesevery full-text attributeAttributes whose text the reranker reads. An attribute named title goes first, then schema order. _hevlayer_* names are rejected.
rerank.docs_per_call30Documents per provider call, 1..50. Calls run concurrently.
rerank.max_chars2000Per-document character budget for reranker text, 1..20000.
rerank.question"generic-1"Named question version. generic-1 is the only one.
rerank.requiredfalsetrue turns a provider failure into 503 instead of a degrade.
explainfalsetrue adds the L1 feature vector to every row.
collapsenone{by, expansion?, missing?}: one result per group, such as per document. See Collapse.

There is no pagination: a reranked page does not compose with a fused cursor. Ask for a larger top_k, up to pool.

Response

{
  "rows": [
    {
      "id": "PMC5461234",
      "score": 0.93,
      "attributes": { "title": "...", "text": "...", "year": 2019 }
    }
  ],
  "routing": { "route": "fused", "policy": "v1", "tokens": 10, "executed": true, "advisory": true },
  "plan": { "executed": false, "reason": "unconfigured" },
  "hybrid": {
    "tokens": ["vitamin", "improve", "bone", "density", "older", "adults"],
    "tokens_dropped": 0,
    "stopwords": "en",
    "stopwords_dropped": ["does", "in"],
    "fuzziness": "auto",
    "rank_constant": 60,
    "per_leg_limit": 200,
    "legs": [
      { "label": "ann", "kind": "ann", "attribute": "text", "rows": 200 },
      { "label": "bm25:text", "kind": "bm25", "attribute": "text", "rows": 143 },
      { "label": "bm25:title", "kind": "bm25", "attribute": "title", "rows": 61 },
      { "label": "fuzzy:text:vitamin", "kind": "fuzzy", "attribute": "text", "rows": 88 }
    ],
    "dropped_legs": 0,
    "surfaced": false
  },
  "rerank": {
    "provider": "jev",
    "model": "jev-1.13.0",
    "question": "generic-1",
    "executed": true,
    "pool": 50,
    "calls": 2,
    "docs_per_call": 30,
    "threshold": 0.0,
    "pruned": 0,
    "input_tokens": 41200,
    "latency_ms": 231
  },
  "performance": {
    "embedding_tokens": 17,
    "embedding_ms": 4,
    "legs_ms": 88,
    "fuse_ms": 1,
    "l1_ms": 0,
    "rerank_ms": 231,
    "total_ms": 340
  }
}

Every response carries the four echo blocks: routing, plan, hybrid, rerank.

FieldMeaning
rows[].scoreThe reranker’s probability in [0, 1] when rerank.executed is true. It is absolute: comparable across rows, legs, shards and requests, so you can threshold on it. When the stage did not execute, score is the RRF sum and is only an ordering. The two are never mixed in one response.
rows[].attributesThe attributes include_attributes asked for. Text the gateway fetched only for the reranker is stripped.
routingThe query router’s decision for this input. advisory: true: /search runs every leg regardless, and reports the route for the UI and the history.
planThe planner stage. Not configured in this release: always {"executed": false, "reason": "unconfigured"}.
hybrid.legsOne entry per leg that ran, in fusion order: label, kind (ann, bm25, fuzzy), the attribute it ran over, and the rows it returned after the shard merge.
hybrid.dropped_legsLegs the 16-leg budget, or the store, did not run.
hybrid.surfacedtrue when every text leg returned nothing and the fuzzy legs were re-run ordered by edit distance, as in the HybridText fallback.
hybrid.threadsShard fan-out width. Present on sharded namespaces only.
hybrid.fuzziness_clampedPresent and true only when a store forced the effective fuzziness to 0.
rerank.executedfalse with a reason when the stage was disabled (disabled), had nothing to score (empty_pool), or the provider failed (provider_error, timeout, rate_limited), or the call never got an in-flight slot (queue_timeout).
rerank.poolCandidates scored.
rerank.callsProvider calls made.
rerank.prunedRows dropped by threshold.
rerank.input_tokensProvider-reported input tokens across all calls.
performanceMilliseconds per stage, plus the embed wire’s token count.

The response also carries traceparent and, when a stable-read cut applied, x-layer-stable-as-of.

Leg budget

A request runs at most 16 legs. The budget is spent in this order:

  1. The ANN leg.
  2. One BM25 leg per full-text attribute, over the whole query, in schema order.
  3. Fuzzy legs from the HybridText tokenizer policy, one per token, round-robin across attributes, until the budget is spent.

A namespace with many full-text attributes spends its budget on BM25 breadth rather than fuzzy depth. Whatever did not fit is counted in hybrid.dropped_legs.

Explain

With explain: true each row gains an explain object: the feature vector the L1 stage saw and each term’s contribution to the L1 score.

{
  "id": "PMC5461234",
  "score": 0.93,
  "attributes": { "title": "..." },
  "explain": {
    "features": { "rrf_sum": 0.0481, "age_seconds": 86400 },
    "contributions": { "rrf_sum": 0.0481, "age_seconds": 0.0 },
    "l1_score": 0.0481,
    "legs": [
      { "label": "ann", "rank": 3, "score": 0.18 },
      { "label": "bm25:text", "rank": 1, "score": 14.2 }
    ]
  }
}

The L1 stage in this release is the identity: it keeps RRF order and cuts to pool, so every contribution other than rrf_sum is 0. An absent feature is absent, not 0.

FeaturePresent
rrf_sumAlways.
age_secondsWhen the row carries its write stamp. That is turbopuffer-backed namespaces; it is absent on hev search and pgvector.
fetch_count_30dReserved. This release keeps no fetch counter, so it is never returned.

Collapse

collapse returns top_k groups, such as documents, instead of top_k pages:

{ "query": "kitchen remodel", "top_k": 10, "collapse": { "by": "document" } }

The reranker scores pages first, then the gateway groups the reranked pages, so a group ranks by its best page’s score. rows keeps each group’s best page; groups lists every reranked page of each group, best first.

To have pages to group, the pool grows to top_k × expansion (default expansion is 10), up to the 200-page ceiling. That is the same single retrieval, with a deeper pool and more pages to rerank: no query runs per group or per page. collapse.read is the pool size and collapse.exhausted is true when retrieval returned fewer pages than the pool, so there are no more groups to find. by, expansion and missing mean what they do in collapse on a ranked query. Name the grouping attribute in include_attributes to see it on each page.

Provider key

The reranker runs on your TypeSafe account. Set TYPESAFE_API_KEY on the gateway. With the stage on and no key, the request fails with 422 rerank_unconfigured: a missing key is never a silent fused-only response.

VariableDefaultMeaning
TYPESAFE_API_KEYunsetGateway-wide reranker key.
LAYER_RERANK_PROVIDERjevReranker provider.
LAYER_RERANK_MODELjev-latestModel name sent to the provider. The response echoes the model that answered.
LAYER_RERANK_MAX_INFLIGHT6Provider calls in flight across the whole gateway.
LAYER_RERANK_TIMEOUT_MS3000Per-call deadline, retries included. It starts once the call holds an in-flight slot. Time queued for a slot is separate, bounded by the same value, and reported as queue_timeout.

Degrade

If the provider errors, times out, or keeps answering 429, or a call waits out its queue allowance for an in-flight slot, the response is still 200: rows in fused order, score as the RRF sum, rerank.executed: false with a reason, and the header x-layer-warning: rerank_degraded. Set rerank.required: true to get 503 rerank_unavailable instead. The 503 message names the provider and its HTTP status; the provider’s response body is never relayed. Calls are all-or-nothing: if one of several fails, no row keeps a probability.

Errors

StatuserrorWhen
404not_foundThe namespace does not exist.
422embed_attribute_missingThe schema declares no embed: attribute.
422embed_attribute_invalidembed.attribute names an attribute without embed:, or the schema declares several and none is named.
422full_text_attribute_missingThe schema has no full_text_search attribute.
422rerank_unconfiguredThe rerank stage is on and no key is set.
422UnsupportedByStoreThe store cannot serve an ANN or a BM25 leg. See Stores.
422validation_errortop_k > pool, pool out of range, unknown provider or question, unknown field, a query that yields no tokens.
503rerank_unavailableThe provider failed and rerank.required is true.

Stores

/search executes on turbopuffer, where reranker text is returned by the store and billed as returned bytes. A store that cannot serve an ANN or a BM25 leg answers 422 UnsupportedByStore.

On a pgvector-backed namespace /search runs the BM25 + dense subset: one BM25 leg per full-text attribute and no fuzzy legs. The fuzzy legs are counted in hybrid.dropped_legs.

One-call search (embed, text legs, RRF, rerank): approximate — BM25 + dense subset: one BM25 leg per full-text attribute, no fuzzy legs (reported in hybrid.dropped_legs).

Data leaves the environment

Candidate text is sent to TypeSafe for the rerank, and query is sent to the embedding provider when the embed: attribute serves natively on turbopuffer rather than locally. rerank: false removes the first. Reserved _hevlayer_* attributes are never a text leg and never reranker input.

History and metrics

Each request writes one search history entry with raw_query taken from the body, so no x-hevlayer-search-query header is needed. Metrics: hevlayer_search_total, hevlayer_search_stage_seconds, hevlayer_rerank_documents_total, hevlayer_rerank_pruned_total, hevlayer_rerank_tokens_total, hevlayer_rerank_degraded_total.

esc