Concepts & Strategy7 min read

How to Model Hierarchical Content (Product Catalogs With Variants) for AI Retrieval

A shopper asks your AI assistant for "trail runners under $150, in stock at the Portland warehouse, men's size 11." Your embedding-based search returns a beautiful list of shoes that are all out of stock, priced at $210, or only made in…

Published August 27, 2026

A shopper asks your AI assistant for "trail runners under $150, in stock at the Portland warehouse, men's size 11." Your embedding-based search returns a beautiful list of shoes that are all out of stock, priced at $210, or only made in women's sizing. The vector similarity was perfect. The answer was useless, and the model, handed nothing that fits, either hallucinates a product that does not exist or hedges its way into a non-answer. That failure has a shape, and it is the same shape every time: a query with a real structural component that pure vector similarity cannot resolve.

Sanity is the Content Operating System for the AI era, the intelligent backend for teams building AI content operations at scale, and the reason it matters here is that the fix for bad retrieval is not a better model, it is a better model of your content. Hierarchical catalogs (products, variants, features, stock locations) break naive retrieval precisely because their meaning lives in structure, not vibes.

This guide reframes catalog modeling as a retrieval problem. Model the hierarchy so that predicates that must hold stay structural, layer semantic ranking only where it earns its place, and keep the whole thing queryable and fresh in one system instead of three you stitch together.

Why hierarchical catalogs break naive AI search

A product catalog with variants is a hierarchy of constraints. A single sneaker is a parent product that fans out into variants by size, color, and fit, each of which carries its own price, stock level, and location. Layer on features (waterproofing, a specific sole compound, a heel-to-toe drop) and category membership, and you have a graph where most of the meaning lives in relationships and hard facts, not in prose.

Pure embeddings ignore all of that. You encode content as vectors, encode the query as a vector, and return the nearest neighbors. That works for fuzzy semantic match, "find me something like a trail runner," and it falls over the moment the query has structure: "trail runners under $150, in stock at the Portland warehouse, men's size 11." A vector has no notion of less-than, no notion of in-stock-here-but-not-there, and no notion that size 11 is a hard filter rather than a preference. Most products that market themselves as AI-powered search are this, and only this.

The consequence is the empty-result problem. When similarity ranking cannot enforce a constraint, it either returns wrong results that pass the vibe check but fail the facts, or returns nothing because nothing was near enough in vector space. A model handed empty or wrong results does what models do under uncertainty: it hallucinates a plausible-sounding product or hedges into a non-answer. Retrieval is where most agents fail, and the failure is almost never the model's reasoning. It is that the model was never given content whose shape it could query. A catalog modeled as flat, embeddable text throws away exactly the structure a shopper's question depends on.

Model your business: structure the hierarchy as content, not blobs

The first pillar of a Content Operating System is model your business, and for a catalog that means capturing the hierarchy as first-class, typed content rather than flattening it into searchable strings. In Sanity that is schema-as-code: you author a product type, a variant type, a productFeature type, and a stockLocation type, and you wire them together with references so the graph is explicit and queryable.

The shape of the modeling matters more than any single field. A parent product references its variants; a variant carries the atomic facts a query filters on (price, size, color, availability); a product references its features through a productFeature record so that a query can chain product to productFeature and match on the feature's id. That second-order reference chain is the pattern that lets "waterproof trail runners" resolve as a structural lookup instead of a hopeful text match. In Sanity Studio these types are portable defineType schemas, and TypeGen turns them into TypeScript so the shape your agent queries is the shape your frontend and your code already agree on.

Here is the honest part that no schema diagram shows you. When we ran schema exploration against Sonos's catalog, an honest nightmare of a dataset, getting to roughly 83 percent accuracy meant teaching the retrieval step three things the schema alone would not reveal: counter-intuitive field names (a field called body that is actually a slug, a hero that references a mediaAsset rather than an image), second-order reference chains the schema shows one hop at a time but never connects end to end, and data-quality issues like a features array that is always empty, so you use support-product instead. None of that is a model problem. It is a context problem. Modeling the hierarchy well is necessary; documenting its real shape is what makes it retrievable.

Hybrid retrieval: the discipline, not vector search alone

The correct mental model for catalog retrieval is not "add embeddings." It is hybrid: keyword search for literal matches, embeddings for semantic ranking, and structured predicates for the filters that have to hold. Anthropic's contextual retrieval research measured the payoff of layering directly. Contextual embeddings cut top-20 retrieval failures by 35 percent, adding contextual BM25 took that to 49 percent, and adding reranking on top brought it to 67 percent. The important reading is not the headline number, it is that none of the three layers alone was enough.

Watch how a real query decomposes. "Trail runners under $150 like a Hoka" splits into three retrieval signals running in parallel: a structured predicate (category is shoes and price is under 150), a BM25 lexical match on the keywords ("trail," "runner"), and a vector similarity score from the embedding of the full query, which is where "like a Hoka" earns its keep. Drop the predicate and you get overpriced or out-of-stock results. Drop the semantic signal and "like a Hoka" becomes noise. You need all three composed, not chosen between.

This is why "we have embeddings" is not a retrieval strategy. Embeddings are one ingredient. A pure structured query language (GROQ, SQL, GraphQL) gives you exactly what you asked for but falls over the moment the user says "the cozy one" or "something like X," which is precisely the exploratory, chatting-with-an-agent mode catalogs get browsed in. Pure vector falls over on constraints. The discipline is combining them, and it applies regardless of which database you reach for.

One GROQ query that blends predicates and relevance

The reason hybrid retrieval usually turns into a systems-integration project is that the three signals live in three places: your database for predicates, your search engine for BM25, and your vector store for similarity. Blending them means orchestrating three round trips and reconciling three ranked lists in application code. GROQ collapses that into one query against Content Lake, the queryable content store underneath Sanity.

Consider the pattern for the shopper's question. You filter first with predicates that must hold: `_type == "product" && category == $category && price < $maxPrice && stockLocation == $warehouse`. Then a `score()` pipeline blends relevance signals: `boost([title] match text::query($queryText), 2)` weights a keyword match on the title twice, because title hits matter more, and `text::semanticSimilarity($queryText)` adds a semantic score across the document. Pipe that into `order(_score desc)[0...10]` and you get a small, ranked list that matches both the structural constraints and the vibe. In the same round trip you project the reference you need: `"stock": stockLocation->{ name, available }`. The predicate did the filtering; the score did the ranking; the `->` join returned the fact the shopper actually asked about, all without a second call.

This is the "power anything" pillar in practice: ask for exactly the shape you need, including projections, references, and filters, in one query. It is also why structure has to come first. The predicates are only expressible because price, category, and stockLocation are modeled as real fields on real types rather than buried in a description string an embedding has to guess at.

The freshness problem the alternatives make you build

Say you build hybrid retrieval on a stack of separate parts: Postgres with pgvector, or Elasticsearch, or Algolia, or Pinecone plus a metadata filter layer. All of them can do structured-plus-relevance. Algolia is built for exactly that case. Hybrid retrieval is not Sanity-exclusive, and it is worth being honest about that. What changes between those stacks and a content-native one is not whether you can blend signals. It is who owns freshness.

A catalog is never static. Prices change, descriptions get rewritten, a variant sells out at one warehouse, a discontinued product gets deleted. Every one of those events has to reach the search index, or your AI confidently recommends a product that shipped its last unit yesterday. When retrieval is a separate vector database plus glue code, keeping the index fresh becomes a permanent roadmap line item: incremental indexing, re-embedding on change, deletion handling, eventual-consistency reasoning, and backfill whenever the schema changes. That is a real project and a class of bug all its own.

Content Lake handles that class of work because the content model, the queryable store, and the index are the same system rather than three you stitch together. When a record changes, the thing you query is already the current thing. Production data backs up why this ordering matters: when agents actually call the Sanity Context MCP endpoint, the heavy majority of calls are structured GROQ queries and schema lookups, semantic search is a small slice, and embeddings are opt-in, off by default, with most projects never turning them on. Model and structure first. Add semantic ranking only once structured retrieval is the proven bottleneck, not before.

Return structured data so the agent doesn't paraphrase your catalog

The last mile of catalog retrieval is the handoff to the agent, and it is where a well-modeled hierarchy quietly gets thrown away. A tool that returns prose forces the model to paraphrase, and paraphrasing is where facts go to die. If a retrieval tool answers the shopper's question with a paragraph describing three shoes, the model has to re-extract the price, the size, and the stock status from that paragraph, and it will get one of them subtly wrong, quoting a price from the wrong variant or dropping the warehouse detail entirely.

The fix is to make the tool return schema-shaped data. If the agent is supposed to return three products, the tool should return three product objects, not a paragraph. When agents get built against the Context MCP endpoint, the ones that work return schema-shaped responses the model can pass straight through; the ones that struggle get a wall of text back and re-narrate it, badly. The GROQ projection from the previous section already does this correctly: `{ _id, title, price, "stock": stockLocation->{ name, available } }` hands back typed fields, not sentences. Portable Text extends the same principle to rich descriptions, giving agents structured, annotated content to read instead of flattened HTML.

This closes the loop back to modeling. A structured hierarchy is only as valuable as the structure that survives to the model. Model the catalog as typed content, query it with predicates and relevance blended in one round trip, keep it fresh in the store it already lives in, and return it as objects. Each step preserves the shape the shopper's question depended on, which is the whole reason the answer comes back right.

Hierarchical catalog retrieval: how the approaches compare

FeatureSanityContentfulStrapiHygraph
Blend predicates and relevance in one queryNative: one GROQ query filters on category, price, and stockLocation, then a score() pipeline blends boost([title] match ...) with text::semanticSimilarity() and orders by _score.GraphQL filters handle predicates well, but semantic ranking routes through a separate search or vector layer, so there is no single query that blends both.REST or GraphQL covers structural filters; blended lexical-plus-semantic scoring means bolting on Meilisearch or pgvector and merging ranked lists in app code.Strong typed GraphQL filtering across federated sources, but blended structural-plus-semantic scoring in one round trip is not native; ranking lives in an external layer.
Second-order reference chains for variants and featuresChain product to productFeature and match on the feature id, projecting variants and stockLocation in the same query via the -> join and defineType references.References and links model the hierarchy in-platform; deep chained resolution is doable in GraphQL but can require multiple queries or client-side stitching.Components and relations model variants and features; deep reference traversal works but often needs populate tuning and custom resolvers.Excellent at typed relations and cross-source federation; multi-hop feature chains are expressible, resolved through GraphQL rather than a blended scoring query.
Index freshness on price, stock, and deletesContent Lake is the model and the queryable store, so a changed record is already the queried record; no separate re-index or re-embed pipeline to own.External search or vector index must be kept fresh with your own sync on every price, stock, or deletion change; freshness is glue code you maintain.Self-hosted search stack means you own incremental indexing, re-embedding on change, and deletion handling as an ongoing project.Content federation is fresh at the GraphQL layer, but any external ranking or vector index still needs its own change-sync to avoid stale results.
Editorial surface for catalog editorsSanity Studio is a React app you ship: custom input components, Structure Builder, and Content Releases for governed catalog changes, all in sanity.config.ts.Polished hosted editor customizable through app extensions in predefined UI slots, rather than a fully code-owned editor you ship.Open-source admin panel, customizable via plugins; self-hosted control at the cost of running and maintaining it yourself.Configurable hosted editing UI with strong content federation; customization is within the platform's extension model.
Structured, schema-shaped responses to agentsGROQ projections return typed objects (three product objects, not a paragraph); Portable Text gives agents annotated structured rich text to read.GraphQL returns typed JSON that agents can consume; the AI retrieval and MCP-style tool layer is assembled from external services.REST or GraphQL returns typed JSON; the retrieval-tool and structured-response plumbing for agents is yours to build.GraphQL returns typed JSON well; blended retrieval and the agent-facing tool layer are stitched from additional components.
When pure embeddings alone are enoughRarely: production data shows structured GROQ dominates and embeddings are opt-in, off by default, with most projects never enabling them.Same reality applies: constraint-heavy catalog queries need predicates, so an embeddings-only add-on fails on size, price, and stock just as it does anywhere.Same reality: a pure-vector bolt-on hits the empty-result problem on structural constraints regardless of the CMS underneath it.Same reality: semantic ranking helps with vibes, but constraints like warehouse stock and version still require structured predicates.

Ready to try Sanity?

See how Sanity can transform your enterprise content operations.