Skip to content

Ask the docs

Answers come from this documentation only, with sources. For account-specific issues, the docs route you to the right owner.

What can I help you find?

You are chatting with an AI assistant. It can make mistakes, so double-check important information.

Install Search on a headless storefront

This page installs Search on a storefront that is not a Shopify theme: a headless front, a custom site, or any stack where you write the frontend yourself. On a Shopify theme, follow Install Search on Shopify instead.

Search is a keyword search over your Dialog catalog: it matches the words of the query against your products, with typo tolerance, synonyms and pinned products. You have three ways to plug it in, from the most to the least ready-made:

Option Use it when
React or Vue components Your storefront runs on React or Vue 3: a search bar and a results panel, ready to drop in.
SDK search controller Any other framework or vanilla JS: the SDK runs the search, you render the results.
REST API You call Search from your own server or client, without the SDK.
  • Your public API key: every option authenticates with the Dialog public API key. See Where to find your API keys. Never use the private key in a storefront.
  • Your catalog in Dialog: Search returns the products of your Dialog catalog. Check that it is synced first (Catalog integration).
  • A language and a currency: an ISO 639-1 language code (en) and an ISO 4217 currency (USD). Search keeps one index per language and currency; a regional locale such as en-US is rejected as a language.

@askdialog/dialog-react and @askdialog/dialog-vue ship a search bar (DialogSearchBar) and a results panel (DialogSearchResults) on top of the SDK search controller: useDialogSearch({ client, language, currency }) creates the controller, the two components render it. The panel shows the products (and collections, if you ask for them), emphasizes the query match in each title and paginates.

The code sample and the full reference of the hook and the components (options, props, router adapter, collections column) live with each package:

Outside React and Vue, use createSearchController from @askdialog/dialog-sdk. The controller sends the requests and keeps the search state; you render the results from that state.

import { createSearchController, Dialog } from '@askdialog/dialog-sdk'
const client = new Dialog({ apiKey: 'YOUR_PUBLIC_API_KEY', locale: 'en-US', currency: 'USD' })
const controller = createSearchController({
search: (request, options) => client.search(request, options),
language: 'en',
currency: client.currency,
analytics: {
surface: 'search_page',
trackViewSearchResults: (params) => client.trackViewSearchResults(params),
trackSelectSearchResult: (params) => client.trackSelectSearchResult(params),
},
})
controller.subscribe((state) => render(state)) // your rendering
input.oninput = () => controller.setQuery(input.value)

The SDK reference: createSearchController gives the full wiring (impressions, clicks, pagination), the options, the methods and the state.

The SDK and the components call the endpoint POST /public/search/lexical. Call it directly when you do not use the SDK, for example from your own server.

POST https://api.askdialog.ai/public/search/lexical
Content-Type: application/json
x-dialog-api-key: YOUR_PUBLIC_API_KEY

Send your public API key in the x-dialog-api-key header. A request without a key, or with an unknown key, answers 401.

The endpoint is rate limited per IP address: 20 requests per second. Past that limit it answers 429 with a Retry-After header.

The body holds one entry per index to search, in requests (1 to 10 entries). All the entries must share the same query, language and currency.

{
"requests": [
{ "indexName": "products_en_usd", "query": "running shoes", "page": 0, "hitsPerPage": 20 },
{ "indexName": "collections_en_usd", "query": "running shoes", "hitsPerPage": 5 }
]
}
Field Type Required Description
indexName string Yes <index>_<language>_<currency>. <index> is products, collections, articles or pages; the currency is lowercase (products_fr_eur). Only request an index your catalog has: a missing one fails the whole request.
query string Yes The shopper’s query: 2 to 200 characters, trimmed.
page number No Zero-based page. Default 0.
hitsPerPage number No Results per page, from 1 to 100. Default 20.
facetFilters array No Value filters, on products entries only. See Filters.
numericFilters array No Number filters, on products entries only. See Filters.

An unknown field in the body or in an entry answers 400.

Filters narrow a products entry on the filterable attributes of your catalog, the ones you publish in Product filters: a filter on any other attribute answers 400. Both fields are a list of groups: the groups combine with AND, the filters inside a group with OR.

{
"indexName": "products_en_usd",
"query": "jacket",
"facetFilters": [[{ "attribute": "color", "value": "black" }, { "attribute": "color", "value": "navy" }]],
"numericFilters": [[{ "attribute": "price", "from": 50, "to": 150 }]]
}
  • A facet filter is { attribute, value, negated? }. Set negated: true to exclude the value.
  • A numeric filter is either { attribute, operator, value }, with operator among <, <=, =, !=, >=, >, or a range { attribute, from, to }.

The response holds one result per entry, in results, in the order of the request.

{
"results": [
{
"index": "products_en_usd",
"hits": [
{
"objectID": "product-123",
"title": "Trail running shoes",
"url": "https://mystore.com/products/trail-running-shoes",
"handle": "trail-running-shoes",
"imageUrl": "https://cdn.mystore.com/trail.jpg",
"priceRange": {
"min": { "amount": "89.90", "currencyCode": "USD" },
"max": { "amount": "99.90", "currencyCode": "USD" }
}
}
],
"nbHits": 42,
"page": 0,
"nbPages": 3,
"hitsPerPage": 20,
"processingTimeMS": 12,
"query": "running shoes",
"queryID": "5f0c..."
}
]
}

Each result:

Field Type Description
index string The indexName of the entry.
hits array The records of this page.
nbHits number Total number of matches, all pages included.
page number Zero-based page returned.
nbPages number Number of pages.
hitsPerPage number Page size applied.
processingTimeMS number Search time, in milliseconds.
query string The query searched.
queryID string Identifier of this search, one per entry.
filters array Optional: the filters applied to the entry, one block per source.

Each hit carries objectID (the record’s identifier) and, when known, its display fields: title, url, handle, imageUrl, and for products a priceRange (min and max, each an amount as a decimal string such as "24.90" and an optional currencyCode).

Product hits can also carry their purchase data, all optional:

Field Type Description
totalVariants number Number of variants.
tracksInventory boolean Whether the product tracks its stock.
totalInventory number Stock across variants.
options array Product options: { name, values }.
variants array Variants: id, title, url, imageUrl, price, compareAtPrice, inventoryQuantity, inventoryPolicy, selectedOptions ({ name, value }).

A variant’s stock is not stored as a flag: read it from inventoryQuantity and inventoryPolicy, against the product’s tracksInventory.

Status Cause
400 Invalid body: unknown field, query under 2 characters, entries with different queries, languages or currencies, or a filter on an attribute that is not filterable.
401 Missing or unknown public API key.
404 Index name that does not exist, for example a malformed name or a missing currency suffix (products_en).
429 Rate limit reached: retry after the Retry-After delay.
502, 503, 504 Search is temporarily unavailable: retry later.

Error bodies carry a statusCode and a message.

On your storefront:

  1. Type a product name in your search field: Dialog products appear.
  2. Type it with a typo: the right products still appear.
  3. Click a result: its product page opens.
  4. Change page with the pagination: the next results load.

If no result ever appears, check your public API key and that your catalog is synced, then the network tab: a 404 points to the index name, a 401 to the key.