Getting started

Integrate a storefront

Build a search results page with facets and an autocomplete box on top of the search and suggest routes.

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}'
Rate limits count per key and per client IP: 600 searches and 1200 suggest requests per minute per key. Every shopper shares that key budget, from the browser too. From the browser each shopper has their own IP, while through your server they also share one IP. See Rate limits and errors.

Search results page

POST/v1/search/:profileKeyPublic key from an allowed origin, or any secret key of the project

The 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.

FieldTypeDefaultWhat it does
qstring, up to 500 characters""The query. An empty query returns every product, which is how a category page works.
filtersfield → filter{}The facet values and other filters the shopper selected.
sortstringthe profile's default sortOne of the profile's sort options, for example price:asc.
pageinteger from 11The page of results.
perPageintegerthe profile's perPage.defaultResults per page, up to the profile's perPage.max.
highlightbooleanfalseAdds _highlight to every hit with the matched words marked.
sessionIdstring, 1 to 100 letters, digits, - or _noneOne 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 total as the result count and build the pager from total and perPage. page times perPage cannot go past 10000, so offer filters rather than page 500.
  • When correctedQuery is set, the query found nothing and did-you-mean found a better spelling. If items is not empty, the results come from that spelling: show "Showing results for correctedQuery". If items is 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.

typeDraw it asFilter to send when the shopper picks a value
termsA list of checkboxes{"brand": ["Samsung", "LG"]}. Several values match any of them.
rangeA list of price bands{"price": {"gte": 100, "lt": 500}}, using the item's from and to.
statsA slider from min to max{"price": {"gte": 150, "lte": 400}}
hierarchyA 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 project

Send 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 text with the profile in searchProfileKey. If the suggestion has a facetHint, 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.com needs www.shop.example.com or *.shop.example.com on the key's allowed origins, not only shop.example.com. Otherwise the API answers 403 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 filterFields first, or the API answers 400 FILTER_NOT_ALLOWED.