Algolia facets vs filterOnly: choosing attribute modes for high-cardinality filters
Regular facets return counts; filterOnly attributes only filter. The mode is fixed in index settings, so choose per attribute before you reindex.
17 Jul 2026, 21:55 UTC

Start with the constraint, not the feature
Algolia decides which attributes are filterable or countable in one place: the attributesForFaceting index setting. That setting is applied at index time, not per query. So the real decision is a schema decision — for each attribute, do you declare it as a regular facet (which can return counts for a refinement UI) or wrap it in the filterOnly() modifier (which makes it filterable but never countable)?
You cannot flip an attribute between the two modes for a single query. Changing the mode later means editing settings and rebuilding the index, so it is worth getting right before a large dataset is indexed.
What the two modes actually do
A regular facet is declared by listing the attribute name plainly, or with a modifier such as searchable(brand). When you request that attribute in the facets parameter of a query, Algolia returns a facets object containing value counts for the current result set. Those counts are what power refinement lists, "show more" expansion, and facet-value search.
A filterOnly attribute is declared as filterOnly(attributeName). It is stored so that exact-match filtering is possible, but it produces no counts and is not part of the facet-value retrieval machinery. It is intended for attributes where you need to constrain results but a count would be meaningless or misleading — product IDs, user IDs, tenant keys, timestamps, SKUs.
Comparison at a glance
| Behavior | Regular facet | filterOnly() |
|---|---|---|
Counts in response.facets when requested | Yes | No |
Usable in the facets query parameter | Yes | No |
Usable in the filters expression syntax | Yes | Yes |
Facet-value search (searchable(), facetQuery) | Yes, if declared | No |
| Count aggregation work at query time | Yes, when requested | None |
| Typical fit | Category, brand, price band, availability | IDs, timestamps, tenant or warehouse keys |
The trade-offs that matter in practice
Counts are a product decision, not just a performance one
A count on a high-cardinality attribute is usually useless to a shopper: "product_id: 1 result" repeated thousands of times. If the UI never renders that list, you are paying for count aggregation and facet memory without a user-visible benefit. That is the strongest argument for filterOnly.
Memory and indexing scale with distinct values
Faceting data is per distinct value, so an attribute with millions of distinct values is expensive to store and slower to aggregate than one with a few dozen. The exact point where a regular facet becomes a problem depends on record count, plan, and query shape, so treat any specific threshold you read as a starting hypothesis and measure it on your own data.
filterOnly removes capabilities you may later want
Once an attribute is filterOnly, you lose facet-value search, maxValuesPerFacet pagination, and any "show more" behavior. If a product manager later asks for a searchable list of that attribute's values, the answer is a settings change plus a reindex — not a query tweak.
Implementation pattern: identifiers filter-only, navigation countable
The common shape is to split attributes by role. Navigational attributes stay countable; identity and high-cardinality attributes become filter-only. Apply this through the dashboard or your client's setSettings call:
{
"attributesForFaceting": [
"filterOnly(product_id)",
"filterOnly(warehouse_id)",
"filterOnly(updated_at)",
"searchable(brand)",
"category",
"price_range"
]
}
Then query with the filters string syntax for the filter-only attributes and the facets parameter for the countable ones:
// Node.js client, server-side. API client v3 and v4 share this settings and
// filters shape; only the task-waiting call differs between versions.
const index = client.initIndex("YOUR_INDEX");
await index.setSettings({
attributesForFaceting: [
"filterOnly(product_id)",
"filterOnly(warehouse_id)",
"searchable(brand)",
"category"
]
});
// Wait for the settings/indexing task to finish before testing.
const { hits, facets } = await index.search("", {
filters: "product_id:12345 AND warehouse_id:eu-west",
facets: ["category", "brand"]
});
// facets contains category and brand counts.
// product_id and warehouse_id never appear in facets.
Placeholders: YOUR_INDEX is the target index name, 12345 and eu-west are example filter values. Run this from a trusted server-side context with a key that is allowed to change settings; a search-only key cannot apply setSettings.
How to verify the change worked
- Call
getSettings(or open the index configuration in the dashboard) and confirm the entries readfilterOnly(product_id)rather thanproduct_id. If the modifier is missing, the attribute is still a regular facet. - Run a query with
filters=product_id:12345and confirm the response contains the expected hits and nofacetsentry forproduct_id. - Run a mixed query —
facets=categorytogether withfilters=product_id:12345— and confirm both the counts and the filtered hits are correct. - If you want a performance comparison, index a copy of the dataset with the attribute as a regular facet and another copy with
filterOnly, then compare index size, indexing duration, and p95/p99 latency for the same filter. Record your own numbers; published figures will not match your data.
Limitations and things to check before committing
- Expression syntax only. Algolia's documentation presents
filterOnlyattributes as usable through thefiltersstring. Treat use insidefacetFiltersas unsupported and confirm against the current API reference for your client version before relying on it. - No facet-value search or pagination.
facetQueryandmaxValuesPerFacetdo not apply to filter-only attributes. - Deduplication. If you need
distinctor grouping behavior on a high-cardinality field, plan a separate index rather than assumingfilterOnlycovers it. - Plan limits. Memory savings matter most on large record counts; smaller plans may not notice the difference.
- Reindex required. Any mode change is a settings change plus a rebuild. Schedule it for low traffic and expect a window where the old and new configurations differ.
Because Algolia's modifier syntax and limits can change between API versions, verify the current filterOnly behavior in the official "Filtering and Faceting" documentation before a production rollout, and have a reviewer confirm the settings diff.
Rolling back
This operation changes index state, so keep the previous attributesForFaceting array before applying the new one. To revert, call setSettings with the saved array, wait for the task to complete, and re-run the verification queries above. Reverting also requires a reindex, so treat rollback as a planned step rather than an instant undo.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.