# Overlays

Correct the catalog's data for search, add search words or give products badges, and keep it all when the next export arrives.

An [overlay](/docs/reference/glossary#overlay) corrects catalog data for search, or gives it badges, without touching the [catalog](/docs/reference/glossary#catalog). It lives in the [release](/docs/reference/glossary#release), so it stays when the next upload arrives. The home store marks its three bestsellers this way, such as the sofa `SOFA-ASKA-2`:

```json title="tests/fixtures/home/overlays.json (trimmed)"
{
    "overlays": [
        {
            "id": "bestseller-sofa-aska-2",
            "target": { "type": "product", "id": "SOFA-ASKA-2" },
            "badges": { "de": ["Bestseller"], "en": ["Best seller"] },
            "note": "One of the three products that sold most in the last 30 days"
        }
    ]
}
```

The product lookup answers with the badge in the request's language:

```sh title="Terminal"
curl -s 'http://127.0.0.1:7800/v1/product?id=SOFA-ASKA-2' | jq -c '.product | {id, badges}'
curl -s 'http://127.0.0.1:7800/v1/product?id=SOFA-ASKA-2&locale=en' | jq -c '.product.badges'
```

```json title="Answer: 200 · 2.4 ms · release b25a6aaa"
{"id":"SOFA-ASKA-2","badges":["Bestseller"]}
["Best seller"]
```

A search hit carries the badges too where its type's `result` lists `badges`, as the home store's product type does. `note` is for the people who keep the file.

**Reference:** [`badges`](/docs/reference/release-files/overlays#badges) · [`Product.badges`](/docs/reference/query-api#Product.badges) · [`result`](/docs/reference/release-files/types#result)

## Fill what the catalog lacks

`fill` writes an attribute value only where a product has none. Three of the home store's four sofas name no room, so this overlay gives them `Wohnzimmer` (living room):

```json title="overlays.json"
{
    "id": "sofas-wohnzimmer",
    "target": { "type": "product", "where": { "category": { "within": "wohnen/sofas" } } },
    "fill": { "attributes": { "room": "Wohnzimmer" } }
}
```

`target` names one entity by its `id` or every entity of its `type` that matches `where`, never both. `where` is a [selector](/docs/reference/query-api#Selector) over attributes, the category and fields such as `brand` or `price`. Values are read as the export's are, so "zwei Meter" (two meters) for a width is a violation, not a value that vanishes.

Once the export names a sofa's room, `fill` steps aside for it. `set`, written as `"set": { "attributes": { "room": "Wohnzimmer" } }`, replaces the export's values instead, the variants' own included.

<Callout type="warn">
    `set` keeps replacing the export's value after the shop has fixed it, until you remove the overlay. Where the export
    lacks a value, use `fill`.
</Callout>

**Reference:** [`target`](/docs/reference/release-files/overlays#target) · [`fill`](/docs/reference/release-files/overlays#fill) · [`set`](/docs/reference/release-files/overlays#set) · [`Selector`](/docs/reference/query-api#Selector)

## Add search words

`keywords` adds search terms to a product's own. A shopper who types "Balkonteppich" (balcony rug) misses the outdoor rug `TEPPICH-MARE`. The why-not answer, which the Playground shows under **Why isn't it here?**, says what to do:

```sh title="Terminal"
curl -s -H "Authorization: Bearer $key" -H 'Content-Type: application/json' \
  -d '{"request": {"query": "Balkonteppich"}, "entity": "product:TEPPICH-MARE"}' $api/preview/find \
  | jq -r '.sentence, .next[].step'
```

```text title="Answer: 200 · 15 ms · release b25a6aaa"
The query's words do not find it: no searched field of it holds “balkonteppich”, as typed, through a synonym or with a typo. Add a synonym that leads “balkonteppich” to what it holds, or add it to its search words.
add_synonym
add_keywords
```

A [synonym](/docs/reference/glossary#synonym) would lead the word to what another word finds, such as every rug. A keyword adds it to this rug alone:

```json title="overlays.json"
{
    "id": "mare-balkonteppich",
    "target": { "type": "product", "id": "TEPPICH-MARE" },
    "keywords": { "de": ["Balkonteppich"] }
}
```

A plain list instead of the map adds the words in every language.

**Reference:** [`keywords`](/docs/reference/release-files/overlays#keywords) · [`POST /api/v1/preview/find`](/docs/reference/management-api#preview-find)

## Hide a product everywhere

`hide` takes its targets out of every request in every channel: search, category pages and the product lookup alike.

```json title="overlays.json"
{ "id": "hide-sessel-tarn", "target": { "type": "product", "id": "SESSEL-TARN" }, "hide": true }
```

To hide a product from some searches or pages only, write a [rule](/docs/rules) instead, and to leave it out of one channel, change that channel's [assortment](/docs/channels). When a shopper misses a hidden product, the why-not answer names the overlay.

**Reference:** [`hide`](/docs/reference/release-files/overlays#hide)

## End at a time

`until` ends an overlay at a moment, and the search is rebuilt without it then, on its own. This badge, "Neu" (new), lasts until December:

```json title="overlays.json"
{
    "id": "neu-stuhl-riga",
    "target": { "type": "product", "id": "STUHL-RIGA" },
    "badges": { "de": ["Neu"], "en": ["New"] },
    "until": "2026-12-01T00:00:00+01:00"
}
```

Without `until`, an overlay holds until you remove it. One whose product left the catalog waits, and applies again when it returns.

**Reference:** [`until`](/docs/reference/release-files/overlays#until)

## How a change goes live

Overlays change what the search's documents are built from, so publishing a change to `overlays.json` rebuilds the search, even for a `note`. Add the sofa overlay to a [draft](/docs/reference/glossary#draft) and ask how it would go live:

```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: {"overlays.json": (.overlays += [{id: "sofas-wohnzimmer",
      target: {type: "product", where: {category: {within: "wohnen/sofas"}}}, fill: {attributes: {room: "Wohnzimmer"}}}])}}' \
    tests/fixtures/home/overlays.json \
  | curl -s -X PATCH -H "Authorization: Bearer $key" -H 'Content-Type: application/json' -d @- $api/imports/$draft >/dev/null
curl -s -H "Authorization: Bearer $key" $api/imports/$draft/publication | jq -r .course.sentence
```

```text title="Answer: 200 · 23 ms"
Rebuilds the search: overlays.json changed what its documents are built from.
```

A draft's file replaces the published one whole, so it holds the home store's three badges and the new overlay. Publishing builds new indexes beside the live ones while the shop keeps answering ([Releases](/docs/releases#how-a-publish-goes-live)). This draft is discarded:

```sh title="Terminal"
curl -s -X DELETE -H "Authorization: Bearer $key" $api/imports/$draft | jq -r .state
```

```text title="Answer: 200 · 18 ms"
discarded
```

**Reference:** [`overlays.json`](/docs/reference/release-files/overlays) · [`PATCH /api/v1/imports/{import}`](/docs/reference/management-api#change-import)

## Next

- [Rules](/docs/rules): hide, pin or bury a product for some searches and pages only.
- [Importing and mapping](/docs/importing): fix how the export itself is read.
- [Releases](/docs/releases): preview, publish and roll back the draft.
