Shopify Predictive Search API: Build a Search Box

Shopify’s predictiveSearch query on the Storefront API answers a partially typed search term with products, collections, pages, articles and suggested queries, capped at ten results. It is the API behind a type-ahead box. You send the characters typed so far, name the types and searchableFields you want, and get typed lists back rather than one mixed connection you have to sort out yourself.

That cap is the first thing to design around. limit accepts a value from 1 to 10, and limitScope decides what the number counts. ALL spends the whole budget across every type together. EACH gives every type its own allowance. A dropdown that shows five products, three collections and two suggestions needs EACH.

Queries in this post were written against Storefront API 2026-07, the current stable version as of September 2026.

What predictiveSearch gives you back

PredictiveSearchResult has five fields, one per resource type: products, collections, pages, articles and queries. Each one is a plain list, not a connection, so there is no cursor and no pageInfo. Whatever comes back is the whole response.

query PredictiveSearch($q: String!) {
  predictiveSearch(
    query: $q
    limit: 5
    limitScope: EACH
    types: [QUERY, PRODUCT, COLLECTION]
    searchableFields: [TITLE, PRODUCT_TYPE, VENDOR, VARIANTS_SKU]
    unavailableProducts: HIDE
  ) {
    queries {
      text
      styledText
      trackingParameters
    }
    products {
      id
      title
      handle
      trackingParameters
      featuredImage {
        url
        altText
      }
      priceRange {
        minVariantPrice {
          amount
          currencyCode
        }
      }
    }
    collections {
      id
      title
      handle
    }
  }
}

queries is the field people skip past. It returns SearchQuerySuggestion objects carrying text, styledText and trackingParameters. styledText is the same suggestion with the matched portion wrapped in HTML tags, so the bold-matched-substring effect comes back already applied.

trackingParameters is documented as “URL parameters to be added to a page URL to track the origin of on-site search traffic for analytics reporting”. Append it to the href you render. Leave it off and the click is never attributed to on-site search, so the merchant’s search reporting undercounts every order that started in the dropdown.

Which fields does predictive search actually look at?

Four, by default: TITLE, PRODUCT_TYPE, VARIANT_TITLE and VENDOR. Passing searchableFields replaces that list. It does not extend it, so repeat any default you still want.

The full SearchableField enum is AUTHOR, BODY, PRODUCT_TYPE, TAG, TITLE, VARIANTS_BARCODE, VARIANTS_SKU, VARIANTS_TITLE and VENDOR.

VARIANTS_SKU earns its place on a trade or parts catalogue, where a buyer pastes a code instead of typing a name. VARIANTS_BARCODE does the same job for anyone scanning. BODY reaches into the product description, which broadens what matches and makes the ranking noisier. On a catalogue with long copy it will surface products whose titles have nothing to do with the term.

Shopify’s help documentation on predictive search notes that for pages, collections and blog posts, only the title is searched.

unavailableProducts decides what happens to out of stock items. LAST is the default and pushes them below everything else. HIDE drops them. SHOW leaves them wherever they were found. With ten slots in total, hiding them keeps the dropdown full of things a shopper can actually buy.

When do you need the search query instead?

predictiveSearch has no pagination, no facets and no result count, because a dropdown needs none of those. The full results page does. That is the search query, and it is a normal connection: first, after, pageInfo, nodes, and a totalCount you can put in the heading.

It also takes sortKey (RELEVANCE by default, or PRICE) with reverse, a prefix argument of LAST or NONE that controls partial matching on the final term, and productFilters. The same productFilters name does double duty: as an argument it narrows the results, and as a field on the connection it returns the Filter objects available for faceted navigation.

const SEARCH = `#graphql
  query Search($q: String!, $after: String) {
    search(
      query: $q
      first: 24
      after: $after
      types: [PRODUCT]
      prefix: LAST
      sortKey: RELEVANCE
      productFilters: [{ available: true }, { productVendor: "Acme" }]
    ) {
      totalCount
      pageInfo { hasNextPage endCursor }
      nodes {
        ... on Product { id title handle trackingParameters }
      }
      productFilters { id label type values { id label input } }
    }
  }
`;

const res = await fetch(
  "https://your-shop.myshopify.com/api/2026-07/graphql.json",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Shopify-Storefront-Access-Token": token,
    },
    body: JSON.stringify({ query: SEARCH, variables: { q, after: cursor } }),
  },
);

const { data } = await res.json();

nodes returns SearchResultItem, which is a union, so every field you want needs an inline fragment. Forget the fragment and the query fails validation outright. That error turns up in development, not as a silently empty result set in production.

The ProductFilter input accepts available, category, price, productMetafield, productType, productVendor, tag, taxonomyMetafield, variantMetafield and variantOption. Feed the input value straight back from a FilterValue and you avoid hand-building the filter objects entirely.

What should a Liquid theme use?

Not this. A theme already has the Ajax Predictive Search API at /search/suggest, with the same ten-result ceiling and query parameters in place of GraphQL. resources[type] takes product, page, article, collection or query. resources[limit] runs from 1 to 10, and resources[limit_scope] is all or each. Two more nest a level deeper: resources[options][unavailable_products] and resources[options][fields].

curl -s "https://your-shop.myshopify.com/search/suggest.json?q=boot&resources[type]=product,query&resources[limit]=6&resources[limit_scope]=each&resources[options][fields]=title,product_type,variants.sku"

Swap the .json suffix for &section_id=predictive-search and Shopify renders the section server side and hands you markup, which is how Dawn does it and what our guide to rendering sections on demand covers.

Four documented limitations are worth knowing before you promise a merchant anything. Individual products cannot be excluded from predictive search results. Query suggestions are English only, and collection suggestions follow the store’s primary language. Partial word matching applies only to the last term of a multi-term query. An unsupported locale returns a 417.

That last one matters on a multi-market storefront, where the @inContext directive is already returning local prices and a 417 from the search endpoint is the thing nobody tested.

What I would ship

Start with predictiveSearch, limitScope: EACH, unavailableProducts: HIDE, and only the QUERY and PRODUCT types until a merchant asks for collections in the dropdown. Add VARIANTS_SKU if buyers arrive already knowing the part number. Wire trackingParameters into the links on the first day, because retrofitting it means auditing every result renderer later. Then build the results page on search, and treat the two queries as one feature with two shapes.

If the store is a Liquid theme with no custom front end, I would not reach for the Storefront API here at all. /search/suggest returns the same results with no access token to manage and no GraphQL client to ship, and section rendering gives you the markup as well.

Whoooop builds headless Shopify storefronts, where the ten-result cap, the tracking parameters and the filter plumbing all tend to land in the same sprint as the search box. If you are weighing up a custom front end against staying on a theme, our Shopify development work is where that starts.

Need this built properly?

Whoooop Ltd has spent 15+ years building and maintaining web applications in TypeScript, React, Node.js and serverless — the same ground this post covers.

Get in touch