Search and suggestions
Two calls cover search: products.list with q
for a results page, and suggest for the dropdown under a search box.
Search results
A search results page is a product list with q (up to 200 characters). It
matches product titles, brands, product types, Google categories, public attribute values and descriptions,
plus SKUs, GTINs and MPNs. With a locale, it also matches the
locale's translated titles and descriptions.
-
Everything else on
products.listcombines with it: filters and facets (with counts for the search), a category or collection, paging and fields. -
Without a
sort, results come byrelevance. Any other sort works too. -
Relevance pages stop at 1,000 products deep: past that the page is empty and
page.cappedistrue. Shoppers rarely go that far; offer filters instead. - The answer's
applied.qechoes the search.
import { createCatalogClient } from '@usethrottle/catalog';
const catalog = createCatalogClient({
publishableKey: 'pc_pk_your_browser_key',
});
// A search results page: the same list call, with `q`. Filters, facets,
// categories and paging all combine with it.
const results = await catalog.products.list({
q: 'ceramic brake pads',
filters: { brand: ['Bosch'] },
facets: true,
limit: 24,
});
// Without a `sort`, results come by relevance.
console.log(results.sort, results.page.total, results.applied.q);
Keep the search in the page URL with the URL helpers; q is part of the shop query they read and write.
Suggestions
suggest(q, { limit }) (GET /v1/storefront/suggest)
answers what matches the start of what the shopper typed. q must be 2 to 100
characters; anything else answers 400 INVALID_REQUEST.
| Key | Holds | How many |
|---|---|---|
products | handle, title, images, priceRange and availability, matched on title, brand and product type | Up to limit: 6 by default, at most 8 |
categories | handle, name and productCount | Up to 5 |
brands | name and productCount | Up to 5 |
tags | tag and productCount | Up to 5 |
collections | handle, title and productCount | Up to 5 |
Categories, brands, tags and collections match by case-insensitive prefix and come most products first. Every
name comes from public products only, so a draft title, a hidden attribute or an archived brand never shows
up. Suggestions take the client's currency and locale. They always show list prices: a suggestion request never carries the
customer token, so a signed-in shopper's group prices (see Customer prices) appear on product
pages and cards, not here.
import { createCatalogClient } from '@usethrottle/catalog';
const catalog = createCatalogClient({
publishableKey: 'pc_pk_your_browser_key',
});
// `q` must be 2 to 100 characters; a shorter one answers 400 INVALID_REQUEST.
const suggestions = await catalog.suggest('bre', { limit: 6 });
for (const product of suggestions.products) {
console.log('product', product.handle, product.title, product.priceRange);
}
for (const category of suggestions.categories) {
console.log('category', category.handle, category.name, category.productCount);
}
for (const brand of suggestions.brands) console.log('brand', brand.name);
for (const tag of suggestions.tags) console.log('tag', tag.tag);
for (const collection of suggestions.collections) {
console.log('collection', collection.handle, collection.title);
}
One request per keystroke pause. Without keyInQuery the answer
is cached under the guest partition. With keyInQuery: true (1.2.0 and later),
suggest bypasses the SDK's ETag cache and sends no If-None-Match: the
request carries only accept plus any client headers you set, so it needs no preflight (a client with custom headers preflights again). See Keys and channels.
Link each kind where it belongs: a product to its page, a category or collection to its page, a tag to products.list({ tags: [tag] }), and a brand to a shop page filtered by
your brand filter (its key is whatever the merchant named it in Storefront › Filters).
Search as you type in React
useSuggest(q, { debounceMs, limit }) from @usethrottle/catalog/react sends no request while the trimmed text is shorter
than 2 characters, and one request per pause in typing: debounceMs, 200 ms by
default. Answers are cached in the provider, so typing back to an earlier text shows its suggestions at once.
Like every hook, it answers { data, dataFor, error, isLoading, isPreviousData, isStale, isValidating, refetch }; with keepPreviousData: true the last suggestions stay while the next ones load, and dataFor.q is the text they answer.
'use client';
import { useState } from 'react';
import { useSuggest } from '@usethrottle/catalog/react';
// Inside a <CatalogProvider>. No request below 2 characters, and one request
// per pause in typing (200 ms by default).
export function SearchBox() {
const [q, setQ] = useState('');
const { data, isLoading } = useSuggest(q, { debounceMs: 250, limit: 5 });
return (
<form action="/search">
<input
name="q"
value={q}
onChange={(event) => setQ(event.target.value)}
autoComplete="off"
/>
{isLoading ? <p>Searching…</p> : null}
<ul>
{data?.products.map((product) => (
<li key={product.handle}>
<a href={`/products/${product.handle}`}>
{product.title ?? product.handle}
</a>
</li>
))}
{data?.categories.map((category) => (
<li key={category.handle}>
<a href={`/categories/${category.handle}`}>{category.name}</a>
</li>
))}
</ul>
</form>
);
}
Suggestions have their own rate limit for browser, app and legacy keys: 600 requests a minute per visitor, twice the limit of the other routes, so a fast typist is not cut off. The React page covers the provider and the cache.