Reference

Meilisearch facets and filters: a complete guide

by the AdminDex team 8 min read

In Meilisearch, "facets" are the per-attribute counts you display on a faceted search UI ("12 products in red, 8 in blue"). "Filters" are the boolean expressions that narrow search results ("price < 50 AND brand = nike"). Both are powered by the same setting: filterableAttributes. Configure them once, use them in many ways.

The 30-second answer

  • Add an attribute to filterableAttributes to make it both filterable and facet-able.
  • Use the filter query parameter at search time to narrow results.
  • Use the facets query parameter at search time to get per-value counts for the UI.
  • Both can be combined in the same query.

Step 1: declare filterable attributes

PATCH /indexes/products/settings/filterable-attributes
[
  "brand",
  "category",
  "color",
  "price",
  "in_stock",
  "rating"
]

Adding an attribute here triggers a reindex — Meilisearch builds an inverted index for each filterable field. After the reindex completes, the attribute is queryable in both filter and facets.

Step 2: filter at query time

The filter parameter accepts a SQL-like expression with AND, OR, and NOT:

POST /indexes/products/search
{
  "q": "running shoes",
  "filter": "brand = nike AND price < 100 AND in_stock = true"
}

Comparison operators

OperatorExampleMeaning
=brand = nikeEquals
!=brand != nikeNot equals
< >price < 100Less than / greater than
<= >=rating >= 4Less or equal / greater or equal
TOprice 50 TO 200Between (inclusive)
INbrand IN [nike, adidas]Any of the listed values
EXISTSdiscount EXISTSField is present (since v1.5)
IS NULLdiscount IS NULLField is null (since v1.10)
IS EMPTYtags IS EMPTYArray field is empty (since v1.10)

Step 3: get facet counts at query time

Pass the facets parameter with the list of attributes you want counts for:

POST /indexes/products/search
{
  "q": "running shoes",
  "filter": "in_stock = true",
  "facets": ["brand", "color", "category"]
}

The response includes a facetDistribution object:

{
  "hits": [...],
  "facetDistribution": {
    "brand": {
      "nike": 24,
      "adidas": 18,
      "asics": 9,
      "brooks": 6
    },
    "color": {
      "black": 30,
      "white": 15,
      "blue": 8
    },
    "category": {
      "running": 57
    }
  }
}

Use this to render the "Brand: Nike (24), Adidas (18)..." UI on your search results page.

The "facets after the filter" rule

By default, the facet counts reflect only the documents that pass the current filter. If you filter in_stock = true, the brand facet shows counts of in-stock products only.

This is usually what you want — but it's wrong for the common e-commerce pattern of "show all brands, but greyed out and zero for ones with no in-stock items". For that pattern you need to run two queries: one for the result set and one for the unfiltered facet counts.

Building a fast filter UI

For a responsive faceted UI, follow this pattern:

  1. Initial page load: one search with no filter, all facets requested. Render the full facet counts.
  2. User clicks a filter: immediately update the URL state, then issue a new search with the filter applied and the same facet list.
  3. Update the UI: render the new hits and the new facet counts. Some facet values may now have count 0 — show them as disabled rather than removing them, so the UI stays stable.
  4. Debounce text input: 200ms is the sweet spot for typed search. Use the same query+filter shape for every keystroke.

Numeric ranges and histograms

Meilisearch returns flat counts per value, not ranges. If you want a price histogram (e.g. "0–50: 12 products, 50–100: 24 products"), you compute the buckets in your application from the raw facetDistribution response. Meilisearch doesn't do server-side bucketing.

Performance tips

  • Don't add every attribute to filterableAttributes. Each one increases index size and indexing time. Add only the ones you actually filter or facet on.
  • Numeric filters are faster than string filters. Filter on price, ratings, dates as numbers, not as strings.
  • Use maxValuesPerFacet in the index faceting settings to cap how many distinct values are returned per facet (default 100). If you have a brand attribute with 50,000 distinct values, capping it makes the response smaller.

FAQ

What's the difference between filterable and sortable attributes?

An attribute can be filterable (used in filter), sortable (used in sort), both, or neither. They're separate settings — adding to one doesn't add to the other. Most numeric attributes you'd filter on (price, rating) you also want to sort on, so they end up in both lists.

Can I filter on array fields?

Yes. tags = sale matches any document where the tags array contains "sale". This is the most common way to do "category" or "tag" filters.

Can filters be case-insensitive?

Filter values are case-insensitive by default. brand = Nike and brand = nike match the same documents.

How does AdminDex help with filterable attributes?

The settings editor has an attribute picker that shows every field name from your real index, so you don't have to type them from memory. Click to add a field to the filterable list, and AdminDex sends the PATCH request with the right format.

Stop running Meilisearch from a terminal.

AdminDex gives you a visual dashboard for every Meilisearch instance you own — settings, API keys, tasks, and search analytics.

Connect your first instance

Related reading