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

# Build faceted navigation

> Build an ecommerce-style faceted sidebar that shows available options with document counts.

Faceted navigation displays filter options alongside the number of matching documents, letting users progressively refine their search. This is the pattern behind product sidebars on ecommerce sites, where users can click "Electronics (42)" or "Books (18)" to narrow results.

This guide walks through the full pattern: configuring filterable attributes, requesting facet distributions, and building an interactive UI.

## Step 1: configure filterable attributes

Only attributes listed in `filterableAttributes` can be used as facets. Suppose you have a `books` index with documents like this:

<CodeGroup>
  ```json theme={null}
  {
    "id": 5,
    "title": "Hard Times",
    "genres": ["Classics", "Fiction"],
    "publisher": "Penguin Classics",
    "language": "English",
    "author": "Charles Dickens",
    "rating": 3
  }
  ```
</CodeGroup>

Add the attributes you want as facets to `filterableAttributes`:

<CodeGroup>
  ```bash cURL theme={null}
  curl \
    -X PUT 'MEILISEARCH_URL/indexes/books/settings/filterable-attributes' \
    -H 'Content-Type: application/json' \
    --data-binary '[
      "genres", "rating", "language"
    ]'
  ```

  ```javascript JS theme={null}
  client.index('movie_ratings').updateFilterableAttributes(['genres', 'rating', 'language'])
  ```

  ```python Python theme={null}
  client.index('movie_ratings').update_filterable_attributes([
    'genres',
    'director',
    'language'
  ])
  ```

  ```php PHP theme={null}
  $client->index('movie_ratings')->updateFilterableAttributes(['genres', 'rating', 'language']);
  ```

  ```java Java theme={null}
  client.index("movie_ratings").updateFilterableAttributesSettings(new String[] { "genres", "director", "language" });
  ```

  ```ruby Ruby theme={null}
  client.index('movie_ratings').update_filterable_attributes(['genres', 'rating', 'language'])
  ```

  ```go Go theme={null}
  filterableAttributes := []interface{}{
    "genres",
    "rating",
    "language",
  }
  client.Index("movie_ratings").UpdateFilterableAttributes(&filterableAttributes)
  ```

  ```csharp C# theme={null}
  List<string> attributes = new() { "genres", "rating", "language" };
  TaskInfo result = await client.Index("movie_ratings").UpdateFilterableAttributesAsync(attributes);
  ```

  ```rust Rust theme={null}
  let task: TaskInfo = client
    .index("movie_ratings")
    .set_filterable_attributes(&["genres", "rating", "language"])
    .await
    .unwrap();
  ```

  ```dart Dart theme={null}
  await client
      .index('movie_ratings')
      .updateFilterableAttributes(['genres', 'rating', 'language']);
  ```
</CodeGroup>

Wait for the settings task to complete before searching.

<Warning>
  If an attribute passed to `facets` has not been added to `filterableAttributes`, Meilisearch silently ignores it. No error is raised; the attribute simply will not appear in the `facetDistribution` response. If a facet is missing from your UI, double-check that it is declared as a filterable attribute.
</Warning>

## Step 2: request facet distributions

Use the `facets` search parameter to tell Meilisearch which attributes should include distribution counts in the response:

<CodeGroup>
  ```bash cURL theme={null}
  curl \
    -X POST 'MEILISEARCH_URL/indexes/books/search' \
    -H 'Content-Type: application/json' \
    --data-binary '{
      "q": "classic",
      "facets": [
      "genres", "rating", "language"
    ]
  }'
  ```

  ```javascript JS theme={null}
  client.index('books').search('classic', { facets: ['genres', 'rating', 'language'] })
  ```

  ```python Python theme={null}
  client.index('books').search('classic', {
    'facets': ['genres', 'rating', 'language']
  })
  ```

  ```php PHP theme={null}
  $client->index('books')->search('classic', [
    'facets' => ['genres', 'rating', 'language']
  ]);
  ```

  ```java Java theme={null}
  SearchRequest searchRequest = SearchRequest.builder().q("classic").facets(new String[]
  {
    "genres",
    "rating",
    "language"
  }).build();
  client.index("books").search(searchRequest);
  ```

  ```ruby Ruby theme={null}
  client.index('books').search('classic', {
    facets: ['genres', 'rating', 'language']
  })
  ```

  ```go Go theme={null}
  resp, err := client.Index("books").Search("classic", &meilisearch.SearchRequest{
    Facets: []string{
      "genres",
      "rating",
      "language",
    },
  })
  ```

  ```csharp C# theme={null}
  var sq = new SearchQuery
  {
      Facets = new string[] { "genres", "rating", "language" }
  };
  await client.Index("books").SearchAsync<Book>("classic", sq);
  ```

  ```rust Rust theme={null}
  let books = client.index("books");

  let results: SearchResults<Book> = SearchQuery::new(&books)
    .with_query("classic")
    .with_facets(Selectors::Some(&["genres", "rating", "language"]))
    .execute()
    .await
    .unwrap();
  ```

  ```dart Dart theme={null}
  await client
      .index('books')
      .search('', SearchQuery(facets: ['genres', 'rating', 'language']));
  ```
</CodeGroup>

The response includes a `facetDistribution` object showing every value for each requested facet and how many documents match:

<CodeGroup>
  ```json theme={null}
  {
    "hits": [
      { "id": 5, "title": "Hard Times", "genres": ["Classics", "Fiction"], "rating": 3 }
    ],
    "query": "classic",
    "facetDistribution": {
      "genres": {
        "Classics": 12,
        "Fiction": 8,
        "Literature": 6,
        "Victorian": 4,
        "Romance": 3
      },
      "language": {
        "English": 15,
        "French": 3,
        "Spanish": 1
      },
      "rating": {
        "3": 5,
        "4": 8,
        "5": 6
      }
    },
    "facetStats": {
      "rating": {
        "min": 1,
        "max": 5
      }
    },
    "processingTimeMs": 1,
    "estimatedTotalHits": 19
  }
  ```
</CodeGroup>

The `facetDistribution` tells you exactly which values exist and how many documents match each one. The `facetStats` object provides minimum and maximum values for numeric facets, useful for building range sliders.

<Note>
  `facetStats` only considers values stored as JSON numbers. String values are ignored, even when the string contains a numeric value such as `"42"`. If you expect `min`/`max` for a given facet and the object is missing, confirm that the underlying field is indexed as a number rather than a string.
</Note>

## Step 3: apply a filter when the user clicks a facet

When a user clicks a facet value, send a new search request with a `filter` parameter:

<CodeGroup>
  ```bash theme={null}
  curl \
    -X POST 'MEILISEARCH_URL/indexes/books/search' \
    -H 'Content-Type: application/json' \
    --data-binary '{
      "q": "classic",
      "filter": "genres = Classics",
      "facets": ["genres", "language", "rating"]
    }'
  ```
</CodeGroup>

The response updates both the `hits` and the `facetDistribution` to reflect the active filter. This means the facet counts adjust dynamically, showing users how many results remain for each option.

## Step 4: combine multiple facet filters

Users often select multiple facet values. Combine them using `AND` and `OR` operators:

<CodeGroup>
  ```bash theme={null}
  curl \
    -X POST 'MEILISEARCH_URL/indexes/books/search' \
    -H 'Content-Type: application/json' \
    --data-binary '{
      "q": "classic",
      "filter": "genres = Classics AND language = English AND rating >= 4",
      "facets": ["genres", "language", "rating"]
    }'
  ```
</CodeGroup>

Use `AND` to require all conditions (narrow results) and `OR` to match any condition (broaden results within a facet group). See the [filter expression syntax](/capabilities/filtering_sorting_faceting/advanced/filter_expression_syntax) reference for the full list of operators:

<CodeGroup>
  ```bash theme={null}
  "filter": "(genres = Classics OR genres = Fiction) AND language = English"
  ```
</CodeGroup>

## Frontend implementation pattern

Here is a JavaScript pattern for building an interactive faceted sidebar:

<CodeGroup>
  ```html theme={null}
  <div id="app">
    <input type="text" id="search-input" placeholder="Search books..." />
    <div style="display: flex;">
      <div id="facets-sidebar" style="width: 250px;"></div>
      <div id="results" style="flex: 1;"></div>
    </div>
  </div>

  <script>
    const MEILI_URL = 'MEILISEARCH_URL';
    const MEILI_KEY = 'your_search_api_key';
    const FACET_ATTRIBUTES = ['genres', 'language', 'rating'];

    let activeFilters = {};

    async function search() {
      const query = document.getElementById('search-input').value;

      // Build filter string from active selections
      const filterParts = Object.entries(activeFilters)
        .filter(([_, values]) => values.length > 0)
        .map(([attr, values]) => {
          const conditions = values.map(v => `${attr} = "${v}"`).join(' OR ');
          return `(${conditions})`;
        });
      const filter = filterParts.join(' AND ');

      const response = await fetch(`${MEILI_URL}/indexes/books/search`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${MEILI_KEY}`
        },
        body: JSON.stringify({
          q: query,
          filter: filter || undefined,
          facets: FACET_ATTRIBUTES
        })
      });

      const data = await response.json();
      renderFacets(data.facetDistribution);
      renderResults(data.hits);
    }

    // Note: when a user selects a facet value (e.g., brand = "Nike"),
    // the facet counts update to reflect only results matching that filter.
    // This means other brand options may show reduced counts or disappear.
    // If you want to preserve the full list of facet values regardless of
    // active filters, use disjunctive facets:
    // /capabilities/filtering_sorting_faceting/advanced/disjunctive_facets

    function renderFacets(distribution) {
      const sidebar = document.getElementById('facets-sidebar');
      sidebar.innerHTML = Object.entries(distribution)
        .map(([attribute, values]) => {
          const items = Object.entries(values)
            .sort((a, b) => b[1] - a[1])
            .map(([value, count]) => {
              const selected = (activeFilters[attribute] || []).includes(value);
              return `<label style="display: block;">
                <input type="checkbox" ${selected ? 'checked' : ''}
                  onchange="toggleFacet('${attribute}', '${value}')" />
                ${value} (${count})
              </label>`;
            })
            .join('');
          return `<div><h4>${attribute}</h4>${items}</div>`;
        })
        .join('');
    }

    function renderResults(hits) {
      document.getElementById('results').innerHTML = hits
        .map(hit => `<div><strong>${hit.title}</strong> by ${hit.author}</div>`)
        .join('');
    }

    function toggleFacet(attribute, value) {
      if (!activeFilters[attribute]) activeFilters[attribute] = [];
      const index = activeFilters[attribute].indexOf(value);
      if (index === -1) {
        activeFilters[attribute].push(value);
      } else {
        activeFilters[attribute].splice(index, 1);
      }
      search();
    }

    document.getElementById('search-input').addEventListener('input', search);
    search(); // initial load
  </script>
  ```
</CodeGroup>

This pattern:

1. Tracks active filter selections in an `activeFilters` object
2. Builds a filter string from active selections on each search
3. Renders facet values as checkboxes with document counts
4. Updates both facets and results when the user toggles a checkbox

## Tune `maxValuesPerFacet`

By default, Meilisearch returns up to 100 distinct values per facet in the `facetDistribution` object. If your UI only displays the top 10 or 20 options, lower `maxValuesPerFacet` to match what you actually render:

<CodeGroup>
  ```bash cURL theme={null}
  curl \
    -X PATCH 'MEILISEARCH_URL/indexes/books/settings/faceting' \
    -H 'Content-Type: application/json' \
    --data-binary '{
      "maxValuesPerFacet": 20
    }'
  ```
</CodeGroup>

<Warning>
  Setting `maxValuesPerFacet` to a high value might negatively impact performance. Raising it only makes sense when you genuinely need to surface a large number of facet values at once.
</Warning>

## Key points

* Always include the `facets` parameter in every search request so the sidebar stays updated
* Facet counts reflect the current filter state, so users see accurate numbers
* Use `OR` within the same attribute (for example, multiple genres) and `AND` across attributes (for example, genre AND language)
* Numeric facets include `facetStats` with `min` and `max` values, useful for range sliders

## Next steps

<CardGroup cols={2}>
  <Card title="Search with facets" href="/capabilities/filtering_sorting_faceting/how_to/filter_with_facets">
    Learn more about facets and facet search
  </Card>

  <Card title="Search API reference" href="/reference/api/search/search-with-post">
    Full documentation for the search endpoint parameters
  </Card>

  <Card title="Combine filters and sort" href="/capabilities/filtering_sorting_faceting/how_to/combine_filters_and_sort">
    Add sorting to your filtered search results
  </Card>
</CardGroup>
