--- title: "Merchandising Rules V3" slug: "merchandising-rules-v3" updated: 2026-06-05T04:04:01Z published: 2026-06-05T04:04:01Z canonical: "docs.xgen.ai/merchandising-rules-v3" --- > ## Documentation Index > Fetch the complete documentation index at: https://docs.xgen.ai/llms.txt > Use this file to discover all available pages before exploring further. # Merchandising Rules V3 # Merchandising V3 Filters and Facets This guide explains how Merchandising V3 decides which products appear in a rule, how facets are calculated, and what each operator and facet option means. It is written for business users who configure merchandising rules and for technical users who need to understand the saved rule JSON. ## Overview Merchandising V3 rules have two main parts: - **Filters** decide which products are eligible to appear. - **Facets** count and group values from those eligible products so a storefront, preview, or API response can show filter options such as material, color, size, category, or price. A filter is built from one or more conditions. Each condition asks a question about a product, user, current product, or runtime value. For example: - Is the product material equal to `Cotton`? - Is the product price less than or equal to `100`? - Does the product have at least one of the selected colors? - Does the category path start with one of the selected category paths? Facets do not filter products by themselves. They summarize the matching product set. For example, after a rule finds 250 matching products, a Material facet may return `Cotton: 83`, `Linen: 54`, and `Wool: 21`. ## Rule Logic At the top of the filter builder, the UI asks whether products should match **ALL** or **ANY** of the conditions. | UI option | JSON value | Meaning | | --- | --- | --- | | `ALL` | `and` | A product must match every condition in the group. | | `ANY` | `or` | A product only needs to match one condition in the group. | Groups can be nested. A common pattern is: - Match **ALL** of: - Product is in stock. - Product matches **ANY** of several materials. In JSON, a group uses `logic` and `conditions`: ``` { "version": "3", "logic": "and", "conditions": [ { "property": "in_stock", "operator": "equals", "value": true }, { "logic": "or", "conditions": [ { "property": "material", "operator": "equals", "value": "Cotton" }, { "property": "material", "operator": "equals", "value": "Linen" } ] } ] } ``` ## Condition Fields Each condition has three core choices in the UI: | UI field | JSON field | Meaning | | --- | --- | --- | | Field | `property` | The product field to evaluate, such as `material`, `price`, `categories`, or `metadata.color`. | | Operator | `operator` | The comparison to use, such as `is equal to`, `contains`, or `is one of`. | | Value type | `value` or `variable` | Where the comparison value comes from. | ### Value Types | UI value type | JSON behavior | Use when | | --- | --- | --- | | Static Value | Uses `value` | The condition always compares against the same typed value, such as `true`, `100`, or `Cotton`. | | Variable | Uses `variable` | The value is provided at runtime, such as selected filters from a storefront. | | User Property | Uses `variable` with an `xu_` value | The value comes from user behavior, such as viewed products or purchased products. | | Current Product | Uses `variable` with an `xp_` value | The value comes from the product currently being viewed. In order to use this, you MUST pass `pdpCode` as context. | Runtime variables can use `"*"` as a pass-through value. When the resolved comparison value is `"*"`, the condition passes for all products. ### User Properties The UI supports these user property variables: | UI label | Variable | | --- | --- | | Viewed Products | `xu_viewed_products` | | Cart Items | `xu_cart_items` | | Most Recent Cart Product | `xu_recent_cart_product` | | Most Recent Cart Category | `xu_recent_cart_category` | | Viewed Multiple Times | `xu_multi_viewed_products` | | Purchased Products | `xu_purchased_products` | | Most Recent Viewed Product | `xu_last_viewed` | | Most Recent Viewed Category | `xu_last_viewed_category` | User property values are cached for a limited time. ### Current Product Properties Current product values are saved as variables prefixed with `xp_`. For example, selecting current product field `material` stores `variable: "xp_material"`. Then when `pdpCode` is sent as context, it is replaced with the value for material on that specific product passed. ## Operator Reference The UI shows friendly operator labels. The saved JSON uses operator slugs. Aliases are also accepted when importing JSON. | UI label | Primary slug | Aliases | Works best with | Meaning | | --- | --- | --- | --- | --- | | is equal to | `equals` | `=`, `===`, `equal_to`, `is`, `is_equal_to` | string, number, boolean | Product value must exactly equal the comparison value. No type coercion. | | is not equal to | `doesnt_equal` | `!=`, `!==`, `not`, `not_equal`, `not_equal_to`, `is_not`, `is_not_equal_to`, `doesnt_equal_to`, `!equals` | string, number, boolean | Product value must not exactly equal the comparison value. No type coercion. | | contains | `contains` | `has`, `includes` | string, list | Product string contains a substring, or product list contains a value. | | does not contain | `doesnt_contain` | `!contains`, `!has`, `!includes`, `doesnt_have`, `not_contains` | string, list | Product string/list does not contain the comparison value. | | is greater than | `greater_than` | `>` | number | Product number is greater than the comparison value. Numeric strings can be coerced. | | is greater than or equal to | `greater_than_or_equal_to` | `>=`, `gte` | number | Product number is at least the comparison value. Numeric strings can be coerced. | | is less than | `less_than` | `<` | number | Product number is less than the comparison value. Numeric strings can be coerced. | | is less than or equal to | `less_than_or_equal_to` | `<=`, `lte` | number | Product number is at most the comparison value. Numeric strings can be coerced. | | is one of | `any` | `some`, `in` | string, list | Product value is present in the comparison list. This is the reverse of `contains`. | | is not one of | `none` | `not_in` | string, list | Product value is not present in the comparison list. | | has at least one of | `has_one_of` | none | list | Product list and comparison list share at least one item. | | has none of | `has_none_of` | none | list | Product list and comparison list do not share any items. | | has all of | `all` | `every` | list | Product list contains every item in the comparison list. | | starts with any path | `path_prefix_any` | none | list | Product hierarchy path starts with at least one selected path prefix. | | matches pattern | `matches_regex` | `regex` | string | Product value matches a regular expression pattern. | | has a value | `exists` | `exist`, `is_not_null`, `is_defined` | string, number, boolean, list | Field is present and not null. No comparison value is needed. | | has no value | `is_null` | `is_empty`, `is_not_defined` | string, number, boolean, list | Field is missing or null. No comparison value is needed. | ### Operator Notes `contains` and `is one of` are similar but point in opposite directions: - `contains`: the product field is the collection or string. Example: product `tags` contains `"sale"`. - `is one of`: the comparison value is the collection. Example: product `material` is one of `["Cotton", "Linen"]`. Use `has_one_of` for multi-select filtering when both sides are lists: ``` { "property": "colors", "operator": "has_one_of", "variable": "selected_colors" } ``` Use `path_prefix_any` for nested category facets: ``` { "property": "categories", "operator": "path_prefix_any", "variable": "selected_categories" } ``` With context: ``` { "selected_categories": [["Furniture", "Living Room"], ["Lighting"]] } ``` An item with `categories: ["Furniture", "Living Room", "Sofas"]` matches because its path starts with `["Furniture", "Living Room"]`. ## Facets Facets define which filter options should be returned with the matching products. A facet selects a field, a behavior, and optional display/grouping controls. Example: ``` { "property": "material", "label": "Material", "mode": "disjunctive", "exclude": ["material_filter"], "order_by": "count_desc", "manual_order_list": ["Cotton", "Linen"], "omit": ["Unknown", ""], "value_type": { "type": "" } } ``` ### Facet Fields | UI field | JSON field | Required | Meaning | | --- | --- | --- | --- | | Field | `property` | Yes | Product field to count. | | Display label | `label` | No | Customer-facing name. Defaults to the field name if empty. | | Behavior | `mode` | Yes | How counts are calculated: narrows results or multi-select. | | Linked condition or filter group | `exclude` | For multi-select behavior | Condition/group IDs to ignore while calculating this facet's counts. | | Value grouping | `value_type` | No | Controls whether values are listed individually, grouped into ranges, returned as min/max, or treated as a hierarchy. | | Sort values by | `order_by` | No | Controls bucket order for regular and nested facets. | | Manual order | `manual_order_list` | No | Pins selected values to the front in a specific order. | | Hide specific values | `omit` | No | Removes values from the facet response. | ## Facet Behavior ### Narrows Results UI label: `Narrows results` JSON mode: `conjunctive` Counts are calculated from the already-filtered product set. Selecting a value narrows the results further (used for single select options). Use this for simple refinement behavior where each selected option should reduce the available pool. ``` { "property": "color", "label": "Color", "mode": "conjunctive" } ``` ### Multi-select UI label: `Multi-select` JSON mode: `disjunctive` Counts are calculated as if the linked condition or group were temporarily ignored. This lets a facet continue showing other available values even after one value is selected. Use this for common multi-select storefront filters such as material, color, size, or category. For multi-select to work well: - The facet should link to the condition or group that controls the same selection. - The linked condition should use a runtime variable, user property, or current product property. - The linked condition or group must have an `id`. Example condition: ``` { "id": "material_filter", "property": "material", "operator": "any", "variable": "selected_materials" } ``` Example linked facet: ``` { "property": "material", "label": "Material", "mode": "disjunctive", "exclude": ["material_filter"] } ``` If a group is linked, only variable-based conditions inside that group are ignored for the count calculation. Static conditions remain applied. ## Facet Value Grouping ### Enumerate Distinct Values UI label: `Enumerate Distinct Values (default)` JSON: ``` { "value_type": { "type": "" } } ``` This creates one bucket for each distinct value. It is best for material, color, size, availability, rating labels, and similar fields. If the field is a list, each list item becomes its own bucket. Example response: ``` { "property": "color", "values": [ { "display_value": "Blue", "value": "Blue", "count": 42 }, { "display_value": "Red", "value": "Red", "count": 35 } ] } ``` ### Number Ranges UI label: `Number ranges (intervals)` JSON: ``` { "value_type": { "type": "interval", "interval": 50 } } ``` This groups numeric values into fixed-width ranges. With an interval of `50`, prices are grouped as `0-50`, `50-100`, `100-150`, and so on. Use this for price, inventory quantity, rating count, discount percentage, or other numeric ranges. Notes: - `interval` must be greater than `0`. - Non-numeric values are skipped. - Interval facets are always sorted by range from low to high. - The `order_by` setting is ignored for interval facets. Example response: ``` { "property": "price", "values": [ { "display_value": "0-50", "value": [0, 50], "count": 10 }, { "display_value": "50-100", "value": [50, 100], "count": 24 } ] } ``` ### Min / Max Values UI label: `Min / Max values` JSON: ``` { "value_type": { "type": "min_max" } } ``` This returns the minimum and maximum numeric values found in the matching product set. It is useful when the storefront needs to render a slider. Notes: - Only numeric values are considered. - If the field is a list, all numeric values in the list are considered. - If no numeric values are found, the facet returns no buckets. - If min and max are the same, both buckets may have the same value. Example response: ``` { "property": "price", "values": [ { "display_value": "19.99", "value": 19.99, "count": 5 }, { "display_value": "299.99", "value": 299.99, "count": 3 } ] } ``` ### Nested Hierarchy UI label: `Nested hierarchy` JSON: ``` { "value_type": { "type": "nested" } } ``` This builds a tree from a list field where each position represents a level in the hierarchy. Example product field: ``` { "categories": ["Furniture", "Living Room", "Sofas"] } ``` Example nested facet response: ``` { "property": "categories", "values": [ { "display_value": "Furniture", "value": "Furniture", "count": 2, "children": [ { "display_value": "Living Room", "value": "Living Room", "count": 2, "children": [ { "display_value": "Sofas", "value": "Sofas", "count": 1 }, { "display_value": "Coffee Tables", "value": "Coffee Tables", "count": 1 } ] } ] } ] } ``` Use `path_prefix_any` to filter products by selected nested facet paths. ## Sorting Facet Values The UI option `Sort values by` maps to `order_by`. | UI label | JSON value | Meaning | | --- | --- | --- | | A to Z | `alphabetical_asc` | Sort by display value ascending. | | Z to A | `alphabetical_desc` | Sort by display value descending. | | Most popular first | `count_desc` | Highest count first, with alphabetical tie-break. | | Least popular first | `count_asc` | Lowest count first, with alphabetical tie-break. | | Lowest number first | `numeric_asc` | Sort by numeric value ascending. Values must be numeric. | | Highest number first | `numeric_desc` | Sort by numeric value descending. Values must be numeric. | If omitted, facet values default to alphabetical ascending. For nested hierarchy facets, sorting applies independently at each level of the tree. ## Manual Ordering Manual order pins specific display values to the front of a facet response. Any values not included in the manual list still appear afterward using the selected sort order. Example: ``` { "property": "color", "order_by": "count_desc", "manual_order_list": ["Black", "White", "Red"] } ``` In this example, `Black`, `White`, and `Red` appear first in that order. Remaining colors appear by count descending. Manual values must match the facet value's `display_value`. Values that do not exist in the result set are ignored. ## Hiding Facet Values The `omit` list hides facet values entirely. ***This should be done by the `display_value`*** Use it to remove values such as: - Empty string: `""` - No value: `"null"` - Placeholder values such as `-` - Internal labels such as `Unknown` Example: ``` { "property": "material", "omit": ["", "Unknown", "-"] } ``` For nested hierarchy facets, omitting a node also omits its children. ## Deduplication The rule setting `Deduplication field` maps to `dedup_field` in JSON. Use deduplication when multiple catalog records represent the same shopper-facing product, such as size variants or color variants. Example: ``` { "version": "3", "logic": "and", "conditions": [], "dedup_field": "master_product_id" } ``` Products are filtered first, then deduplicated by the selected field. ## Full Example This example filters to in-stock products, supports multi-select material filtering, supports nested category filtering, deduplicates by `sku`, and returns material, price, and category facets. ``` { "version": "3", "logic": "and", "conditions": [ { "property": "in_stock", "operator": "equals", "value": true }, { "id": "material_filter", "property": "material", "operator": "any", "variable": "selected_materials" }, { "id": "category_filter", "property": "categories", "operator": "path_prefix_any", "variable": "selected_categories" } ], "dedup_field": "master_id", "facets": [ { "property": "material", "label": "Material", "mode": "disjunctive", "exclude": ["material_filter"], "order_by": "count_desc", "manual_order_list": ["Cotton", "Linen"], "omit": ["", "Unknown"], "value_type": { "type": "" } }, { "property": "price", "label": "Price", "mode": "conjunctive", "value_type": { "type": "min_max" } }, { "property": "categories", "label": "Category", "mode": "disjunctive", "exclude": ["category_filter"], "order_by": "alphabetical_asc", "value_type": { "type": "nested" } } ] } ``` Runtime context example: ``` { "selected_materials": ["Cotton", "Linen"], "selected_categories": [["Shoes", "Running"]] } ``` ## Troubleshooting | Issue | Likely cause | Resolution | | --- | --- | --- | | Save is disabled because a condition needs a field. | A condition was added without selecting a product field. | Select a field or remove the condition. | | Save is disabled because a condition needs a value. | The operator requires a value, but the value is blank. | Enter a static value or choose a variable/user/current-product source. | | A multi-select facet does not show other available values. | The facet is not linked to the condition controlling that selection. | Set Behavior to `Multi-select` and choose the linked condition or group. | | A disjunctive facet still looks filtered. | The linked condition uses a static value, or the wrong condition/group is linked. | Use a variable-based condition for selected values and link that condition's ID. | | Numeric sorting fails or behaves unexpectedly. | The facet value is not numeric. | Use alphabetical/count sorting, or clean the catalog field so values are numeric. | | Interval facet is invalid. | Interval size is blank, zero, or negative. | Enter an interval size greater than `0`. | | A nested category facet is flat or empty. | The category field is not stored as a path array. | Store each product category as a flat ordered array, such as `["Furniture", "Living Room", "Sofas"]`. | | A filter unexpectedly matches everything. | Runtime context value is `"*"`. | Replace `"*"` with the intended selected value, unless pass-through behavior is desired. | ## Best Practices - Use `ALL` for required business rules, such as in-stock, active, correct locale, or not discontinued. - Use `ANY` groups for alternatives, such as several acceptable materials or categories. - Use variables for shopper-selected filters so storefront selections can change without editing the rule. - Use `has_one_of` when both the product field and selected values are lists. - Use `any` when the product field is a single value and the selected values are a list. - Use disjunctive, multi-select facets for common storefront filter groups. - Give linked conditions readable IDs such as `material_filter`, `color_filter`, or `category_filter`. - Use `omit` to hide blank, unknown, or placeholder values before exposing facets to shoppers. - Use `min_max` for slider controls and `interval` for fixed price buckets. - Use `nested` plus `path_prefix_any` for hierarchical categories.