Skip to content

Search Rules: Pins, Redirects, Banners

Text relevance, stock and sales decide the order of a listing (Relevance) until a merchandiser has a view: the new Kestrel drill should open the Power Tools page, a search for "screws" shouldn't start with the discontinued pack, and the own-brand range should sit ahead of the rest. Rankings say that, per category and per search term, by product SKU.

Some searches aren't for products at all ("returns", "delivery"), or are better answered by a page than a list ("kestrel" by the Kestrel brand page), and some deserve a word above the results ("Trade accounts get 10% off fixings"). A search rule handles a search term in up to three ways, any combination of:

Part Effect
Products Pins, boosts and buries for the term's results (below)
Send to (redirect) A fresh search for the term goes straight to a page on the site
Banner A message, with an optional link, above the term's results

A rule needs at least one of the three.

What a ranking does

A ranking has three lists of SKUs:

List Effect
Pinned Shown first, in the order given
Boosted Shown after the pinned products, ahead of everything else
Buried Shown last

Everything not named keeps its normal order between the boosted and the buried products, and each list keeps the normal order within it (best match, then best sellers, as the Featured sort does).

  • Stock. Pinned products keep their place even when sold out: the merchandiser chose them. Boosted, unranked and buried products each go in stock first, so a sold-out boosted product comes after the boosted ones in stock but still before every unranked product.

  • It only reorders. A pinned product that isn't in the category, or doesn't match the search, isn't added. Filters still apply: a pinned product filtered out is simply not shown.

  • Only under Featured. A shopper who picks Price, low to high or Newest gets exactly that.
  • A variant's SKU means its product. The listing holds products, so DRILL-18V-KIT is saved as the drill's own SKU.
  • A SKU is in one list. Pinned wins over boosted, boosted over buried.
  • At most 100 SKUs in each list. Every SKU must be a product (or variant) in the site; saving names any that aren't.
  • By SKU, not id, so rankings survive site transfer, where product ids are regenerated.

Where rankings apply

Listing Ranking used
A category page (/c/…), nothing searched The category's ranking
/search?q=… The rule for that search term
Searching within a category (/c/…?q=…) The rule for that search term (not the category's)
A manual-list category None: the list is already in the order wanted

A search term's rule applies when what the shopper typed matches the rule's term as a whole, ignoring case and extra spaces: a rule for drill applies to "Drill" and " drill ", not to "cordless drill". A category's ranking is about browsing it; once the shopper searches, the order follows the search.

The public REST endpoints /v1/search/products and /v1/merchandising/products use the same query layer, so they follow the same rankings.

Redirects

A rule's redirect sends a search for its term to a page of the site instead of the results: returns to /p/returns, kestrel to /c/tools/brand/kestrel.

  • Only a fresh search on /search: page 1, no filters, no sort chosen. Paging, filtering or sorting the term's results stays on the results, so a shopper who wants them can still get them. Searching within a category (/c/…?q=…) never redirects.
  • Site paths only. A redirect starts with a single / and has no spaces, backslashes or control characters, so it can't send shoppers to another site (no open redirect). It can't be /search, which would loop.
  • Recorded in search analytics as a search with a result count of 1: it went somewhere, so it doesn't show up among the searches that found nothing.
  • The REST search (/v1/search/products) doesn't redirect; it returns the rule's redirect with the results, and a headless storefront should send the shopper there on a fresh search.

Banners

A rule's banner is shown above the results on /search and when searching within a category.

  • Plain text, at most 300 characters; themes escape it, so HTML shows as typed.
  • An optional link: a site path (/p/trade-accounts) or an https:// address, with link text that defaults to "Find out more". A link that's neither is refused.
  • /v1/search/products returns it as banner (text, linkUrl, linkText).

Managing rules

  • Search terms: Search > Rules and ranking (/admin/search/ranking) lists the rules with their products, redirect and banner, with a link to see each term's results, and adds, edits and deletes them. The editor's products are optional; Send this search to takes a site path and Banner takes text, a link URL and link text. ?term=drill opens the editor for that term.
  • Categories: Catalogue > Merchandising trees, open a tree, Ranking on a node. The Ranking column shows each node's counts. Manual-list and group nodes don't offer it. Preview shows the ranked order.
  • REST (spec-first, see the API reference): /v1/admin/search/ranking-rules (list, create, get, update, delete; each rule has pinned, boosted, buried, redirect and banner, and an update replaces the whole rule) and /v1/admin/search/category-ranking/{nodeId} (get, put; three empty lists clear it).
  • MCP: productRanking and manageProductRanking (see MCP), so an agent can act on plain requests such as "pin the new Kestrel 18V drill at the top of Power Tools", "bury the discontinued screws when people search screws" or "what's pinned on drills?".
  • Names, not just SKUs. A product can be given as its SKU, a variant SKU, its URL name (seoName) or its exact name ignoring case; a name two products share is refused with each candidate's name and SKU. A category (category) can be its node id, its path (/c/tools/power-tools or tools/power-tools) or its name in the primary tree ignoring case; a name two categories share is refused with their paths.
  • One product at a time. pin (at position, 1-based, default after the existing pins), boost, bury and unrank (back to the normal order) take products and a category or a search term (query), and change only those products: a product moves out of any other list, the rest of the ranking, and a rule's redirect and banner, are kept. Pinning, boosting or burying for a term with no rule creates one; unranking a rule's last product deletes it unless it has a redirect or banner.
  • Whole lists. setRule (SKU or name lists, redirect, bannerText, bannerLinkUrl, bannerLinkText) and setCategory replace everything, so anything left out is cleared; deleteRule and clearCategory remove it.
  • Readable results. rules and category list each product as SKU and name. preview (a category, a query, or both for a search within a category) runs the real query, under Featured, and returns the first results (limit, default 12, at most 48) as shoppers see them, each marked pinned, boosted, buried or normal, plus the pinned products the listing doesn't hold (pinnedNotListed).

A site may have up to 500 search-term rules. Changes apply from the next page load.

How it works

Rankings are applied at query time, so nothing is reindexed when one changes.

  • A category's ranking is MerchandisingNode.ranking (ProductRanking), set through MerchandisingTreeService.setNodeRanking. Search-term rules are SearchRankingRule documents (searchRankingRules collection, unique on catalogue and term), through SearchRankingRuleService. Both check SKUs with ProductRankingResolver, which swaps variant SKUs for their product's. For MCP, ProductReferenceResolver turns names into SKUs (one ProductService.findByReferences query for every reference) and CategoryReferenceResolver names into nodes (findNode, then findNodeByPath, then MerchandisingTreeService.findNodesByName); the one-list edits are ProductRanking.pin, boost, bury and without.
  • MerchandisingQueryService.search picks the ranking (above) and RankingSort turns it into the Typesense sort_by:
_eval([(skuId:=`A`):7,(skuId:=`B`):6,(skuId:=[`C`] && inStock:true):5,(skuId:=[`C`] && inStock:false):4,
       (skuId:!=[`A`,`B`,`C`,`D`] && inStock:true):3,(skuId:!=[`A`,`B`,`C`,`D`] && inStock:false):2,
       (skuId:=[`D`] && inStock:true):1]):desc,_text_match:desc,popularity:desc

Pins score from 5 + the number of pins down to 6, boosted 5 in stock and 4 sold out, unranked 3 and 2, buried 1 in stock and 0 sold out. The clauses don't overlap, so each product scores one tier. The tail is Featured's own order (best match, then best sellers). Typesense allows three sort fields, which this uses. A product with no SKU counts as unranked. - Facet counts are unaffected: only the main query, which returns the products, is sorted. - Looking up a search term's rule is one indexed Mongo read per search (SearchRankingRuleService.findRuleForQuery, which returns the whole rule: ranking, redirect and banner). If it fails, the search runs without a ranking, redirect or banner. - The storefront's SearchController redirects a fresh search and puts the banner on the model as searchBanner; MerchandisingPageController does the banner for a search within a category. Redirects and banner links are validated on save (SearchRankingRuleServiceImpl.sitePath and link) and again on import.

Tests

  • RankingSortTest: the sort expression, tiers, de-duplication across lists, quoting.
  • MerchandisingQueryServiceTest: a category's ranking under Featured, a chosen sort ignoring it, a search term's rule replacing the category's, a term with no rule, manual lists.
  • MerchandisingQueryServiceTypesenseRuntimeTest (needs TYPESENSE_RUNTIME_URL): a rule's order from a real Typesense, and a chosen sort winning.
  • ProductRankingResolverTest, SearchRankingRuleServiceImplTest: variant SKUs, unknown SKUs, limits, term normalisation, one rule per term, import.
  • ProductRankingAdminRenderTest, SearchRankingAdminControllerTest and SearchControllerRulesTest (REST: admin rules, and the redirect and banner on /v1/search/products), ProductRankingToolsTest (MCP).
  • ProductReferenceResolverTest, CategoryReferenceResolverTest, ProductRankingTest: names, variant SKUs, ambiguity, unknown products and categories, pin positions, unranking.