> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wristler.eu/llms.txt
> Use this file to discover all available pages before exploring further.

# Searching

> Words, filters, facets, counts and sorts — the whole of what the catalogue screen needs.

```http theme={null}
GET /v1/watches?filter[q]=rolex submariner blue onder de 10k
```

Scope `catalogue:read`. This is not a second search: the same parser reads the
words and the same index ranks what is left, because a phone showing other
results than the website for the same words is a bug nobody can explain.

> [!NOTE]
>
> **You do not need words.** A request that only narrows — `filter[brand][]=Rolex`
> — is answered by the index too, with the same counts. Browsing by brand is the
> same question as searching for one, and a filter panel is used far more often
> than a search box.

### Drawing the panel before anybody has touched it

```http theme={null}
GET /v1/watches?filter[q]=
```

An empty `filter[q]` is a search with no words: the whole catalogue, ranked, with
every facet and its counts. That is the call to make when the screen opens, before
somebody has typed or picked anything.

Leaving the parameter off altogether is a different request — the plain
cursor-paged list, which carries no facets because nothing was searched.

`filter[seller]` narrows any of this to one business — the shop page's question.
It takes an id rather than a name, because two businesses share a name, and it
combines with words and facets like anything else: "everything this shop has under
€5,000" is one request.

That is the *published* catalogue. A seller's own stock, drafts and sold listings
included, is `/v1/sellers/{id}/watches` and needs `watches:read` for that business.

A price range needs only the end you have: `filter[price][min]=500000` on its own
means "from €5,000", and `filter[price][max]` on its own means "up to". Amounts are
cents, like every other amount here.

## What the parser does with the words

Before anything is searched, the words are read for things that are not words:

| Typed | Understood as |
| - | - |
| `rolex` | brand **Rolex** |
| `submariner` | model **Submariner**, within that brand |
| `dj41` | model **Datejust** and case size **41** |
| `41mm` | case size **41** |
| `onder de 10k`, `under 10k`, `max 5000` | a price ceiling |
| `van 2021`, `from the 90s`, `1990s` | a year, or a range of them |
| `goedkoopste`, `cheapest`, `najtańszy` | a sort, not a search term |
| `blue`, `blauem`, `azul` | dial **blue** |
| `met doos en papieren`, `full set` | what is included |
| `310.30.42.50.01.002` | a reference, kept whole |

Whatever is left over is what the index actually searches on. It reads Dutch,
English, German, French, Spanish, Italian, Portuguese, Finnish and Polish,
because sellers and buyers type in all of them.

## What comes back

```json theme={null}
{
  "data": [ "…listings, most relevant first…" ],
  "meta": {
    "total": 214,
    "pages": 9,
    "facets": {
      "brand": {
        "name": "Brands",
        "group": "Brand",
        "options": [
          { "value": "Rolex", "label": "Rolex", "count": 118, "active": true },
          { "value": "Tudor", "label": "Tudor", "count": 14, "active": false }
        ]
      },
      "price": { "name": "Price", "group": "Price", "range": { "min": 150000, "max": 2500000 } }
    },
    "page": { "currentPage": 1, "from": 1, "lastPage": 9, "perPage": 25, "to": 25, "total": 214 }
  },
  "links": { "first": "…", "next": "…", "last": "…" }
}
```

**meta.total** `integer`

A **count**, not an estimate. The index is asked for a numbered page, which makes
it count the whole matching set rather than stopping as soon as it can answer.

**meta.pages** `integer`

How many pages that is, at the size you asked for.

**meta.facets** `object`

The filter panel, keyed by the name you send back to filter on. A facet has a
`name` and a `group` to head it with, and either `options` — each a `value`, a
`label`, a `count` and whether it is `active` — or a `range`, for price.

**On the first page only.** They describe the whole result set rather than the
slice, so they are the same on every page and repeating them was most of what
made one heavy. Keep the ones you were given.

**The hundred biggest**, plus anything already active — except `brand` and
`seller.name`, which come whole so an A–Z list of them misses nothing. The index knows every
distinct value in the result set, which for `watchModel` is over a thousand and
for `reference` is roughly one per watch — a list nobody scrolls and a response
nobody wants. `reference` offers none for that reason, and so is **left out
entirely** rather than sent as a heading with nothing under it —
`filter[reference]` still works, because somebody looking for one types it. Any
facet that ends up with no options and no range is absent for the same reason, so
draw what you are given rather than expecting a fixed set.

**In the language you asked for.** `name`, `group` and every option `label`
follow `Accept-Language`; `value` never does, because that is what you send back.

**meta.page** `object`

Where in the list you are, in the same shape every other collection here uses —
so a client reads one thing whether somebody typed anything or not. `meta.total`
and `meta.pages` beside it are the index's own counts.

**meta.capped** `boolean`

Only present, and only `true`, when the count stopped at the index's own ceiling
(100,000). A search somebody actually runs never sees it; "everything we have"
might.

### A facet

| | |
| - | - |
| `name` | What to call it, in the reader's language |
| `group` | Which section of a panel it belongs in — `Brand`, `Case`, `Movement`… |
| `options[].value` | What you send back |
| `options[].label` | What to show |
| `options[].count` | How many of the current results have it |
| `options[].active` | Whether this request already filtered on it |
| `range` | For `price` only: the lowest and highest in the current results, **in cents** |

Counts describe the *narrowed* set, so the panel stays honest as somebody ticks
things. A facet that nothing in the results has is left out rather than listed
with a zero.

## Every facet you can filter on

Send them back exactly as they are keyed. All of them are arrays — several values
of the same facet widen, different facets narrow.

| Facet | |
| - | - |
| `brand` | The brand |
| `watchModel` | The model |
| `reference` | The manufacturer's reference |
| `price` | `filter[price][min]` and `[max]`, in cents |
| `attributes.year_of_production` | Year |
| `attributes.state` | New or pre-owned |
| `attributes.condition` | Condition |
| `attributes.category` | Category |
| `attributes.case_size` | Case size in mm |
| `attributes.case_material` | Case material |
| `attributes.bezel_material` | Bezel material |
| `attributes.dial` | Dial colour |
| `attributes.crystal` | Crystal |
| `attributes.movement` | Movement |
| `attributes.bracelet_material` | Bracelet material |
| `attributes.bracelet_color` | Bracelet colour |
| `attributes.clasp` | Clasp |
| `attributes.clasp_materials` | Clasp material |
| `attributes.water_resistance` | Water resistance |
| `attributes.gender` | Who it is made for |
| `attributes.availability` | How quickly it ships |
| `attributes.includes` | Box, papers, full set |
| `seller.location` | Where the business is |
| `seller.name` | Which business |

```http theme={null}
GET /v1/watches
  ?filter[q]=submariner
  &filter[brand][]=Rolex
  &filter[attributes.dial][]=BLACK&filter[attributes.dial][]=BLUE
  &filter[price][min]=500000&filter[price][max]=1500000
```

> [!TIP]
>
> **The values, before you have searched anything**: `/v1/specifications` lists
> every specification with every value it accepts, and tells you what each one is
> called as a filter. That is how to draw a full panel on a screen nobody has
> searched on yet — the facets then fill in the counts.

> [!WARNING]
>
> Facet filters only work on the catalogue. On a seller's own inventory they are
> refused with `400`, because there is no index behind it: a filter that quietly
> does nothing looks exactly like one that worked.

## Sorting

| `sort` | |
| - | - |
| *(omitted)* | Relevance, and whatever the words asked for |
| `price` / `-price` | Cheapest or dearest first |
| `year` / `-year` | By year of production |
| `createdAt` / `-createdAt` | Newest listed first |

The words themselves can sort: "goedkoopste rolex" is a price sort with a brand
filter, and a `sort` you send wins over one the words implied.

`year` is the one sort that only works in a search — it is a specification rather than a
column, so anywhere else it is refused with `400 search_only_sort` rather than ordering
"1998" against "2010" as text: on an inventory, and on a plain `/v1/watches` with neither
`filter[q]` nor a facet (send `filter[q]=` to sort the whole catalogue by year).

## Paging

```http theme={null}
?page[number]=2&page[size]=25
```

Numbered, up to 50 per page, because relevance order has no stable cursor and an
app wants page two. `links.next` is there until there is no next; `meta.pages`
says how many there are.

> [!NOTE]
>
> A seller's own inventory pages by **cursor** instead — `page[after]`,
> `page[limit]` — because walking a live inventory with an offset skips or repeats
> rows when something is inserted halfway through.

## When there is nothing to show

An empty `data` with `meta.total: 0` means nothing matched. If the index is
unreachable the answer is `503` with `code: search_unavailable` — never an empty
page, because empty is an answer and it is not this one.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.