Integrate a storefront
What you build
A shop search has three parts, and each one is a single API call:
- The search results page calls
POST /v1/search/<profileKey>with the query, the selected filters, the sort and the page. - The facets next to the results come back in the same answer. A click on a facet value adds a filter and searches again.
- The autocomplete box calls
POST /v1/suggest/<suggestKey>on every keystroke and shows the suggestions.
Before you start, you need an index with documents, a search profile with its profileKey, and the index's suggestKey, which the cabinet shows on the index's Autocomplete page. Quickstart covers the first two.
Call the API from the browser
Create a public key (zs_pk_…) on the project's API keys page and list your shop's domains under Allowed origins, for example shop.example.com and *.shop.example.com. A public key can only search and suggest, so it is safe in frontend code. The browser sends the page's Origin header with every call, and the API answers only the origins on the key's list; any other site gets 403 ORIGIN_NOT_ALLOWED. See API keys.
const API = 'https://api.zaproo-search.com/v1';
const KEY = 'zs_pk_...'; // public key, allowed on https://shop.example.com
const PROFILE_KEY = '...'; // the search profile's profileKey
export async function search(body) {
const res = await fetch(`${API}/search/${PROFILE_KEY}`, {
method: 'POST',
headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error((await res.json()).error.code);
return res.json();
}
Or from your server
Server-rendered pages can call the same routes from the shop's server. Use a secret key with read access, limited to the index the shop searches (see API keys): a server call sends no Origin header, so a public key answers 403 ORIGIN_NOT_ALLOWED there.
curl -X POST https://api.zaproo-search.com/v1/search/$PROFILE_KEY \
-H "Authorization: Bearer $ZS_KEY" -H "Content-Type: application/json" \
-d '{"q": "oled tv", "filters": {"brand": ["Samsung", "LG"]}, "sort": "price:asc", "page": 1}'
Search results page
POST/v1/search/:profileKeyPublic key from an allowed origin, or any secret key of the projectThe request body holds only what the shopper chose. Everything else, such as the fields to search and the facets to show, comes from the profile.
| Field | Type | Default | What it does |
|---|---|---|---|
q | string, up to 500 characters | "" | The query. An empty query returns every product, which is how a category page works. |
filters | field → filter | {} | The facet values and other filters the shopper selected. |
sort | string | the profile's default sort | One of the profile's sort options, for example price:asc. |
page | integer from 1 | 1 | The page of results. |
perPage | integer | the profile's perPage.default | Results per page, up to the profile's perPage.max. |
highlight | boolean | false | Adds _highlight to every hit with the matched words marked. |
sessionId | string, 1 to 100 letters, digits, - or _ | none | One id per visitor, so paging through results stays stable. |
The answer:
{
"items": [{ "id": "tv-1", "title": "55 inch OLED TV", "brand": "Samsung", "price": 899 }],
"total": 42,
"page": 1,
"perPage": 24,
"facets": [
{ "type": "terms", "field": "brand", "label": "Brand", "items": [{ "key": "Samsung", "count": 18, "selected": true }] }
],
"correctedQuery": null,
"tookMs": 12
}
- Show
totalas the result count and build the pager fromtotalandperPage.pagetimesperPagecannot go past 10000, so offer filters rather than page 500. - When
correctedQueryis set, the query found nothing and did-you-mean found a better spelling. Ifitemsis not empty, the results come from that spelling: show "Showing results for correctedQuery". Ifitemsis empty, the profile only suggests it: show "Did you mean correctedQuery?" as a link. - A hit carries the fields the profile returns. A hit placed by a ranking rule also has
"_pinned": true.
Facets
Each entry in facets describes one facet to draw. Its items are the values with their product counts, and selected marks the values the current filters picked.
type | Draw it as | Filter to send when the shopper picks a value |
|---|---|---|
terms | A list of checkboxes | {"brand": ["Samsung", "LG"]}. Several values match any of them. |
range | A list of price bands | {"price": {"gte": 100, "lt": 500}}, using the item's from and to. |
stats | A slider from min to max | {"price": {"gte": 150, "lte": 400}} |
hierarchy | A category tree, one level at a time | {"category": ["TV>OLED"]}. The facet then lists the children of the deepest selected node. |
Filters on different fields must all match. A request may filter only on fields the profile has as facets or lists under filterFields. Any other field answers 400 FILTER_NOT_ALLOWED. A filter also accepts a single value, {"inStock": true}, and {"exists": true}. Search profiles explains how to set up facets and filter fields.
Autocomplete box
POST/v1/suggest/:suggestKeyPublic key from an allowed origin, or any secret key of the projectSend what the shopper has typed so far, up to 100 characters:
curl -X POST https://api.zaproo-search.com/v1/suggest/$SUGGEST_KEY \
-H "Authorization: Bearer $ZS_KEY" -H "Content-Type: application/json" \
-d '{"q": "sams"}'
{
"suggestions": [
{ "text": "samsung tv", "kind": "query", "facetHint": { "key": "category", "value": "TV" } },
{ "text": "Samsung", "kind": "field", "facetHint": null }
],
"searchProfileKey": "Ab3dE9xQk2LmP0sT",
"tookMs": 3
}
- With an empty
q, the answer lists the most searched queries, if autocomplete is set to show them. Ask for them when the search box gets focus. - When the shopper picks a suggestion, search for its
textwith the profile insearchProfileKey. If the suggestion has afacetHint, add it as a filter:{"category": ["TV"]}. - Wait until the shopper pauses typing, about 150 ms, before you send the request. Drop answers that arrive after a newer request went out.
Common mistakes
- Forgetting a domain on the public key. A page on
www.shop.example.comneedswww.shop.example.comor*.shop.example.comon the key's allowed origins, not onlyshop.example.com. Otherwise the API answers403 ORIGIN_NOT_ALLOWED. - Putting a full-access secret key into frontend code. Anyone can read it and delete your indices. Keep secret keys on the server, and use read access for search.
- Sending a sort the profile does not offer. The API answers
400 SORT_NOT_ALLOWED. Show only the profile's sort options. - Filtering on a field that is not a facet. Add the field to the profile's
filterFieldsfirst, or the API answers400 FILTER_NOT_ALLOWED.