Merchandising without the ticket queue.
"Pin this SKU during the weekend sale." "Boost our exclusive brand above the private-label stuff." "Hide the discontinued line from search." These are merchandising decisions. They shouldn't require a pull request.
Three rule types, one engine
Trooply's merchandising layer runs on every search — image, text, voice, fusion, widget. It's a stack of rules with three possible effects:
- Pin. Force one or more products to the top of the results in a specific order, ignoring their CLIP score. Use this to guarantee a SKU ships above organic hits for a named query.
- Boost. Multiply a product's final score by
boost_multiplier(0.01–5.0). Soft promotion. A 1.5× boost gives a product a nudge; a 0.5× boost de-emphasises without hiding. - Bury. Remove listed products from the response entirely. The shopper never sees them for the matching query.
Scope: when does a rule fire?
Every rule has a scope_type. Four options, each covering a distinct merchandising intent:
| scope_type | Fires when… | Use case |
|---|---|---|
always | Every search | "Our featured brand stays 1.3× boosted on every query." |
query_exact | Shopper query equals scope_value (case-insensitive) | "When someone searches 'sale', pin these three SKUs." |
query_contains | Shopper query contains scope_value as a substring | "Anything with 'sneaker' in it? Boost the new drop." |
category_match | The result's metadata.category equals scope_value | "Bury two discontinued SKUs from the Footwear category." |
A pin rule end to end
The API call is stable across rule types — only the fields that apply differ.
POST /v1/merchandising/rules
{
"rule_type": "pin",
"scope_type": "query_exact",
"scope_value": "sale",
"product_ids": ["SKU-HERO", "SKU-FEATURED-2"],
"priority": 5,
"start_at": "2026-04-25T00:00:00Z",
"end_at": "2026-04-28T00:00:00Z",
"description": "Spring-sale campaign pins"
}
Three things to notice:
- Order matters for pins. The first
product_idlands at position #1, the second at #2, and so on. Forboostandbury, the list is a set — order is ignored. - The time window is optional. If you fill it in, the rule is a no-op before
start_atand afterend_at. No cron job required; no one has to remember to turn it off Monday morning. - Priority defaults to 100. Lower numbers run first. Use it to order rules that could conflict.
Apply order (this is the important bit)
When multiple rules match the same query, the engine runs them in a fixed order:
- Bury first. Drop every buried
product_idfrom the candidate list. Pinning or boosting a buried product has no effect — it's already gone. - Boost next. Multiply each matched product's similarity score by the winning multiplier (the highest one when multiple boost rules match the same product). Re-sort.
- Pin last. Prepend pinned products to the top in list order. Pinned products keep their real similarity score for display, but get positioned above the organic leaders.
Inside each phase, rules are ordered by priority ascending, then by created_at descending (newest wins ties).
If boost ran first, a buried product could be re-boosted by a conflicting rule and end up back in the response anyway. Bury is the most destructive effect, so it has to be unconditional. If you want a product gone, one bury rule is enough — no boost or pin elsewhere in the stack can override it.
Explainability: every moved result carries a breadcrumb
When a rule affects a result, we stamp the rule's ID and the effect into the product's _match_breakdown.applied_rules array:
"metadata": {
"_match_breakdown": {
"applied_rules": [
{"rule_id": "…", "rule_type": "pin", "effect": "pinned", "description": "Spring-sale pins"},
{"rule_id": "…", "rule_type": "boost", "effect": "×1.50 boost", "description": ""}
],
"explanation": "Strong match (91%) — pinned for campaign."
}
}
The portal's Match Analysis modal reads this so a merchandiser can click any result and see exactly which rule moved it. Six months later when a colleague asks "why is SKU-HERO ranked #1 on 'sale'?", the answer is one click away instead of spelunking through Git.
Boost multiplier reality check
It's tempting to set boost_multiplier: 5.0 and call it a day, but scores are capped at 1.0 after boosting — so anything above ~2.5× produces identical results in practice. The useful range is narrow:
- 1.2–1.5×: subtle nudge. Good for featured brands or new arrivals.
- 1.5–2.0×: strong promotion without overriding relevance.
- 0.5–0.8×: soft suppression — the product stays discoverable but drops below competitors.
- ≥ 2.0× is usually a sign that
pinis the right rule instead — you actually want a guarantee, not a nudge.
Operational notes
Rules are cached per tenant for 60 seconds in-process. When you create, update, or delete a rule via the API or portal, we invalidate the cache for that tenant immediately — so a weekend-sale campaign goes live on the next search, not a minute later.
The portal at /portal/merchandising is the recommended surface for business users. It mirrors the API exactly — dropdowns for rule and scope types, a boost-multiplier slider with a live preview, datetime pickers for the window, an active toggle, and a per-row visual dim for paused rules.
Next up
Merchandising rules work on whatever metadata your catalog ships with. When the default fields (category, brand, price) aren't enough, you need custom fields — that's the next post.