> ## Documentation Index
> Fetch the complete documentation index at: https://algolia.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Filters and arrays of objects

> Why a filter on an array of objects can match values from different elements, and how to structure records to avoid it.

export const Records = () => <Tooltip tip="A record is a searchable object in an Algolia index. Each record consists of named attributes." cta="Algolia records" href="/doc/guides/sending-and-managing-data/prepare-your-data#algolia-records">
    records
  </Tooltip>;

When <Records /> store an array of objects, Algolia indexes the values for each attribute path independently.
It doesn't preserve which values came from the same array element.
A filter combining two conditions can then match a record where a different element satisfied each one.

Consider a product sold at two warehouses at different prices:

```json JSON icon=braces theme={"system"}
{
  "objectID": "thermostat-1",
  "name": "Thermostat",
  "prices": [
    { "warehouse": "north", "amount": 93100 },
    { "warehouse": "south", "amount": 100000 }
  ]
}
```

With `prices.warehouse` declared in [`attributesForFaceting`](/doc/api-reference/api-parameters/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`](/doc/api-reference/api-parameters/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:

| Goal                                                                          | Record shape                                      |
| ----------------------------------------------------------------------------- | ------------------------------------------------- |
| Sort, facet, or rank on each element independently                            | [One record per element](#one-record-per-element) |
| Filter on a property you know at query time, with a bounded number per record | [One attribute per key](#one-attribute-per-key)   |
| Filter one fixed combination of values together                               | [A composite attribute](#composite-attribute)     |

### One record per element

Index one record per array element, then group the results at query time:

```json JSON icon=braces theme={"system"}
[
  {
    "objectID": "thermostat-1-north",
    "productID": "thermostat-1",
    "name": "Thermostat",
    "warehouse": "north",
    "amount": 93100
  },
  {
    "objectID": "thermostat-1-south",
    "productID": "thermostat-1",
    "name": "Thermostat",
    "warehouse": "south",
    "amount": 100000
  }
]
```

Set [`attributeForDistinct`](/doc/api-reference/api-parameters/attributeForDistinct) to `productID` and [`distinct`](/doc/api-reference/api-parameters/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`](/doc/api-reference/api-parameters/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`](/doc/api-reference/api-parameters/facetingAfterDistinct), and Analytics and Recommend operate on the split records rather than the product.
See [Deduplicate results with distinct](/doc/guides/managing-results/refine-results/grouping) 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 JSON icon=braces theme={"system"}
{
  "objectID": "thermostat-1",
  "name": "Thermostat",
  "prices": {
    "north": 93100,
    "south": 100000
  }
}
```

Each key is now its own path holding a single value, not a set: `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`](/doc/api-reference/api-parameters/attributesForFaceting), unless [`numericAttributesForFiltering`](/doc/api-reference/api-parameters/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.
Declaring `prices` 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 JSON icon=braces theme={"system"}
{
  "objectID": "thermostat-1",
  "name": "Thermostat",
  "warehouses": ["north", "south"],
  "prices": {
    "north": 93100,
    "south": 100000
  }
}
```

Declaring `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.

If you keep both attributes, build them from the same source at indexing time.
Otherwise, a key in `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`](/doc/api-reference/api-parameters/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](/doc/guides/scaling/algolia-service-limits#application-record-and-index-limits). Declaring properties for faceting also increases index size.
* **Sorting.** Sorting by one property requires [one replica per property](/doc/guides/managing-results/refine-results/sorting/in-depth/replicas), 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](/doc/guides/scaling/algolia-service-limits#filters-facets-and-rules-limits). Use one record per element instead. See [Boolean rewrites for keyed attributes](#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 JSON icon=braces theme={"system"}
{
  "objectID": "shirt-1",
  "variants": ["red|XL", "blue|S"]
}
```

Declare `variants` in [`attributesForFaceting`](/doc/api-reference/api-parameters/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](#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 filter `price.net > 5 AND price.gross < 20` behaves as written. Array-valued fields under `price` would break pairing.
* **Independent values.** Use nested objects when cross-matching is the intended behavior. For example, filtering a `categories` array together with a `store` array 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, so `north-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.

<Info>
  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.
</Info>

### Test if keys are present

Records don't all have the same keys: a product stocked in the north has `prices.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 boolean `true` 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 in `attributesForFaceting` doesn't match anything.
It doesn't raise an error.
This applies to both `filters` and [`facetFilters`](/doc/api-reference/api-parameters/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. An `OR` 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`:

```text Invalid icon=circle-x theme={"system"}
(prices.north < 100 AND stock.north > 2) OR
(prices.south < 100 AND stock.south > 2)
```

Rewrite it as a conjunction of disjunctions, with one term per key in each clause:

```text Filter icon=circle-check theme={"system"}
(prices.north < 100 OR prices.south < 100) AND
(prices.north < 100 OR stock.south > 2) AND
(stock.north > 2 OR prices.south < 100) AND
(stock.north > 2 OR stock.south > 2)
```

This form is equivalent to the original paired query while using only syntax the filter grammar accepts.

The number of clauses equals the product of the per-key term counts. Two keys produce four clauses. Ten keys produce 1,024 clauses and more than 10,000 individual filters, well past the limit of [1,000 filters per query](/doc/guides/scaling/algolia-service-limits#filters-facets-and-rules-limits). The filter also grows exponentially in size, reaching about 188 KB at ten keys. **In practice, treat about seven keys as the limit.** Beyond that, use one record per element.

Two shorter rewrites look correct but aren't:

* **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 `OR` binds tighter than `AND`. The filter parses as `prices.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.

## See also

* [Filters and boolean operators](/doc/guides/managing-results/refine-results/filtering/in-depth/combining-boolean-operators)
* [Create nested attributes](/doc/guides/sending-and-managing-data/prepare-your-data/how-to/creating-and-using-nested-attributes)
* [Handle data relationships](/doc/guides/sending-and-managing-data/prepare-your-data/how-to/handling-data-relationships)
* [Structure ecommerce product records](/doc/guides/sending-and-managing-data/prepare-your-data/how-to/ecommerce-records)
