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

# Search & Discovery

> Fetch config, run instant + full search, render filters, do image search, and attribute interactions with the Glood Hydrogen Search SDK

# Search & Discovery

The SDK wraps Glood's headless **Search v1** APIs so you can build a fully custom search
experience — instant (type-ahead) search, a full results page, faceted filters, and visual
(image) search — rendering everything with your own React components.

<Note>
  Search is a **v1** headless API on its own host (`https://search.glood.ai`), independent of the
  recommendations app's version. Registering `search()` pins it to v1 automatically — you can
  run search (v1) and recommendations (v3) on the same client.
</Note>

## Setup

Register the search app on your Glood client, alongside (or instead of) recommendations:

```tsx theme={null}
// app/lib/glood.ts
import { createGlood, search } from '@glood/hydrogen';

export const glood = createGlood({
  apiKey: 'gl_sf_your_storefront_token',
  myShopifyDomain: 'your-store.myshopify.com',
  debug: import.meta.env.DEV,
}).use(search());
```

Then wrap your app in `<GloodProvider>` (inside Shopify's `Analytics.Provider`) exactly as
for recommendations — see [Installation](/for-developers/glood-hydrogen-sdk/installation).

<Warning>
  Add `https://search.glood.ai` to your CSP `connectSrc`, or the browser blocks every search
  fetch and event. See [Content Security Policy](/for-developers/glood-hydrogen-sdk/content-security-policy).
</Warning>

## Session bootstrap

When `search()` is registered, `GloodProvider` fires the search **init** call once on mount —
it resolves the visitor, loads their history (browsed / cart / purchased product ids), and
returns a lean feature-flag config plus the storefront token and a fresh `visit_id`. You don't
wire this up: `useSearch` and `useInstantSearch` automatically wait for it to settle before
firing. Read the result with `useSearchInit()` (or `useSearchInitReady()` for the settled flag):

```tsx theme={null}
import { useSearchInit } from '@glood/hydrogen';

function Recents() {
  const init = useSearchInit();               // SearchInitResponse | null
  return <RecentlyViewed productIds={init?.visitor.browsed_products ?? []} />;
}
```

For SSR / non-React use, call the standalone `fetchSearchInit(options, params?)` directly.

## Loading the config

The config carries feature flags, filter definitions, the shop currency/locale, and the
Shopify storefront token. Fetch it once (e.g. in a layout) with `useSearchConfig`:

```tsx theme={null}
import { useSearchConfig } from '@glood/hydrogen';

function SearchProvider() {
  const { config, loading, error } = useSearchConfig();
  if (loading || error || !config) return null;

  const minChars = config.config.minimum_type_ahead_input_count; // e.g. 2
  // ...expose config to your search UI
}
```

## Instant (type-ahead) search

`useInstantSearch` runs a type-ahead query and returns products, collections, pages,
articles, keyword suggestions and autocorrect. On an empty / too-short query the response
carries a `trending` block instead. Pass `{ skip }` to hold fetching until the input clears
the minimum length:

```tsx theme={null}
import { useState } from 'react';
import { useInstantSearch } from '@glood/hydrogen';

function SearchBox() {
  const [term, setTerm] = useState('');
  const { products, suggestions, collections, data, loading } = useInstantSearch(
    { query: term },
    { skip: term.length < 2 },
  );

  return (
    <div>
      <input value={term} onChange={(e) => setTerm(e.target.value)} placeholder="Search…" />
      {data?.corrected_query && <p>Did you mean {data.corrected_query}?</p>}
      {loading && <span>…</span>}
      <ul>
        {suggestions.map((s) => (
          <li key={s.text}>{s.text}</li>
        ))}
      </ul>
      <div className="grid">
        {products.map((p) => (
          <a key={p.product_id} href={`/products/${p.handle}`}>
            <img src={p.image?.url} alt={p.title} />
            <span>{p.title}</span>
            <span>{p.display_price ?? p.price}</span>
          </a>
        ))}
      </div>
    </div>
  );
}
```

<Note>
  The `products`, `suggestions`, `collections` and `pages` return values are convenience
  accessors. The full response — including `articles`, `autocorrect`, `trending` and
  `metadata` — is on `data`.
</Note>

## Full search results

`useSearch` runs a full hybrid (text + vector) product search with applied facets, sort and
page-based pagination. It also returns attribution helpers (see below):

```tsx theme={null}
import { useSearch } from '@glood/hydrogen';

function SearchResults({ query }: { query: string }) {
  const { products, data, loading, error, trackResultClick } = useSearch({
    query,
    sort: 'relevance',
    facets: [{ key: 'color', operator: 'or', value: ['red'] }],
    pagination: { page: 1, limit: 24 },
    options: { includeFacets: true },
  });

  if (loading) return <p>Searching…</p>;
  if (error) return <p>{error.message}</p>;

  return (
    <>
      <p>{data?.pagination.total} results</p>
      <div className="grid">
        {products.map((product) => (
          <a
            key={product.product_id}
            href={`/products/${product.handle}`}
            onClick={() =>
              trackResultClick({
                request_id: data?.request_id,
                search_term: query,
                search_type: 'text',
                products: [{ product_id: product.product_id }],
              })
            }
          >
            <img src={product.image?.url} alt={product.title} />
            <span>{product.title}</span>
          </a>
        ))}
      </div>
    </>
  );
}
```

### Sort & pagination

`sort` accepts `relevance` (default), `title_asc`, `title_desc`, `price_asc`, `price_desc`,
`best_selling`, `oldest`, `newest`. The response `pagination` block gives `total`, `page`,
`limit`, `total_pages`, `has_next_page`/`has_prev_page` and `next_page`/`prev_page`.

## Filters & aggregations

There are two facet endpoints, exposed via the `SearchApp` (or the standalone functions):

| Call                         | Returns                                                                     |
| ---------------------------- | --------------------------------------------------------------------------- |
| `filters()` / `fetchFilters` | DB-driven facet **definitions** (labels, value lists, swatches) — no counts |
| `fetchFilterAggregations`    | **Live counts** for a query + applied filters                               |

```tsx theme={null}
import { useGloodAnalytics, fetchFilterAggregations } from '@glood/hydrogen';

// Facet definitions (structure)
const app = useGloodAnalytics()?.getApp('search');
const { facets } = await app.filters({ collection: '123' });

// Live counts (numbers) — standalone, works in loaders too
const agg = await fetchFilterAggregations(
  { apiKey, myShopifyDomain },
  { query: 'shoe', appliedFilters: [{ key: 'color', value: ['red'] }], facets: [{ key: 'size' }] },
);
```

Render the definitions from `filters()` for stable labels/order, and merge in the live
`aggregations` counts as the query/selection changes.

## Image (visual) search

Pass exactly one image source — a base64 image, a `data:` URL, or an http(s) URL:

```tsx theme={null}
import { useGloodAnalytics } from '@glood/hydrogen';

async function searchByImage(file: File) {
  const app = useGloodAnalytics()?.getApp('search');
  const dataUrl = await toDataUrl(file); // FileReader.readAsDataURL
  const res = await app.imageSearch({ image: dataUrl, pagination: { limit: 24 } });
  return res.products;
}
```

<Note>
  Images are strictly validated server-side: `jpeg` / `png` / `webp` only, 1 KB–5 MB,
  100×100 to 4096×4096 px. Anything else returns `400`.
</Note>

<Note>
  Image search always runs against the app's **`controlEndpoint`** (the non-edge host), because the
  vision/embedding pipeline it needs isn't available on regional edges. If you point `endpoint` at an
  edge, image search stays on the control plane automatically — see
  [Configuration → Edge vs. non-edge routing](/for-developers/glood-hydrogen-sdk/configuration#edge-vs-non-edge-routing-controlendpoint).
</Note>

## Server-side (SSR)

To render search results in the initial HTML, call the standalone fetchers in a Hydrogen
route loader. Pass credentials explicitly — there is no browser context on the server:

```tsx theme={null}
import { fetchSearchResults } from '@glood/hydrogen';

export async function loader({ request, context }) {
  const url = new URL(request.url);
  const query = url.searchParams.get('q') ?? '';

  const results = await fetchSearchResults(
    { apiKey: context.env.GLOOD_API_KEY, myShopifyDomain: 'your-store.myshopify.com' },
    { query, pagination: { page: 1, limit: 24 } },
  ).catch(() => null); // never let search break the page

  return { results };
}
```

Every standalone fetcher (`fetchSearchConfig`, `fetchInstantSearch`, `fetchFilters`,
`fetchFilterAggregations`, `fetchSearchResults`, `fetchImageSearch`, `sendSearchEvent`) takes
`(options, params)` and is isomorphic.

## Events & attribution

Two kinds of events flow to the search backend:

1. **Standard Shopify events** (`product_viewed`, `search_submitted`, `product_added_to_cart`,
   …) are forwarded **automatically** by `GloodProvider` once `search()` is registered — no
   wiring required. Narrow them with `search({ events: ['search_submitted', 'product_added_to_cart'] })`.
2. **Custom `glood:search:*` attribution events**, which only your UI can time, are sent via
   the track helpers:

| Helper                    | Event                            | Call when                          |
| ------------------------- | -------------------------------- | ---------------------------------- |
| `trackSearch(track)`      | `glood:instant_search_triggered` | A search / results set is rendered |
| `trackResultView(track)`  | `glood:search_result_rendered`   | A result enters the viewport       |
| `trackResultClick(track)` | `glood:search_result_clicked`    | A result is clicked                |
| `trackAddToCart(track)`   | `glood:search:add_to_cart`       | A result is added to cart          |
| `trackFilter(track)`      | `glood:search_filter_updated`    | A facet is applied/changed         |

The helpers are returned by `useSearch` and also live on the `SearchApp`
(`useGloodAnalytics().getApp('search')`) for SSR / non-hook use. Each takes a `SearchTrack`
(`request_id`, `search_term`, `search_type`, `products`, `filters`, `source`, `page_type`).

For SSR / non-React contexts, send a single event directly with
[`sendSearchEvent(options, event)`](/for-developers/glood-hydrogen-sdk/api-reference/search-api#sendsearchevent).

<Note>
  Both kinds of events are ingested at the app's **`controlEndpoint`** (non-edge), not `endpoint`.
  This keeps event ingestion on the control plane even when `endpoint` points at a regional edge —
  see [Configuration → Edge vs. non-edge routing](/for-developers/glood-hydrogen-sdk/configuration#edge-vs-non-edge-routing-controlendpoint).
</Note>

## Error handling

Fetchers throw (and the hooks surface via `error`) a `GloodApiError` carrying the HTTP status
and response body:

```tsx theme={null}
import { GloodApiError } from '@glood/hydrogen';

try {
  await fetchSearchResults(options, params);
} catch (error) {
  if (error instanceof GloodApiError) {
    console.error(error.status, error.body); // e.g. 401, { error: 'Invalid authentication' }
  }
}
```

| Status    | Cause                                                                                                                                           |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`     | Wrong `apiKey`, or the key doesn't match `myShopifyDomain`                                                                                      |
| `400`     | Invalid params, or shop plan not set up (`{ error: 'Select a plan' }`)                                                                          |
| `403`     | The shop's plan requires a charge that isn't active                                                                                             |
| CSP error | `https://search.glood.ai` missing from `connectSrc` — see [Content Security Policy](/for-developers/glood-hydrogen-sdk/content-security-policy) |

## See Also

* [Search API Reference](/for-developers/glood-hydrogen-sdk/api-reference/search-api) — full signatures, parameters, returns
* [Search Example](/for-developers/glood-hydrogen-sdk/examples/search) — complete working route
* [Event System](/for-developers/glood-hydrogen-sdk/api-reference/event-system) — how events are transmitted
* [Content Security Policy](/for-developers/glood-hydrogen-sdk/content-security-policy) — required CSP entries
