Skip to main content

September 18, 2026

Map null keys collapse Shopify live variants

Shopify PLPs showed wrong Size facets when variant overrides keyed a Map by null shopifyProductId on consolidated locales. Use the store Product GID.

Sander Korf3 min read
algoliashopifysanity

I run search for a multi-store Shopify fashion storefront where Algolia powers the product listing page. Some European locales share one English store instance, what we call consolidated locales. Merchandisers and shoppers started seeing Size facets that made no sense: XS through 3XL on trousers that only sell Waist and Length. The products looked fine in Shopify admin. InstantSearch filters lied.

The bug lived in how we built live variant overrides before indexing. Each product gets locale-specific variant rows from Shopify, but consolidated locales never had a locale-local Shopify product id. That field was null. I keyed the override Map by shopifyProductId. JavaScript Map accepts null as a key without complaint. Every product on those locales collided on the same key. The last product written won. Earlier products kept their titles in Algolia but inherited someone else's variant list. Facets followed the wrong shape.

Nullable ids look like a valid key

This is not a React key warning. Map.set(null, variants) is legal. No console noise. The failure mode looks like bad merchandising or a stale index, not a data structure bug. I wasted time on facet configuration before I logged the override map size and saw one entry for hundreds of products.

The locale-local id was the wrong identity on consolidated locales anyway. What search and Shopify both mean by "this product" is the store-instance Product GID, the global id that survives when several front-end locales share one backend store.

Project the GID in GROQ, key the Map once

Sanity holds the product document. I changed the GROQ projection so shopifyProductId is always the store-instance Product GID, not the per-locale shadow id that can be null when locales consolidate.

The indexer fetch builds the override map like this:

const overrides = new Map<string, VariantRow[]>();
 
for (const product of products) {
	const gid = product.shopifyProductId;
	if (!gid) continue;
 
	overrides.set(gid, await fetchLiveVariants(gid));
}

Never map.set(nullableId, rows). If the GID is missing, skip the product and log it. Do not insert a null key "just to keep the loop simple."

When building Algolia records, variant overrides come from overrides.get(gid) where gid matches the same field on the record. One product, one key, one variant list.

Stale hits during reindex turnover

Existing Algolia objects may still carry the old shape: locale-local id in shopifyProductId and the English GID in globalShopifyProductId. Until the next full reindex finishes, client code on consolidated locales should prefer globalShopifyProductId when resolving overrides or deep links. After turnover, a single GID field is enough.

I added a one-line guard in the record mapper so null GIDs never reach the map on partial nightly runs. That stopped the facet bleed within one deploy even before the big reindex completed.

The reusable rule: treat nullable commerce ids as toxic Map keys. Project the canonical GID at the content layer, skip nulls at the indexer, and let Algolia facets reflect the product type the shopper is actually viewing. I now grep indexer code for .set( on any field that GROQ marks optional before every catalog deploy.


Happy coding! Sander