Search items
/v1/searchRuns a semantic search over your catalogue and returns the best-matching items, ranked by a blend of vector similarity, metadata agreement with your filters, price fit and (optionally) colour distance and keyword boosts.
How a search runs
- Negations in the
queryare read out of it: "dress not black", "sweater without wool", "jacket, no hood". The negated words are added toexclude_termsand removed from the text that is embedded, because an embedding cannot negate — "not black" would otherwise pull black items forward. The response'sparsedQueryshows what was read. A negation of degree ("not too tight") is left alone. - The query is embedded and matched against the vector index. Only
items that pass the hard filters take part: the datasets being
searched,
gender,slot,familyandage_group. Up to 150 candidates come back from this stage. - The candidates are priced in the shopper's market when
countryis sent: price, currency, each variant's price, sale price and stock, andonSale/discount. Products that market does not sell are dropped. Withoutcountrythey keep the store's default prices, flaggedproduct.marketExact: false(see Prices in the shopper's market below). exclude_terms,filters.onSale,filters.inStockand, whenstrictPriceFilteristrue, the price range are applied as hard filters on those candidates, against the prices from step 3.- Candidates less than 65% as similar to the query as the best
remaining candidate are dropped (the relevance floor), and the rest
are scored (see Scoring below). The top
limitare returned, so a search can return fewer thanlimititems when only a few are close to the query. Each result carries its full product record plus ascoringbreakdown so you can see why it ranked where it did.
Prices in the shopper's market
A Shopify store can sell in several markets at prices the merchant
sets for each (not converted), and some products are not sold in
every market. Send the shopper's country (two letters, such as
US) and every result is priced as that shopper would pay: price,
currency and each variant's price, compareAtPrice and inStock
are their market's, and product.market names it. The price band,
price scoring and filters.onSale then use those prices too, so
minPrice and maxPrice are in the shopper's currency. Products
their market does not sell are left out.
On a Shopify storefront the shopper's country is Shopify.country on
the page. Without it, or for a country the store does not sell into,
results come back at the store's default market's prices with
product.marketExact: false and the reason in
product.marketFallback, so you can label the prices rather than
present them as the shopper's. product.referencePrice and
referenceCurrency always carry the stored default-market price.
How relevant the results are
Every response carries relevance: the best candidate's raw cosine
similarity (top), how many candidates there were and how many cleared
the floor, and weak. weak is true when top is under 0.2, which
is how a query for something the catalogue does not stock tends to look
("sunglasses" in a clothing store scores about 0.14). It is a hint, not
a verdict: very loose requests ("something for a beach holiday") can sit
near the same level, so use it to word the results as "the closest we
have" rather than to hide them. Nothing is dropped because of it.
Which datasets are searched
- Omit
datasetIds(or send[]) to search every active dataset in your organization. If none is active the request fails with404. - Pass
datasetIdsto search specific datasets. Draft datasets are searched but produce a warning; archived datasets are skipped with a warning; an id that does not belong to your organization fails the request with422.
Hard filters vs. ranking hints
gender, slot, family, age_group, exclude_terms, onSale, inStock and
the strict price range remove items. Everything else in filters — color_family,
fit, pattern, material, features, color_rgb, boost_terms and
a non-strict price range — only re-orders the results. An item that
misses a soft filter is pushed down, not dropped, so a search never comes
back empty just because one preference could not be met. A preference
reorders items that are relevant to the query; it cannot bring back one
the relevance floor dropped.
Every filter that takes a string also accepts an array of strings,
except gender, which takes exactly one value. A single value is
matched exactly; an array matches any of its values. Values are
compared case-insensitively after trimming.
slot, family and age_group are closed vocabularies from the
catalogue taxonomy, and so is every entry of features. A value outside
them could only ever match nothing, so it is refused with 422 and the
error lists what is allowed. The soft filters are not checked: an
unknown colour or fit simply boosts nothing.
Send gender on shopper-facing searches. Without it every department
is searched.
Scoring
Everything is on one scale: relevance to the query. vector is the
item's similarity divided by the best candidate's, so the best match is
1 and one a fifth less similar is 0.8. Each other component is
bounded and added with a weight that says how much relevance it is
worth:
final = vector + 0.2 × metadata + 0.15 × price + 0.2 × rgbColor + 0.15 × keywordBoost
| component | weight | range | what it measures |
|---|---|---|---|
vector |
1 | 0.65–1 | Cosine similarity to the query relative to the best candidate. Candidates under 0.65 are not returned. |
metadata |
0.2 | −1–1 | Agreement with the soft filters. Per field, a match adds and a miss subtracts: color_family 1.5 / 1.5, pattern 0.5 / 0.3, fit 0.4 / 0.2, material 0.3 / 0, features 0.5 / 0 per matching feature, capped at 1.5 (match / miss). Summed and normalised by the match weights of the filters supplied. A field the item has no value for counts neither way. 0 when no soft filters are sent. |
price |
0.15 | −1–0 | 0 inside [minPrice, maxPrice]; outside it a penalty that grows with the relative distance from the nearer bound, −(1 − e^(−distance × pricePenalty / 0.35)): about −0.25 at 10% outside, −0.76 at 50%. 0 when no price range is sent or the item has no price. |
rgbColor |
0.2 | −1–1 | Closeness of item.metadata.color_rgb to color_rgb, as colour difference in CIE Lab (ΔE): 1 for the same colour, 0 at ΔE 30 (roughly white against light blue), −1 at ΔE 60 and beyond (light blue against black). 0 when the item has no measured colour. Only present when color_rgb is sent. |
keywordBoost |
0.15 | 0–1 | Fraction of boost_terms found (case-insensitive substring) in the product name or description. Only present when boost_terms is sent. |
Being inside the price band earns nothing, so an item is never ranked above a more relevant one for its price alone.
Attaching results to a conversation
Pass the chatId of a chat you own and the slotId of a search slot in
that chat's most recent assistant message, and the results are written
into that slot so the stylist can refer to them later. Problems with the
slot never fail the search — they are reported in warnings and the
results are returned regardless. A slotId without a chatId is
ignored with a warning; an unknown or inaccessible chatId does fail
the request.
Warnings
warnings is only present when there is something to say. Slot-related
warnings are plain strings; dataset warnings are objects with a
message field.
Request body
application/jsonrequiredNatural-language description of what to find. Trimmed before use. "not X", "without X" and "no X" are read as exclusions (see How a search runs).
Datasets to search. Omit or send [] to search every active dataset
in your organization. Draft datasets are searched with a warning;
archived datasets are skipped with a warning; ids from another
organization fail the request.
Narrow and re-rank the results. Fields marked Hard. remove items;
everything else only changes ordering. Any string-valued field except
gender also accepts an array of strings (match any). Unknown fields
are ignored.
Hard. One of mens, womens or unisex, the same values items
carry in metadata.gender. mens or womens searches that
gender's items plus those tagged unisex; unisex searches unisex
items only; omitting the field searches every department. A single
string only: do not send an array.
menswomensunisexHard. Where the item is worn, or what kind of thing it is,
matched against metadata.slot. A string matches that slot; an
array matches any listed slot. An unknown value returns 422.
Hard. What kind of thing the item is within its slot, matched
against metadata.family: a taxonomy family name such as dress,
pant, t shirt, sneaker or blazer. Families are shared by
every catalogue, so jeans are pant and a tee is t shirt; put the
finer detail ("jeans", "chelsea") in query or boost_terms. A
string matches that family; an array matches any listed family. An
unknown family returns 422 listing the allowed names.
Hard. adult, kids or baby, matched against
metadata.age_group. adult means "not kids and not baby", so items
processed before the field existed still match it. kids and
baby match only items labelled as such. An unknown value returns
422.
Ranking hint. Colour family of the item (navy, white, olive, …). Strongest metadata signal (1.5 added on a match, 1.5 subtracted on a miss).
Ranking hint. Target colour as [r, g, b] integers in 0–255. Adds an
rgbColor component to every result's scoring from the colour
difference (CIE Lab ΔE) to the item's own metadata.color_rgb.
Validated strictly; send null or omit to disable.
Ranking hint. One of skinny, slim, regular, relaxed, oversized or other (0.4 match / 0.2 miss). Items with no fit (footwear, bags) are neither rewarded nor penalised.
Ranking hint. Pattern such as solid, striped, checked, floral (0.5 match / 0.3 miss). See DatasetItemMetadata.pattern for the full list.
Ranking hint. Fibre such as linen, cotton, wool (see DatasetItemMetadata.material for the full list). Items may list several fibres; each match adds 0.3, with no penalty for a miss.
Ranking hint. Taxonomy features as axis:value strings, matched
against metadata.featureKeys (for example neckline:v neck,
sleeve:long, dress length:midi, leg cut:wide leg). Each match
adds 0.5, capped at 1.5 in total; an item is never penalised or
excluded for lacking one. A string that is not a taxonomy
axis:value returns 422.
Hard. Items whose product name or metadata description mentions
any of these terms are removed. Terms match whole words,
case-insensitively, with plurals and participles folded: hood
removes "hooded", stripes removes "striped" and "stripe", sleeve
removes "long sleeve" but not "sleeveless", black does not remove
"blackwatch". A term of several words matches them in order. Must be
an array; a single string is ignored. Negations read from query
are added to this list (see parsedQuery in the response).
Ranking hint. Terms to look for in the product name and metadata
description (case-insensitive substring). Adds a keywordBoost
component equal to the fraction of terms found. Must be an array; a
single string is ignored.
Hard. true keeps only products the shopper can buy: sold in
the market results are priced in (see country), with at least one
variant in stock there. A product whose source lists no variants is
kept. false or omitted applies no filter. The Stylor agent always
sends true.
Hard. true keeps only products on sale for this shopper: an
in-stock variant has a compareAtPrice above its price in the
market results are priced in (see country). false or omitted
applies no filter.
Lower bound of the preferred price band, in the currency results are priced in — the shopper's market's when country is sent, otherwise the store's default. Enables price scoring when sent alone or with maxPrice.
Upper bound of the preferred price band, in the same currency as minPrice. Enables price scoring when sent alone or with minPrice.
How quickly the price penalty grows outside the band; 2 reaches a given penalty at half the distance. Must be positive; anything else uses the default. Only read when minPrice or maxPrice is set. The penalty stays within −1 whatever this is.
When true and a price band is set, items outside the band — and
items with no price — are removed instead of penalised.
The shopper's country, ISO 3166-1 alpha-2 in any case (US, ca).
Results are priced in that shopper's market, the price band,
price scoring and filters.onSale use those prices, and products
their market does not sell are left out. On a Shopify storefront,
read it from Shopify.country. Omitted, results carry the store's
default market's prices, flagged product.marketExact: false.
Anything but two letters returns 422.
Most results to return. Must be 1–100; out-of-range values are rejected, not clamped. Fewer come back when fewer candidates clear the relevance floor (see How a search runs).
Id of a chat owned by your organization. When sent, the chat must exist; combine with slotId to store the results in the conversation.
Id of a search slot in the chat's most recent assistant message. On
success the top results are written into that slot as
{ itemId, description, displayOrder } entries. Ignored (with a
warning) when chatId is absent or the slot cannot be found.
Response
Up to limit items, best match first. Fewer when fewer candidates clear the relevance floor; empty when nothing passed the hard filters.
Unique item id. It is the same id the queue entry was given at upload time.
The dataset the item belongs to.
The organization that owns the dataset.
Only active items are listed and searchable.
activearchivedIngest pipeline version the item was processed with.
What Stylor's product labeller read from the item's photos and listing
when it was processed. Fields with an enum are closed lists; the
taxonomy (slot, family, features) is shared by every catalogue.
Items processed before a field existed may lack it.
Merchant-facing product data. Everything here comes from your upload or your dataset configuration, not from AI.
Where the item came from. Copied from the dataset's store configuration at processing time.
When the processed item was first written.
When the item was last rewritten by a sync or reprocess.
Raw cosine similarity from the vector index, before normalisation. Comparable across searches, unlike scoring.vector.
Per-result breakdown of the ranking formula. Components that were not active for this request are omitted.
Legacy alias of scoring.vector.
Legacy alias of scoring.metadata.
Legacy alias of scoring.final.
How well the best candidate matched the query. A signal for how to present the results; nothing is dropped because of it.
The best candidate's raw cosine similarity, after the hard filters. null when there were no candidates.
true when top is under 0.2 (or null): the catalogue may not carry what was asked for, and the results are only the nearest items. Very loose requests can also land here.
Candidates that passed the hard filters.
Candidates that cleared the relevance floor; results holds up to limit of them.
Only present when negations were read out of query. What was searched for, and the terms added to exclude_terms.
The text that was embedded, with the negated phrases removed. The original query when nothing else would have been left.
Terms read from the query and excluded, in addition to any sent in filters.exclude_terms.
Only present when at least one warning was raised.
Errors
40Every error is JSON with a single error string unless noted. Match on the status code; the message is for humans.
Request body must be valid JSON.The request body could not be parsed as JSON.
Request body must be a JSON object.The request body is empty, or parsed to something other than a JSON object (for example an array or a string).
Chat not found or you do not have permission to access it.chatId was sent but no chat with that id belongs to your organization.
No active datasets found for this organization. Activate one via the dashboard (Dataset → Settings) or through the REST API before proceeding.datasetIds was omitted or empty and none of your datasets is active.
All requested datasets are archived. Reactivate at least one via the dashboard or REST API.Every dataset in datasetIds is archived.
No datasets found for this organization. Create one via the dashboard or REST API, or pass datasetIds to use draft/archived datasets before proceeding.Your organization has no datasets at all.
chatId must be a non-empty string.chatId was sent but is not a string or is only whitespace.
The 'query' field must be a non-empty string.query is missing, not a string, or blank after trimming.
The 'query' string must be shorter than 512 characters.query is longer than 512 characters after trimming (exactly 512 is accepted).
The 'limit' must be a number.limit was sent but is not a finite number (for example a string or null).
Limit must be between 1 and 100.limit is below 1 or above 100. The value is not clamped.
datasetIds must be an array.datasetIds is present but not an array (including null).
filters must be an object.filters is present but is not an object (including null or an array).
country must be a two-letter ISO 3166 country code, such as 'US'.country was sent (non-null) but is not a string of exactly two letters. Case does not matter.
filters.onSale must be a boolean.filters.onSale was sent (non-null) but is not true or false.
filters.inStock must be a boolean.filters.inStock was sent (non-null) but is not true or false.
Unknown slot ["<value>", …]. Allowed: accessory, bag, jewellery, …A value in filters.slot is not a taxonomy slot. The same message names family or age_group when one of those holds an unknown value, and lists every allowed value.
Unknown features ["<value>", …]. Each is "axis:value" from the taxonomy, e.g. "neckline:v neck".An entry of filters.features is not a taxonomy axis:value string.
Invalid color_rgb: RGB must be an arrayfilters.color_rgb was sent (non-null) but is not an array.
Invalid color_rgb: RGB must have exactly 3 values [r, g, b]filters.color_rgb does not have exactly three entries.
Invalid color_rgb: Each RGB value must be an integer between 0 and 255An entry of filters.color_rgb is not an integer in 0–255.
Invalid datasetIds for this organization: <id, id, …>One or more values in datasetIds do not belong to your organization. The offending ids are listed, comma-separated.
Failed to generate embedding for the provided query.The embedding provider failed to embed the query. Safe to retry.
curl -X POST "https://stylor.ai/api/v1/search" \
-H "Authorization: Bearer $STYLOR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "relaxed linen shirt for a summer wedding",
"filters": {
"gender": "mens",
"family": "shirt",
"color_family": [
"white",
"beige"
]
},
"limit": 10
}'{
"results": [
{
"itemId": "9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f",
"organizationId": "0b7c3d9e-1f2a-4b3c-9d8e-7f6a5b4c3d2e",
"datasetId": "c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"status": "active",
"version": 4,
"product": {
"name": "Linen Camp Collar Shirt",
"sku": "LCS-WHT",
"url": "https://shop.example.com/products/linen-camp-collar-shirt",
"price": 89,
"currency": "USD",
"market": "us",
"marketExact": true,
"onSale": true,
"discount": 20,
"referencePrice": 119,
"referenceCurrency": "CAD",
"description": "<p>Breathable linen with a relaxed camp collar.</p>",
"careInstructions": "Machine wash cold",
"variants": [
{
"variantId": "gid://shopify/ProductVariant/41611609636909",
"sku": "LCS-WHT-M",
"label": "M / White",
"options": [
{
"name": "Size",
"value": "M"
},
{
"name": "Color",
"value": "White"
}
],
"inStock": true,
"quantity": 4,
"price": 89,
"compareAtPrice": 111,
"referencePrice": 119
}
],
"assets": {
"images": [
{
"originalImageUrl": "https://shop.example.com/cdn/lcs-wht-1.jpg",
"type": "model full body",
"uploaded": {
"id": "5d6e7f80-1a2b-4c3d-9e8f-7a6b5c4d3e2f",
"key": "images/5d6e7f80-1a2b-4c3d-9e8f-7a6b5c4d3e2f.jpeg",
"url": "https://stylor.ai/api/v1/image/images/5d6e7f80-1a2b-4c3d-9e8f-7a6b5c4d3e2f.jpeg"
}
}
],
"generated": {
"thumbnail": {
"url": "https://stylor.ai/api/v1/image/thumbnails/5d6e7f80-1a2b-4c3d-9e8f-7a6b5c4d3e2f.jpeg"
},
"segmented": []
}
},
"setItems": [],
"recommendedItems": [],
"model": {
"height": "6'1\"",
"size": "M"
}
},
"metadata": {
"description": "linen camp collar shirt shirts mens white relaxed collared button front short sleeve",
"searchTextVersion": 2,
"gender": "mens",
"age_group": "adult",
"color_rgb": [
246,
243,
236
],
"color_family": "white",
"colors": [
"white"
],
"pattern": "solid",
"material": [
"linen"
],
"fit": "relaxed",
"slot": "top",
"family": "shirt",
"features": [
{
"axis": "neckline",
"value": "collared",
"confidence": 0.95
},
{
"axis": "placket",
"value": "button front",
"confidence": 0.98
},
{
"axis": "sleeve",
"value": "short",
"confidence": 0.99
}
],
"featureKeys": [
"neckline:collared",
"placket:button front",
"sleeve:short"
],
"taxonomyVersion": 4
},
"origin": {
"siteUrl": "https://shop.example.com",
"brandId": "example",
"brand": "Example Co.",
"country": "CA"
},
"createdAt": "2026-05-02T14:11:08.000Z",
"updatedAt": "2026-08-19T09:40:22.000Z",
"score": 0.4812,
"scoring": {
"vector": 1,
"metadata": 0.6667,
"price": 0,
"final": 1.1333
},
"vecNorm": 1,
"metaBonus": 0.6667,
"blendedScore": 1.1333
}
],
"relevance": {
"top": 0.4812,
"weak": false,
"candidates": 142,
"aboveFloor": 9
},
"warnings": [
{
"message": "Dataset \"c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d\" is still in draft mode and may not have complete or production-ready data."
}
]
}