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, 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

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:

Terminal
curl -s 'http://127.0.0.1:7800/v1/search?query=sofa' | jq .detail
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:

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 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:

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
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 holds the store's products, categories and guides, one JSON object per line. Upload it and wait for the index run that makes it searchable:

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}]}'
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 the store for "graues Sofa":

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

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'
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 holds every step of the search, from reading the query to assembling the answer, each with what it decided:

Trace3.86 ms
  1. normalizeReads “graues Sofa”
  2. resolve“Sofa” → category wohnen/sofas · “graues” → color grey
  3. rules1 rule checked, none applied
  4. sortThe request has words, which rank by relevance.
  5. plan5 searches, listing products
  6. executeOne call to Meilisearch: 5 searches
  7. facets10 facets counted
  8. rankSOFA-ASKA-2 first of 2 products
  9. assemble2 products

OrbSearch 0.55 msMeilisearch 3.31 ms

The trace walks through them.

Open the panel

Open 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

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:

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:

.env
CONTROL_PORT=8001
APP_URL=http://localhost:8001

Then use the new port in the commands above. 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:

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

Next

On this page