Search
Search is a first-class surface in a B2B storefront, because buyers who know their SKU don't browse. The base layer ships the whole thing: a search input with autocomplete, a results listing with facets, and the blocks that render both.
What backs it
| Surface | Composable | BFF route |
|---|---|---|
| Query and results | useSearch | a search route in the layer's BFF |
| Results as a listing | useSearchListing | the same, plus the listing state |
| Facet filters | useFacetFilters | GET /api/products/facet-meta |
| Facet metadata | useFacetMeta | GET /api/products/facet-meta |
Everything goes through your own origin. The platform's search surface is reached from the server route, never from the browser — see The BFF pattern.
The blocks that render these are search, listing_search_header, listing_results, listing_filters, listing_filters_bar, listing_active_filters and listing_toolbar. See the block catalog.
Autocomplete
The search input suggests as the buyer types, with a threshold so it doesn't fire on every keystroke. The threshold is configuration, not code:
export default defineAppConfig({
search: {
autocompleteMinChars: 2,
},
})
Two is a reasonable default for SKU-heavy catalogs, where a buyer's first two characters are often already discriminating. Raise it for consumer-style catalogs where short prefixes match everything.
Typo tolerance is per query, not per field
POST /v1/search/query takes no relevance settings in the body: typo tolerance comes from the view, which the tenant tunes in Cockpit. The lower-level multi_search and per-collection search endpoints take num_typos — how many typos are tolerated per query token. It applies to the whole query, across every field searched, so num_typos=0 switches typo tolerance off for that one query. The public contract gives num_typos one value for the whole query, with no documented per-field form (as of 2026-10), so don't design around SKUs exact and names fuzzy in one query. When a buyer types something that looks like a SKU, have your server route send that query with num_typos=0.
Faceted filtering
A listing's filters come from facet metadata — for each filterable attribute, the values present in the current result set and how many results each has. That's what lets a filter panel grey out a value with no matches instead of offering a dead end.
useFacetMeta fetches the metadata; useFacetFilters holds which filters are applied and reflects them into the query. The pieces you compose:
listing_filters— the full filter panel, for a sidebar.listing_filters_bar— the same filters as a horizontal bar, for narrow layouts.listing_active_filters— the applied-filter chips, so a buyer can see and undo what's narrowing their results.listing_toolbar— sort order, view mode, page size.
Include the active-filter chips. A buyer looking at three results and not knowing why is the most common complaint about faceted search, and the chips are the entire fix.
What the index contains
When the catalog is on live data, product reads resolve against the platform's search index, exposed publicly at /v1/search/*. The query endpoint, its X-Revenexx-Locale and X-Revenexx-Channel headers and the response shape are on Search collections. The document carries the identifying and descriptive fields:
| Field | |
|---|---|
id, entity_id, sku | Identity |
name, description | Text, and what full-text search matches against |
manufacturer, ean | Additional identifiers buyers search by |
categories, category_labels | Category membership, and what category facets are built from |
attrs, attrs_locale | Attribute maps, carried for display and not full-text searched. Attributes marked filterable are also facet fields attrs.<code>, typed as numbers or booleans where the attribute says so. |
locale | Which write key the document is for (de in a tenant that keeps text per language). Send X-Revenexx-Locale to search one. |
enabled, updated_at | State |
The index facets on kind, enabled, manufacturer and categories, and on every attribute marked filterable as attrs.<code>. The search, listing and category views return those facets without further setup.
useOffers — see Catalog, cart and checkout.The practical consequence for search specifically: you cannot facet or sort on price or stock from the index. An attribute such as voltage can be a facet once it is marked filterable; price and stock cannot. If a customer's requirement is "filter by price range", say so during scoping rather than discovering it during implementation.
Search results are a listing
useSearchListing deliberately produces the same state shape as useCategoryListing, so a search-results page and a category page are the same components with a different query source. That's worth preserving when you customize: an override that improves the result card improves both surfaces.
The difference is only in the header — listing_search_header shows the query and the result count, listing_header shows the category.
Multi-tenant note
Search results are per tenant, like everything else. If you cache a query's results or its facet metadata, put the tenant in the key — a shared cache of "results for pump" is a cross-tenant leak. See Multi-tenant themes.
Next steps
- Search collections —
POST /v1/search/query, headers, filters, the response, suggestions and events. - Catalog, cart and checkout — the catalog surfaces search shares components with.
- The BFF pattern — why the search call goes out from a server route.
- Block catalog — the listing and filter blocks.
- Service modes — search against fixtures versus the live index.