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
make .env
docker compose up --detach --waitmake .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:
curl -s 'http://127.0.0.1:7800/v1/search?query=sofa' | jq .detail"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:
key=$(sed -n 's/^ORBSEARCH_MANAGEMENT_KEY=//p' .env)
api=http://127.0.0.1:8000/api/v1The 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:
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 .nextThe 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:
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}]}'{
"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":
curl -s 'http://127.0.0.1:7800/v1/search?query=graues%20Sofa' \
| jq -c '.resolution, [.sections[] | select(.type == "product") | .hits[].entity]'{"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:
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'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:
- normalizeReads “graues Sofa”
- resolve“Sofa” → category wohnen/sofas · “graues” → color grey
- rules1 rule checked, none applied
- sortThe request has words, which rank by relevance.
- plan5 searches, listing products
- executeOne call to Meilisearch: 5 searches
- facets10 facets counted
- rankSOFA-ASKA-2 first of 2 products
- 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
docker compose downThe 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:
docker compose down --volumesIf 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:
CONTROL_PORT=8001
APP_URL=http://localhost:8001Then 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:
curl -s -H "Authorization: Bearer $key" $api/index-runs | jq -c '.data[] | {id, state, error}'Next
- Your own catalog: send your shop's export instead of the example store.
- Concepts: releases, snapshots and index runs, and how they meet.
- Search and browse: what a search asks and what comes back.