Live demo → shelf.hevlayer.com
Book search that shows its routing. A micro-app over hev layer whose hero is the routing badge: it shows how the gateway searched, not just the results. Type an author, a title, or a vibe, and watch the same search box route the query to keyword, semantic, or a fusion of both, and say why.
shelf is a UX-shaped demo. Its sibling hybrid-text-fusion-demo
(SciFact) is eval-shaped: it proves hybrid retrieval with qrels and recall
numbers. shelf proves the query router: it makes the routing decision
legible on a corpus where the three routes have obviously different intents.
hev layer's Auto rank expression (RFC 0044)
picks a retrieval strategy from the shape of the query and returns the
decision alongside the results. shelf renders that decision as a badge.
| Query | Tokens | Route | Why |
|---|---|---|---|
Sanderson |
1 | hybrid_text | Short and keyword-shaped — BM25 + fuzzy over the text field, no vector needed. |
branden sandersn |
2 | hybrid_text | Same route; the fuzzy legs (RFC 0057) still find Brandon Sanderson through the typos. |
the name of the wind |
4 | fused | Mid-length — exact-title BM25 and semantic, merged by RRF. |
sprawling epic fantasy with morally grey characters and political intrigue |
10 | semantic | Long and natural-language — ANN over description embeddings. |
The v1 routing policy keys purely on token count: ≤2 → hybrid_text,
3–7 → fused, ≥8 → semantic. The four canned chips above are chosen to land
on each route so the badge visibly changes.
shelf reimplements nothing. Fusion, routing, and fuzzy matching all live in
the gateway; the app only composes them:
- Hybrid text fusion (RFC 0022) —
the
HybridTextexpansion: per-token fuzzy legs + a BM25 leg → RRF. - Query router (RFC 0044, phase 1) — the
Autoexpression and its routing decision block. - Fuzzy surfacing (RFC 0057) — typo'd queries still return rows.
- Facet snapshots — the genre rail is a materialized facet histogram with
visible provenance (corpus-wide counts, not a tally of the returned rows).
Declared on the namespace's
IndexCR; see Declarative config.
Wire shapes are authoritative in the gateway docs, not here: see the
Hybrid text fusion and Query routing sections of
api/query.mdx in the layer repo.
Pinned to Eitanli/goodreads
on the HuggingFace Hub: an MIT-licensed set of ~10k popular Goodreads books
across genres, at revision 622b9c6. The indexer loads it from the Hub at that pinned
revision and upserts through the gateway — the same source hev layer's
HuggingFace Warehouse kind
(RFC 0053)
reads declaratively in a full cluster deployment. The data is downloaded at run
time, never committed.
Fields used:
| Source field | Use |
|---|---|
Book |
title (series suffix in parens split into series) |
Author |
author — powers the Sanderson route |
Description |
semantic leg (embedded) |
Genres |
genre facet |
Avg_Rating |
rating facet |
Num_Ratings |
popularity facet / sort |
URL |
result link |
Being ~10k popular books is deliberate: the canned queries are guaranteed to hit recognizable titles, and it's cheap to index for a UX demo. The dataset carries no publication-year / page-count / language fields; if a richer facet rail is ever wanted, swap in the UCSD Book Graph "Fantasy & Paranormal" subset (258,585 books, full fields) under its academic terms via the same loader-not-redistribute pattern.
One hev search-backed namespace (shelf-books) behind the gateway:
| Attribute | Index | Role |
|---|---|---|
id |
key | book id |
text |
FTS + fuzzy | composed title + authors + description; the field Auto ranks over |
vector |
ANN (384-d) | embedding of title + ". " + description |
title, authors |
filterable (FTS) | display; reachable by a future field-aware router |
description |
— | display |
genres |
filterable | genre facet |
avg_rating |
filterable | rating facet |
num_ratings |
filterable | popularity facet / sort |
url |
— | result link |
Embedder: BAAI/bge-small-en-v1.5 (384-d, CPU-friendly). The same model embeds
the query at search time so semantic/fused routes resolve in one hop (see below).
Eitanli/goodreads (HF, pinned 622b9c6)
→ indexer/ CLI: load → embed (fastembed bge-small) → upsert + snapshot genres
→ search/app.py FastAPI (dev): embed query → Auto+vector → rows + routing + hybrid
→ src/worker.js Cloudflare Worker (prod): same, query embedding via Workers AI bge-small
→ web/static/ vanilla single-page UI: search, route badge, Routing inspector, genre rail
→ deploy/ declarative config: search VectorStore + in-cluster CR bundle
The genre rail loads from /api/facets (a snapshot read), separate from search.
Two backends, one UI — the same split as the SciFact demo (server.py +
src/worker.js). The FastAPI service is the local-dev/reference path (fastembed,
identical to the indexer's embeddings); the Cloudflare Worker is the production
deploy and embeds queries with Workers AI @cf/baai/bge-small-en-v1.5 — the same
model, with bge's query-instruction prefix applied so the vectors match the
index. Both inject the gateway key server-side and return the gateway's
routing/hybrid echo blocks; the UI renders them in the Routing inspector.
RFC 0044 phase 1 says the gateway never embeds. So for the semantic and
fused routes the app supplies the query vector itself: search/ embeds every
query with bge-small and sends the vector alongside the Auto expression, so
the router resolves in a single round trip on any route. The only cost is a
wasted embed on short keyword queries that route to hybrid_text and never use
the vector — exactly the inefficiency RFC 0044 phase 2 (gateway-side query
embedding, currently unscheduled) would remove. shelf lives happily on
phase 1; phase 2 is a footnote, not a blocker.
shelf runs the shipped single-field router over the composed text
field, which is why Sanderson works at all: the author's name is in text.
But a book that merely mentions Sanderson in its description ranks too. Real
catalogs have several text fields with different query intents (exact author /
exact title / semantic description) that token count alone can't disambiguate.
That gap is field-aware routing: the design question this corpus raises,
the way SciFact raised the fuzzy-leg ranking question that RFC 0057
resolved. shelf observes it; it does not solve it.
The fused route raises a second one. A fused row's aggregate RRF $score
doesn't tell you which leg found it: BM25, one of the fuzzy-token legs, or
semantic ANN. shelf opts in to Layer's $fused.legs response shape with
include_leg_breakdown: true, so each result can show the exact per-leg ranks
from the same fused request. That is enough to spot a semantic carry, a
keyword-only rider, or a row that both sides agreed on. The original finding
that drove the gateway work is preserved in
docs/fused-leg-attribution.md.
hev layer apps get a clean separation: what the data is and how a namespace
behaves live in config the operator reconciles, not in application code.
shelf runs against the shared deployed gateway, so it can't own that cluster —
it configures the gateway imperatively (schema on write, the dataset pin in
config.py, a POST /snapshots call after indexing). deploy/ is
the declarative equivalent: the in-cluster CR bundle that same setup would
be, with a one-to-one map back to the imperative paths (see
deploy/README.md). It's the same move the §Dataset section
already makes for the loader and the Warehouse kind. The manifests are
illustrative; the runtime paths they mirror are real.
This is where facet snapshots are declared. A snapshot in hev layer is a
materialized facet histogram — a field's distinct values and their counts,
written durably to S3. You turn one on with
Index.spec.snapshot.facetFields:
spec:
snapshot:
facetFields: [genres] # the genre rail; ratings are continuous, not faceted
interval: 5m
retention: neverThe gateway then re-materializes the genre histogram on each upstream-stable
advance, and the search backends read the latest body to draw the rail —
corpus-wide counts with the snapshot's sha and row_count shown as
provenance. Against the shared gateway the indexer does the same thing by
calling create_snapshot(field="genres", source="origin") once after upserting:
the imperative twin of the auto-writer the CR turns on. Wire shapes are
authoritative in the gateway docs — see
Snapshot History and the
Index CRD in the
layer repo.
uv sync --extra search # install deps
cp .env.example .env # add LAYER_GATEWAY_API_KEY (Layer inbound key)
uv run python -m indexer # populate shelf-books (~10k books); --dry-run to preview
uv run uvicorn search.app:app --reload # serve UI + API at http://127.0.0.1:8000After the shared gateway is repointed to kind: search, run the cutover gate:
uv run python scripts/validate_cutover.py --reindex-limit 500Production is a Cloudflare Worker (mirrors the SciFact demo): npm install, set
the key with wrangler secret put LAYER_API_KEY, then npm run deploy.
v1 scope: the routing showcase above, on the shipped gateway, ~10k books.
In v1: facet snapshots over the raw genres field power the genre rail (see
Declarative config).
Deliberately out of v1:
- UDF-minted facets. A genre/mood tagging Function over descriptions
(cleaner facets than the raw
Genrestags the snapshot histograms) is a v1.1 transform-runtime cameo. - Field-aware routing. Observed above; needs a gateway RFC, not demo code.
MIT; see LICENSE. The Goodreads data is MIT-licensed upstream and is downloaded at build time, not redistributed here.