--- title: Product variants description: How variant groups behave in discovery — result collapsing, group totals, resolving a group's siblings, and group-level ranking signals. slug: discovery/variants docKind: guide hub: luigisbox-ai --- A variant group is a set of products a shopper thinks of as one thing — the same t-shirt in five sizes, the same phone in three colors. Each variant stays a complete, independently indexed product with its own `@id`; the group is a shared key, not a separate catalog object. This page covers what variants do at query time. For how to send them, see [Variant groups](/concepts/catalog-object-model/#variant-groups) in the catalog object model and the [product feed reference](/indexing/feeds/products/#variants). ## What Luigi's Box knows about a group You send grouping on a product as `@group_id` and `@group_primary`. Discovery does not read those fields directly — Luigi's Box derives a search attribute from each of them, and every behavior below is driven by the derived pair: | Derived attribute | Value | |---|---| | `lbx:group_id` | The product's `@group_id`, or the untyped part of its own `@id` when you send none. | | `lbx:group_primary` | The product's `@group_primary`, or `false` when you send none. | The `lbx:` namespace belongs to Luigi's Box: these attributes are derived from what you send and cannot be set directly — a feed or API request carrying an `lbx:` field is rejected. They are public, so they come back on search hits like any other field and you can read a hit's group straight from a result. Because `lbx:group_id` falls back to the product's own identity, **a product that belongs to no group is a group of one**. Nothing in discovery treats an ungrouped product as a special case. ## Collapsing search results By default, every matching variant is its own search result. A shirt stocked in five sizes takes five positions on the results page. With collapsing enabled, a surface returns **one result per variant group**. Which member represents the group depends on the surface's strategy: | Strategy | Which products are retrieved | Which one is served | |---|---|---| | `variant_or_primary` | All members. | The best-ranked member of the group. | | `primary` | Only members marked `lbx:group_primary`. | The group's primary product. | | `variant` | Only members not marked primary. | The best-ranked non-primary member. | `variant_or_primary` is the default and needs no primaries marked: it folds each group down to whichever member ranked best for the query. Use it unless the storefront needs to land shoppers on a specific record. Collapsing is configured per surface. Ask your Luigi's Box contact to enable it and to pick the strategy for a given search, collections, or recommendation surface. :::caution[`primary` and `variant` need primaries in the catalog] Both strategies retrieve through a filter on `lbx:group_primary`, so they assume every group has exactly one product marked `@group_primary: true` — a standalone product included, which means sending it a `@group_id` and marking it primary. On a catalog where nothing is marked primary, a `primary` surface returns no results and a `variant` surface behaves like `variant_or_primary`. ::: ### What changes when collapsing is on - **`total` counts groups, not products.** A query matching 40 shirts across 8 groups reports 8. - **Pagination pages through groups.** A group served on page 1 is not served again on page 2, whichever of its members ranked best. Filters and sorting still apply to individual products. A filter on `size: XL` matches the XL variants and the groups they belong to survive collapsing; groups with no matching member drop out entirely. ## Resolving the rest of a group A collapsed result gives you one product per group. To render swatches, a size picker, or a "3 more colors" badge, ask for the group's other members with `POST /discovery/v1/variants`. It is a follow-up to a search or collections response, like `/facets` — a token authorized on either surface can call it. ```bash curl -X POST 'https://api.eu1.luigisbox.ai/discovery/v1/variants?channel_id=lbn_4hj9tv' \ -H 'Authorization: Bearer ' \ -H 'X-Lbx-Visitor-Id: 8f14e45f-ea0f-4b5c-9a1d-2b3c4d5e6f70' \ -H 'Content-Type: application/json' \ -d '{ "ids": ["product/sku-1001", "product/sku-1042"], "guid": "3ab549eb64a20a0c", "return_fields": ["@title", "url", "image_url", "color", "size"] }' ``` `channel_id` is a query parameter, as on every discovery endpoint. There is no `surface_id`: the request is authorized by the search or collections surface it follows up on. Everything else goes in the JSON body. | Query parameter | Required | Notes | |---|---|---| | `channel_id` | Yes | Serving destination | | Body field | Required | Default | Notes | |---|---|---|---| | `ids` | Yes | — | Catalog `@id` values, used verbatim — the same ids search returned. 1 to 200 per request. | | `guid` | No | none | The `guid` from the originating `/search` or `/collections` response. Groups are then read from that result set. Omit it and every id resolves from the catalog. An empty string is rejected. | | `return_fields` | No | every attribute | [Field projection](/discovery/fields/) applied to returned members. `@id` and `@type` are always included. | Unknown body fields are rejected with a `422` rather than ignored. The response maps each requested id to its group: ```json { "variants": { "product/sku-1001": { "siblings": [ { "@id": "product/sku-1002", "@type": "product", "@title": "Blue Cotton T-Shirt, S", "size": "S" }, { "@id": "product/sku-1003", "@type": "product", "@title": "Blue Cotton T-Shirt, L", "size": "L" } ], "total": 3 }, "product/sku-1042": { "siblings": [], "total": 1 } } } ``` Reading the response: - `siblings` never contains the requested id itself. - `total` is the group's full size, the requested id included. `"total": 3` with two siblings means a group of three. - `siblings` is capped at 100 members, returned in `@id` order. When a group is larger, `total` still reports its full size, so truncation is detectable. - A product that stands alone returns empty `siblings` and `"total": 1`. - An id that is not in the catalog returns empty `siblings` and `"total": 0`. See [`get_variants`](/api/operations/get_variants/) in the API reference for the full schema and error responses. ## Variants and ranking Interactions recorded on any member of a group also credit the group. A shopper who clicks the blue shirt in size M contributes to that product's own popularity and to the group's, so a group's signal is the sum of what all its members earned. Without group aggregation, a shirt selling well in six sizes would look six times less popular than an equally successful single-record product. An ungrouped product forms a group of one and is ranked on its own signal. ## See also - [Catalog object model](/concepts/catalog-object-model/#variant-groups) — the `@group_id` and `@group_primary` slots and their partial-update semantics. - [Product feeds](/indexing/feeds/products/#variants) — mapping variant fields from a source feed. - [Content API](/indexing/content-api/) — sending grouping over the API. - [Sorting and pagination](/discovery/sorting-and-pagination/) — why a collapsed total counts groups. - [Discovery overview](/discovery/overview/) — where the `guid` comes from.