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

# Implementing search with the SDK

> Build a Shopify theme search experience with the @commerce-blocks/sdk using autocomplete, prepare/execute, facets, and sorts from Liquid metaobjects.

## Overview

This is the SDK equivalent of [Implementing search in Liquid](/developers/implementing-search-in-liquid). The same four moving pieces apply — autocomplete, prepare, execute, facets/sorts — but each is a method on a controller instead of a hand-rolled `fetch`. The SDK handles request deduplication, the `search_id` lifecycle, abort signals, and reactive state for you.

Use this version when you've already installed [`@commerce-blocks/sdk`](/sdk/installation) in your theme. For a no-dependencies version, see the [Liquid + Fetch guide](/developers/implementing-search-in-liquid).

## Prerequisites

* The Layers Shopify app installed and synced.
* The SDK installed with sorts and facets mirrored from metaobjects — see the [setup snippet](/developers/rendering-facets-with-the-sdk#prerequisites).
* The [Storefront Pixel](/shopify-integration/storefront-pixel) installed so `deviceId` / `sessionId` are populated automatically.

## Controllers used

| Controller         | Purpose                                             |
| :----------------- | :-------------------------------------------------- |
| `client.suggest()` | Typeahead suggestions, including semantic redirects |
| `client.search()`  | Prepare + execute the full search request           |

Both expose the same reactive `state` signal and `subscribe()` shape as the collection controller.

## 1. Autocomplete in the header

`client.suggest()` debounces input changes, aborts the previous request, and emits suggestions on its state signal. It returns the same redirect metadata as the Autocomplete API — when a typed query matches a [semantic redirect](/platform/semantic-redirects), `_meta.redirect.url` is populated and you should navigate the shopper directly.

```html theme={null}
<form action="/search" method="get" role="search">
  <input type="search" name="q" autocomplete="off" id="search-input" />
  <ul id="search-suggestions" hidden></ul>
</form>
```

```js theme={null}
const input = document.getElementById('search-input')
const list = document.getElementById('search-suggestions')

const suggest = window.layers.suggest({ debounce: 200 })

suggest.subscribe(({ data }) => {
  if (!data) return

  if (data._meta?.redirect?.url) {
    window.location.href = data._meta.redirect.url
    return
  }

  list.innerHTML = data.matchedQueries
    .map(
      (q) => `<li><a href="/search?q=${encodeURIComponent(q)}">${q}</a></li>`,
    )
    .join('')
  list.hidden = data.matchedQueries.length === 0
})

input.addEventListener('input', (event) => {
  const query = event.target.value.trim()
  if (!query) {
    list.hidden = true
    return
  }
  suggest.execute(query)
})
```

The controller handles debounce, abort, and dedup internally. No manual `AbortController` plumbing.

## 2. Prepare the search before the results page

`prepare()` is a first-class method on `client.search()`. Call it the moment a shopper commits to a search — the SDK kicks off the request, caches the resulting `search_id` against the query, and `execute()` automatically reuses it on the next page.

**Option A — Prepare on submit.** Best when you can intercept the form submit so prepare keeps running during navigation.

```js theme={null}
const form = document.querySelector('form[role="search"]')
const search = window.layers.search()

form.addEventListener('submit', async (event) => {
  event.preventDefault()
  const query = form.q.value.trim()
  if (!query) return

  // Fire and forget — the SDK persists the search_id internally
  search.prepare({ query })

  window.location.href = `/search?q=${encodeURIComponent(query)}`
})
```

**Option B — Prepare on the search results page.** Simpler, but loses the in-flight head start.

```liquid theme={null}
<script>window.layersSearchQuery = {{ search.terms | json }}</script>
```

```js theme={null}
const search = window.layers.search()
await search.prepare({ query: window.layersSearchQuery })
```

<Tip>
  Don't `await` `prepare()` on the critical path. It returns a result the SDK can keep working on in the background while you render the next page.
</Tip>

## 3. Execute the search

On the search results page, `execute()` reuses the prepared `search_id` automatically — you don't pass it. The SDK falls back to standard processing if the 15-minute cache has expired or no prepare ran.

```js theme={null}
const search = window.layers.search()

search.subscribe(({ data, isFetching, error }) => {
  if (isFetching) showLoading()
  if (error) showError(error.message)
  if (data) {
    renderGrid(data.products)
    renderFacets(data.facets, data.priceRange)
    renderPagination(data.page, data.totalPages)
  }
})

const query = window.layersSearchQuery
const params = new URLSearchParams(location.search)

await search.execute({
  query,
  sort: params.get('sort') || undefined,
  filters: readActiveFiltersFromUrl(params),
  page: Number(params.get('page')) || 1,
})
```

Controller options merge across calls, so subsequent interactions only pass what's changing:

```js theme={null}
search.execute({ sort: 'price-asc', page: 1 }) // change sort, reset page
search.execute({ filters: { color: 'Red' } }) // add filter, keep sort
search.execute({ page: 2 }) // pagination, keep everything else
```

## 4. Render facet and sort controls from Liquid

The control surfaces are identical to the collection-page equivalents — see [Rendering facets with the SDK](/developers/rendering-facets-with-the-sdk) and [Rendering sort orders with the SDK](/developers/rendering-sort-orders-with-the-sdk). On a search page, filter sort metaobjects by the `search` scope:

```liquid theme={null}
{% assign layers_sorts = shop.metaobjects['app--278936322049--sort_order'] | sort: 'order' %}
{% assign layers_facets = shop.metaobjects['app--278936322049--facet'] %}

<section class="search-controls" data-query="{{ search.terms | escape }}">
  <select id="sort-order" name="sort">
    <option value="">Relevance</option>
    {% for sort in layers_sorts %}
      {% assign scopes = sort.scope.value %}
      {% if scopes contains 'search' %}
        <option value="{{ sort.code.value }}">{{ sort.name.value }}</option>
      {% endif %}
    {% endfor %}
  </select>

  <aside class="filters">
    {% for facet in layers_facets %}
      <section data-facet-code="{{ facet.code.value }}">
        <h3>{{ facet.name.value }}</h3>
        <div class="filter-values" data-loading="true"></div>
      </section>
    {% endfor %}
  </aside>
</section>
```

## Recommended request flow

The end-to-end timeline:

<Steps>
  <Step title="Header submit">
    Shopper hits enter. Call `search.prepare({ query })` (non-blocking) and navigate to `/search?q=...`. The SDK persists the resulting `search_id` so the next page can use it.
  </Step>

  <Step title="Results page render">
    Liquid renders the sort dropdown and empty facet groups from metaobjects, including the active sort/filter state from the URL.
  </Step>

  <Step title="Execute">
    Page JS calls `search.execute({ query, sort, filters, page })`. The SDK reuses the prepared `search_id` automatically. The subscriber renders the grid, facets, and pagination as soon as data lands.
  </Step>

  <Step title="Interaction">
    On sort change or filter toggle, call `search.execute({ … })` with just the changed fields. Mirror the state to the URL via `history.replaceState`.
  </Step>
</Steps>

## Why the SDK over Fetch

| Concern                      | Fetch version                                      | SDK version                                                                                          |
| :--------------------------- | :------------------------------------------------- | :--------------------------------------------------------------------------------------------------- |
| Debounced autocomplete       | DIY debounce + `AbortController`                   | `client.suggest({ debounce })`                                                                       |
| `search_id` lifecycle        | `sessionStorage` plumbing across pages             | Handled internally                                                                                   |
| Re-renders                   | DIY DOM updates                                    | Signal subscriber                                                                                    |
| Sticky options               | Track manually                                     | Merged across `execute()` calls                                                                      |
| Click feedback / attribution | Read `attributionToken`, POST to feedback endpoint | Storefront Pixel handles standard themes; SDK exposes `attributionToken` on results for custom flows |
| Errors                       | Hand `try/catch`                                   | Structured `ClientError` with `isRetryable()`                                                        |

## Troubleshooting

**Suggestions show but redirects don't fire.** The redirect lives at `data._meta.redirect.url` on the suggest state. Make sure your subscriber checks for it before rendering the suggestion list.

**`execute` is slow on the results page.** Prepare wasn't called, or more than 15 minutes have passed. The SDK falls back to normal processing — confirm prepare runs before navigation.

**Filters don't apply.** Aliases must be registered in `filterAliases` on the client. URL-friendly keys (`color`) only resolve to API codes (`options.color`) if the alias is configured.

## See also

* [Implementing search in Liquid (Fetch)](/developers/implementing-search-in-liquid) — the no-dependency equivalent
* [Rendering facets with the SDK](/developers/rendering-facets-with-the-sdk)
* [Rendering sort orders with the SDK](/developers/rendering-sort-orders-with-the-sdk)
* [Rendering blocks with the SDK](/developers/rendering-blocks-with-the-sdk) — recommendation blocks anchored to search/collection pages
* [Client methods](/sdk/api-reference/client-methods) — `client.search()` and `client.suggest()`
* [Filtering](/sdk/api-reference/filtering) — filter DSL reference
