Reading: Semantic IDs - Product Understanding at Scale
Instacart on compressing product embeddings into short discrete codes (“semantic IDs”) so recommendation and generative-retrieval models can reason about products that share no category, using catalog taxonomy as a free training signal instead of engagement data.
Source: Instacart Tech Blog on Medium
Link: Semantic IDs: Product Understanding at Scale
The problem: taxonomy is a tree, shopping isn’t
Instacart’s catalog is a strict tree: Dairy > Cheese > Parmesan. A customer building a cheese board wants Parmigiano Reggiano plus Castelvetrano olives (Pantry > Condiments > Olives) plus olive tapenade (Deli > Olives Dips and Spreads) — three branches with no shared ancestor below “Food.” A tree can’t express that these belong together. That blind spot hits hardest for new products (no purchase history to lean on), tail categories (too little interaction data), and mislabeled items (a protein bar filed under “Candy”).
What a semantic ID is
A short sequence of integers, e.g. 6_19_32_63, produced by compressing a product’s embedding through a residual vector quantizer (RQ-VAE). Products that mean similar things share a prefix; they only split apart at the level where they actually differ.
1 | 6_19 → the "cheese board" neighborhood (Italian cheese, olives, tapenade, deli trays) |
Nobody hand-wrote a rule linking Pecorino to olive tapenade — the model learned it by compressing their embeddings into codes that agree on the first two integers.
From embedding to code: how RQ-VAE actually quantizes
Start from a normal continuous embedding
For level
Each level asks “which codeword best explains what’s left over from the previous levels” — that’s why it’s residual: level 2 doesn’t re-approximate the whole embedding, only the error level 1 couldn’t capture. Reconstruction is just summing the chosen codewords back up:
Toy version, runnable
1 | import numpy as np |
1 | parmigiano -> 0_0 (err=0.062) |
Parmigiano and pecorino — genuinely similar embeddings — collapse onto the same code; olives, a different region entirely, gets a different one. (These toy codebooks were hand-placed so the demo is legible; in a real system every codeword is learned by gradient descent over the whole catalog, which is the next section.)
Why quantize at all instead of just using the embedding?
Millions of raw float vectors are expensive to store, expensive to nearest-neighbor search, and unusable as input/output tokens for an LLM-style generative model. A 4-level code with ~2,000 total codewords turns every product into 4 small integers — cheap to store, cheap to index, and directly usable as vocabulary tokens for a sequence model (more on that below).
The catch: a vanilla RQ-VAE doesn’t know what “similar” means
Trained on reconstruction alone, the quantizer only tries to compress
Teaching it with catalog structure, not engagement data
Instacart doesn’t have purchase/click history for brand-new products (that’s the cold-start problem this whole system exists to solve), so they can’t use behavioral co-purchase signal the way YouTube’s PLUM does. Instead they use something they always have: the taxonomy tree itself, turned into a graded similarity target.
A simple way to turn that into a loss — push cosine similarity of the embeddings toward the taxonomy-implied target:
$$
\mathcal{L}{\text{contrastive}} = \frac{1}{|\text{pairs}|}\sum{(i,j)} \big(\text{sim}(z_i, z_j) - \text{target}(i,j)\big)^2
$$
Two products with no shared ancestor get pushed apart (target 0); two exact-substitute products get pulled together (target 1); siblings sit in between. No one hand-labels pairs — the label is just read off the tree.
Hierarchical batch sampling — why random batches don’t work
Random sampling over millions of SKUs means almost every batch is entirely unrelated products — no positive pairs to learn from, so the contrastive term above has nothing to push together. Fix: construct batches on purpose.
1 | import random |
Sampling several products within the same leaf (multiple marinara brands) gives strong positives “for free”; sampling several sibling leaves under the same parent gives moderate positives; sampling unrelated parents gives hard negatives — all without ever writing an explicit pair label, the same trick as idealo’s implicit in-batch negatives from the previous post, just driven by tree structure instead of batch position.
The full training objective
$$
\mathcal{L}{\text{total}} = \mathcal{L}{\text{reconstruction}} + \mathcal{L}{\text{rq}} + \lambda \cdot \mathcal{L}{\text{contrastive}}, \qquad \lambda = 0.01
$$
— standard “did the codes add back up to the original embedding” term. — the usual VQ-VAE codebook/commitment loss, pulling each codeword toward the residuals assigned to it and vice versa, so the codebooks themselves stay well-formed. — the taxonomy-guided term from above, kept small (0.01) so it nudges the code space toward catalog structure without overpowering reconstruction fidelity. Coarser levels (L1, L2) get weighted more, so broad groupings (“is this even food”) get priority over fine ones (“which brand of hard cheese”).
Two embedding flavors feeding the same quantizer
| ESCI (precision) | ESCI+Gemma (discovery) | |
|---|---|---|
| input | raw product text through the search-relevance model | Gemini Flash extracts clean attributes (type, ingredients, diet tags) → embedded with Gemma |
| optimized for | “is this the exact same thing?” | broader lifestyle/usage similarity |
| example cluster | interchangeable whole-bean coffees, any brand | products that fit the same meal/occasion |
| used for | substitution, search, reordering | homepage feeds, cross-sell, exploration |
Same RQ-VAE mechanism, two different embedding inputs — the quantizer just compresses whatever structure the embedding space already has, so the embedding choice is the product decision.
Checking it actually worked
Rather than trust downstream metrics alone, they measure the codes directly:
- Similarity–depth correlation: correlation between raw cosine similarity and how many code levels two products share, 0.69–0.84. Among pairs with cosine similarity ≥ 0.9, 98–99% agree on level 1, dropping to 18–37% agreement by level 4 (expected — L4 is supposed to split near-duplicates apart).
- LLM-as-judge on clusters: prompt an LLM to score each leaf cluster on functional coherence, purchase-together likelihood, and shopping-context relevance — a scalable stand-in for human review across thousands of clusters.
- Taxonomy alignment, used backwards: when a product’s code disagrees with its taxonomy label — a “Protein Bar” filed under “Candy” clustering with other protein bars — that’s usually the taxonomy label that’s wrong, not the code. This turns the whole system into an automated catalog-audit signal.
From codes to a generation task
With only ~2,000 codeword tokens total, a product’s semantic ID becomes something a sequence model can generate, one level at a time, instead of something you have to retrieve by nearest-neighbor search:
Exactly the chain rule an LLM uses for next-token prediction — except the “vocabulary” is ~2,000 product codewords instead of English subwords, and the output is the next relevant product rather than the next word. This generative-retrieval approach, tried first on product carousels, delivered +34% add-to-carts and surfaced products from 2.7x more emerging brands — biggest gains in exactly the tail categories that had no behavioral signal to lean on before.
Takeaways
- A semantic ID = embedding compressed level-by-level by residual vector quantization; each level explains what the previous level’s codeword missed.
- Reconstruction-only RQ-VAE doesn’t know “similar” — catalog taxonomy distance is used as a free, always-available supervision signal instead of engagement data.
- Batches need deliberate structure (parent + siblings + unrelated categories) or the contrastive term has nothing to learn from.
- Loss = reconstruction + codebook + small taxonomy-contrastive term (
), coarse levels weighted heavier. - Embedding choice determines what the codes capture — precision (ESCI) vs. discovery (ESCI+Gemma) aren’t competing, they’re for different surfaces.
- Where codes and taxonomy disagree, the code is often right — mislabeled catalog entries surface for free.
- A small, discrete codeword vocabulary turns “find the right product” into “generate the right tokens” — same math as next-token prediction.
Reading: Semantic IDs - Product Understanding at Scale