Search
POST /v2/namespaces/{namespace}/search takes a query string and returns ranked,
scored rows. In one request, inside one namespace, the gateway:
- Embeds
queryfrom the schema’sembed:attribute, through that attribute’s serving preference. - Expands text legs over every
full_text_searchattribute: one BM25 leg per attribute, then fuzzy legs per token, under a 16-leg budget. - Adds the ANN leg over the derived vector column.
- Scatters every leg across the namespace’s shards under one stable-read
cut, with
filtersreplicated to every leg. - Fuses with reciprocal rank fusion (
rank_constant60), deduplicating by id. - Cuts the fused list to
poolcandidates (the L1 stage). - Reranks the pool with Jev: one yes/no relevance question per document, a calibrated probability back.
- Prunes below
rerank.thresholdand returnstop_krows with the probability asscore.
/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
}
| Field | Default | Meaning |
|---|---|---|
query | required | The search text. Embedded for the ANN leg, ranked as-is by the BM25 legs, tokenized for the fuzzy legs. |
top_k | 10 | Rows returned. 1..100, and at most pool. |
filters | none | Filter expression in the query grammar. A hard filter, replicated to every leg. |
include_attributes | true | true, 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. |
pool | 50 | Candidates handed to the reranker after the L1 cut. 50..200. Also sets per-leg depth: clamp(5 × pool, 50, 200). |
embed.attribute | the only one | Which 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. |
rerank | on | false disables the stage. Otherwise an object; every key is optional. |
rerank.provider | "jev" | The only provider today. |
rerank.threshold | 0.0 | Drop 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.attributes | every full-text attribute | Attributes whose text the reranker reads. An attribute named title goes first, then schema order. _hevlayer_* names are rejected. |
rerank.docs_per_call | 30 | Documents per provider call, 1..50. Calls run concurrently. |
rerank.max_chars | 2000 | Per-document character budget for reranker text, 1..20000. |
rerank.question | "generic-1" | Named question version. generic-1 is the only one. |
rerank.required | false | true turns a provider failure into 503 instead of a degrade. |
explain | false | true adds the L1 feature vector to every row. |
collapse | none | {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.
| Field | Meaning |
|---|---|
rows[].score | The 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[].attributes | The attributes include_attributes asked for. Text the gateway fetched only for the reranker is stripped. |
routing | The query router’s decision for this input. advisory: true: /search runs every leg regardless, and reports the route for the UI and the history. |
plan | The planner stage. Not configured in this release: always {"executed": false, "reason": "unconfigured"}. |
hybrid.legs | One 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_legs | Legs the 16-leg budget, or the store, did not run. |
hybrid.surfaced | true when every text leg returned nothing and the fuzzy legs were re-run ordered by edit distance, as in the HybridText fallback. |
hybrid.threads | Shard fan-out width. Present on sharded namespaces only. |
hybrid.fuzziness_clamped | Present and true only when a store forced the effective fuzziness to 0. |
rerank.executed | false 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.pool | Candidates scored. |
rerank.calls | Provider calls made. |
rerank.pruned | Rows dropped by threshold. |
rerank.input_tokens | Provider-reported input tokens across all calls. |
performance | Milliseconds 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:
- The ANN leg.
- One BM25 leg per full-text attribute, over the whole
query, in schema order. - Fuzzy legs from the
HybridTexttokenizer 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.
| Feature | Present |
|---|---|
rrf_sum | Always. |
age_seconds | When the row carries its write stamp. That is turbopuffer-backed namespaces; it is absent on hev search and pgvector. |
fetch_count_30d | Reserved. 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.
| Variable | Default | Meaning |
|---|---|---|
TYPESAFE_API_KEY | unset | Gateway-wide reranker key. |
LAYER_RERANK_PROVIDER | jev | Reranker provider. |
LAYER_RERANK_MODEL | jev-latest | Model name sent to the provider. The response echoes the model that answered. |
LAYER_RERANK_MAX_INFLIGHT | 6 | Provider calls in flight across the whole gateway. |
LAYER_RERANK_TIMEOUT_MS | 3000 | Per-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
| Status | error | When |
|---|---|---|
404 | not_found | The namespace does not exist. |
422 | embed_attribute_missing | The schema declares no embed: attribute. |
422 | embed_attribute_invalid | embed.attribute names an attribute without embed:, or the schema declares several and none is named. |
422 | full_text_attribute_missing | The schema has no full_text_search attribute. |
422 | rerank_unconfigured | The rerank stage is on and no key is set. |
422 | UnsupportedByStore | The store cannot serve an ANN or a BM25 leg. See Stores. |
422 | validation_error | top_k > pool, pool out of range, unknown provider or question, unknown field, a query that yields no tokens. |
503 | rerank_unavailable | The 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.
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.