Search

Full-text search, autocomplete and faceted filtering in a storefront — the composables and routes behind the search surfaces, and what the index contains.

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

SurfaceComposableBFF route
Query and resultsuseSearcha search route in the layer's BFF
Results as a listinguseSearchListingthe same, plus the listing state
Facet filtersuseFacetFiltersGET /api/products/facet-meta
Facet metadatauseFacetMetaGET /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:

app/app.config.ts
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, skuIdentity
name, descriptionText, and what full-text search matches against
manufacturer, eanAdditional identifiers buyers search by
categories, category_labelsCategory membership, and what category facets are built from
attrs, attrs_localeAttribute 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.
localeWhich write key the document is for (de in a tenant that keeps text per language). Send X-Revenexx-Locale to search one.
enabled, updated_atState

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.

Prices, stocks and images are not in the index yet. The layer's mapping fills those contracts with neutral defaults so every surface renders, but a result card will show a placeholder image and no price until the index carries them. Price and availability resolve on their own axis through 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

Was this page helpful?