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

# Product-level indexing

> Index one Algolia record per Shopify product instead of one per variant, and migrate an existing store.

export const Records = () => <Tooltip tip="A record is a searchable object in an Algolia index. Each record consists of named attributes." cta="Algolia records" href="/doc/guides/sending-and-managing-data/prepare-your-data#algolia-records">
    records
  </Tooltip>;

export const Index = () => <Tooltip tip="An Algolia index is a searchable dataset that consists of records and configuration settings. These settings define how the records are searched and ranked.">
    index
  </Tooltip>;

<Note>
  Autocomplete, InstantSearch, and Recommend in the Algolia app blocks now support version 5 of the Algolia API client.
  Most stores only need to switch the [search client version setting](/doc/integration/shopify/getting-started/configuration#search-client-version) to **Version 5**.
  If you use custom storefront JavaScript, follow the [version 5 upgrade guide](/doc/libraries/sdk/upgrade/javascript).
  Support for version 4 ends on August 14, 2026.
</Note>

The Algolia AI Search & Discovery app can index your catalog in one of two *indexing modes*:

* **Variant-level.** One Algolia record per Shopify variant. The default.
* **Product-level.** One record per Shopify product, with its variants nested inside the record.

Both modes write to the same <Index /> `PREFIX_products`.
The mode changes the shape of the records in it, which attributes you can facet and sort on,
and how your storefront renders results.

## How to get access

<Callout icon="flask-conical" color="#14b8a6">
  This is a **beta feature** according to [Algolia's Terms of Service ("Beta Services")](https://www.algolia.com/policies/terms/).
</Callout>

To get access to this feature, contact the [Algolia support](https://support.algolia.com/hc/en-us/requests/new) team.
The **Indexing mode** setting stays hidden until it's enabled,
or if your store already uses product-level records.

## Choose an indexing mode

Choose **product-level indexing** if you want:

* One card per product
* Built-in color swatches
* A lower record count
* [Click and conversion events](/doc/integration/shopify/building-search-ui/events) consolidated on product IDs
* Algolia features that read per-product signals, such as [Recommend](/doc/guides/algolia-recommend/overview), [Dynamic Re-Ranking](/doc/guides/algolia-ai/re-ranking), and [NeuralSearch](/doc/guides/ai-relevance/neuralsearch/get-started). Algolia's [ecommerce record guidance](/doc/guides/sending-and-managing-data/prepare-your-data/how-to/ecommerce-records#choose-your-record-model) recommends the product-level model for them

Choose **variant-level indexing** if:

* Shoppers need to land on a specific variant from search results
* You facet or sort on per-variant attributes, such as SKU or weight
* Your shoppers filter on two or more facets at once, such as color and size. See [Facet combinations across variants](#facet-combinations-across-variants)

|                                    | Variant-level                                                                                                                                                                    | Product-level                                                                                                                                   |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| Record count                       | One per variant                                                                                                                                                                  | One per product                                                                                                                                 |
| Object ID                          | Variant ID                                                                                                                                                                       | Product ID                                                                                                                                      |
| Search results                     | One card per variant, or one per product with [**Display one result per product**](/doc/integration/shopify/sending-and-managing-data/schemas#show-products-instead-of-variants) | One card per product                                                                                                                            |
| Color and option swatches on cards | Custom code                                                                                                                                                                      | Built in                                                                                                                                        |
| Variant-level facets               | Direct, on top-level attributes                                                                                                                                                  | Nested, under `variants`                                                                                                                        |
| Facet combinations across variants | Accurate per variant                                                                                                                                                             | Can match across variants                                                                                                                       |
| Sort by discount or weight         | Supported                                                                                                                                                                        | Not supported                                                                                                                                   |
| Search by SKU or barcode           | Works by default                                                                                                                                                                 | Needs configuration                                                                                                                             |
| Click and conversion events        | Attributed to variant IDs                                                                                                                                                        | Attributed to product IDs                                                                                                                       |
| The app's Recommend widgets        | Supported                                                                                                                                                                        | Not supported. See [Product cards](#product-cards)                                                                                              |
| Algolia record quota               | Higher                                                                                                                                                                           | Lower                                                                                                                                           |
| Record size                        | Smaller per record                                                                                                                                                               | Larger, and grows with the variant count, so products with many variants can [exceed the record size limit](#limits-to-check-before-you-switch) |

### Facet combinations across variants

If your shoppers filter across two or more facets, use variant-level indexing with
[**Display one result per product**](/doc/integration/shopify/sending-and-managing-data/schemas#show-products-instead-of-variants).
That returns one card per product while filters still apply per variant, so combinations stay accurate.
Product-level indexing turns that setting off, so the two approaches are mutually exclusive.

Product-level records don't fit this case because a product record carries the option values of all its variants in one place.
Algolia treats each nested attribute as multi-valued, so two refinements combine at the product level, never within a single variant.

Take a product with two variants, Black/128GB and White/256GB.
The record holds `Black`, `White`, `128GB`, and `256GB`.
Filtering on Color `Black` and Size `256GB` returns that product, even though no such variant exists.

The same applies to any pair of per-variant attributes:

* **Option against option.** Color and size, or color and material.
* **Option against price.** If Black costs 300 and White costs 150, filtering on Color `Black` and a 100 to 200 price bucket returns the product.
* **Option against stock.** `inventory_available` and `variants_inventory_count` describe the whole product, so Color `Black` plus in-stock means the product has a Black variant and has stock somewhere, not that Black is in stock.

The card also preselects the refined color and links to that variant, so a product matched on a
combination it doesn't stock looks like a correct result.

Single-option catalogs aren't affected, and selecting several values within one facet still filters correctly.

### Limits to check before you switch

* **High variant counts.** A product record holds every variant, so a product that fit as many small variant records can exceed Algolia's record size limit as one product record. Algolia rejects the oversized record, and that product goes missing from search results. Selecting more optional fields makes this more likely.
* **SKU and barcode search.** These don't work until you adjust your searchable attributes. See [Searchable attributes](#searchable-attributes).
* **Discount and weight sorts.** Neither has a product-level equivalent. See [Sort orders](#sort-orders).

## Turn on product-level indexing

<Warning>
  Changing the indexing mode triggers a full reindex and replaces every record in your products index.
  On a store with a live storefront, read [Migrate from variant-level indexing](#migrate-from-variant-level-indexing) first.
</Warning>

1. In your Shopify admin, open the Algolia AI Search & Discovery app.

2. Go to **Indexing**.

3. In the **Indexing** section, make sure **Index my Catalog** is selected.

4. Under **Indexing mode**, read the **Review required before changing indexing mode** banner.
   Select the checkbox to confirm you've reviewed it, then select **Confirm**.
   The mode options, the field checklist, and **Save** stay locked until you do.

5. Select **Product**.

6. Optional: to add per-variant data to your records, select the fields you want in the checklist under **Product**.
   See [Product record fields](#product-record-fields).

7. Select **Save**, then select **Continue** to confirm the reindex.

The app rewrites your facets and sort orders for the new record shape, then reindexes your catalog.
Search keeps serving the old records until the new ones land.

* **The acknowledgement is stored in your browser**, per store and per browser. It isn't saved to your account. In a private window, or in a browser that blocks site data, you confirm again each time you open the app.
* **The reindex is queued, not immediate.** The reindex indicator appears on the next status refresh rather than on save, so a short delay isn't a failed save.

Changing the indexing mode and the record fields in the same save reindexes once.
Changing other indexing settings in the same save, such as inventory locations or metafields, can trigger a second reindex.

Some controls only update after you save.
Until then, the **Search options** tab still lists the sort orders that product-level indexing deactivates.

## Product record schema

Algolia stores your products in the following format:

```js JavaScript icon=code expandable theme={"system"}
{
  objectID,                       // Product id (unique identifier also used by Algolia)
  title,                          // Name of the product
  handle,                         // URL handle for the product
  product_type,
  vendor,
  tags,
  named_tags: {                   // Key value association from tags like this: "key:value" (See Named tags section)
    TAG_KEY
  },
  named_tags_names: [
    TAG_KEY                       // Array of keys from named_tags
  ],
  body_html_safe,                 // Description (without HTML, truncated to roughly 2,000 characters)
  images: [                       // Product images, each with the variants that use it
    {
      url,
      variant_ids: []             // Empty for the product's hero image, unless a variant reuses it
    }
  ],
  swatches: [                     // Option values with a Shopify swatch (See Swatches section)
    {
      option_name,
      values: [
        {
          name,
          color,                  // Hex color, when the swatch is a color
          image: { url, alt }     // Swatch image, when the swatch is an image
        }
      ]
    }
  ],
  option_names,                   // Array of the product's option names, in order
  variants_min_price,             // Min price across the product's variants
  variants_max_price,             // Max price across the product's variants
  variants_compare_at_price_min,  // Min compare_at_price across the product's variants
  variants_compare_at_price_max,  // Max compare_at_price across the product's variants
  price_ranges,                   // Array of every price bucket the product's variants fall into
  inventory_available,            // True if at least one variant is available
  variants_inventory_count,       // Total inventory count of all variants
  requires_shipping,              // Opt-in (See Product record fields)
  template_suffix,                // Opt-in (See Product record fields)
  meta: {                         // Product metafields (See Metafields section)
    namespace1: {
      key1,
      key2
    },
  },
  variants: [                     // One entry per variant (See Variant sub-records)
    {
      variantID,
      variant_title,
      options: {                   // Hash containing the option `names: value`
        OPTION_NAME
      },
      price,                       // Actual (optionally discounted) price
      compare_at_price,            // Initial price
      price_ratio,
      inventory_available,
      meta: {},                    // Variant metafields
      option1,                     // Opt-in (See Product record fields)
      option2,                     // Opt-in
      option3,                     // Opt-in
      sku,                         // Opt-in
      barcode,                     // Opt-in
      inventory_quantity,          // Opt-in
      inventory_policy,            // Opt-in
      grams,                       // Opt-in
      weight,                      // Opt-in. Weight with its original unit
      taxable,                     // Opt-in
      locations_inventory: {       // Added when locations inventory is enabled
        key1: value1               // Object with location_id:quantity - location_id is auto-generated
      }
    }
  ],
  collections: [
    'collection-1-handle',         // Array of product collection handles
    // ...
  ],
  collection_ids: [
    123456,
    // ...                         // Array of product collection IDs
  ],
  category: {},                    // Hierarchical categories from Shopify's product taxonomy. Absent when uncategorized
  recently_ordered_count,          // How much have recently been ordered : useful for ranking
  published_at,                    // Always present. Integer Unix seconds, not an ISO-8601 string
  _tags: [],                       // Array of internal tags. Please do not modify.
}
```

Product-level records reuse the definitions on the [index schema](/doc/integration/shopify/sending-and-managing-data/schemas) page for
[recently ordered count](/doc/integration/shopify/sending-and-managing-data/schemas#recently-ordered-count),
[named tags](/doc/integration/shopify/sending-and-managing-data/named-tags),
[metafields](/doc/integration/shopify/sending-and-managing-data/metafields),
and the rules that [remove products from search results](/doc/integration/shopify/sending-and-managing-data/schemas#remove-products-from-search-results).

If you enabled [Markets](/doc/integration/shopify/sending-and-managing-data/markets), each market index gets its own product records,
and every record carries a `market_pricing_CURRENCY` object.
In product-level mode, that object holds the product-level price fields, such as `variants_min_price` and `variants_max_price`,
instead of the variant-level `variant_min_price` and `variant_max_price`.

Product records don't include `created_at` or `updated_at`.
The field checklist doesn't have an entry for either, so theme code that reads them at the top level of a product record finds nothing.
`published_at` is unaffected and always present.

### Variant sub-records

Each product record holds a `variants` array with one entry per variant, excluding variants whose title contains `[hidden]`.
Publication status, the `algolia-ignore` tag, and `[hidden]` in the product title all apply to the whole product rather than to single variants.

Variant sub-records keep the per-variant data that a product record can't hold at the top level:
price, inventory, option values, and variant metafields.

`variantID` isn't an `objectID`.
Algolia ranks, facets, and counts the product record, not its variant entries.
A query that matches one variant returns the whole product.

Metafields appear at both levels.
Product metafields land in the record's `meta`, and variant metafields in `variants[].meta`.

### Product record fields

By default, product records stay small and carry only what search, faceting, and the product card need.
Per-variant fields are opt-in.

The checklist sits under the **Product** option in the **Indexing** section, and only in product-level mode.
Select the fields you want, then select **Save** and confirm the reindex.

| Field              | Adds to each record                                                                                    |
| ------------------ | ------------------------------------------------------------------------------------------------------ |
| SKU                | `variants[].sku`                                                                                       |
| Barcode            | `variants[].barcode`                                                                                   |
| Variant options    | `variants[].option1`, `variants[].option2`, `variants[].option3`                                       |
| Weight             | `variants[].weight`, `variants[].grams`                                                                |
| Taxable            | `variants[].taxable`                                                                                   |
| Requires shipping  | `requires_shipping`, a single value on the product record, `true` if **any** variant requires shipping |
| Inventory quantity | `variants[].inventory_quantity`                                                                        |
| Inventory policy   | `variants[].inventory_policy`                                                                          |
| Template suffix    | `template_suffix`                                                                                      |

`variants[].options`, `variants[].price`, `variants[].compare_at_price`, `variants[].price_ratio`,
and `variants[].inventory_available` are always included.
Clearing **Variant options** removes only the flat `option1`, `option2`, and `option3` fields.
Option data stays in `variants[].options` and `option_names`.

If you enabled [locations inventory](/doc/integration/shopify/sending-and-managing-data/locations), the app adds `variants[].locations_inventory` for you.

Selecting more fields makes each record larger.
On a catalog with many variants per product, that raises the risk of exceeding Algolia's record size limit.
See [Limits to check before you switch](#limits-to-check-before-you-switch).

Older per-attribute settings for weight, requires shipping, and inventory locations combine with this checklist rather than replacing it.
If your store has one turned on, that field stays in your records even when you clear the checkbox here.
Product-level mode also hides those older toggles, so contact
[Algolia support](https://support.algolia.com/hc/en-us/requests/new) to check or turn them off.

<Warning>
  When the app creates your index, it marks `inventory_quantity`, `locations_inventory`, `variants_inventory_count`,
  and `recently_ordered_count` as [unretrievable](/doc/api-reference/api-parameters/unretrievableAttributes).
  Those are top-level paths and don't cover `variants.inventory_quantity` or `variants.locations_inventory`.
  If you select **Inventory quantity** in product-level mode, add both nested paths to your index's unretrievable
  attributes in the Algolia dashboard to keep per-variant stock levels out of public search responses.
</Warning>

### Swatches

Product-level records index Shopify's [option value swatches](https://help.shopify.com/en/manual/custom-data/metafields/category-metafields/using-category-metafields#options-with-swatches).
For every product option whose values carry a swatch, the app adds an entry to `swatches` with the option name and its values,
each with a hex `color`, an `image`, or both.

The app skips options without swatch data, so a product with no swatches in Shopify has no `swatches` attribute.
The app also skips a custom option with plain text values, such as a Color option with "Red" and "Blue" and no swatches set.
There's no setting to turn swatches on or off.

Swatch colors and images live on Shopify metaobjects rather than on the product, and the app's real-time
updates follow product changes.
Editing a swatch's color or image therefore doesn't reach your records on its own, and the `swatches`
attribute keeps its last indexed value.
Other edits to the product reach your records as usual.
To pick up a swatch edit, update the product or trigger a
[full reindex](/doc/integration/shopify/sending-and-managing-data/introduction-to-shopify-indexing)
from the **Indexing** tab.

If a product card doesn't show a swatch row, check that the product's option values have swatches assigned in
the Shopify admin, then update the product or reindex.

For how swatch chips render on search results, see [Product cards](#product-cards).

<Tip>
  Only the first `swatches` entry renders chips on product cards, and only that option drives preselection.
  Refining any other swatch option preselects nothing.
  If your store has several options with swatches, move the one you want on product cards to
  [position 1](https://help.shopify.com/en/manual/products/variants/add-variants) in Shopify.
  To reorder, cap, or filter the chips without changing Shopify, use the `beforeInstantSearchSwatchOptionResolver`
  [hook](/doc/integration/shopify/building-search-ui/instantsearch-hooks).
</Tip>

### Preselected swatches

A card doesn't always show the product's default option value.
The app blocks preselect a swatch chip, swap the card image to that value's photo, and point the card
at that value's variant when either of these happens:

* **The shopper refines a swatch option.** Refining Color to "Blue" preselects Blue on every card that has it. When the shopper refines several values, the first of the card's own swatch values wins. That order is the one `beforeInstantSearchSwatchOptionResolver` produces, so a hook that reorders chips changes which refined value wins.
* **The query matches a swatch name.** Searching "blue" preselects the Blue chip, because the app adds `swatches.values.name` to your searchable attributes.

A refinement outranks a query match.
The app prefers whole-value matches, but a query of "blue" can still preselect a chip named "Light Blue".
Typos don't preselect, because the app disables typo tolerance on `swatches.values.name`.

The app marks the chip on first render and doesn't fire the `afterInstantSearchSwatchClick` hook.
To override which chip is marked, or to sort matched chips first, use `beforeInstantSearchTransformItems`
and edit `_matched` on the swatch option.

## Product cards

Product-level cards describe a product, so they differ from variant-level cards:

| Element                         | Behavior                                                                              |
| ------------------------------- | ------------------------------------------------------------------------------------- |
| Title link                      | `/products/HANDLE`, with `?variant=ID` added when a swatch is preselected or selected |
| Price                           | A single price, or a range from `variants_min_price` and `variants_max_price`         |
| Compare-at price and sale badge | Not shown                                                                             |
| Image                           | The product's hero image, swapped to the selected swatch value's photo                |
| Swatch row                      | Chips from the first `swatches` entry. Sold-out values render crossed out             |
| Card title                      | The product title, with no variant suffix. It doesn't change with the selected swatch |
| Add to cart                     | Not shown. The shopper picks a variant on the product page                            |

Selecting a swatch chip swaps the image, marks the chip selected, and updates the card link.
It doesn't navigate.
Availability comes from the variants that match the chip, so sold-out values render crossed out and faded.
Sold-out chips stay selectable, and the chips aren't reachable by keyboard.

Autocomplete rows show the product's hero image, the same price range, and a plain product link.
They don't have a swatch row.

<Warning>
  The app's Recommend widgets don't handle product-level records.
  They read attributes that only exist on variant records, so tiles render an invalid price and a placeholder
  image, and the tile link points at a product handle with the product ID in the `variant` parameter.
  Check your storefront for Recommend widgets before you switch.
</Warning>

Custom code that reads record attributes needs to know the mode.
Use `hit._productLevel` inside template and transform hooks, because it describes the record being rendered.
Use `window.algoliaShopify.config.indexing_mode`, which returns `"product"` or `"variant"`, only when building query parameters.
The two can disagree during a reindex, and the per-hit value is the reliable one.

## Facets

The app rewrites the facets you configure in the **Search options** tab onto the product record shape,
for search and for collection pages:

| Facet in the app      | Attribute in the index         |
| --------------------- | ------------------------------ |
| `options.OPTION_NAME` | `variants.options.OPTION_NAME` |
| `price`               | `variants.price`               |
| `price_range`         | `price_ranges`                 |
| Anything else         | Unchanged                      |

The app always keeps these facets on: `tags`, `option_names`, `variants.options`,
`filterOnly(inventory_available)`, and `filterOnly(variants_inventory_count)`.
Named tags add `named_tags_names` and `named_tags`, and collection indexing adds `searchable(collections)` and `collection_ids`.

Your saved configuration keeps the variant-level names, and the app translates them on each write.
The Algolia dashboard therefore shows `variants.options.color` in your index settings while the app shows `options.color`.
That mismatch is deliberate.

<Warning>
  Facets on any other per-variant attribute pass through unchanged and don't return any values,
  because the attribute no longer exists at the top level of the record.
  This affects facets on `sku`, `barcode`, `variant_title`, `compare_at_price`, `price_ratio`, `inventory_quantity`,
  `inventory_policy`, `grams`, `weight`, `taxable`, `option1`, `option2`, `option3`, and variant metafields.
  Re-create them with a `variants.` prefix, such as `variants.grams`.
</Warning>

If your store's stock policy is **deny**, the app blocks filter on `variants_inventory_count > 0`
instead of `inventory_quantity > 0`.

Filters in your own theme code have the same problem.
If you filter on `inventory_quantity > 0`, as the
[out-of-stock products](/doc/integration/shopify/going-further/out-of-stock-products) page describes,
switch to `inventory_available` or `variants_inventory_count`.

### Facet panels with no values

A facet panel on your storefront can render its header without returning any values.
Three causes:

* **Price bucket facets.** The app blocks query `price_range` while your index is faceted on `price_ranges`. Use a price range facet only if you configure the attribute by hand in the Algolia dashboard.
* **Hierarchical facets.** A hierarchical facet on an option or on price sends a variant-level attribute path for each level of the hierarchy, so it doesn't match the remapped attributes. Use a standard facet instead.
* **A stale storefront configuration.** The remapping runs from the app's configuration in your theme. If that configuration is missing or predates the mode change, the storefront queries variant-level attribute names against product records. Cards still render correctly, which makes this hard to spot. Saving in the app rewrites it.

## Sort orders

Each [sort order](/doc/integration/shopify/getting-started/configuration#sort-orders) is backed by a replica index that ranks on one attribute.
Product-level mode re-points the sort orders whose attribute moved, and turns off the ones with no product-level equivalent.

| Sort order                     | Ranks on in variant-level mode | Ranks on in product-level mode   |
| ------------------------------ | ------------------------------ | -------------------------------- |
| Most popular                   | `desc(recently_ordered_count)` | Unchanged. Counted per product   |
| Cheapest first                 | `asc(price)`                   | `asc(variants_min_price)`        |
| Most expensive first           | `desc(price)`                  | `desc(variants_max_price)`       |
| Most available                 | `desc(inventory_quantity)`     | `desc(variants_inventory_count)` |
| Newest first, Oldest first     | `published_at`                 | Unchanged                        |
| Vendor, Vendor Z-A             | `vendor`                       | Unchanged                        |
| Name, Name Z-A                 | `title`                        | Unchanged                        |
| Highest discount first         | `asc(price_ratio)`             | Turned off                       |
| Heaviest first, Lightest first | `grams`                        | Turned off                       |

These are the default labels. You can rename any sort order, so your list can differ.

The app deactivates **Highest discount first** and **Heaviest first** on save and hides them from the
**Sort orders** list, so you can't activate them.
Selecting **Weight** in the field checklist still adds weight data to your records, but the app doesn't provide a product-level weight sort.

Deactivating a sort order deletes its replica index.
The search analytics attached to that replica go with it.
Switching back to variant-level mode lists both sort orders again, still deactivated, and re-enabling them builds new replicas.

Product-level records don't need **Display one result per product**, described on the
[index schema](/doc/integration/shopify/sending-and-managing-data/schemas#show-products-instead-of-variants) page,
so the app turns it off, hides the control, and stops applying
[`distinct`](/doc/api-reference/api-parameters/distinct) on search results.
A stale [`attributeForDistinct`](/doc/api-reference/api-parameters/attributeForDistinct) value can remain in your
index settings, where it's visible in the Algolia dashboard but doesn't have any effect.

After any full reindex, including a mode change, `recently_ordered_count` restarts from nothing and
rebuilds as orders come in, so **Most popular** ranks on an empty attribute until orders accumulate.

## Searchable attributes

When you switch to product-level mode, the app adds `unordered(swatches.values.name)` to your
[searchable attributes](/doc/guides/managing-results/must-do/searchable-attributes) so shoppers can search on swatch names,
and disables typo tolerance on that attribute.
It leaves the rest of the list alone, and the default list targets variant-level records:

1. `unordered(vendor)`
2. `unordered(title)`
3. `unordered(variant_title)`
4. `product_type`
5. `unordered(tags)`
6. `unordered(handle)`
7. `sku`
8. `barcode`
9. `unordered(body_html_safe)`

The app applies that default only when it creates the index, so your live list is whatever your store has accumulated.

In product-level mode, `variant_title`, `sku`, and `barcode` don't exist at the top level of the record,
so those three entries match nothing and shoppers can't find products by SKU or barcode.

To restore them, edit **Searchable attributes** in the Algolia dashboard, under
**Search > Configuration > Relevance Essentials > Searchable attributes**, and replace those entries with their nested paths:

* `unordered(variants.variant_title)`
* `variants.sku`
* `variants.barcode`

`variants[].sku` and `variants[].barcode` only exist if you selected **SKU** and **Barcode** in
[Product record fields](#product-record-fields), so select those first.

Switching back to variant-level mode doesn't remove the swatch entry.
It matches nothing on variant records, so it's safe to leave in place.

<Tip>
  Option values aren't searchable by default in either mode.
  `option_names` holds the option names, such as "Color", not their values.
  To match a query like "red shirt" on the option value, add `unordered(variants.options)` to your searchable attributes.
  It repeats every option value once per variant and makes them matchable on every query, which can cost you relevance.
</Tip>

## Custom ranking

The default [custom ranking](/doc/guides/managing-results/must-do/custom-ranking) the app applies to a new index
references `recently_ordered_count`, `updated_at`, and the flat variant option fields.
On product-level records, `updated_at` is absent and `option1`, `option2`, and `option3` exist only if you select **Variant options**.

Review your custom ranking in the Algolia dashboard after you switch, and replace any criterion
that points at an attribute your product records don't carry.

## Migrate from variant-level indexing

The app changes indexing mode in place and runs a full reindex.

The app doesn't move the work you did in the Algolia dashboard.
Your rules, pins, and synonyms reference variant `objectID`s and variant-level attributes, and every variant record disappears at the swap.

Plan the migration in the order below.

### Understand what changes in your records

Every attribute that describes a single variant moves under `variants`.
Everything that describes the product stays where it is.

| Variant-level attribute                    | Product-level equivalent                                                               |
| ------------------------------------------ | -------------------------------------------------------------------------------------- |
| `objectID` (variant ID)                    | `objectID` (product ID)                                                                |
| `id` (product ID)                          | Removed. `objectID` is the product ID                                                  |
| `variant_title`                            | `variants[].variant_title`                                                             |
| `sku`, `barcode`                           | `variants[].sku`, `variants[].barcode`                                                 |
| `price`, `compare_at_price`, `price_ratio` | `variants[].price`, `variants[].compare_at_price`, `variants[].price_ratio`            |
| `price_range` (one bucket)                 | `price_ranges` (array of buckets)                                                      |
| `options`, `option1`, `option2`, `option3` | `variants[].options`, `variants[].option1`, `variants[].option2`, `variants[].option3` |
| `inventory_quantity`, `inventory_policy`   | `variants[].inventory_quantity`, `variants[].inventory_policy`                         |
| `locations_inventory`                      | `variants[].locations_inventory`                                                       |
| `grams`, `weight`, `taxable`               | `variants[].grams`, `variants[].weight`, `variants[].taxable`                          |
| `image`, `product_image`                   | `images[]`, each with the `variant_ids` that use it                                    |
| `variants_count`                           | Removed. Count the entries in `variants`, which excludes `[hidden]` variants           |
| `position`                                 | Removed                                                                                |
| `variants_inventory`                       | Removed. Use `variants[].inventory_quantity`, keyed by `variantID`                     |
| Variant metafields in `meta`               | `variants[].meta`                                                                      |
| Not available                              | `swatches[]`                                                                           |

These stay unchanged: `title`, `handle`, `vendor`, `product_type`, `tags`, `named_tags_names`, `body_html_safe`,
`option_names`, `variants_min_price`, `variants_max_price`, `variants_compare_at_price_min`, `variants_compare_at_price_max`,
`variants_inventory_count`, `inventory_available`, `collections`, `collection_ids`,
`category`, `recently_ordered_count`, and `_tags`.

The table above doesn't capture these:

| Attribute                              | What changes                                                                                                                                                                                                                  |
| -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `published_at`                         | Still on every record, but the value format changes. Variant-level records serialize it as an ISO-8601 string, product-level records as an integer Unix timestamp. Theme code doing `new Date(hit.published_at)` breaks       |
| `created_at`, `updated_at`             | Absent from product-level records. On variant-level records they're always present, and they carry the variant's timestamps, not the product's                                                                                |
| `requires_shipping`, `template_suffix` | Present by default on variant-level records, opt-in on product-level records. Select them in the field checklist to keep them                                                                                                 |
| `named_tags`                           | Present in both modes, but variant-level records carry only the named tags that apply to that variant. Product-level records carry every named tag on the product, so tags such as `color:red` no longer scope to one variant |

`variants_inventory_count` and `recently_ordered_count` are unretrievable by default on new indices.
They're on the record and available for filtering and sorting, but your theme can't read them unless you change that setting.

### Know what you lose

* **Variant-level search results.** A query for "red" returns the whole product, not its red variant. Product cards link to the product detail page, where the shopper picks the variant. Selecting a swatch chip points the card at that value's variant, so shoppers can still reach one from search results.
* **Accurate facet combinations.** See [Facet combinations across variants](#facet-combinations-across-variants).
* **Per-variant pricing on cards.** Cards show a price range built from `variants_min_price` and `variants_max_price`, with no compare-at price and no sale badge.
* **Discount and weight sort orders.** Their replica indices, and the analytics on them, go too. See [Sort orders](#sort-orders).
* **Recommend widget rendering.** See the warning in [Product cards](#product-cards).
* **Click and conversion history.** Your Algolia analytics, Recommend models, Dynamic Re-Ranking, and [Personalization](/doc/guides/personalization/classic-personalization/what-is-personalization) profiles are keyed on variant `objectID`s. Those IDs stop existing. Recommend results stay degraded until the models retrain against the product records. Historical analytics don't carry over.
* **Rules, pins, and synonyms that reference variants.** A pin on a variant `objectID` never matches a record again.

### Know what the app updates for you

When you change indexing mode, the app:

* Re-points its indexing pipeline to build product records, and reindexes the catalog.
* Remaps the facets you configured in the app onto the product record shape, for search and for collection pages.
* Rebuilds sort-order replicas on the product-level ranking attributes, and deactivates the two with no equivalent.
* Turns off **Display one result per product**.
* Adds `swatches` to your records.
* Adds `swatches.values.name` to your searchable attributes.
* Sends product `objectID`s from the [Shopify web pixel](/doc/integration/shopify/building-search-ui/events#shopify-web-pixels), so add-to-cart and purchase events attribute to your product records.
* Keeps your saved configuration, so switching back to variant-level mode restores the facets and sort orders the app remapped.

The app doesn't update:

* **Collection facet ordering rules.** They keep their variant-level attribute names. Re-save the facets on each collection after the switch.
* **Index settings you changed by hand.** The reindex preserves them rather than rewriting them for the new record shape, and saving settings in the app can overwrite them.

### Update your configuration and theme

Before you change the mode:

1. **Audit your facets.** Any facet on a per-variant attribute needs a `variants.` prefix. See the warning in [Facets](#facets).

2. **Audit your searchable attributes.** SKU and barcode search stops working until you re-point them. See [Searchable attributes](#searchable-attributes).

3. **Audit your theme code and custom hooks.** Search for `hit.id`, `hit.price`, `hit.sku`, `hit.variant_title`, `hit.inventory_quantity`, `hit.options`, and `hit.image`. Each one moved. `hit.objectID` replaces `hit.id`.

4. **Check any code that reads dates.** `hit.published_at` changes from an ISO-8601 string to an integer Unix timestamp, so date parsing and formatting on it needs updating. `hit.created_at` and `hit.updated_at` disappear from product records.

5. **Consolidate variant metafields you facet or filter on.** Copy the value to a product metafield in the Shopify admin so it lands in the product record's `meta`.

6. **Export your rules, pins, and synonyms.** Record which ones reference variant `objectID`s. You re-create those as product `objectID`s after the switch.

7. **Select your Product record fields.** Adding a field later triggers another full reindex.

8. **Check your largest products.** Count the variants on the products with the most of them, and test those in a development store. See [Limits to check before you switch](#limits-to-check-before-you-switch).

After you change the mode:

1. Re-create pins and rules against product `objectID`s.

2. Re-save the facets on each collection.

3. Review your custom ranking. See [Custom ranking](#custom-ranking).

4. Check that each sort order returns the expected order. Replicas re-rank after the records land, so give them a few minutes on a large catalog.

5. Expect degraded Recommend results until the models retrain on the product index.

### Verify the switch

Check the search requests your storefront sends, not how the cards look.
Product cards read the record they're given, so they render correctly even when your storefront still
queries variant-level attribute names.
Faceting and stock filtering fail silently in that state.

For each of these, confirm the request and the response rather than the page:

* Facet panels return values, and the counts change as you refine.
* The stock policy filter names `variants_inventory_count` or `inventory_available`.
* Price faceting names `price_ranges`.
* A search for a SKU returns the product, if you re-pointed your searchable attributes.

Incremental updates only write the attributes that changed.
An attribute you cleared from the field checklist can therefore persist on existing records until the next full reindex.
