Pre-launch API. The concepts described here are settled, but the API shape is not: request and response fields, parameters and defaults can still change. Build against it, and talk to your Luigi's Box contact before you put an integration into production.
Filters are a JSON tree in the request body. One object restricts a result set:
{ "$and": [ { "availability": { "eq": 1 } }, { "price": { "lt": 50 } }, { "brand": { "in": ["northwear", "atlas"] } } ]}This page is the reference for that syntax. The node grammar and the evaluation rules are the same wherever Luigi’s Box AI accepts a filter; what changes between surfaces is which operators the surface accepts. Discovery takes one on three endpoints:
| Surface | Field |
|---|---|
| Search | filters |
| Collections | collection_filters (the page’s scope) and filters (the shopper’s narrowing) |
| Recommendations | filters |
Nothing needs quoting or escaping: a value with spaces, commas or quotes is a JSON string, and a field name with a colon is a JSON key.
A filter is a tree of nodes. Every node is one of two things.
A condition node maps field names to operator objects:
{ "brand": { "eq": "northwear" } }A group node holds exactly one of $and, $or or $not:
{ "$or": [{ "color": { "eq": "blue" } }, { "color": { "eq": "navy" } }] }$and and $or take an array of nodes. $not takes a single node. A node may not mix the
two forms — a $-key and a field name in the same object is a 422.
Everything adjacent is AND
Section titled “Everything adjacent is AND”Two shorthands reduce $and wrappers. Several operators on one field combine as AND:
{ "price": { "gte": 25, "lte": 75 } }And several fields in one node combine as AND:
{ "availability": { "eq": 1 }, "color": { "eq": "blue" } }$and is needed to place a group beside a condition, or to repeat a field:
{ "$and": [ { "availability": { "eq": 1 } }, { "$or": [{ "color": { "eq": "blue" } }, { "color": { "eq": "navy" } }] } ]}There is no operator precedence; nesting is explicit.
Operators
Section titled “Operators”| Operator | Means | Value | Example |
|---|---|---|---|
eq | Equals | Scalar | { "color": { "eq": "blue" } } |
neq | Does not equal | Scalar | { "color": { "neq": "blue" } } |
gt gte lt lte | Numeric comparison | Scalar | { "price": { "gte": 25 } } |
in | Is one of | Non-empty array | { "brand": { "in": ["northwear", "atlas"] } } |
not_in | Is none of | Non-empty array | { "color": { "not_in": ["black", "white"] } } |
exists | Field is present | true / false | { "image_url": { "exists": true } } |
in and not_in require at least one entry. There is no empty-set form — when a shopper
has selected no brands, omit the condition rather than sending an empty array.
const clauses = [{ availability: { eq: 1 } }];if (brands.length) clauses.push({ brand: { in: brands } });const filters = clauses.length === 1 ? clauses[0] : { $and: clauses };{ "exists": false } is the same as wrapping { "exists": true } in $not. Both mean
“objects that do not carry this field at all”.
Fields
Section titled “Fields”A field is a catalog attribute name, used verbatim — the same name you indexed it under, as a JSON key.
{ "color": { "eq": "blue" } }{ "price": { "lte": 50 } }{ "@category": { "in": ["category/shirts"] } }{ "lbx:group_primary": { "eq": true } }{ "params.material": { "eq": "cotton" } }{ "my odd field": { "eq": 1 } }Namespaced names contain a colon, nested attributes use a dot-path, and a name with spaces
or punctuation needs nothing special — a JSON key holds any of them as-is. The only
restrictions are that a field name must not begin with $, which is reserved for group
keys, and must not have leading or trailing whitespace.
To see which attributes a catalog holds, list its attributes and their values.
Values
Section titled “Values”A value is a JSON scalar: a string, a number, a boolean, or null.
{ "in_stock": { "eq": true } }{ "price": { "gt": -0.5 } }{ "category": { "eq": "musical instrument" } }{ "brand": { "eq": "Atlas & Co" } }{ "name": { "eq": "salt and pepper" } }Types must match the field’s. A number compared against a text field is a
filter.type_mismatch.
Size limits
Section titled “Size limits”A filter tree is bounded by:
| Limit | Value |
|---|---|
| Nesting depth | 10 levels; the root node is level 1 |
Nodes in one $and / $or array | 50 |
| Fields in one condition node | 50 |
Exceeding any of them is a 422. Fifty selected brands are one in condition, not fifty
nodes.
Missing attributes
Section titled “Missing attributes”Not every object has every attribute. When an attribute is missing:
- A positive test on a missing attribute is false.
eq,in,gt,gte,lt,ltedo not match an object that lacks the attribute. - A negative test on a missing attribute is true.
neqandnot_indo match it.
For example:
{ "color": { "neq": "blue" } }matches every object with no color at all, alongside the red and green ones. For “has a
colour, and it is not blue”, add exists:
{ "color": { "neq": "blue", "exists": true } }The same applies to a rule that bans products outside a category: @category with neq
will fire on products with no category.
Worked examples
Section titled “Worked examples”Everything in stock under €50, from two brands:
{ "availability": { "eq": 1 }, "price": { "lt": 50 }, "brand": { "in": ["northwear", "atlas"] }}Two categories, excluding clearance:
{ "@category": { "in": ["category/shirts", "category/knitwear"] }, "clearance": { "neq": true }}A price band, either of two colours:
{ "$and": [ { "price": { "gte": 25, "lte": 75 } }, { "$or": [{ "color": { "eq": "blue" } }, { "color": { "eq": "navy" } }] } ]}Only the primary product of each variant group:
{ "lbx:group_primary": { "eq": true } }Everything except what is already in the cart:
{ "availability": { "eq": 1 }, "@id": { "not_in": ["product/sku-1001", "product/sku-2087"] }}Only products that have an image:
{ "image_url": { "exists": true } }Building a filter from facet selections
Section titled “Building a filter from facet selections”Rebuild the tree from the selected values on each request rather than mutating one across requests; this keeps the “clear all filters” case correct:
function buildFilters({ brands, maxPrice, inStockOnly }) { const clauses = []; if (brands.length) clauses.push({ brand: { in: brands } }); if (maxPrice != null) clauses.push({ price: { lte: maxPrice } }); if (inStockOnly) clauses.push({ availability: { eq: 1 } });
if (clauses.length === 0) return undefined; // omit the field entirely if (clauses.length === 1) return clauses[0]; return { $and: clauses };}Omit filters rather than sending null or {} when there is nothing to filter on: an
empty object is not a valid node.
When a filter is rejected
Section titled “When a filter is rejected”A filter is checked in two stages, and the two rejections have different shapes.
A tree that does not match the node grammar — an unknown operator, an empty in array, a
node mixing $and with a field name, anything over a size limit — is rejected before the
request is served, as a
validation error. Each entry’s
loc names the offending path:
{ "reason": "Request validation failed", "exception_details": { "validation_errors": [ { "loc": ["body", "filters"], "msg": "Value error, filters.$and[2]: unknown group operator '$nor' (allowed: $and, $or, $not)", "type": "value_error" } ] }}A tree that is well-formed but does not fit the catalog is rejected when it is applied,
and carries an error_code instead:
exception_details.error_code | Cause |
|---|---|
filter.unknown_field | The field is not present in this catalog |
filter.type_mismatch | The value’s type does not match the field’s — a number compared to text |
A filter.type_mismatch usually means the attribute was indexed as text when you expected
a number: price arriving as "29.90" rather than 29.90 in one feed run types the field
as text for good. Check the field’s type with
catalog metadata and fix it at the
mapping, not in the filter.
Other surfaces that filter
Section titled “Other surfaces that filter”Everything above about values, missing attributes and evaluation holds wherever a filter appears. Two things vary by surface: which operators it accepts, and how the tree is written.
| Surface | Written as | Operators |
|---|---|---|
| Discovery — search, collections, recommendations | The map syntax on this page | The nine above |
Business rules — match and record_match | field / operator / value leaves in a conditions array | Those nine, plus matches for substring and range for a bounded interval |
Channel visibility — visibility_filter | The same tree as business rules | The same as business rules |
// A business rules leaf, for comparison with a condition node above{ "field": "brand", "operator": "eq", "value": "northwear" }A surface rejects an operator it does not accept; each surface’s own reference lists the operators it takes. For campaigns, the trigger field metadata reports the operators each field accepts.
See also
Section titled “See also”- Search · Collections · Recommendations
- Facets — turning filters into a sidebar
- Catalog metadata — which fields and values exist
- Business rules — the same semantics, in a different tree shape
Was this page helpful?
Thanks.