Show the steps for

Rules: pin, boost, bury, hide, redirect

Put a product first, move others up or down, hide one or send a search elsewhere, each with a rule whose condition and effects the trace explains.

A rule changes what a search or a category page answers: when its condition holds (when), its effects take place (then). Rules stand in an order, and where two disagree, the higher one wins. Here are the home store's rules, first to last:

Terminal
curl -s -H "Authorization: Bearer $key" $api/rules | jq -c '.rules[] | {id, state: .state.id, summary}'
200 · 5.4 ms · release b25a6aaa
{"id":"home/sofa-sale","state":"active","summary":"Places banner \"Sofas im Angebot\" on top, places teaser \"Sessel aus Leder, Samt und Stoff\" at 4 on \"Sofas\""}
{"id":"home/out-of-stock-last","state":"disabled","summary":"Buries availability is \"out_of_stock\" (strong)"}
{"id":"home/guides-for-questions","state":"disabled","summary":"Orders the sections content, product, category when query starts with \"wie\" or \"welche\" or \"welcher\" or \"welches\" or \"how\" or \"which\" and on search"}

The sale stands on the sofa page, and the other two rules are switched off. summary is the Query API's own line, which the panel shows too.

Reference: rules.json · GET /api/v1/rules

Effects

A rule holds at least one effect. This one puts the Niko sofa bed first in every search that contains "sofa":

rules.json
{
    "id": "niko-leads-every-sofa-search",
    "description": "Niko leads every sofa search",
    "when": { "query": { "contains": "sofa" } },
    "then": { "pin": [{ "entity": "product:SOFA-NIKO-SCHLAF", "position": 1 }] }
}

The effects merchants reach for most:

  • Pin puts a product at a position, counted from 1, wherever it meets the request's filters.
  • Boost and bury weigh products up or down within relevance, slight, medium or strong: { "where": { "availability": "out_of_stock" }, "strength": "strong" }.
  • Hide takes a product out, such as an outdoor rug: { "entity": "product:TEPPICH-MARE" }. A hide beats a pin.
  • Filter narrows the results, { "where": { "availability": "in_stock" } }, and the answer lists it with source: "rule".
  • Redirect sends the shopper to a page, such as { "to": "content:guide-samt-pflege" } (velvet care), or to a URL.

Before the query is read, resolve decides what a word means and rewrite removes words. sections orders the sections, and place puts banners and teasers beside the results.

Reference: then · pin · boost · hide · filter · redirect

Conditions

when reads the request, and all its keys must hold. A rule without one always applies:

tests/fixtures/home/rules.json (trimmed)
{ "when": { "query": { "starts_with": ["wie", "welche"] }, "on": "search" } }
  • query compares the words typed: is, contains (whole words), starts_with or empty. It reads them literally, so "couch" does not contain "sofa", though the two are synonyms.
  • category holds on a category page: is that page, within it and the pages below.
  • on names the surface: search, category_page or autocomplete.
  • resolved, channel, locale and context read more of the request, and any, all and not combine conditions.

Reference: when · query · resolved

What the trace says

The trace names every rule a request met and what became of it. Here is the sofa page's:

Terminal
curl -s 'http://127.0.0.1:7800/v1/search?on=category_page&category=wohnen/sofas&debug=1' \
  | jq -c '.trace.steps[] | select(.step == "rules") | .rules[] | {rule, state, matched, reason} | del(.. | nulls)'
200 · 3.6 ms · release b25a6aaa
{"rule":"home/sofa-sale","state":"applied","matched":"category is \"wohnen/sofas\""}
{"rule":"home/out-of-stock-last","state":"inactive","reason":"disabled"}

The sale applied on wohnen/sofas (living/sofas), and the bury is switched off. home/guides-for-questions waits for a question such as "wie" (how), so this request could not meet it and the trace leaves it out.

Trace2.22 ms
  1. rulesApplied: home/sofa-sale
  2. rankSOFA-ASKA-2 first of 4 products

OrbSearch 0.48 msMeilisearch 1.74 ms

A rule ends in one state:

  • applied: every effect took place.
  • partly_applied: some effects were moved or skipped, each with a reason, such as "position 1 taken by niko-leads-every-sofa-search".
  • not_applied: it matched, but a higher rule's redirect ended planning.
  • not_matched: the condition failed, and failed names the part.
  • inactive: switched off or outside its schedule, and reason says which.

Reference: rules step · state

Add a rule

To put the Niko sofa bed first in every sofa search, add the rule to a draft. Shoppers see nothing of it until you publish.

Show the steps for

Merchandising → Rules lists the rules in the order they win in, each with its state and what it does. A mark such as 1 conflict says what a higher rule takes from it.

  1. Press Start a draft, then New rule.
  2. Under Applies to, choose Searches that contain words, type sofa under Words and press Enter.
  3. Under Then, choose Add an effect → Pin products, and pick "Niko Schlafsofa" (sofa bed) under Add a product. It takes position 1.
  4. To run it for a while, set Starts and Ends under Only between (optional).
  5. Save puts the rule first. Drag its handle, or press the arrow keys on it, to move it.

Review and publish says how the draft goes live and publishes it, and Discard throws it away.

Show the steps for

Start a draft with the store's export, then add the rule, first in the order unless position says otherwise:

Terminal
draft=$(curl -s -X POST -H "Authorization: Bearer $key" -H 'Content-Type: application/x-ndjson' \
  --data-binary @tests/fixtures/home/catalog.jsonl "$api/imports?name=catalog.jsonl" | jq -r .import.id)
version=$(curl -s -H "Authorization: Bearer $key" $api/imports/$draft | jq -r .version)
jq -n --arg version "$version" '{version: $version, name: "Niko leads every sofa search", fields: {
    when: {trigger: {kind: "query", matching: "contains", phrases: ["sofa"]}},
    then: {pin: [{entity: "product:SOFA-NIKO-SCHLAF", position: 1}]}}}' \
  | curl -s -H "Authorization: Bearer $key" -H 'Content-Type: application/json' -d @- $api/imports/$draft/rules \
  | jq -c '.rules.rules[] | {id, state: .state.id, conflicts}'
201 · 65 ms
{"id":"niko-leads-every-sofa-search","state":"active","conflicts":null}
{"id":"home/sofa-sale","state":"active","conflicts":null}
{"id":"home/out-of-stock-last","state":"disabled","conflicts":null}
{"id":"home/guides-for-questions","state":"disabled","conflicts":null}

Where a higher rule takes something from a rule, such as its pin's position, conflicts says so in a sentence.

Open the search page for "sofa" with the draft's files. redirect=0 keeps the search page instead of the sofa page the query names:

Terminal
body=$(curl -s -H "Authorization: Bearer $key" $api/imports/$draft | jq '{url: "/suche?q=sofa&redirect=0", files}')
curl -s -H "Authorization: Bearer $key" -H 'Content-Type: application/json' -d "$body" $api/preview/page \
  | jq -c '.results.trace.steps[] | select(.step == "rules") | .rules[0]
      | {rule, state, effects: [.effects[] | {effect, target, placed}]}'
200 · 8.7 ms · release 7750c384
{"rule":"niko-leads-every-sofa-search","state":"applied","effects":[{"effect":"pin","target":"product:SOFA-NIKO-SCHLAF","placed":1}]}

Ask how the draft goes live, then publish it with POST $api/imports/$draft/publish, or discard it:

Terminal
curl -s -H "Authorization: Bearer $key" $api/imports/$draft/publication | jq -r .course.sentence
curl -s -X DELETE -H "Authorization: Bearer $key" $api/imports/$draft | jq -r .state
200 · 16 ms
Goes live at once: only rules.json changed what the Query API reads.
discarded

Every write names the draft's version and is refused with 409 if the draft changed since. A rule the fields cannot express, such as the bury, changes through the draft's files.

Reference: POST .../rules · PATCH .../rules/{rule} · PUT .../position · RuleFields · POST /api/v1/preview/page

Who wins

Rules run in three phases: resolve and rewrite, then the highest redirect, then the effects that shape the results. Where two rules collide:

  • Two decide one term, redirect, or set the section order: the higher one.
  • Two pin one product, or boost or bury one target: the higher one's position or strength.
  • Two pins ask for one position: the higher one takes it, the other moves to the next free one.
  • A hide and a pin of one product: the hide, whichever rule is higher.
  • A redirect and rules that shape: the redirect, and the others are not_applied.
  • Filters, hides and removed words: all of them apply.

A rule that holds a redirect beside effects that shape the results or place banners is refused, since the redirect would end planning first.

Reference: conflicts

Schedules

A schedule runs a rule for a while, such as Black Week:

rules.json (trimmed)
{ "schedule": { "from": "2026-11-23T00:00:00+01:00", "until": "2026-11-30T00:00:00+01:00" } }

from is included, until is not, and either end may be open. The Query API checks it against the time the request arrived, never a time the request brings. Before the window the rule is inactive, with the reason "schedule starts 2026-11-23T00:00:00+01:00". A preview takes time, so you can see the first day before it comes.

Reference: schedule · time

Limits

A request switches on at most 4 boost and bury targets and 100 pins per section, in rule order, and the trace marks the rest "skipped: limit of ...". A pin needs a type listed one tile per product, so a type listed per variant or per color skips it.

Reference: targets_per_query · pins_per_query · every limit

Next

On this page