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.
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, 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 | Attribute map — the source of attribute facets |
locale | Which language the document is for |
enabled, updated_at | State |
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
- 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.