# 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: producttagscontains"sale".is one of: the comparison value is the collection. Example: productmaterialis 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:
intervalmust be greater than0.- Non-numeric values are skipped.
- Interval facets are always sorted by range from low to high.
- The
order_bysetting 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
ALLfor required business rules, such as in-stock, active, correct locale, or not discontinued. - Use
ANYgroups 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_ofwhen both the product field and selected values are lists. - Use
anywhen 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, orcategory_filter. - Use
omitto hide blank, unknown, or placeholder values before exposing facets to shoppers. - Use
min_maxfor slider controls andintervalfor fixed price buckets. - Use
nestedpluspath_prefix_anyfor hierarchical categories.