Indexa API
API reference
Indexa is a managed search & discovery engine for Shopify storefronts. Your catalogue is synced into a dedicated index, and your storefront talks to one host: search, autocomplete, recommendations and the interaction beacon are all served by your indexer, ranking and merchandising included. Hosts are provisioned for you at onboarding — this document is how you query them.
Concepts
How it fits together
Your Shopify catalogue is synced into a dedicated index, one collection per language (products_it, products_en). Three components are involved:
| Field | Type | Description |
|---|---|---|
Indexer service | $INDEXER_URL | Everything a storefront calls: /search, /suggest, /events, /reco/*. Plus admin reindex and Shopify webhooks. One instance per client. |
Search engine | $TYPESENSE_URL | The index itself. Reachable directly with a read-only key when you need a query shape /search does not expose. |
Control plane | console | Analytics, synonyms, relevance weights, merchandising rules, curated lists and query redirects. Managed by us; edited in your console. |
Search logic lives in the indexer
Auth
Authentication
The storefront surface — /search, /suggest, /events, /reco/* — requires a tenant API key in X-Indexa-Key. Use the public pk_ key in the browser and the secret sk_ key only on your server. Keys are available in the console. Browser requests also follow origin and rate limits. Admin and direct engine access use separate credentials below.
Admin key
A bearer token for the indexer's /admin/* routes (reindex, mint a search key). Server-side only.
Authorization: Bearer {your_admin_key}Search key
A read-only key scoped to documents:search on your language collections, for talking to the engine directly. Only needed if you build engine queries yourself.
X-TYPESENSE-API-KEY: {your_search_key}Scoping rule
products_it,products_en). Globs such as products_* are not accepted and a key scoped that way is rejected at query time.Webhook signature
Shopify signs product and collection webhooks with your shared secret; the indexer verifies the X-Shopify-Hmac-Sha256 header and rejects anything unsigned.
Environments
Base URLs
Every tenant gets its own isolated hosts — there is no shared public gateway. Your exact URLs are delivered at onboarding and shown in your console under API keys.
| Field | Type | Description |
|---|---|---|
$INDEXER_URL | https://YOUR-SLUG.api.indexa.now | Your indexer host, one per store. Replace YOUR-SLUG before sending anything: the placeholder is a real hostname shape that resolves to nothing, so a request you forgot to configure fails at DNS instead of hitting someone else. Your host is in your welcome email and in the console. |
$TYPESENSE_URL | https://YOUR-SLUG.search.indexa.now | Your search engine host. Only needed if you build engine queries yourself. Same rule: replace YOUR-SLUG. |
Languages default to it, en. Every collection name is products_{lang}, and GET $INDEXER_URL/health reports the ones your indexer serves.
Quickstart
Your first query
One GET with your public API key. Ranking, merchandising and query understanding are already applied:
curl -H "X-Indexa-Key: $PUBLIC_KEY" "$INDEXER_URL/search?q=maglione+verde+xl&lang=it&per_page=12&analytics=false"A shopper typing gets the same call per keystroke with a smaller per_page, plus /suggest for query completions.
Search
Search
Called straight from a shopper's browser. Every endpoint here takes your store's API key: pk_… from a browser, sk_… from your own server. CORS and per-IP rate limits still apply on top. The pk_ key ships inside your pages and is public by construction — what it buys is revocation without redeploying your theme, and traffic we can attribute to you; the sk_ key is real authentication, and additionally lifts the per-IP limit, which matters when one server fronts every shopper in the shop.
$INDEXER_URL/searchRun a search. Redirects, relevance weights, query understanding, merchandising and did-you-mean are applied server-side, in that order.
X-Indexa-Key: $PUBLIC_KEY from a browser, $SECRET_KEY from your own server — or ?key= where headers are impossible (sendBeacon).
| Field | Type | Description |
|---|---|---|
qreq | string | The shopper's query. An empty q returns an empty result unless a collection listing is requested. |
lang | string | Language to search. Defaults to the store's first language. |
page | int | 1-based page number. Default 1. |
per_page | int | Page size. Default 24, capped at 100. |
collection | string | Render a category listing instead of a search: everything inside this collection handle, ordered by the same relevance, with merchandising looked up by collection rather than by query. Leave `q` empty. A handle that is not a valid Shopify handle matches nothing. On configured and enabled tenants, Indexa Intelligence may rerank a bounded leading window on page 1 with no query or explicit sort; hard collection boosts skip AI. Pins and filters are preserved. Failure retains deterministic order. This does not publish an order to Shopify. |
sid | string | Anonymous session ID, max 64 characters. Rotate after 30 minutes of inactivity; used for search-assisted attribution and co-view recommendations. |
analytics | boolean | Set false to suppress search logging until analytics consent. Search still works. |
sort | string | Order results with relevance (default), price_asc, price_desc, discount_desc, newest or popularity. Declared scalar product metafields support mf_namespace_key:asc or :desc after reindex, e.g. mf_custom_priority:asc. Availability is validated against this tenant and language's live schema; unknown/malformed metafield sorts return 400. Unsupported preset names retain the relevance fallback. Explicit sorting replaces text relevance; pinned products stay pinned. Missing metafield values go last in either direction. Integer priorities use int64 within the JavaScript safe range; decimals use engine float precision and sufficiently close values may tie. Lists, structured values and variant metafields are excluded. |
filter_<field> | string | Facet filter on any indexed field. Comma-separated values are an OR within that field: filter_vendor=Nike,Adidas means Nike or Adidas. Repeatable, and every filter_* parameter is ANDed with the others: filter_vendor=Nike&filter_sizes=XL,L means Nike AND (XL or L). Field names are validated against ^[a-z][a-z0-9_]{0,40}$ and values are quoted, so neither can break out of the filter expression. |
filter_not_<field> | string | Everything but these values: filter_not_vendor=Nike,Adidas excludes both. Same validation and same AND with the other filters. |
filter_<field>_min / filter_<field>_max | number | Numeric range on an indexed number field, either end optional: filter_price_min=50&filter_price_max=100 is 50 <= price <= 100. A bound that is not a finite number is dropped rather than guessed at, so a malformed URL narrows nothing instead of erroring. Note a field whose own name ends in _min or _max cannot be filtered for equality through this parameter. |
{
"hits": [ { "id": "7523891", "title": "Maglione girocollo in lana", "price": 39.9, "…": "see Product document" } ],
"found": 128,
"search_event_id": "aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa",
"facets": [ { "field_name": "vendor", "counts": [ { "value": "Atelier Nord", "count": 12 } ] } ],
"page": 1,
"redirect": null,
"didYouMean": null,
"understood": { "sizes": ["XL"], "colors": ["Verde"], "genders": ["Donna"] },
"relaxed": { "terms": ["inverno"], "sizes": ["XL"] }
}- •
search_event_idis an optional UUID for a logged search. Carry it withqueryon result clicks; previews, category listings and analytics=false omit it. Logging is best effort. Analytics zero results use displayed results after relaxation; original counts remain available for debugging. - •
redirectis an answer, not a ranking: when a merchant has mapped the query to a page,hitsis empty andredirectholds the target. Validate it against your own origin before navigating — the engine already refuses anything that is not a relative path or a same-origin URL. - • Metafields declared in your console are ordinary fields here: filter_mf_<namespace>_<key> (mfv_ for variant-level ones), with the same equality, OR, and filter_not_ forms, and matching is case-insensitive. They are always indexed as string lists, so the _min/_max range form does not apply to them — it would match nothing rather than error.
- •
facetscarries value counts for the built-in facets, plus any metafield ticked as *Show as filter* in your console (up to 8) once it is in the index — that is what a filter sidebar is built from. Filtering a metafield never requires the tick; only knowing its values does. - • A filter naming a field the index does not have answers 400
Unknown filter field: <name>— a bookmarked URL carrying a facet that has since been renamed is the caller's mistake, not an outage, and dropping the clause instead would answer a filtered request with the whole catalogue. - • Filters combine as AND across parameters and OR within one field. OR *across different fields* is deliberately not expressible here: it needs a filter grammar, and a grammar on a browser-facing endpoint is a surface that cannot be closed again once it is open. If you need one, run the query server-side against
/admin/previewwith your admin key, which accepts a raw expression. - • Paging is capped:
page×per_pagemay not exceed 1000, and a request past that answers 400 rather than silently returning an earlier page. Shoppers do not reach page 11; catalogue scrapers do nothing else. Narrow with filters instead. - •
didYouMeanis offered when fewer than 3 products match, and suggests a query shoppers have actually run — not a dictionary word. - •
understoodreports what was pulled out of the phrase into filters — size, colour and department — so you can show it back to the shopper and let them drop one. - •
relaxedis present only when the exact query matched nothing and a weaker one was answered instead. It names what was given up, in the order the engine gives things up: trailing words first (a word the catalogue has no vocabulary for is usually a typo or a marketing phrase), then size, then colour. Never the department, and never a filter the shopper set themselves. Say it on the page — a list that silently ignores half the query reads as a broken filter.
Query understanding
Before searching, Indexa parses attributes out of the raw term and turns them into filters — so "maglione verde XL da donna" filters by colour, size and department instead of matching those words as text — a word left in the query text filters nothing, and on a catalogue whose womenswear is titled differently that is how a shopper ends up in the children's aisle. Sizes and colours are read from your live facet vocabulary, so the parser knows what your catalogue actually contains.
- • Department —
donna,uomo,bambini,women,kids… matched to thegenderfacet. A word that names more than one department (kids,baby) becomes one OR clause, never two ANDed ones. - • Sizes — matched to the
sizesfacet (e.g.xl → XL). - • Colours — matched to the
colorsfacet, including Italian gender/number inflections (nero → nera/nere/neri,verde → verdi). - • EN → IT synonyms —
green→Verde, black→Nero, blue→Blu, red→Rosso…added only when the Italian value exists in the facet. - • Stopwords dropped:
da, di, del, con, per, the, of, for, with, and…
What was understood comes back under understood, so you can show the shopper which filters were applied on their behalf — and let them remove one.
Filtering & facets
Add filter_{field} parameters, one per field, with comma-separated values. They are ANDed together and combined with whatever query understanding detected. Facet counts for the result set come back in facets.
curl "$INDEXER_URL/search?q=maglione&lang=it\
&filter_vendor=Atelier%20Nord,Casa%20Lume\
&filter_sizes=L,XL\
&filter_on_sale=true"Injection is handled for you
^[a-z][a-z0-9_]{0,40}$ and values are quoted, so neither a crafted field name nor a value containing a quote or a backtick can break out of the filter expression.Redirects
A merchant can map a query to a page — "spedizioni" to the shipping page — from the console. When one matches, the response carries redirect and no hits: it is an answer, not a ranking.
{ "hits": [], "found": 0, "redirect": "/it/pages/spedizioni", "didYouMean": null }Validate the target before navigating
location.assign(): only the browser knows the storefront's domain.Autocomplete
Query completions drawn from what shoppers on this store actually searched — not a dictionary, so a suggestion always leads somewhere.
$INDEXER_URL/suggestQuery suggestions for an autocomplete box, drawn from what shoppers on this store actually searched.
X-Indexa-Key: $PUBLIC_KEY from a browser, $SECRET_KEY from your own server — or ?key= where headers are impossible (sendBeacon).
| Field | Type | Description |
|---|---|---|
qreq | string | What has been typed so far. |
lang | string | Language. Defaults to the store's first language. |
limit | int | How many suggestions. Default 5, capped at 20. |
{ "queries": ["maglione donna", "maglione lana", "maglioncino"] }Discovery
Recommendations
Seven models, one response shape. Same guards as search — CORS, per-IP rate limit — because a Liquid theme has no server to proxy through.
| Field | Type | Description |
|---|---|---|
best-sellers | Shopify | Shopify's own best-selling ranking, optionally within a collection. |
related | Shopify | Shopify product recommendations, auto-generated. No setup. |
complete-the-look | Indexa | Rule-driven pairings: destination categories, weighted fields, required criteria and manual pins. Configure and preview in the admin. |
complementary | Shopify | Shopify complementary recommendations. Requires Search & Discovery; separate from Indexa Complete the look. |
popular | your data | Most viewed in a rolling window. Needs view events. |
also-viewed | your data | Co-viewed in the same session. Needs view events carrying a session id. |
custom | your data | A curated list, in the order a merchant arranged it in the console. |
- • Every model answers
products[], already resolved. The popular, also-viewed, custom and complete-the-look models also keepproduct_ids[]for integrations that resolve ids themselves. - • Order is the recommendation — a curated list is a merchant's running order, popular is a ranking — so it is preserved exactly. Sold-out products are dropped.
- • An unknown model under /reco/ returns 404, not an empty 200.
$INDEXER_URL/reco/best-sellersShopify's own best-selling ranking, optionally within one collection.
X-Indexa-Key: $PUBLIC_KEY from a browser, $SECRET_KEY from your own server — or ?key= where headers are impossible (sendBeacon).
| Field | Type | Description |
|---|---|---|
lang | string | Language. |
limit | int | Default 12, capped at 50. |
collection | string | Collection handle to rank within. |
{ "products": [
{ "id": "7523891", "handle": "felpa-cappuccio-donna", "title": "Felpa con cappuccio",
"price": 29.99, "image_url": "https://cdn.shopify.com/.../felpa.jpg",
"url": "/it/products/felpa-cappuccio-donna" }
] }Shopify's product recommendations for one product.
X-Indexa-Key: $PUBLIC_KEY from a browser, $SECRET_KEY from your own server — or ?key= where headers are impossible (sendBeacon).
| Field | Type | Description |
|---|---|---|
lang | string | Language. |
limit | int | Default 12, capped at 50. |
intent | enum | RELATED (default, auto-generated by Shopify) or COMPLEMENTARY (needs the Shopify Search & Discovery app configured). Anything else is read as RELATED. |
{ "products": [
{ "id": "7523891", "handle": "felpa-cappuccio-donna", "title": "Felpa con cappuccio",
"price": 29.99, "image_url": "https://cdn.shopify.com/.../felpa.jpg",
"url": "/it/products/felpa-cappuccio-donna" }
] }$INDEXER_URL/reco/complete-the-look/{productId}Indexa-generated complementary products from merchant-defined categories and weighted field rules.
X-Indexa-Key: $PUBLIC_KEY from a browser, $SECRET_KEY from your own server — or ?key= where headers are impossible (sendBeacon).
| Field | Type | Description |
|---|---|---|
lang | string | Configured catalog language for this tenant. |
limit | int | Default 12, capped at 50; a rule can produce at most 18 products. |
{ "product_ids": ["7523891"], "products": [
{ "id": "7523891", "handle": "felpa-cappuccio-donna", "title": "Felpa con cappuccio",
"price": 29.99, "image_url": "https://cdn.shopify.com/.../felpa.jpg",
"url": "/it/products/felpa-cappuccio-donna" }
] }- • Configure and enable rules in the Complete the look admin section. Product scope wins over custom-pick list, category and default; priority then rule ID breaks ties. One rule applies, with no merging.
- • Ranks up to 200 available candidates per destination category plus explicit pins. Weighted equality, shared values, directional compatibility, numerical distance and ratio determine ranking. Required criteria, exclusions and availability remain mandatory for pins.
- • No matching rule or eligible products returns empty products and product_ids arrays. Invalid product ID or catalog language returns 400; unavailable generation returns 503. Internal weights and explanations are never public.
- • Separate from Shopify COMPLEMENTARY: no Shopify metafield writes or automatic fallback. Playground Complete the look uses this Indexa endpoint and saved enabled rules. Use data-indexa-reco=complete-the-look in Liquid or type=complete-the-look in the Hydrogen example proxy; storefront placement is opt-in.
$INDEXER_URL/reco/popularMost-viewed products in a rolling window, from your own traffic.
X-Indexa-Key: $PUBLIC_KEY from a browser, $SECRET_KEY from your own server — or ?key= where headers are impossible (sendBeacon).
| Field | Type | Description |
|---|---|---|
lang | string | Language. |
days | int | Window in days. Default 30, capped at 365. |
limit | int | Default 12, capped at 50. |
{ "product_ids": ["7523891", "7519001"], "products": [
{ "id": "7523891", "handle": "felpa-cappuccio-donna", "title": "Felpa con cappuccio",
"price": 29.99, "image_url": "https://cdn.shopify.com/.../felpa.jpg",
"url": "/it/products/felpa-cappuccio-donna" }
] }- • Empty until view events arrive. An empty list is the correct answer for a store with no traffic yet, not an error.
$INDEXER_URL/reco/also-viewed/{productId}Products co-viewed in the same session as this one.
X-Indexa-Key: $PUBLIC_KEY from a browser, $SECRET_KEY from your own server — or ?key= where headers are impossible (sendBeacon).
| Field | Type | Description |
|---|---|---|
lang | string | Language. |
limit | int | Default 12, capped at 50. |
{ "product_ids": ["7519001"], "products": [
{ "id": "7523891", "handle": "felpa-cappuccio-donna", "title": "Felpa con cappuccio",
"price": 29.99, "image_url": "https://cdn.shopify.com/.../felpa.jpg",
"url": "/it/products/felpa-cappuccio-donna" }
] }- • Needs view events carrying a session id (
sid).
$INDEXER_URL/reco/custom/{slug}A curated list, in the order a merchant arranged it in the console.
X-Indexa-Key: $PUBLIC_KEY from a browser, $SECRET_KEY from your own server — or ?key= where headers are impossible (sendBeacon).
| Field | Type | Description |
|---|---|---|
lang | string | Language. |
limit | int | Default 12, capped at 50. |
{ "product_ids": ["7523891", "7519001"], "products": [
{ "id": "7523891", "handle": "felpa-cappuccio-donna", "title": "Felpa con cappuccio",
"price": 29.99, "image_url": "https://cdn.shopify.com/.../felpa.jpg",
"url": "/it/products/felpa-cappuccio-donna" }
] }- • Create and edit lists in /custom-picks using catalog search, product thumbnails and drag-and-drop order. Use the saved slug in this endpoint; list creation does not install a storefront carousel. Missing or unavailable products are omitted from resolved products.
Analytics
Events
The interaction beacon. It is the only public endpoint that writes, and the only thing that makes popular and also-viewed work: no events, no recommendations — those two models answer with an empty list until traffic arrives.
$INDEXER_URL/eventsInteraction beacon. The only public endpoint that writes, and the only source of data for the popular and people-also-viewed models.
X-Indexa-Key: $PUBLIC_KEY from a browser, $SECRET_KEY from your own server — or ?key= where headers are impossible (sendBeacon).
{ "product_id": "7523891", "event_type": "view", "lang": "it", "sid": "a1b2c3d4" }(no body)- • Result clicks should include query (up to 512 chars) and search_event_id (UUID from /search). Optional quantity is a positive integer up to 100000. Both sid and session_id are supported.
- • Revenue, unit_price, currency, order_id, source and created_at from this public endpoint are ignored, including with sk_ keys. Browser purchase events never contribute authoritative revenue or conversion. Send only after required analytics consent.
- • Verified purchases come from POST /webhooks/orders/paid with Shopify raw-body HMAC, shop domain and orders/paid topic headers. The indexer acknowledges after duplicate-safe storage; failures return 503 for retry. Configure the tenant Shopify subscription and preserve consented indexa_session_id/indexa_lang cart attributes through checkout. No customer PII is stored. Revenue is paid merchandise after discounts, excluding tax/shipping; refunds and later edits are not adjusted in V1.
- • Send one object or an array of up to 20. The response comes back before the write: a beacon must not hold a page open while it navigates away.
- •
event_typeis one of view, click, add_to_cart, purchase. A missing key means view; a key that is present but not a string is rejected rather than coerced — junk filed underviewwould poison the most used signal in the table. - •
product_idmust match ^[A-Za-z0-9_-]{1,64}$. Numbers are accepted and stringified. An unknownlangfalls back to the store's default. - • Content type is not checked: sendBeacon picks its own, and cannot retry a failed preflight.
curl -X POST "$INDEXER_URL/events" -H "X-Indexa-Key: $PUBLIC_KEY" \
-H "Content-Type: application/json" \
-d '{ "product_id": "7523891", "event_type": "view", "lang": "it", "sid": "a1b2c3d4" }'On a Shopify theme this is automatic
window.Indexa.track(productId, 'add_to_cart').Merchant guide
Analytics V1
Understand how Indexa search contributes to discovery, conversion and revenue. Reporting uses anonymous sessions and verified orders, with an explicit attribution model.
| Field | Type | Description |
|---|---|---|
Searches / search sessions | count | Valid searches / distinct nonempty session IDs among those searches. |
Search CTR | rate | Search sessions with at least one search-result click ÷ search sessions. |
Add-to-cart rate | rate | Search sessions with at least one attributed add-to-cart ÷ search sessions. |
Conversion rate | rate | Search sessions with at least one attributed verified purchase ÷ search sessions. |
Search revenue | by currency | Sum of verified purchase-line revenue attributed to Indexa searches. |
Search AOV | by currency | Search revenue ÷ distinct attributed orders in that currency. |
Zero-result rate | rate | Searches with zero displayed results after relaxation ÷ all searches. The absolute count is also shown. |
Read the dashboard
Open /analytics in your Indexa console. The default is the last 30 days; choose today, 7, 30, 90 days or a custom range of up to 366 days. All KPIs, query tables and the daily timeline use the same UTC period and optional language filter. Revenue currencies are always shown separately; choose a currency for revenue ranking and the chart.
Last search, same session
A search session contains a valid Indexa search with an anonymous session ID. An interaction is attributed to the last valid search strictly before it in that same tenant and session. Result clicks should carry search_event_id and query from their results. Purchase attribution is resolved on the server: search A → search B → purchase credits B. A purchase before any search, or without a matching session, contributes no search revenue. Attribution measures search assistance, not incremental sales caused by search.
Period and funnel behavior
Reports select searches in [from, to) and count their subsequent interactions only before to. Daily values are assigned to the search date, including later purchases; this is not an order-date sales report. The funnel counts distinct sessions progressing through search → click → add to cart → verified purchase in order. Missing click/cart signals can make funnel purchases lower than the conversion KPI. Repeated clicks in one session never become additional clicked sessions.
Connect search and consent
Send the same anonymous sid on search requests and product events; rotate it after 30 minutes of inactivity. Before required analytics consent, use analytics=false on searches and suppress product events/session storage. Native Shopify searches and direct engine queries are not automatically Indexa search events. The Liquid integration respects its configured consent gate. Headless storefronts must connect their consent manager explicitly. Do not add customer names, email, addresses or phone numbers to analytics payloads.
Verify purchases through Shopify
Configure a per-tenant orders/paid webhook at POST /webhooks/orders/paid with the matching Shopify signing secret. The indexer checks the raw-body HMAC, shop domain and topic, then records paid non-test orders before acknowledging. Preserve consented indexa_session_id and indexa_lang cart attributes through checkout, awaiting the metadata update before redirecting and preserving other cart attributes. On revocation, clear browser context and cart attribution metadata. Test checkout paths explicitly: accelerated checkout that bypasses the integration may remain unattributed. Browser purchase events never supply authoritative revenue or conversion. Retries are duplicate-safe by tenant/order/line; storage failures return 503 for retry.
Revenue and currencies
Revenue is paid merchandise value after allocated discounts, excluding embedded tax and shipping, in the order shop currency. Multi-line orders count once for AOV within each currency. Currency amounts are never added together. Refunds, cancellations after payment and later order edits are not adjusted in V1; no historical revenue is invented or backfilled. Treat this as search attribution, not an accounting ledger.
Queries and opportunities
Queries are normalized by trimming, collapsing whitespace and lowercasing. Browse top queries, revenue queries, zero results and opportunities; each table returns up to 20 queries. Query CTR/conversion use distinct query sessions. Low CTR opportunities default to at least 20 searches and CTR below 10%, with configurable thresholds. Zero-result opportunities use zero-result count; the revenue highlight identifies the highest-revenue query in the selected currency. These are deterministic rules, not predictions.
Tenant-scoped reporting
Analytics uses the existing search_events and product_events in Postgres. The server-side analytics_overview RPC aggregates tenant-scoped data under RLS; the console checks tenant membership. Storefront pk_/sk_ keys cannot read another tenant's analytics. Reporting is an authenticated console capability, not a new public /analytics HTTP endpoint. Deployment requires the analytics migration, updated indexer/admin, consented storefront tracking and a verified order subscription. Empty revenue can mean missing integration or unmatched sessions, not necessarily no orders.
Activate the whole path
Search
Merchandising
Merchandising rules pin, hide and boost products in both search results and category pages. Rules are authored and managed in your console — there is no public write endpoint for them — and are read at query time by the same shared query builder that assembles every Indexa search.
The rule model
Rules live in a merch_rules table on the control plane. Each row has:
| Field | Type | Description |
|---|---|---|
lang | string | The language the rule applies to. |
scope | query | collection | Whether target is a search query or a Shopify collection handle. |
target | string | The search query (scope: query) or the Shopify collection handle (scope: collection). |
match_type | exact | contains | Applies only when scope is query. Collection rules always match the handle exactly. |
pinned_ids | string[] | Ordered product ids. A product's position in the results is its index in the array plus one. |
hidden_ids | string[] | Product ids removed from the results. |
strategy | object | null | Reserved for automatic rules. Currently only strategy.boost_collections (an array of collection handles) is read. |
Automatic rules are not shipped yet
Rule selection
At most one rule applies to a given search. Selection is deterministic — it never depends on the order rules happen to be stored or fetched in:
- • Exact beats contains — a scope: query rule with match_type: exact matching the whole normalized query wins over any contains rule.
- • Longest target wins among contains rules — if the query contains more than one rule's target as a substring (e.g. "scarpe" and "scarpe donna"), the longer, more specific target is selected.
- • Ties broken by the lowest rule id — when two competing contains targets have equal length, the rule with the lower id wins.
- • Collection rules match the handle exactly — match_type is not considered for scope: collection.
Matching is case-insensitive and ignores leading/trailing whitespace on both the rule target and the incoming query or handle.
From rules to request parameters
The selected rule is not pushed into the index as a stored override object. It is translated, at query time, into two parameters on the same engine call used for every other search:
| Field | Type | Description |
|---|---|---|
pinned_hits | string | id:position pairs, comma-separated. Positions are 1-based, taken from the product's index in pinned_ids. |
hidden_hits | string | Comma-separated product ids removed from the result set. |
// merch_rules row
"pinned_ids": ["4102938", "4109221"]
// engine request parameter
"pinned_hits": "4102938:1,4109221:2"Cache freshness
filter_curated_hits is always set
filter_curated_hits: true. Without it, a pinned product is returned even when the filter excludes it: a product pinned to a category page would keep appearing there after leaving the collection, and would survive a shopper's facet filters. With it, pinning decides ORDER while filters still decide MEMBERSHIP — a pinned product that no longer matches is simply not returned.Category pages
A category listing is not a separate endpoint — it is the same query, assembled by the same shared builder, with:
- •
qset to*(match everything). - • A collection filter added, with the handle backtick-quoted:
collections:=[`handle`]. - • The merchandising rule lookup run with
scope: 'collection'instead ofscope: 'query'.
For a wildcard query, _text_match is dropped from sort_by — it is meaningless when there is no query text to match against:
score:desc_eval(collections:=[`giacche-uomo`]):desc,score:descSort field limit
Pinned-product edge cases
Verified against a live engine — two behaviours worth knowing before you rely on pinning:
- • Pinning an id outside the result set leaves no gap. If a pinned product would not otherwise appear, the remaining hits simply fill in.
- • A pinned product that fails a filter is excluded, not injected. filter_curated_hits (above) means pinning never overrides an active filter.
Shopify collections
Collection strategies
Rank and publish the product order of an existing Shopify collection with sales, engagement, stock and commercial rules. Each collection has its own strategy and starts disabled.
From draft to a Shopify collection strategy
Open Collection in the Indexa console and choose a Shopify collection. New strategies start disabled. Run Prepare data once the store is connected and wait for success. Choose signals, directions and weights, a 7- or 30-day lookback and a managed depth (1–20,000 leading positions; default 200). Compare the current Shopify order with the proposed order in the full-width editor. Both lists show photos, titles and position changes, including identity for products that are not indexed. Pages show 25 products at a time; the complete collection is still ranked by rules. Optionally select Intelligence to rerank a bounded leading window with the tenant model; see the Indexa Intelligence guide. Inspect signal coverage, scores and applied rules before saving.
18 signals and commercial rules
Sales signals are units sold, revenue and order count. Engagement signals are views, add to cart, click-through rate, conversion rate and revenue per view. Availability signals are inventory, in-stock variants ratio and days to sell out. Price signals are price, compare-at price and discount percentage. Catalog and adjustment signals are days since published, tag score, vendor score and daily shuffle. Each signal is ranked as a percentile among indexed collection members; direction and weight determine its contribution. Weights range from 0 to 100, with at least one positive weight. Tag and vendor score maps range from −100 to 100 and are converted to percentiles rather than added as raw points. Daily shuffle is stable within the day.
Rules, missing data and unindexed products
After scoring, groups put unavailable products last when enabled, then prioritize push-up tags and new arrivals before ordinary products and push-down tags. Exact one-based pins are applied after grouping. Earlier tags win within each ordered tag list. Missing price, publication or stock values receive a neutral contribution; valid indexed products without events receive zero event metrics. Products absent from the index have no score and stay in their existing relative order after scored products, except where explicitly pinned. Showing a Shopify photo and title does not mean that a product has ranking data.
Save, enable, publish and keep updated
Saving a disabled draft and previewing do not reorder Shopify. After successful preparation, enable and save the strategy for the chosen collection. Enabling opts that collection into automatic publication; Publish now requests an immediate update using the saved revision. Later updates can follow the configured schedule (hourly by default) or relevant catalog, stock and metrics events. Only enabled strategies publish. AI previews may run for disabled drafts when tenant Intelligence is available. Intelligence-mode publication requires fresh successful AI inference; a Rules fallback preview cannot be published as Intelligence. If someone else saves first, reload and review the latest revision instead of overwriting it. An edit makes the previous preview stale; wait for the latest preview before saving or publishing.
Shopify scope and restore
Publication writes Shopify’s collection order and changes its sort mode to MANUAL. The first publication captures the original order and sort mode. The order is shared across Shopify markets and appears where the storefront uses Shopify’s collection order; an explicit price sort or Indexa search ranking can display a different order. This differs from query-time Merchandising, search sort presets and Custom picks. External order changes pause automatic overwrites. Review them, then explicitly Restore Shopify order to return to the captured baseline and disable the strategy. Restore is not a general version history. An unresolved or interrupted mutation requires inspection and explicit restore before another publication.
Connect the data and check readiness
Your Indexa team configures the compatible tenant service, storage, server-side Shopify Admin token, webhook signing secret and HTTPS callback URL. The Shopify installation must actually grant read_orders and write_products; declaring them in app settings alone is insufficient. Prepare data reconciles paid-order and product/collection webhooks, backfills 30 days of currently paid non-test orders, sweeps inventory and projects stock into all indexed catalog languages. The console shows progress and counts; retry failed or interrupted preparation. Enabling and publishing require successful preparation and current prerequisites. Browser engagement signals additionally require connected Indexa event tracking. Stock is shop-wide tracked inventory, not a market/location allocation. Unknown stock stays unknown. Historical refunds/edits can affect the paid-order backfill; mixed purchase currencies block readiness rather than being silently converted.
Open Collection in the console. Server integrations use the authenticated Collection endpoints in the Admin API below.
AI ranking
Indexa Intelligence
Train a store-specific model, evaluate it before activation and preview bounded AI ranking while keeping your commercial rules.
What Indexa Intelligence does
Indexa Intelligence is optional product reranking, using a model and feature configuration scoped to your store. Existing search and commercial rules first select and order candidates. Intelligence compares a bounded leading window, then preserves collection membership, exact pins and effective priority groups. It does not replace catalog indexing or rank every product with AI. A shared read-only base can provide an explicitly approved starting point; store-specific datasets and immutable versioned checkpoints remain isolated. Availability requires a compatible tenant deployment and configured source, model and cloud worker.
Where shoppers see it
Query-time Intelligence is eligible only for the first page of a collection listing with an empty search query and no explicit sort. Keyword searches, later pages and explicit sorting retain their existing ranking; a hard collection boost skips AI. Typesense still selects candidates and applies filters, hidden hits and merchandising. Curated hits and pinned products retain their positions. The configured topN is 2–32 products (default 16), with at most 32 unique pair comparisons in a single provider request. Rounds select opponents rather than creating sequential network calls. Win-share ordering with stable ties approximates ranking; it is not an exhaustive comparison of the catalog.
Shopify source and tenant setup
Your Indexa team verifies each store's Shopify identity, granted read_reports scope and actual ShopifyQL reporting access before commercial training. Declaring a scope alone does not prove access: missing history, unavailable reports, incomplete responses or a mismatched shop block readiness. Each store needs its own authorized installation and tenant-scoped configuration. Collection preparation's read_orders/write_products access is a separate prerequisite and does not establish commercial-training readiness. Tokens stay server-side. Settings → Intelligence / AI Ranking holds the advanced recipe, feature mappings and provider routing; catalog mapping values refer to verified indexed fields, not guessed Shopify metafields. Catalog-backed mapped fields must exist in all served language schemas; commercial feature inputs are supplied by the tenant/schema-scoped serving cache.
Historical data and versioned datasets
The Shopify commercial recipe learns product preferences from product/day sales aggregates. Its preset requests 120 days of history, uses the preceding 28 days for features and later 14-day outcomes for labels, excluding incomplete recent days using the store calendar and source latency. The default reward is 70% net units and 30% net sales, adjustable in the saved recipe. Historical units, net sales, recent units, trend, return fraction and sales percentile supply commercial inputs. Current inventory, price or score cannot stand in for unavailable historical observations. Source snapshots retain tenant identity, extraction bounds and version because returns can revise history; each dataset records the exact source, schema, label strategy, counts and generation time. Missing reports are not fabricated zero sales. Insufficient coverage, products or pair diversity can stop a run before training.
Start and follow online training
Open Intelligence in the authenticated console to inspect your store's active model, source status, cloud-worker contact, training stages, results and recent runs. Save the recipe in Settings, then choose Start online training. The action snapshots the saved configuration and allows one pending run per tenant. Missing source/configuration, a worker error or cloud contact older than ten minutes prevents new submissions with an explanation. The page refreshes every ten seconds while visible; a tenant-scoped CPU scheduler normally checks the queue every three minutes. It builds the dataset, submits a separate asynchronous GPU training job and later evaluates its checkpoint. Closing the browser or your computer does not stop queued work. These intervals are status/queue polling, not automatic training every three minutes or continuous learning. A healthy heartbeat proves recent scheduler contact, not GPU activity or model quality. These are authenticated console actions, not additional storefront API endpoints.
Evaluation before activation
Training creates a candidate, never an automatic production switch. Commercial data uses chronological outcome windows and product-disjoint training/holdout sets. Internal validation selects the training epoch; the untouched outer holdout compares candidate, active or approved base checkpoint, and a past-sales heuristic. Review coverage, accuracy, Brier error, baseline results and acceptance reasons. The commercial preset requires at least 256 holdout examples, accuracy ≥0.65, Brier error ≤0.30, no accuracy loss against the active/base comparison, a Wilson lower bound ≥0.55 and at most 0.02 accuracy loss against the sales heuristic. Saved acceptance criteria and lineage are checked before promotion. Offline pairwise metrics are observational proxies: repeated products across pairs and missing randomized exposure/inventory history do not support causal claims or prove sales lift. The initial worker trains/evaluates the first configured catalog language; validate other languages separately. Once a holdout informs a recipe decision, a subsequent experiment needs an audited fresh product cohort.
Promote, roll back or disable
Review an accepted result and explicitly choose Promote evaluated candidate. Activation atomically checks the expected active checkpoint, dataset/evaluation lineage and saved configuration; changed settings or a changed baseline invalidate stale comparisons. Rejected or failed runs preserve the current model. Roll back to this checkpoint selects a previously promoted version and restores its matching feature configuration without retraining. Keep artifacts required for rollback. Model promotion, tenant AI enablement and collection publication are separate controls. AI starts disabled; both the deployment flag and tenant setting must allow inference. The deployment kill switch bypasses the subsystem immediately; tenant setting changes can take up to the 30-second settings-cache interval. Disabling shopper AI does not stop the separately scheduled training worker.
Use Intelligence in a collection strategy
In Collection, choose Rules or Intelligence as the ranking mode. Rules is the default for existing strategies. The rules strategy orders the complete collection; Intelligence considers at most its first 32 positions within the managed depth, then the tenant's smaller topN and pin/unindexed exclusions determine the products actually evaluated. Exact pins, effective unavailable/push-up/new/normal/push-down groups and the remaining order are preserved. The source panel shows the actual mode, checkpoint, feature schema, latency, comparisons and evaluated count. An Intelligence preview can run while the strategy remains a disabled draft: the collection Enabled switch controls publication and scheduling, while tenant AI availability is separate. Saving the disabled draft changes Indexa configuration only. Older services remain usable in Rules mode; Intelligence selection requires provenance support.
Understand evaluated products and movements
Products evaluated is the count submitted to the model, not the number of changed positions. Moved by Indexa Intelligence marks final constrained movements relative to the rules-only baseline, with one-based before/after positions. It is different from comparing the proposed order with the current Shopify order. Loading, stale or fallback previews suppress movement markers. For an evaluated product, Why this order? shows observed tournament wins and opponents plus up to eight actual numeric/boolean mapped inputs. For example, a product can be evaluated and win a comparison while staying in its position because of a tie or a hard rule. Sales totals may be decoded from logarithmic inputs for display, fractions shown as percentages and trends labeled increasing/decreasing/stable. These are observed decisions and inputs, not causal feature contributions or a generated model rationale. Rules scores explain candidate generation, not the model's reasoning. Evidence is admin-only, absent from shopper output and fallback explanations.
Preview, publish and restore safely
Collection planning allows a separate inference deadline of up to 45 seconds for provider cold starts. Failed or ineligible AI previews explicitly report Rules fallback. A strategy saved in Intelligence mode cannot publish a fallback order: fresh successful Intelligence inference is required, including recomputations after Shopify sort changes or membership retries. Publication recomputes the order, so a preview is not a frozen publication snapshot. Enabling a prepared strategy opts it into scheduled/event-driven publication; Publish now requests an immediate update at the saved revision. Successful publication stores actual provenance in the publication state (last_ranking; exposed as strategy.last_ranking in the Collection API). Restore Shopify order returns the captured original order/sort mode, disables the strategy and clears that provenance. A disabled draft never changes Shopify order merely because its AI preview succeeds.
Serving budgets, feature freshness and fallback
Shopper inference has a hard 1,000 ms maximum including settings resolution; the tenant timeout is 50–1,000 ms, default 350 ms, and includes elapsed work once settings are loaded. This is an inference budget, not a guarantee of full request latency. Timeout, outage, invalid response, schema/checkpoint mismatch or stale/missing commercial features preserve the prior deterministic order. Cold starts can therefore fall back on the storefront even when a longer admin preview succeeds. A separate CPU process normally refreshes commercial serving features every six hours with a 48-hour expiry. Reads are bounded and tenant/schema-scoped; shopper requests do not query Shopify or train models. Refreshing features does not retrain or promote a checkpoint.
Developer and operator integration
Use the existing /search collection parameters for eligible shopper listings and the authenticated Collection endpoints for strategy previews and publication. The optional config.ranking_mode accepts rules or intelligence; omitting it selects rules. No training endpoint or browser provider token is required. The current technical provider identifier is laya_modal: modelKey selects a provider model family, while checkpointKey identifies the actual tenant-scoped immutable model and featureSchemaVersion identifies its compatible encoding. Laya/Modal are implementation details; the product is Indexa Intelligence. Operators configure HTTPS provider endpoints with exact host allowlists and server-only credentials, tenant-scoped base registrations and a separate per-tenant Modal CPU worker/GPU training service. Queue state, source versions, sanitized failures and candidate decisions provide operational evidence. A deployment or healthy worker alone does not establish source readiness, successful model acceptance or commercial benefit.
Open Intelligence in the console.
Schema
Product document
Every search hit and every recommended product has this shape.
| Field | Type | Description |
|---|---|---|
id | string | Numeric Shopify product id. |
handle | string | Shopify handle (stored, not searchable). |
title | string | Product title — the primary search field. |
description | string? | Plain-text description. |
sku | string[] | Variant SKUs. |
model_code | string? | Model code metafield. |
vendor | string? | Brand / vendor (facet). |
gender | string? | Gender metafield (facet). |
primary_category | string? | Primary category (facet). |
category_2 / category_3 | string? | Sub-categories (facets). |
season / year | string? | Merch attributes (facets). |
collections | string[] | Collection handles (facet). |
tags | string[] | Product tags (facet). |
sizes | string[] | Size option values (facet). |
colors | string[] | Colour option values (facet). |
main_color | string? | Primary colour. |
price | float | Active sale price if any, else full price (facet, sortable). |
compare_at_price | float? | Struck-through full price when on sale. |
discount_pct | float | Discount percent (facet, sortable). |
on_sale | bool | discount_pct > 0 (facet). |
available | bool | In stock (facet). Sold-out products are dropped from recommendations. |
score | float | Blended rank (popularity, discount, freshness, availability). Default sort. |
popularity | int | View count from your own analytics. |
published_at | int64? | Publish time, epoch seconds (sortable). |
image_url | string? | Featured image URL. |
url | string | /{lang}/products/{handle}. |
embedding | float[]? | Semantic vector — only when embeddings are enabled. |
Pricing
custom.discounts metafield (per-market, date-bounded) rather than Shopify's native compare-at price — the lowest currently-active discount wins.Storefront
Integration
Two shapes of storefront, one API. A Shopify theme gets a drop-in widget; a headless storefront calls the endpoints and renders with its own components.
Shopify theme (Liquid)
Install the Indexa app, then enable its app embed in the theme editor (Theme settings → App embeds). The embed loads a dependency-free widget that attaches to the theme's own search input, renders inside a shadow root — so the theme's CSS and the widget cannot reach each other — and sends the interaction beacon. Configure your indexer URL, public API key and consent behavior. No file in the theme is modified, so a theme update cannot undo it.
Recommendation carousels are an app block: add it to any section, then pick a model, heading and product count. A block with nothing to show removes itself instead of leaving an empty heading — which is the normal state of popular on a store that has just launched.
Tracking on its own
Two of the six recommendation models are built from view events and nothing else, so a storefront that never sends them has popular and also-viewed permanently empty. If you render your own search and only need the events, the app embed has a Tracking only mode that draws nothing. A headless storefront copies the same file into its build, or calls the beacon itself.
<script type="module" src=".../indexa-analytics.js"
data-indexa-url="https://YOUR-SLUG.api.indexa.now"
data-indexa-key="pk_YOUR_PUBLIC_KEY"
data-indexa-lang="it"
data-indexa-consent="required"></script>Deploy the matching analytics-context.js alongside the tracking module. Keep required consent connected to your consent manager.
| Field | Type | Description |
|---|---|---|
Indexa.view(id?) | function | Report a product view. Without an id it reads the product of the page from Shopify's analytics meta. |
Indexa.click(id, context) | function | Report a search result click with context containing query and search_event_id from that search response. |
Indexa.addToCart(id) / purchase(id) | function | Browser commerce signals. purchase() does not create verified revenue or conversion; use the Shopify webhook. |
Indexa.pageChanged() | function | For a router that does not use the History API. pushState and replaceState are followed automatically. |
Indexa.consent(true | false) | function | With data-indexa-consent="required", nothing is stored or sent until this says so. |
Which client an event belongs to
<script type="module"
src="{{ 'indexa-search.js' | asset_url }}"
data-indexa-url="https://YOUR-SLUG.api.indexa.now"
data-indexa-key="pk_YOUR_PUBLIC_KEY"
data-indexa-lang="it"
data-indexa-currency="EUR"
data-indexa-input="input[name='q'], #Search-In-Modal"></script>
<div data-indexa-reco="related"></div>
<div data-indexa-reco="custom" data-indexa-slug="homepage-hero"
data-indexa-title="La selezione della settimana"></div>| Field | Type | Description |
|---|---|---|
window.Indexa.track(id, type) | function | Report an event the theme knows about and the widget cannot see — an add-to-cart, a purchase. |
window.Indexa.mountReco() | function | Re-scan for carousel placeholders after the theme injects a section at runtime. |
Headless (Hydrogen, Remix, Next)
Call /search, /suggest and /reco/* server-side and render with your own product cards. Recommendations answer with products[] already resolved; the analytics-backed models also return product_ids[], so you can resolve them against your own catalogue when a card needs fields the index does not carry.
Keep the beacon same-origin
/events behind a thin route of your own. A cross-origin sendBeacon that fails a preflight cannot retry — and it fires exactly when the page is navigating away.Advanced
Engine (direct)
The index underneath, reachable with a read-only key. Reach for it when you need a query shape /search does not expose — everything else is better served by /search, which applies redirects, weights, query understanding and merchandising for you.
$TYPESENSE_URL/multi_searchRaw engine query. One or several searches per call.
X-TYPESENSE-API-KEY: $SEARCH_KEY — read-only, collection-scoped.
{
"searches": [{
"collection": "products_it",
"q": "maglione verde",
"query_by": "title,vendor,model_code,sku,tags,primary_category",
"query_by_weights": "6,3,4,4,2,2",
"sort_by": "_text_match:desc,score:desc",
"per_page": 12
}]
}{ "results": [ { "found": 128, "hits": [ { "document": { "id": "7523891", "…": "…" } } ], "facet_counts": [] } ] }Parameters
| Field | Type | Description |
|---|---|---|
collectionreq | string | Target collection, e.g. products_it or products_en. |
qreq | string | The query. Use * to match everything (browse mode). |
query_by | string | Comma-separated fields to search. Default: title,vendor,model_code,sku,tags,primary_category. |
query_by_weights | string | Per-field weights, aligned to query_by. Default: 6,3,4,4,2,2 — tuned per store from real search data. |
filter_by | string | Boolean filter expression: field:=value, field:=[a,b], price:>=n, joined with &&. |
sort_by | string | Sort clause. Default: _text_match:desc,score:desc. At most three fields. |
facet_by | string | Comma-separated facet fields to compute counts for. |
per_page | int | Page size, up to 250. |
page | int | 1-based page number. |
typo_tokens_threshold | int | Min results before typo tolerance kicks in. Indexa uses 5. |
drop_tokens_threshold | int | Set to 0 when a strong attribute is detected, so key terms are never dropped. |
exclude_fields | string | Fields to omit from hits. Always exclude embedding on a results page. |
prefix | bool | Prefix matching. Must be false when embedding is in query_by. |
Semantic (hybrid) search
When embeddings are enabled, append the embedding field to query_by with a trailing weight and set prefix: false. Keyword relevance and vector similarity are fused in one call — no separate vector query.
{
"searches": [{
"collection": "products_it",
"q": "qualcosa di caldo per l'inverno",
"query_by": "title,vendor,model_code,sku,tags,primary_category,embedding",
"query_by_weights": "6,3,4,4,2,2,1",
"prefix": false,
"exclude_fields": "embedding",
"sort_by": "_text_match:desc,score:desc",
"per_page": 24
}]
}Sort by indexed metafields
Keep the standard sort presets, or use sort=mf_custom_priority:asc / :descfor a declared product metafield after reindexing. No additional preset configuration is needed. The Playground discovers available sorts for each language from the live schema.
Shopify single-line text sorts alphabetically; integer/decimal values sort numerically; dates sort chronologically. Decimal sorting uses the engine’s float precision, like prices: very close or large decimal values may tie. Use integer priorities when exact ordering matters (within JavaScript’s safe integer range). Lists, structured values, references and variant metafields are excluded. Missing or invalid values go last in both directions. Existing search and filter values are preserved. A Shopify type change conflicting with an existing sort field needs operator review; the indexer does not destructively recreate that field.
curl -H "X-Indexa-Key: $PUBLIC_KEY" "$INDEXER_URL/search?q=jeans&lang=it&sort=mf_custom_priority:desc&analytics=false"The backend validates the current tenant declaration and language schema. Unknown or malformed metafield sorts return 400. Manual merchandising pins still keep their assigned positions.
Sorting & pagination
The default sort is relevance then score: _text_match:desc,score:desc. score is a blended rank (popularity, discount, freshness, availability) computed at index time. Sortable fields: price, discount_pct, popularity, published_at, score.
Sync
Webhooks
Shopify pushes catalogue changes here. You register the URLs once at onboarding; they are documented because they are part of the surface, not because you call them.
$INDEXER_URL/webhooks/products/{create|update|delete}Upserts or deletes one product across every language collection.
Signed by Shopify (X-Shopify-Hmac-Sha256) and verified against your webhook secret.
{ "received": true }$INDEXER_URL/webhooks/collections/{update|delete}Keeps collection membership honest between reindexes — an edited smart-collection rule changes which products belong without touching any product.
Signed by Shopify (X-Shopify-Hmac-Sha256) and verified against your webhook secret.
{ "received": true }- • Signed with
X-Shopify-Hmac-Sha256and verified against your webhook secret; anything unsigned is rejected with 401. - • On collection-enabled tenants, product and collection webhooks return 200 after persisting a durable job, before catalog and strategy processing. Persistence failure returns a retryable error. Legacy optional deployments may use bounded in-memory processing.
Merchant guide
Use the Indexa console
Sign in, navigation and languages
Open https://app.indexa.now and sign in with the email magic link. Check the active client before changing rules. The sidebar groups Monitoring, Search management, Recommendations, and Resources and settings into collapsible sections; the current page's group opens automatically. The language selector is at the bottom. The interface supports Italian, English, Spanish, Polish, French, German, Portuguese, Dutch, Japanese and Simplified Chinese. Interface language, configured catalog languages and tutorial video language are independent. Changing the panel language does not translate products or add search languages.
Choose the right tool
Analytics measures search performance. Playground tests live search and recommendations without generating analytics events. Relevance weights searchable fields; Synonyms connects equivalent terms. Merchandising pins or hides products for a query/category; save to apply at query time, with no reindex or Apply to Typesense step. Redirects send a query to a validated storefront destination. Collection strategies rank and publish existing Shopify collections using weighted signals and rules, with preparation, per-collection activation and explicit restore. Custom picks defines an exact ordered selection. Complete the look generates product-specific pairings using rules. Synonym changes still require their Apply action.
Custom picks: search, arrange, publish
In /custom-picks, create a list with a stable slug and titles. Search the tenant catalog by product name, SKU or model code, then add from results with thumbnails and titles. Select the catalog language independently of the panel language. Drag selected rows by their handles to reorder; keyboard users can use Space, arrows, Space. Order saves automatically. Missing catalog products remain manageable by ID in the editor; available API products retain saved order. Preview the slug in Playground. Connect GET /reco/custom/{slug}?lang=it&limit=12 to a storefront carousel, or use a Liquid custom recommendation block with data-indexa-slug. Creating a list does not add a theme section.
Complete the look: five steps
Open /complete-the-look and select the catalog language. Step 1 sets name, priority and enabled status. Step 2 selects source products, a Custom Pick list, a category/collection or default scope. Step 3 defines ordered destination categories, product counts and optional pins; blank labels derive from selected category values. Step 4 configures fields, matching methods, weights, required criteria, minimum score and exclusions. Step 5 previews a source product and saves. Previous/Next and step buttons preserve the draft but do not save. Selecting another rule or catalog language returns to step 1. Preview either the unsaved draft or saved enabled rules; enable and save a rule for storefront use.
How a look is selected and ranked
Exactly one enabled rule wins: product scope before list, category, then default. Higher numeric priority wins within a scope; rule ID breaks ties. Define up to six destination categories with one to three products each and up to eight weighted criteria. Methods include same value, shared values, directional compatibility, numeric distance and numeric ratio. Score is 100 times the weighted match sum divided by all configured weights; missing values score zero. Required criteria must match fully, including on pinned products. Pins may bypass the minimum score but still require availability and category eligibility; exclusions win. Candidates are bounded to 200 per destination category plus pins, so results are not exhaustive ranking of all catalog products. Preview reports truncated pools. New metafields need reindexing; edits to existing rules do not.
Indexa looks versus related products
Playground's Complete the look uses GET /reco/complete-the-look/{productId} and saved enabled Indexa rules. Related products uses Shopify RELATED. Shopify COMPLEMENTARY remains a separate API option requiring Shopify Search & Discovery configuration. Indexa does not write shopify--discovery--product_recommendation.complementary_products and does not fall back to Shopify when a look is empty. Empty looks can mean no applicable enabled rule, unavailable products or restrictive criteria. Add a complete-the-look Liquid block or connect the API to the product-page carousel; enabling a rule alone does not install storefront UI.
Fixed sorting and dynamic metafields
Playground keeps the six standard sort presets. Declare a supported scalar product metafield, complete the tenant reindex and reload Playground to see its Ascending/Descending choices under Indexed metafields. Send sort=mf_namespace_key:asc or :desc to /search. Availability depends on the live schema and catalog language. Lists, structured values and variant metafields are not sortable. Missing values go last, and merchandising pins retain their assigned positions. An external storefront's sort selector must be wired by its integrator; adding a metafield does not automatically change the theme.
Read Analytics with consistent filters
Open /analytics, choose a UTC period and optional catalog language; the default is 30 days. KPIs, query rankings, session funnel and daily chart use that selection. Revenue and AOV remain separate per currency. Search-assisted revenue is credited to the last search before a verified paid order in the same session; it is not a measure of incremental sales caused by search. The ordered funnel may show fewer purchases than the conversion KPI when browser click/cart signals are missing. Zero revenue alone does not prove no sales: check consent, checkout session attributes and the paid-order webhook. See the Analytics V1 guide for formulas and limitations.
Academy and video coverage
The full-width /tutorials library has a featured video, scrollable covers, learning outcomes and use cases. Eight guides cover console navigation, Merchandising, Custom picks, the Complete the look wizard, recommendation models, zero results, Analytics and sorting. Switch Italian/English video independently of interface language. Captions are embedded, and YouTube is contacted only after play. Public English tutorials are at https://indexa.now/tutorials. The refreshed Custom picks video demonstrates catalog search, thumbnails and drag-and-drop ordering. The Complete the look video follows all five steps through preview, activation and saving. All eight topics have separate Italian and English captioned videos.
Admin
Admin
Server-side only, behind a bearer token. Never call these from a storefront.
$INDEXER_URL/admin/previewRun a search the way a shopper would get it, without recording it, and get back what the engine did. This is what the console's playground calls.
Authorization: Bearer $ADMIN_KEY — server-side only.
| Field | Type | Description |
|---|---|---|
q | string | The query to try. |
lang | string | Language. |
collection | string | Preview a category page instead of a search. |
per_page | int | Default 24. |
sort | string | Same fixed presets and validated dynamic metafield sorts as /search. |
filter_<field> | string | Every filter form the public /search takes — equality, filter_not_<field>, filter_<field>_min / _max — works here too. |
raw_filter | string | A raw Typesense filter_by expression, ANDed with everything else. Accepted only on this route, because it is interpreted rather than bound: on a browser-facing endpoint it would hand any visitor arbitrary filters and arbitrarily expensive queries. Use it for the OR across different fields that the filter_* grammar cannot express. |
{
"hits": [ /* … */ ], "found": 5,
"understood": { "sizes": ["XL"], "colors": ["Verde"], "genders": [] },
"relaxed": { "terms": ["inverno"], "sizes": ["XL"] },
"debug": {
"searchTerm": "maglione",
"queryBy": "title,vendor,model_code,sku,tags,primary_category",
"weights": "6,3,4,4,2,2",
"filters": ["available:true", "colors:=["Verde"]"],
"rule": null,
"attempts": [ { "found": 0, "filters": ["available:true", "sizes:=["XL"]", "colors:=["Verde"]"] } ],
"tookMs": 62
}
}- • Never written to
search_events. A merchant testing a phrase twenty times would otherwise invent a popular query and move the suggestions their own shoppers see. - •
debugexposes this store's configuration — effective weights, the merchandising rule matched — which is why it needs the admin key rather than being a flag on the public endpoint. - • A malformed
raw_filtercomes back as 400 carrying the engine's own message (Could not find a filter field named \zzz\in the schema) plus the expression you sent. The field is written by iterating, so the error has to be readable; every other failure is still a 500.
$INDEXER_URL/admin/collectionsList Shopify collections, saved strategies, readiness and preparation progress.
Authorization: Bearer $ADMIN_KEY — server-side only.
{ "collections": [ /* {collection, strategy, revision, status} */ ], "prerequisites": { "ready": false, "issues": [] }, "sync": { /* preparation status and counts */ } }- • Use the tenant TYPESENSE_ADMIN_API_KEY as ADMIN_KEY. Public search keys and storefront API keys are not accepted. JSON bodies are limited to 64 KiB.
- • Collection handles must match ^[a-z0-9][a-z0-9_-]{0,254}$. Read the current revision before saving or publishing. Errors use {error:{code,message}}: 400 invalid, 401 unauthorized, 409 preparation/revision/lease/drift conflicts, 413 payload_too_large, 503 unavailable.
- • Each collection has id, handle, title, sortOrder, productCount and automated. strategy is null and revision is 0 before the first save. Setup failures are explicit errors, not an empty collection list.
$INDEXER_URL/admin/collections/syncPrepare the tenant: reconcile webhooks, backfill paid orders, sweep inventory and update indexed stock.
Authorization: Bearer $ADMIN_KEY — server-side only.
{ "sync": { /* status, phase, started_at, completed_at, orders, products, error */ } }- • Returns 202 while preparation runs asynchronously. Poll GET /admin/collections for sync progress. Completion requires status=succeeded and completed_at, plus ready prerequisites. Retry failed or interrupted runs; this does not enable or publish a strategy.
- • POST /admin/collections/sync is reserved for preparation; GET at that path reads a collection whose handle is sync.
$INDEXER_URL/admin/collections/{handle}Read a collection, saved strategy, revision, live preview and operational status.
Authorization: Bearer $ADMIN_KEY — server-side only.
{ "collection": { /* metadata and productIds */ }, "strategy": null, "revision": 0, "preview": null, "preview_error": { "code": "unavailable", "message": "Collection signals unavailable" }, "prerequisites": { "ready": false, "issues": [] }, "sync": { /* preparation state */ }, "status": "disabled" }- • Illustrative unconfigured response. A successful preview is populated; if strict signals cannot be loaded it is null with preview_error. Collection status is disabled, preparation_required, ready_to_publish, published, unresolved, drift or restore_required.
- • preview includes collection, config, revision, currentIds, proposedIds, moves, changedProducts, ranking and products. products is keyed by ID with available presentation fields such as title and image_url, including unindexed Shopify members. ranking reports orderedIds, per-product scores/contributions/rules, signal coverage and unevaluatedIds. Compatible services also return preview.provenance: requestedMode and usedMode (rules or intelligence), nullable provider/modelKey/checkpointKey/featureSchemaVersion, latencyMs, comparisons, fallback, reason, candidateCount and rerankedCount. candidateCount counts indexed products in the bounded strategy window; rerankedCount counts products evaluated by the model, not changed positions. Rules scores describe candidate generation, not AI feature attribution.
$INDEXER_URL/admin/collections/{handle}/previewPreview an unsaved configuration without saving it or changing Shopify.
Authorization: Bearer $ADMIN_KEY — server-side only.
{
"config": {
"enabled": false,
"sorters": [
{
"param": "units_sold",
"direction": "desc",
"weight": 100
}
],
"lookback_days": 30,
"depth": 200,
"rules": {
"push_down_tags": [],
"push_up_tags": [],
"new_days": 0,
"unavailable_last": true,
"pins": [],
"tag_scores": {},
"vendor_scores": {}
}
}
}{ "preview": { /* collection, config, revision, currentIds, proposedIds, moves, changedProducts, ranking, products, provenance */ } }- • Config fields: enabled (boolean), optional ranking_mode (rules or intelligence; defaults to rules when omitted; null/unknown values rejected), sorters (1–18 unique signals with direction asc/desc and weight 0–100; at least one positive), lookback_days (7 or 30), depth (1–20,000), and rules. Signal names: price, compare_at_price, discount_pct, days_since_published, units_sold, revenue, orders_count, views, add_to_cart, ctr, conversion_rate, revenue_per_view, inventory, in_stock_variants_ratio, days_to_sell_out, tag_score_adjust, vendor_score_adjust, random. Rules contain push_down_tags, push_up_tags, new_days, unavailable_last, pins ({id: numeric product ID string, position: one-based integer}), tag_scores and vendor_scores. Unknown fields are rejected.
- • The example is a disabled draft using units sold descending. Rules rank the complete collection. Intelligence only reranks a bounded leading window of at most 32 positions within depth, further limited by tenant topN and fixed/unindexed products. Exact pins, effective priority groups, membership and the remaining order survive reranking. A disabled draft can preview AI if tenant/deployment Intelligence is enabled. Missing strict signal data returns 503 rather than a fabricated score.
- • Successful Intelligence provenance can include productMovements: [{id, fromPosition, toPosition}], with one-based final constrained positions relative to the rules-only baseline. This is distinct from preview.moves/changedProducts, which compare against current Shopify order. Optional productEvidence contains [{id, wins, comparisons, opponents:[{id, preferred}], features:[{name, source, value}], salesLookbackDays}]. value is numeric or boolean; at most eight actual mapped inputs are exposed per evaluated product. wins/opponents describe observed tournament decisions, not causal feature importance. Evidence is admin-only and omitted on fallback; legacy services may omit provenance entirely. A successful inference can evaluate products without moving any of them.
- • Collection inference has a separate maximum 45-second deadline. Inspect usedMode and fallback rather than assuming requested Intelligence was used. Shopper inference remains bounded by at most 1,000 ms. On older services omit ranking_mode for Rules compatibility; Intelligence requires provenance support.
$INDEXER_URL/admin/collections/{handle}Save a strategy with revision checking. Disabled drafts do not reorder Shopify.
Authorization: Bearer $ADMIN_KEY — server-side only.
{
"config": {
"enabled": false,
"sorters": [
{
"param": "units_sold",
"direction": "desc",
"weight": 100
}
],
"lookback_days": 30,
"depth": 200,
"rules": {
"push_down_tags": [],
"push_up_tags": [],
"new_days": 0,
"unavailable_last": true,
"pins": [],
"tag_scores": {},
"vendor_scores": {}
}
},
"revision": 0
}{ "strategy": { /* saved config and publication journal */ }, "revision": 1, "status": "disabled" }- • Use revision 0 only for the first save; subsequently use the revision from detail/save. A stale revision returns 409.
- • Enabling requires successful data preparation and current prerequisites. An enabled strategy opts into scheduled and event-driven publication; save is not an immediate publish command. The config accepts the same ranking_mode as preview. Saving a disabled Intelligence draft neither enables tenant AI nor changes Shopify order.
$INDEXER_URL/admin/collections/{handle}/publishPublish the saved strategy to Shopify using the expected revision.
Authorization: Bearer $ADMIN_KEY — server-side only.
{
"revision": 1
}{ "status": "published", "changedProducts": 3, "strategy": { /* saved config and publication journal */ } }- • Requires the current saved revision. Enabled publication needs successful preparation and prerequisites. Optional minChangedProducts is an integer from 1 to 20,000.
- • May return status=skipped with reason disabled, unchanged or below_threshold. First publication captures the original baseline and switches Shopify sort mode to MANUAL. Order is shared across markets. Drift or an unresolved mutation blocks automatic overwrite. Intelligence mode requires fresh successful Intelligence inference, including after sort/membership recomputation; fallback blocks publication. Preview is not a frozen publication snapshot. Successful publication stores provenance in strategy.last_ranking; restoring the baseline clears it.
$INDEXER_URL/admin/collections/{handle}/restoreRestore the captured original Shopify order and sort mode, then disable the strategy.
Authorization: Bearer $ADMIN_KEY — server-side only.
{ "status": "restored", "strategy": { /* disabled strategy and updated journal */ } }- • Send no body or an empty JSON object. Restore is explicit and does not require a new successful preparation. It needs the captured original baseline and access to Shopify/storage. Retry an interrupted restore to reconcile its state; do not force another publication.
$INDEXER_URL/admin/reindexFull reindex of every language, run asynchronously.
Authorization: Bearer $ADMIN_KEY — server-side only.
{ "started": true }$INDEXER_URL/admin/search-keyMint a fresh read-only engine key, scoped to your collections.
Authorization: Bearer $ADMIN_KEY — server-side only.
{ "search_key": "…" }$INDEXER_URL/healthLiveness, and the languages this indexer serves.
{ "ok": true, "languages": ["it", "en"] }curl -X POST "$INDEXER_URL/admin/reindex" \
-H "Authorization: Bearer $ADMIN_KEY"Reference
Rate limits & CORS
The public endpoints are reachable by anyone who knows your host, so they carry a per-IP budget. Counting happens in the indexer process — one instance per client — rather than in a shared database: a round trip per keystroke would cost more latency than the search itself.
| Field | Type | Description |
|---|---|---|
/search, /suggest | 120 requests / minute / IP | Search runs on every keystroke. |
/reco/* | 60 requests / minute / IP | A couple of calls per page load, and each one can cost a Shopify call. |
/events | 60 requests / minute / IP | A browsing session sends far fewer beacons than keystrokes. |
Over budget returns 429 { "error": "rate limit exceeded" }. Need a higher ceiling for a campaign or a bulk job? Tell us.
CORS
Your store has an origin allow-list. When it is empty every origin is accepted — the data is public catalogue, and guessing a shop's domains wrong breaks its search. When it is set, a request from an origin outside it gets 403 { "error": "origin not allowed" }, and preflights answer 204. Ask us to set it once your domains are final.
Reference
Errors & status codes
| Field | Type | Description |
|---|---|---|
200 | OK | Search, suggest, recommendations, admin search-key, webhook received. |
202 | Accepted | Reindex started. |
204 | No content | Beacon accepted. Sent before the write. |
400 | Bad request | Malformed body on a write. |
401 | Unauthorized | Missing or invalid API key on a storefront endpoint, bad admin bearer key, or missing/invalid webhook HMAC. |
403 | Origin not allowed | The Origin header is not in this store's allow-list. |
404 | Not found | Unknown route, or an unknown recommendation model. |
405 | Method not allowed | Wrong verb on a public endpoint (GET on /events, POST on /search). |
413 | Payload too large | A beacon body over 8 KB. A beacon is a handful of small objects. |
429 | Rate limit exceeded | Per-IP budget spent. See Rate limits. |
500 | Internal error | Unhandled exception. |
Error bodies are JSON, e.g. { "error": "unauthorized" }.
Take it with you
Postman & LLM reference
Both files are generated from the same source as this page at build time, so a parameter cannot exist in one and be missing from the other.
Postman collection
Every endpoint, pre-wired with variables for your own host and keys. Set INDEXER_URL and the collection runs as-is — the storefront endpoints need no credential at all.
Reference for an LLM
The complete reference as plain text — endpoints, parameters, responses, limits and integration notes — written to be pasted into Claude Code, Codex or any agent, or fetched by one.
Working with a coding agent? Point it at the text file directly — it is the whole reference, endpoints, limits and integration notes included:
curl https://docs.indexa.now/llms-full.txthttps://docs.indexa.now/llms.txt is the short index, for agents that follow the llms.txt convention.