# Quickstart

Run OrbSearch on your machine, load a small furniture store and read why a search found what it found.

From a fresh clone to a search that explains itself. You need Docker, `make`, `curl` and [jq](https://jqlang.org), and every command runs from the repository root.

The example store sells furniture in German, so its queries are German: "graues Sofa" is a grey sofa.

### Start the stack

```sh title="Terminal"
make .env
docker compose up --detach --wait
```

`make .env` writes your settings with fresh secrets. The first start builds the images from source and takes a few minutes.

Search answers at once, with nothing to find yet:

```sh title="Terminal"
curl -s 'http://127.0.0.1:7800/v1/search?query=sofa' | jq .detail
```

```json title="Answer: 503 · 3.2 ms"
"Nothing is searchable yet: upload a catalog and publish a release; the first index run makes it searchable."
```

The next steps do what the answer asks.

### Keep the management key at hand

Searching needs no key, but changing what search finds does. Keep the key and the management API's address in two variables:

```sh title="Terminal"
key=$(sed -n 's/^ORBSEARCH_MANAGEMENT_KEY=//p' .env)
api=http://127.0.0.1:8000/api/v1
```

The commands on every other page reuse `$key` and `$api`.

### Publish a release

A [release](/docs/reference/glossary#release) is the store's whole search configuration, one JSON file per area, such as `synonyms.json`. Store the example store's files as a release and publish it:

```sh title="Terminal"
id=$(jq -n '{files: [inputs | {(input_filename | split("/") | last): .}] | add}' tests/fixtures/home/*.json \
  | curl -s -H "Authorization: Bearer $key" -H 'Content-Type: application/json' -d @- $api/releases \
  | jq -r .id)
curl -s -X POST -H "Authorization: Bearer $key" $api/releases/$id/publish | jq -r .next
```

```text title="Answer: 202 · 21 ms"
The release is published and indexed once a catalog arrives: send one with PUT /api/v1/catalog.
```

jq gathers the files into one request body, keyed by file name. The release now waits for a catalog to build the search from.

### Upload the catalog

The [catalog](/docs/reference/glossary#catalog) holds the store's products, categories and guides, one JSON object per line. Upload it and wait for the [index run](/docs/reference/glossary#index-run) that makes it searchable:

```sh title="Terminal"
run=$(curl -s -X PUT -H "Authorization: Bearer $key" -H 'Content-Type: application/x-ndjson' \
  --data-binary @tests/fixtures/home/catalog.jsonl $api/catalog | jq -r .run.id)
until curl -s -H "Authorization: Bearer $key" $api/index-runs/$run \
  | jq -e '.state | IN("queued", "running") | not' >/dev/null; do sleep 1; done
curl -s -H "Authorization: Bearer $key" $api/index-runs/$run \
  | jq '{state, rejected: [.report.rejections[]?.examples[] | {id, message}]}'
```

```json title="Answer: 200 · 13 ms"
{
  "state": "succeeded",
  "rejected": [
    {
      "id": "TEPPICH-SILO",
      "message": "Invalid type: string \"249,00 €\", expected a JSON number."
    }
  ]
}
```

The run left one row out on purpose: the rug `TEPPICH-SILO` writes a price as text. Everything else is live.

### Search

Search the store for "graues Sofa":

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

```json title="Answer: 200 · 19 ms · release b25a6aaa"
{"category":"wohnen/sofas","filters":{"color":"grey"},"text":"","complete":true}
["product:SOFA-ASKA-2","product:SOFA-NIKO-SCHLAF"]
```

The query was [resolved](/docs/reference/glossary#resolution), not matched as text: "Sofa" named the category `wohnen/sofas` and "graues" the color grey. So the answer lists the two grey sofas and nothing else.

### Read the trace

Add `debug=1`, and the answer says why each product is where it is. Here is the first one:

```sh title="Terminal"
curl -s 'http://127.0.0.1:7800/v1/search?query=graues%20Sofa&debug=1' \
  | jq -r '.trace.steps[] | select(.step == "rank")
      | .sections[] | select(.type == "product")
      | .hits[0] | .entity, .why[].sentence'
```

```text title="Answer: 200 · 5.3 ms · release b25a6aaa"
product:SOFA-ASKA-2
In the results because “Sofa” named the category “Sofas”, read by the title "Sofas" of the category wohnen/sofas.
In the results because “graues” named Farbe “Grau”, read by the label "Grau" of the color grey.
Its business score of 0.58 orders it among the hits that match as well; sales_30d adds the most, scoring 0.86 at weight 0.5.
```

The [trace](/docs/reference/glossary#trace) holds every step of the search, from reading the query to assembling the answer, each with what it decided:

| Step        | Decision                                               |
| ----------- | ------------------------------------------------------ |
| `normalize` | Reads “graues Sofa”                                    |
| `resolve`   | “Sofa” → category wohnen/sofas · “graues” → color grey |
| `rules`     | 1 rule checked, none applied                           |
| `sort`      | The request has words, which rank by relevance.        |
| `plan`      | 5 searches, listing products                           |
| `execute`   | One call to Meilisearch: 5 searches                    |
| `facets`    | 10 facets counted                                      |
| `rank`      | SOFA-ASKA-2 first of 2 products                        |
| `assemble`  | 2 products                                             |

OrbSearch's own steps took 0.55 ms and the engine's call 3.31 ms, 3.86 ms in all.

[The trace](/docs/search#the-trace) walks through them.

### Open the panel

Open [localhost:8000](http://localhost:8000) and create your account: the first person to open the panel becomes its owner. Its **Playground** shows any search as shoppers see it, with the reasons beside every product.

## Stop the stack

```sh title="Terminal"
docker compose down
```

The data stays, and `docker compose up --detach --wait` brings the store back as you left it. To start over from nothing, delete the data too:

```sh title="Terminal"
docker compose down --volumes
```

## If something fails

Most failures are a busy port or an index run that did not succeed.

### A port is busy

`docker compose up` stops when another program holds 8000, 7800, 7700 or 5432. Move the port in `.env` and start again:

```sh title=".env"
CONTROL_PORT=8001
APP_URL=http://localhost:8001
```

Then use the new port in the commands above. [Self-hosting](/docs/self-hosting) lists every port.

### Search still finds nothing

List the index runs. A failed run says why in `error`, and no run at all means the release is not published or the catalog not uploaded:

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

## Next

- [Your own catalog](/docs/your-own-catalog): send your shop's export instead of the example store.
- [Concepts](/docs/concepts): releases, snapshots and index runs, and how they meet.
- [Search and browse](/docs/search): what a search asks and what comes back.
