Skip to content

    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

    View source

    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:

    SurfaceField
    Searchfilters
    Collectionscollection_filters (the page’s scope) and filters (the shopper’s narrowing)
    Recommendationsfilters

    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.

    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.

    OperatorMeansValueExample
    eqEqualsScalar{ "color": { "eq": "blue" } }
    neqDoes not equalScalar{ "color": { "neq": "blue" } }
    gt gte lt lteNumeric comparisonScalar{ "price": { "gte": 25 } }
    inIs one ofNon-empty array{ "brand": { "in": ["northwear", "atlas"] } }
    not_inIs none ofNon-empty array{ "color": { "not_in": ["black", "white"] } }
    existsField is presenttrue / 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”.

    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.

    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.

    A filter tree is bounded by:

    LimitValue
    Nesting depth10 levels; the root node is level 1
    Nodes in one $and / $or array50
    Fields in one condition node50

    Exceeding any of them is a 422. Fifty selected brands are one in condition, not fifty nodes.

    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, lte do not match an object that lacks the attribute.
    • A negative test on a missing attribute is true. neq and not_in do 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.

    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 } }

    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.

    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_codeCause
    filter.unknown_fieldThe field is not present in this catalog
    filter.type_mismatchThe 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.

    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.

    SurfaceWritten asOperators
    Discovery — search, collections, recommendationsThe map syntax on this pageThe nine above
    Business rulesmatch and record_matchfield / operator / value leaves in a conditions arrayThose nine, plus matches for substring and range for a bounded interval
    Channel visibilityvisibility_filterThe same tree as business rulesThe 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.