JSON
prices.warehouse declared in attributesForFaceting, the filter prices.warehouse:"north" AND prices.amount=100000 shouldn’t match, because the north warehouse charges 93100.
It matches anyway.
The first element satisfies one condition, the second satisfies the other, and the record satisfies both.
Why the cross-match happens
Indexing collects every value at an attribute path into a single set. For the preceding record,prices.warehouse contains ["north", "south"] and prices.amount contains [93100, 100000], but indexing doesn’t preserve that north pairs with 93100.
Filters on array attributes match if any element matches.
Combining filters doesn’t require them to match the same element, and there’s no filter syntax to enforce that.
The same applies to parallel arrays.
Given prices: [50, 200] and stockCount: [1, 5], the filter prices < 100 AND stockCount > 2 matches even though the cheap item has low stock.
Filtering ignores array position.
Geo queries are the exception.
_geoloc stores coordinates as latitude/longitude pairs, so insideBoundingBox and related filters evaluate each pair together.
Other filters still cross-match independently.
The filter returns more records than expected without raising an error.
The extra records satisfy all conditions, but not for the same element.
Choose a record structure
Three record shapes avoid the cross-match, at different costs. Choose by what you need to filter, sort, or facet:One record per element
Index one record per array element, then group the results at query time:JSON
attributeForDistinct to productID and distinct to true.
Each search returns one row per product, and every filter applies to a single warehouse.
Each attribute now holds one value per record, so the usual declarations behave as written: add it to attributesForFaceting to facet on it, or to a replica’s ranking to sort by it.
Use this shape when per-element sorting, faceting, or ranking matters, and when a filter combines two conditions without naming the element: price < 100 AND stockCount > 2 stays two conditions here, where keyed attributes need one clause per key.
The cost is record count.
Shared product data repeats in every row, facet counts reflect either elements or products depending on facetingAfterDistinct, and Analytics and Recommend operate on the split records rather than the product.
See Deduplicate results with distinct for the full pattern.
One attribute per key
When you know at query time which property you’ll filter on, make that property the attribute name. Replace the array with an object keyed by it:JSON
prices.north is 93100 and nothing else.
A filter like prices.north=100000 has one value to test, so it can’t cross-match.
Numeric attributes are filterable without appearing in attributesForFaceting, unless numericAttributesForFiltering restricts them to a list.
Use this shape when the number of keys per record stays bounded.
Faceting keyed attributes
For facet counts or facet statistics, declare the parent object. Declaringprices covers every nested attribute below it, so the setting remains a single entry as nested attributes accumulate.
Request the child paths, such as prices.north, because requesting prices itself doesn’t return any facet.
Keyed attributes support filtering but not browsing.
You can’t ask “Which warehouses exist, and how many products does each one stock?” because each key becomes a separate attribute.
The facet values are the prices, not the keys.
To browse by warehouse, store the warehouse names alongside the prices:
JSON
warehouses gives one facet with a count for each warehouse, and searchable(warehouses) lets users search the list.
- Keep it if users browse or search by warehouse.
- Drop it if you only filter. The array duplicates every warehouse name and can make records larger than the array of objects it replaced.
prices but missing from warehouses remains filterable while disappearing from the facet.
The index doesn’t return an error.
To detect drift, search with facets: ["*"] and compare the warehouses facet values with the prices.* child names.
Increase maxValuesPerFacet to include every warehouse.
Otherwise, the default of 100 truncates the warehouses values but not the prices.* names, so the comparison reports false positives.
Limitations
- Cost. Each property increases record size. Your plan limits both the size of individual records and the average record size across the index. Declaring properties for faceting also increases index size.
- Sorting. Sorting by one property requires one replica per property, and each index supports only a limited number of replicas. Sort on a shared base attribute instead.
- Expressiveness. Filters that don’t specify a key, such as “cheap and well stocked anywhere”, must enumerate every key. The rewritten filter grows with the product of the per-key term counts. With more than about seven keys, it exceeds the documented limit of 1,000 filters per query. Use one record per element instead. See Boolean rewrites for keyed attributes for the construction and examples of rewrites that look correct but aren’t.
Composite attribute
When the same combination is always filtered together, join the values at indexing time:JSON
variants in attributesForFaceting, then filter with variants:"red|XL".
The record keeps its shape, and one condition replaces two.
Undeclared, the filter doesn’t match anything and doesn’t raise an error (see Missing and negated attributes).
This is the smallest change, but it only covers the combination you encoded.
Filtering on color alone takes a separate attribute, and composite values match as a whole value rather than supporting numeric ranges.
When nesting is safe
Pairing breaks when an attribute path has more than one value. The number of elements doesn’t matter: a one-element array whose fields are arrays breaks pairing, and so does an object whose fields are arrays. Nesting is safe in two cases:- A single value at every path. Given
price: { "net": 10, "gross": 12 }, the filterprice.net > 5 AND price.gross < 20behaves as written. Array-valued fields underpricewould break pairing. - Independent values. Use nested objects when cross-matching is the intended behavior. For example, filtering a
categoriesarray together with astorearray should match records that have both, in any combination.
Reference
Attribute naming and filter edge cases referenced throughout this guide.Attribute names with delimiters
Attribute names can contain dashes and underscores, sonorth-west doesn’t need rewriting.
Dots are different: a dot becomes an extra path level, so a literal "north.west" key and a nested {"north": {"west": ... }} share one filter path and one facet entry.
No filter syntax can tell the two apart, so replace or remove dots in property names before indexing.
The delimiters
:, =, <, >, !, (, and spaces only break the unquoted spelling.
Quote the key to keep it: filters: '"prices.north:west" = 100'.
Unquoted, facetFilters splits at the first colon and can match the wrong record.
numericFilters doesn’t have a quoted form and rejects the key outright.Test if keys are present
Records don’t all have the same keys: a product stocked in the north hasprices.north but might not have prices.south.
To test whether a key exists, use a comparison that covers the full range of valid values.
For example, prices.north >= 0 works only if prices can’t be negative.
Don’t use != for this: prices.north != 0 also matches records that don’t have prices.north.
Value types
A booleantrue matches numerically as 1, and a numeric string such as "7" doesn’t match anything.
Missing and negated attributes
Filtering on a string attribute that’s not inattributesForFaceting doesn’t match anything.
It doesn’t raise an error.
This applies to both filters and facetFilters, even if no record has the attribute.
Negation does the opposite: NOT colour:red and facetFilters: ["colour:-red"] match every record, even those with colour: red.
Boolean rewrites for keyed attributes
A query such as “cheap and well stocked anywhere” doesn’t name a key. To preserve pairing, the filter must consider every key. This requires a Boolean rewrite. The rewrite is only possible when both conditions use the same kind of term. AnOR clause can’t mix numeric comparisons and facet values, so queries such as “open somewhere and cheap in the same warehouse” can’t be rewritten.
The natural form is invalid because the filter grammar doesn’t allow AND inside OR:
Invalid
Filter
- Cross-match.
(prices.north < 100 OR prices.south < 100) AND (stock.north > 2 OR stock.south > 2)reintroduces cross-matching. It matches products that are cheap in one warehouse and well stocked in another. - Operator precedence. Omitting the parentheses silently changes the meaning because
ORbinds tighter thanAND. The filter parses asprices.north < 100 AND (stock.north > 2 OR prices.south < 100) AND stock.south > 2, requiring one warehouse to be cheap and another to be well stocked.