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.

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 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
attrsAttribute map — the source of attribute facets
localeWhich language the document is for
enabled, updated_atState
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. 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?