# Patterns

Turn a phrase of the query, such as "bis 220 cm", into a filter, so shoppers find what fits.

A shopper who types "Sofa bis 220 cm" (sofa up to 220 cm) wants a sofa that fits the wall. A [pattern](/docs/reference/glossary#pattern) reads such a phrase and turns it into a filter:

```sh title="Terminal"
curl -s 'http://127.0.0.1:7800/v1/search?query=Sofa%20bis%20220%20cm&debug=1' \
  | jq -c '(.trace.steps[] | select(.step == "resolve") | .detections[] | select(.pattern) | {words, as, pattern}),
      [.sections[] | select(.type == "product") | .hits[].entity]'
```

```json title="Answer: 200 · 5.6 ms · release b25a6aaa"
{"words":"bis 220 cm","as":{"width":{"lte":220}},"pattern":"width-at-most"}
["product:SOFA-ASKA-2","product:SOFA-LUND-3","product:SOFA-NIKO-SCHLAF"]
```

The home store's pattern `width-at-most` read "bis 220 cm" as a width up to 220 cm. Three sofas fit; the Vidar corner sofa, 268 cm wide, does not.

## Write a pattern

A pattern is an entry of `patterns.json`, a [release](/docs/reference/glossary#release) file that no panel screen edits. Here is `width-at-most`, trimmed:

```json title="tests/fixtures/home/patterns.json (trimmed)"
{
    "patterns": [
        {
            "id": "width-at-most",
            "phrases": {
                "de": ["bis {width} cm", "unter {width} cm", "{width} cm breit"],
                "en": ["up to {width} cm", "under {width} cm"]
            },
            "captures": "at_most"
        }
    ]
}
```

- `{width}` captures a number for the attribute `width`, in its unit. A placeholder can also name `price`, `delivery_days` or `rating`, or an option attribute, whose values it reads by code, label or alias.
- `phrases` lists the phrases per locale, or in one list for every language. They match whatever the case.
- `captures` says how the number filters.

**Reference:** [`phrases`](/docs/reference/release-files/patterns#phrases) · [`captures`](/docs/reference/release-files/patterns#captures)

## How the number filters

`captures` makes the number a value or a range. The home store has a pattern of each kind:

| `captures`           | Pattern          | Reads                            | As                                       |
| -------------------- | ---------------- | -------------------------------- | ---------------------------------------- |
| `exact`, the default | `seats-number`   | "3-sitzer" (3-seater)            | `{"seats": 3}`                           |
| `at_most`            | `width-at-most`  | "bis 220 cm"                     | `{"width": {"lte": 220}}`                |
| `at_least`           | `width-at-least` | "ab 200 cm" (from 200 cm)        | `{"width": {"gte": 200}}`                |
| `{"within": 5}`      | `round-diameter` | "durchmesser 120" (diameter 120) | `{"diameter": {"gte": 115, "lte": 125}}` |

**Reference:** [`captures`](/docs/reference/release-files/patterns#captures)

## Add fixed values

`filter` adds what a phrase means without saying it. `round-diameter` reads a diameter, and its `"filter": { "shape": "round" }` adds the shape:

```sh title="Terminal"
curl -s 'http://127.0.0.1:7800/v1/search?query=Tisch%20Durchmesser%20120' \
  | jq -c '.resolution.filters, [.sections[] | select(.type == "product") | .hits[].entity]'
```

```json title="Answer: 200 · 6.3 ms · release b25a6aaa"
{"diameter":{"lte":125,"gte":115},"shape":"round"}
["product:TISCH-RUNA"]
```

"Tisch" (table) names the tables, and the phrase adds a diameter of 115 to 125 cm and the shape round: the Runa dining table. A pattern without a placeholder is a filter alone, such as `on-sale`, which reads "im Angebot" (on sale) as `on_sale: true`.

**Reference:** [`filter`](/docs/reference/release-files/patterns#filter)

## Built-in patterns

Two patterns are built in. `quantity` reads a number with a unit and filters the one attribute that measures it, and `price` reads an amount in the channel's currency. Here they read "Leuchte 3000 Kelvin" (lamp, 3000 kelvin) and "Sofa unter 1000 €" (sofa under 1000 €):

```sh title="Terminal"
for q in 'Leuchte 3000 Kelvin' 'Sofa unter 1000 €'; do
  curl -s -G http://127.0.0.1:7800/v1/search --data-urlencode "query=$q" -d debug=1 \
    | jq -c '.trace.steps[] | select(.step == "resolve") | .detections[] | select(.pattern) | {words, as, pattern}'
done
```

```json title="Answer: 200 · 5.6 ms · release b25a6aaa"
{"words":"3000 Kelvin","as":{"color_temperature":{"lte":3150,"gte":2850}},"pattern":"built-in:quantity"}
{"words":"unter 1000 €","as":{"price":{"lte":1000}},"pattern":"built-in:price"}
```

A number without a bound means about that much, 5 % either way. Bounds such as "bis" and "under" are read in German and English, whatever the shop's language.

When phrases overlap, the longer one wins, and on the same words the release's own pattern beats a built-in one. Words that are exactly a value's label or alias stay that value: the home store lists "rund 120" as an alias of a size, so the size wins over `round-diameter`. `"built_in": { "quantity": false }` switches a built-in pattern off.

**Reference:** [`built_in`](/docs/reference/release-files/patterns#built_in)

## A length that could mean several things

A shelf has a width, a depth and a height, so the built-in pattern cannot tell what "80 cm" means. It keeps the phrase as text and says why in the resolve step's `kept`:

```sh title="Terminal"
curl -s 'http://127.0.0.1:7800/v1/search?query=Regal%2080%20cm&debug=1' \
  | jq -rc '(.trace.steps[] | select(.step == "resolve") | .kept[].reason),
      [.sections[] | select(.type == "product") | .total]'
```

```text title="Answer: 200 · 6.8 ms · release b25a6aaa"
"80 cm" could be width, depth, height, seat_height or diameter; only a pattern of the merchant's own can tell which.
[0]
```

| Step      | Decision                                                             |
| --------- | -------------------------------------------------------------------- |
| `resolve` | “Regal” → category wohnen/regale                                     |
| `relax`   | Nothing matched, so it searches again without category wohnen/regale |

OrbSearch's own steps took 0.56 ms and the engine's call 4.87 ms, 5.43 ms in all.

"Regal" (shelf) names the shelves, but no shelf holds the words "80 cm", so the search finds nothing, even without the category. Only a pattern of your own can say what a length means. In the home store a length in cm is the width, so start a draft and add a pattern that says so:

```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 --arg version "$version" '{version: $version, files: {"patterns.json":
      (.patterns += [{id: "width-about", phrases: {de: ["{width} cm"]}, captures: {within: 5}}])}}' \
    tests/fixtures/home/patterns.json \
  | curl -s -X PATCH -H "Authorization: Bearer $key" -H 'Content-Type: application/json' -d @- $api/imports/$draft \
  | jq -c '.files | keys'
```

```json title="Answer: 200 · 36 ms"
["patterns.json"]
```

Open the search page with the draft's files in place of the live ones:

```sh title="Terminal"
body=$(curl -s -H "Authorization: Bearer $key" $api/imports/$draft | jq '{url: "/suche?q=Regal+80+cm", files}')
curl -s -H "Authorization: Bearer $key" -H 'Content-Type: application/json' -d "$body" $api/preview/page \
  | jq -c '.results.resolution.filters, [.results.sections[] | select(.type == "product") | .hits[].entity]'
```

```json title="Answer: 200 · 35 ms · release bc860b8f"
{"width":{"lte":85,"gte":75}}
["product:REGAL-STEG"]
```

"80 cm" is now a width of 75 to 85 cm, and the search finds the Steg bookshelf.

**Reference:** [`PATCH /api/v1/imports/{import}`](/docs/reference/management-api#change-import) · [`POST /api/v1/preview/page`](/docs/reference/management-api#preview-page) · [`kept`](/docs/reference/trace#resolve.kept)

## How patterns go live

Only the Query API reads `patterns.json`, so a draft that changes only patterns goes live at once, without a rebuild ([Releases](/docs/releases)). The draft says so before it is published; this one is discarded instead:

```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 · 17 ms"
Goes live at once: only patterns.json changed what the Query API reads.
discarded
```

`POST $api/imports/$draft/publish` would make it live.

**Reference:** [`GET .../publication`](/docs/reference/management-api#show-import-publication)

## Next

- [Synonyms](/docs/synonyms): words that mean the same, such as "couch" and "sofa".
- [How search understands a query](/docs/query-understanding): everything a query can name.
- [Releases](/docs/releases): draft, preview, publish and roll back.
