--- title: Search description: Query a catalog with POST /discovery/v1/search — the request body, response fields, and the patterns behind autocomplete and results pages. slug: discovery/search docKind: guide hub: luigisbox-ai --- `POST /discovery/v1/search` answers a shopper's query. It serves both the as-you-type panel and the results page, usually from the same surface. ## Request ```bash curl -X POST 'https://api.eu1.luigisbox.ai/discovery/v1/search?channel_id=lbn_4hj9tv&surface_id=lbs_search_main' \ -H 'Authorization: Bearer ' \ -H 'X-Lbx-Visitor-Id: 8f14e45f-ea0f-4b5c-9a1d-2b3c4d5e6f70' \ -H 'Content-Type: application/json' \ -d '{ "type": "product", "query": "blue cotton shirt", "size": 24, "filters": { "availability": { "eq": 1 }, "price": { "lt": 50 } }, "sort": { "field": "price", "direction": "asc" } }' ``` `channel_id` and `surface_id` are query parameters. Everything else goes in the JSON body. | Query parameter | Required | Notes | |---|---|---| | `channel_id` | Yes | Serving destination | | `surface_id` | Yes | Must be a `search` surface | | Body field | Required | Default | Notes | |---|---|---|---| | `type` | Yes | — | `product`, `category`, `brand`, `article`, `query`, … | | `query` | No | `""` | The shopper's input. Up to 256 characters. | | `size` | No | `10` | 1–100 | | `filters` | No | none | [Filter tree](/discovery/filters/) | | `sort` | No | relevance | `{ "field": …, "direction": "asc" \| "desc" }` — see [sorting](/discovery/sorting-and-pagination/#sorting) | | `cursor` | No | first page | From a previous `next_page_cursor`. Omit it for the first page; an empty string is a `422`. | | `return_fields` | No | surface default | [Field projection](/discovery/fields/) | | `user_id` | No | anonymous | Signed-in shopper | | `personalize` | No | `true` | [Personalize this request](/discovery/personalization/) | Unknown body fields are rejected with a `422`. An empty `query` is valid: the surface answers with whatever it is configured to show for no input. To browse a category instead, use [`/collections`](/discovery/collections/), which takes a scope rather than a query. ## Response ```json { "hits": [ { "@id": "product/sku-1001", "@type": "product", "@title": "Blue Cotton T-Shirt", "price": 29.9, "list_price": 39.9, "availability": 1, "image_url": "https://cdn.example.com/products/sku-1001.jpg", "url": "https://example.com/products/blue-cotton-t-shirt" } ], "total": 412, "total_approx": 412, "guid": "3ab549eb64a20a0c", "next_page_cursor": "eyJvIjoyNCwic…", "pagination_status": "more" } ``` | Field | Meaning | |---|---| | `hits` | The ranked results, best first. Flat objects; `@id` and `@type` always present. | | `total` | Exact count, or `null` when the result set is too large to count exactly | | `total_approx` | Always present; the fallback when `total` is `null` | | `guid` | Identifies this result set — pass it to [`/facets`](/discovery/facets/) | | `next_page_cursor` | Pass back as `cursor` for the next page, or `null` | | `pagination_status` | Why there is or is not a cursor — see [pagination](/discovery/sorting-and-pagination/) | ## Autocomplete Autocomplete is not a separate endpoint. It is the same `search` surface, called with a partial query and a small `size`, typically once per type you want to show: ```js const url = `${API}/discovery/v1/search?channel_id=${channelId}&surface_id=lbs_search_main`; const [products, categories, suggestions] = await Promise.all( ['product', 'category', 'query'].map((type) => fetch(url, { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ type, query: input.value, size: 6 }), }).then((r) => r.json()), ), ); ``` Recommended client behaviour: - **Debounce, do not throttle.** Wait about 120 ms after the last keystroke. A request per keystroke spends rate-limit budget on queries that are never shown. - **Abort superseded requests.** Keep an `AbortController` per input and cancel the previous request when a new keystroke arrives, so a slow early response cannot overwrite a fast later one. - **Mint the token before the first keystroke.** Fetching a token on the first keystroke delays the first response. When the panel and the results page share a surface, the top autocomplete result is the top result on the results page, and retuning one retunes both. ## Filtering as the shopper narrows Facet selections become a `filters` tree. Rebuild it from the selected values on each request rather than accumulating fragments: ```js function buildFilters({ brands, maxPrice, inStockOnly }) { const clauses = []; if (brands.length) clauses.push({ brand: { in: brands } }); if (maxPrice != null) clauses.push({ price: { lte: maxPrice } }); if (inStockOnly) clauses.push({ availability: { eq: 1 } }); if (clauses.length === 0) return undefined; if (clauses.length === 1) return clauses[0]; return { $and: clauses }; } ``` Omit `filters` entirely when nothing is selected — `{}` is not a valid filter node. Each new filter combination is a new result set with a new `guid`, so re-request facets after the search returns. See [Facets](/discovery/facets/). ## Zero results An empty `hits` array with `total: 0` is a normal response, not an error. The [no-results reports](/analytics/overview/#what-you-can-report-on) list the queries your catalog cannot answer. A common recovery is to fall back to a recommender surface — bestsellers, or items related to the shopper's session — so the page still has something to offer. ## Reporting what happened The slate a surface returned is recorded when the request is served. You report what the shopper did next: an `interaction` when they click a result or add it to the cart, and a `transaction` when they buy. Each of those events names the object with a [reference](/analytics/references/), which is what lets Luigi's Box credit it to this search. See [Sending events](/analytics/sending-events/). ## See also - [Filters](/discovery/filters/) · [Facets](/discovery/facets/) - [Sorting and pagination](/discovery/sorting-and-pagination/) · [Selecting fields](/discovery/fields/) - [Product variants](/discovery/variants/) — collapsing size and colour duplicates - [Personalization](/discovery/personalization/)