Complete the Look Recommendations
Complete the look recommendations
POST /api/v1/indexes/{index_name}/recommendations/complete-the-look
Purpose
Surface items that pair with the product a shopper is viewing — "complete the look" / "pairs well with" — rather than alternatives to it. A bikini top surfaces its matching bottoms, cover-ups and bags; a board short surfaces a rash guard; a tech suit surfaces goggles, a cap and anti-fog. The goal is a cohesive, cross-category upsell that raises attach rate and Average Order Value (AOV), complementing (not duplicating) the alternatives shown under Similar Items.
Background
Complete-the-look is content-based: it combines embedding similarity to the viewed product with a curated complement mapping (configured per index by Marqo) that says which product families pair with which. Because it works from product content rather than interaction history, it produces on-theme results on every product detail page — including brand-new products and long-tail items with little or no engagement data.
This makes it distinct from the other recommendation endpoints:
| Endpoint | Returns | Basis |
|---|---|---|
/recommendations/similar | Alternatives / substitutes to the item | Content (vector similarity) |
/recommendations/complementary | Items frequently bought / viewed together | Engagement (co-interaction signals) |
/recommendations/complete-the-look | Items that pair with the item (cross-category) | Content (similarity + curated complement mapping) |
complete-the-look is a good primary source where engagement data is sparse, and a good fallback beneath /complementary where it is rich — both can share the same request filter.
When to use
- PDP "Pairs Well With" / "Complete the Look" shelf: cross-category upsells alongside the viewed product.
- Cold-start / long-tail products: a full, on-theme shelf even with zero engagement history.
- Well-filled shelf: the endpoint tops up thin complement pools with broader on-theme similarity (see How it works).
Example uses
| Use Case | Description | Input Products | Business Impact |
|---|---|---|---|
| PDP "Pairs Well With" | Show items that complete the outfit/set next to the viewed product | Current product being viewed | Raises attach rate & AOV with coordinated items |
| New / long-tail products | Populate the shelf for products with no purchase history | The new product as anchor | On-theme recs from day one, no cold-start gap |
| Content-based fallback | Fill the complementary shelf where co-purchase data is thin | Product from PDP | Keeps the shelf full and relevant |
How it works
- Complement targeting — the viewed product's category resolves to a product family; results are restricted to that family's curated complement families, so the true counterpart ranks first by embedding similarity (e.g. a solid navy top surfaces the matching solid navy bottom first).
- No self / no variants — the product itself is excluded. Its variants are excluded when a product parent field is configured for the index, and results are collapsed by product parent when the index configures collapsing, so each product appears once (no colour/size duplicates).
- Fills the shelf — if the complement pool is thin, the endpoint tops up with broader on-theme similarity (still excluding the item's own family) to fill toward
limit. A shelf can still come back short when the filtered catalog does not hold enough matching products, and a top-up that fails is skipped rather than failing the request. - Optional diversification — results can be rotated across complement families (e.g. a bottom, then a cover-up, then a bag, rather than five bottoms). This, the complement mapping, and optional score tuning are configured per index — contact Marqo to set up or tune complete-the-look for your catalog.
Until the complement mapping is configured for your index, the endpoint still answers: it returns plain content similarity to the seed products, under the request filter, rather than an error.
Filtering (gender, availability, etc.)
The endpoint applies exactly the filter you send (Marqo Filter DSL), in addition to its own product/variant exclusion. Pass the constraints the shelf needs — most importantly in-stock and, on gendered catalogs, the opposite-gender exclusion for the viewed product — so out-of-stock or mismatched-gender items don't appear. For example, on a women's PDP: anyVariantInventoryAvailable:true AND NOT gender:(Men's).
Example (cURL)
curl -X POST https://ecom.marqo-ep.ai/api/v1/indexes/${index_name}/recommendations/complete-the-look \
-H "x-marqo-index-id: ${MARQO_INDEX_ID}" \
-H "Content-Type: application/json" \
-d '{
"documentIds": ["bikini_top_123"],
"limit": 6,
"filter": "in_stock:true AND NOT gender:(Men'\''s)"
}'
Parameters
Filters use the Marqo Filter DSL. Results automatically exclude the input item(s) and their variants. The endpoint uses strict validation: any unrecognised parameter returns a 400 error.
| Name | Type | Required | Description | Example |
|---|---|---|---|---|
| documentIds | array[string] | yes | An array of 1-10 Marqo document IDs (the _id field) to anchor the recommendations on (typically the viewed product). Each value must exactly match the _id of an indexed document, the same _id returned in search and collection responses. | ["bikini_top_123"] |
| limit | integer | no | Max number of results to return. Defaults to 10. | 6 |
| offset | integer | no | Offset for pagination. Defaults to 0. | 0 |
| filter | string | no | Server-side constraints (e.g. in-stock, opposite-gender, brand, price ranges). See Marqo Filter DSL. Applied in addition to the endpoint's own product/variant exclusion. | "in_stock:true" |
| attributesToRetrieve | array[string] | no | Attributes to return for each document. If omitted, the attribute list configured for the index is returned, the same default as search. | ["title", "price", "image_url"] |
| sortBy | object | no | Custom result ordering, in the same shape as the search sortBy. When set, it takes precedence over the endpoint's similarity ranking (and disables diversification). Omit to rank by pairing relevance. | { "fields": [{ "fieldName": "price", "order": "asc" }] } |
| userId | string | no | Accepted for consistency with the other recommendation endpoints, but currently has no effect: this shelf is not personalised. | "abc123" |
| sessionId | string | no | Accepted but currently has no effect (see userId). | "xyz789" |
| profileId | string | no | A named search configuration profile for the request (e.g. different behaviour on PDP vs collection pages). If the profile does not exist, Marqo falls back to the default configuration. | "pdp_default" |
| merchandisingProfileId | string | no | The merchandising profile whose rules (pins, exclusions, boosts, buries, filters) are applied to this shelf. This is separate from profileId, which selects search configuration. If no such profile matches, or the value is empty, the shelf's published rules apply. | "summer_campaign" |
limit and offset must be whole, non-negative numbers. The API does not check this itself: a fractional or negative value is passed to the search engine, which rejects the request with Complete-the-look recommendations failed: <reason>.
Merchandising
Rules published for the Complete the Look context in the Marqo console apply to the shelf. When exactly one documentIds value is sent, the pins published for that product are placed at their positions, and pinned products are fetched under the request filter, so a pinned product that fails the filter (for example, out of stock) is left out.
The remaining rules resolve per seed, because the request is split into one search per seed, up to six searches. With two to six seeds, each search is merchandised with the rules published for its own seed (filters, exclusions, boosts and buries), and no pins are placed. With seven or more seeds the seeds share searches, so only the index-wide rules apply. merchandisingProfileId swaps in the rules of a merchandising profile instead; a profile's per-product rules are looked up only when exactly one seed is sent, and with several seeds the profile's index-wide rules apply.
Response (example)
{
"hits": [
{
"_id": "bikini_bottom_456",
"_score": 0.9012,
"title": "Solid Navy Bikini Bottom",
"price": 45.0,
"image_url": "https://cdn.example.com/bikini_bottom_456.jpg"
}
],
"processingTimeMs": 64,
"limit": 6,
"offset": 0
}
| Field | Type | Description |
|---|---|---|
| hits | array[object] | The recommended documents, each with _id, _score and the retrieved attributes. Pinned products carry the score of their neighbour so the shelf stays sortable by _score. |
| processingTimeMs | integer | Server processing time in milliseconds. |
| limit | integer | The limit applied to the request. |
| offset | integer | The offset applied to the request. |
There is no totalHits field (the shelf is assembled from several searches and has no catalog-wide count) and no personalized field.
| Header | Description |
|---|---|
x-marqo-cache-hit-count, x-marqo-cache-miss-count | How many of the underlying searches were served from cache and how many were not. |
Errors
Error responses have the body {"error": "<message>"}.
| Status | Message |
|---|---|
| 400 | Parameter 'documentIds' is required and must be a non-empty array |
| 400 | All documentIds must be non-empty strings |
| 400 | Number of 'documentIds' must be less than or equal to 10 |
| 400 | Unrecognized param(s): '<name>' for any parameter not listed above |
| 400 | Complete-the-look recommendations failed: <reason> when the underlying search rejects the request (for example, an invalid filter, or none of the seed products exist in the index) |
Unlike /similar, this endpoint does not fall back to a regular search when the seed products are missing from the index.