# 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](/docs/reference/glossary#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:

```sh title="Terminal"
curl -s -H "Authorization: Bearer $key" $api/rules | jq -c '.rules[] | {id, state: .state.id, summary}'
```

```json title="Answer: 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`](/docs/reference/release-files/rules) · [`GET /api/v1/rules`](/docs/reference/management-api#list-rules)

## Effects

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

```json title="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`](/docs/reference/release-files/rules#then.resolve) decides what a word means and [`rewrite`](/docs/reference/release-files/rules#then.rewrite) removes words. [`sections`](/docs/reference/release-files/rules#then.sections) orders the sections, and `place` puts [banners and teasers](/docs/banners) beside the results.

**Reference:** [`then`](/docs/reference/release-files/rules#then) · [`pin`](/docs/reference/release-files/rules#then.pin) · [`boost`](/docs/reference/release-files/rules#then.boost) · [`hide`](/docs/reference/release-files/rules#then.hide) · [`filter`](/docs/reference/release-files/rules#then.filter) · [`redirect`](/docs/reference/release-files/rules#then.redirect)

## Conditions

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

```json title="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`](/docs/reference/release-files/rules#when) · [`query`](/docs/reference/release-files/rules#when.query) · [`resolved`](/docs/reference/release-files/rules#when.resolved)

## What the trace says

The [trace](/docs/reference/glossary#trace) names every rule a request met and what became of it. Here is the sofa page's:

```sh title="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)'
```

```json title="Answer: 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.

| Step    | Decision                        |
| ------- | ------------------------------- |
| `rules` | Applied: home/sofa-sale         |
| `rank`  | SOFA-ASKA-2 first of 4 products |

OrbSearch's own steps took 0.48 ms and the engine's call 1.74 ms, 2.22 ms in all.

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](/docs/reference/trace#rules) · [`state`](/docs/reference/trace#rules.rules.state)

## Add a rule

To put the Niko sofa bed first in every sofa search, add the rule to a [draft](/docs/reference/glossary#draft). Shoppers see nothing of it until you publish.

**In the panel:**

**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.

**Through the API:**

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

```sh title="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}'
```

```json title="Answer: 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:

```sh title="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}]}'
```

```json title="Answer: 200 · 8.7 ms · release 7750c384"
{"rule":"niko-leads-every-sofa-search","state":"applied","effects":[{"effect":"pin","target":"product:SOFA-NIKO-SCHLAF","placed":1}]}
```

<Callout type="warn">
    A preview searches the live index. A pin, boost or bury the draft adds or changes shows in the trace, but moves hits
    only once the draft is live. Hides, filters, redirects, banners and switching a rule on or off show at once.
</Callout>

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

```sh title="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
```

```text title="Answer: 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`](/docs/reference/management-api#create-rule) · [`PATCH .../rules/{rule}`](/docs/reference/management-api#change-rule) · [`PUT .../position`](/docs/reference/management-api#move-rule) · [`RuleFields`](/docs/reference/management-api#RuleFields) · [`POST /api/v1/preview/page`](/docs/reference/management-api#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`](/docs/reference/management-api#ListedRule.conflicts)

## Schedules

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

```json title="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`](/docs/reference/release-files/rules#schedule) · [`time`](/docs/reference/management-api#preview-page.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`](/docs/reference/limits#targets_per_query) · [`pins_per_query`](/docs/reference/limits#pins_per_query) · [every limit](/docs/reference/limits)

## Next

- [Sponsored products](/docs/sponsored-products): sell a pin to a brand, and count its views and clicks.
- [Banners and teasers](/docs/banners): place a campaign above, among or below the results.
- [Releases](/docs/releases): draft, preview, publish and roll back.
