Filtering and faceted search

You can use MeiliSearch’s filters to refine search results.

Filters have several use-cases, such as restricting the results a specific user has access to or creating faceted search interfaces. Faceted search interfaces are particularly efficient in helping users navigate a great number of results across many broad categories.

Configuring filters

Filters use document fields to establish filtering criteria.

To use a document field as a filter, you must first add its attribute to the filterableAttributes index setting.

This step is mandatory and cannot be done at search time. Filters need to be properly processed and prepared by MeiliSearch before they can be used.

Updating filterableAttributes requires recreating the entire index. This may take a significant amount of time depending on your dataset size.

WARNING

By default, filterableAttributes is empty. This means that filters do not work without first explicitly adding attributes to the filterableAttributes list.

Filters work with numeric and string values. Empty fields or fields containing an empty array will be ignored. Fields containing object values will throw an error.

Example

Suppose you have a collection of movies containing the following fields:

  1. [
  2. {
  3. "id": "458723",
  4. "title": "Us",
  5. "director": "Jordan Peele",
  6. "genres": [
  7. "Thriller",
  8. "Horror",
  9. "Mystery"
  10. ],
  11. "overview": "Husband and wife Gabe and Adelaide Wilson take their […]",
  12. },
  13. ]

If you want to filter results based on the director and genres attributes, you must add them to the filterableAttributes list:

cURL

  1. curl \
  2. -X POST 'http://localhost:7700/indexes/movies/settings' \
  3. --data '{
  4. "filterableAttributes": [
  5. "director",
  6. "genres"
  7. ]
  8. }'

JavaScript

  1. client.index('movies')
  2. .updateFilterableAttributes([
  3. 'director',
  4. 'genres'
  5. ])

Python

  1. client.index('movies').update_filterable_attributes([
  2. 'director',
  3. 'genres',
  4. ])

PHP

  1. $client->index('movies')->updateFilterableAttributes(['director', 'genres']);

Ruby

  1. client.index('movies').update_filterable_attributes([
  2. 'director',
  3. 'genres'
  4. ])

Go

  1. resp, err := client.Index("movies").UpdateFilterableAttributes(&[]string{
  2. "director",
  3. "genres",
  4. })

Rust

  1. let progress: Progress = movies.set_filterable_attributes(["director", "genres"]).await.unwrap();

Using filters

Once you have configured filterableAttributes, you can start using the filter search parameter. Search parameters are added to at search time, that is, when a user searches your dataset.

filter expects a filter expression containing one or more conditions. A filter expression can be written as a string, as an array, or as a mix of both.

Conditions

Conditions are a filter’s basic building blocks. They are always written in the attribute OPERATOR value format, where:

  • attribute is the attribute of the field you want to filter on
  • OPERATOR is the comparison operator and can be =, !=, >, >=, <, or <=
  • value is the value condition for the filter

NOTE

>, >=, <, and <= only operate on numeric values and will ignore all other types of values.

When operating on strings, = and != are case-insensitive.

Examples

A basic condition could request movies containing the horror genre:

  1. genres = horror

Note that string values containing whitespace must be enclosed in single or double quotes:

  1. director = 'Jordan Peele'
  2. director = "Tim Burton"

Another condition could request movies released after 18 March 1995 (written as 795484800 in UNIX Epoch time):

  1. release_date > 795484800

WARNING

As no specific schema is enforced at indexing, the filtering engine will try to coerce the type of value. This can lead to undefined behaviour when big floats are coerced into integers and reciprocally. For this reason, it is best to have homogeneous typing across fields, especially if numbers tend to become large.

Filter expressions

You can build filter expressions by grouping basic conditions. Filter expressions can be written as strings, arrays, or a mix of both.

WARNING

The GET route of the search endpoint only accepts string filter expressions.

Creating filter expressions with strings

String expressions combine conditions using three logical connectives—AND, OR and NOT— and parentheses:

  • NOT only returns documents that do not satisfy a condition : NOT genres = horror
  • AND operates by connecting two conditions and only returns documents that satisfy both of them: genres = horror AND director = 'Jordan Peele'
  • OR connects two conditions and returns results that satisfy at least one of them: genres = horror OR genres = comedy

TIP

String expressions are read left to right. NOT takes precedence over AND and AND takes precedence over OR. You can use parentheses to ensure expressions are correctly parsed.

For instance, if you want your results to only include comedy and horror movies released after March 1995, the parentheses in the following query are mandatory:

(genres = horror OR genres = comedy) AND release_date > 795484800

Failing to add these parentheses will cause the same query to be parsed as:

genres = horror OR (genres = comedy AND release_date > 795484800)

Translated into English, the above expression will only return comedies released after March 1995 or horror movies regardless of their release_date.

Creating filter expressions with arrays

Array expressions establish logical connectives by nesting arrays of strings. They can have a maximum depth of two—array filter expressions with three or more levels of nesting will throw an error.

Outer array elements are connected by an AND operator. The following expression returns horror movies directed by Jordan Peele:

  1. ["genres = horror", "director = 'Jordan Peele']

Inner array elements are connected by an OR operator. The following expression returns either horror or comedy films:

  1. [["genres = horror", "genres = comedy"]]

Inner and outer arrays can be freely combined. The following expression returns both horror and comedy movies directed by Jordan Peele:

  1. [["genres = horror", "genres = comedy"], "director = 'Jordan Peele'"]

Combining arrays and strings

You can also create filter expressions that use both array and string syntax.

The following filter is written as a string and only returns movies not directed by Jordan Peele that belong to the comedy or horror genres:

  1. "(genres = comedy OR genres = horror) AND director != 'Jordan Peele'"

You can write the same filter mixing arrays and strings:

  1. [["genres = comedy, genres = horror"], "NOT director = 'Jordan Peele'"]

Example

Suppose that you have a dataset containing several movies in the following format:

  1. [
  2. {
  3. "id": "458723",
  4. "title": "Us",
  5. "director": "Jordan Peele",
  6. "poster": "https://image.tmdb.org/t/p/w1280/ux2dU1jQ2ACIMShzB3yP93Udpzc.jpg",
  7. "overview": "Husband and wife Gabe and Adelaide Wilson take their…",
  8. "release_date": 1552521600,
  9. "genres": [
  10. "Comedy",
  11. "Horror",
  12. "Thriller"
  13. ],
  14. "rating": 4
  15. },
  16. ]

If you want to enable filtering using director, release_date, genres, and rating, you must add these attributes to the filterableAttributes index setting.

You can then restrict a search so it only returns movies released after 18 March 1995 with the following filter containing a single condition:

  1. release_date > 795484800

You can use this filter when searching for recent Avengers movies:

cURL

JavaScript

Python

PHP

Ruby

Go

Rust

  1. curl 'http://localhost:7700/indexes/movies/search' \
  2. --data '{ "q": "Avengers", "filter": "release_date > 795484800" }'
  1. client.index('movies').search('Avengers', {
  2. filter: 'release_date > 795484800'
  3. })
  1. client.index('movies').search('Avengers', {
  2. 'filter': 'release_date > 795484800'
  3. })
  1. $client->index('movies')->search('Avengers', ['filter' => 'release_date > 795484800']);
  1. client.index('movies').search('Avengers', { filter: 'release_date > 795484800' })
  1. resp, err := client.Index("movies").Search("Avengers", &meilisearch.SearchRequest{
  2. Filter: "release_date > \"795484800\"",
  3. })
  1. let results: SearchResults<Movie> = movies.search()
  2. .with_query("Avengers")
  3. .with_filter("release_date > 795484800")
  4. .execute()
  5. .await
  6. .unwrap();

You can also combine multiple conditions. For instance, you can limit your search so it only includes recent movies directed by either Tim Burton or Christopher Nolan:

  1. release_date > 795484800 AND (director = "Tim Burton" OR director = "Christopher Nolan")

Here, the parentheses are mandatory: without them, the filter would return movies directed by Tim Burton and released after 1995 or any film directed by Christopher Nolan, without constraints on its release date. This happens because AND takes precedence over OR.

You can use this filter when searching for Batman movies:

cURL

JavaScript

Python

PHP

Ruby

Go

Rust

  1. curl 'http://localhost:7700/indexes/movies/search' \
  2. --data '{ "q":"Batman", "filter": "release_date > 795484800 AND (director = \"Tim Burton\" OR director = \"Christopher Nolan\")" }'
  1. client.index('movies').search('Batman', {
  2. filter: 'release_date > 795484800 AND (director = "Tim Burton" OR director = "Christopher Nolan")'
  3. })
  1. client.index('movies').search('Batman', {
  2. 'filter': 'release_date > 795484800 AND (director = "Tim Burton" OR director = "Christopher Nolan")'
  3. })
  1. $client->index('movies')->search('Batman', ['filter' => 'release_date > 795484800 AND (director = "Tim Burton" OR director = "Christopher Nolan")']);
  1. client.index('movies').search('Batman', {
  2. filter: 'release_date > 795484800 AND (director = "Tim Burton" OR director = "Christopher Nolan")'
  3. })
  1. resp, err := client.Index("movies").Search("Batman", &meilisearch.SearchRequest{
  2. Filter: "release_date > 795484800 AND (director = \"Tim Burton\" OR director = \"Christopher Nolan\")",
  3. })
  1. let results: SearchResults<Movie> = movies.search()
  2. .with_query("Batman")
  3. .with_filter(r#"release_date > 795484800 AND (director = "Tim Burton" OR director = "Christopher Nolan")"#)
  4. .execute()
  5. .await
  6. .unwrap();

Note that filtering on string values is case-insensitive.

If you only want well-rated movies that weren’t directed by Tim Burton, you can use this filter:

  1. rating >= 3 AND (NOT director = "Tim Burton")

You can use this filter when searching for Planet of the Apes:

cURL

JavaScript

Python

PHP

Ruby

Go

Rust

  1. curl 'http://localhost:7700/indexes/movies/search' \
  2. --data '{ "q": "Planet of the Apes", "filter": "rating >= 3 AND (NOT director = \"Tim Burton\")" }' \
  1. client.index('movies').search('Planet of the Apes', {
  2. filter: 'rating >= 3 AND (NOT director = "Tim Burton")'
  3. })
  1. client.index('movies').search('Planet of the Apes', {
  2. 'filter': 'rating >= 3 AND (NOT director = "Tim Burton")'
  3. })
  1. $client->index('movies')->search('Planet of the Apes', ['filter' => 'rating >= 3 AND (NOT director = "Tim Burton")']);
  1. client.index('movies').search('Planet of the Apes', {
  2. filter: 'rating >= 3 AND (NOT director = "Tim Burton")'
  3. })
  1. resp, err := client.Index("movies").Search("Planet of the Apes", &meilisearch.SearchRequest{
  2. Filter: "rating >= 3 AND (NOT director = \"Tim Burton\"",
  3. })
  1. let results: SearchResults<Movie> = movies.search()
  2. .with_query("Planet of the Apes")
  3. .with_filter(r#"rating >= 3 AND (NOT director = "Tim Burton")"#)
  4. .execute()
  5. .await
  6. .unwrap();

MeiliSearch filters can be used to build faceted search interfaces. This type of interface allows users to refine search results based on broad categories or facets. For example, a clothing webshop can use faceted search to allow users to easily explore items of a certain size or belonging to a specific brand.

Faceted search provides users with a quick way to narrow down search results by selecting categories relevant to what they are looking for. A faceted navigation system is an intuitive interface to display and navigate through content. Facets are used in the UI as filters which users can apply to refine the results in real-time.

This is common in ecommerce sites like Amazon: when users perform a search, they are presented not only with a list of results, but also with a list of facets which you can see on the sidebar in the image below:

Amazon UI

Filters or facets

In MeiliSearch, facets are a specific use-case of filters. The question of whether something is a filter or a facet is mostly one pertaining to UX and UI design.

Using facets

Like any other filter, attributes you want to use as facets must be added to the filterableAttributes list in the index’s settings before they can be used.

Once they have been configured, you can search for facets with the filter search parameter.

Example

Suppose you have added director and genres to the filterableAttributes list, and you want to get movies classified as either Horror or Mystery and directed by Jordan Peele.

  1. [["genres = horror", "genres = mystery"], "director = 'Jordan Peele'"]

You can then use this filter to search for thriller:

cURL

JavaScript

Python

PHP

Ruby

Go

Rust

  1. curl 'http://localhost:7700/indexes/movies/search' \
  2. --data '{ "q": "thriller", "filter": [["genres = Horror", "genres = Mystery"], "director = \"Jordan Peele\""] }'
  1. client.index('movies')
  2. .search('thriller', {
  3. filter: [['genres = Horror', 'genres = Mystery'], 'director = "Jordan Peele"']
  4. })
  1. client.index('movies').search('thriller', {
  2. 'filter': [['genres = Horror', 'genres = Mystery'], 'director = "Jordan Peele"']
  3. })
  1. $client->index('movies')->search('thriller', ['filter' => [['genres:Horror', 'genres:Mystery']], 'director' => "Jordan Peele"']);
  1. client.index('movies').search('thriller', {
  2. filter: [['genres = Horror', 'genres = Mystery'], 'director = "Jordan Peele"']
  3. })
  1. resp, err := client.Index("movies").Search("thriller", &meilisearch.SearchRequest{
  2. Filter: [][]string{
  3. []string{"genres = Horror", "genres = Mystery"},
  4. []string{"director = \"Jordan Peele\""},
  5. },
  6. })
  1. let results: SearchResults<Movie> = movies.search()
  2. .with_query("thriller")
  3. .with_filter("(genres = Horror AND genres = Mystery) OR director = \"Jordan Peele\"")
  4. .execute()
  5. .await
  6. .unwrap();

Facets distribution

When creating a faceted search interface it is often useful to have a count of how many results belong to each facet. This can be done by using the facetsDistribution search parameter in combination with filter when searching.

NOTE

MeiliSearch does not differentiate between facets and filters. This means that, despite its name, facetsDistribution can be used with any attributes added to filterableAttributes.

Using facetsDistribution will add an extra field to the returned search results containing the number of matching documents distributed among all the values of a given facet.

In the example below, IMDbFiltering and faceted search - 图2 (opens new window) displays the facet count in parentheses next to each faceted category. This UI gives users a visual clue of the range of results available for each facet.

IMDb facets

Using facet distribution

facetsDistribution is a search parameter and as such must be added to a search request. It expects an array of strings. Each string is an attribute present in the filterableAttributes list.

Using the facetsDistribution search parameter adds two new keys to the returned object: facetsDistribution and exhaustiveFacetsCount.

facetsDistribution contains an object for every given facet. For each of these facets, there is another object containing all the different values and the count of matching documents. Note that zero values will not be returned: if there are no romance movies matching the query, romance is not displayed.

  1. {
  2. "exhaustiveFacetsCount": false,
  3. "facetsDistribution" : {
  4. "genres" : {
  5. "horror": 50,
  6. "comedy": 34,
  7. }
  8. }
  9. }

exhaustiveFacetsCount is a boolean value that informs the user whether the facet count is exact or just an approximation. For performance reasons, MeiliSearch chooses to use approximate facet count values when there are too many documents across several different fields.

WARNING

exhaustiveFacetsCount is not currently implemented in and will always return false.

Example

You can write a search query that gives you the distribution of batman movies per genre:

cURL

JavaScript

Python

PHP

Ruby

Go

Rust

  1. curl \
  2. -X POST 'http://localhost:7700/indexes/movies/search' \
  3. --data '{ "q": "Batman", "facetsDistribution": ["genres"] }'
  1. client.index('movies')
  2. .search('Batman', {
  3. facetsDistribution: ['genres']
  4. })
  1. client.index('movies').search('Batman', {
  2. 'facetsDistribution': ['genres']
  3. })
  1. $client->index('movies')->search('Batman', ['facetsDistribution' => ['genres']]);
  1. client.index('movies').search('Batman', {
  2. facetsDistribution: ['genres']
  3. })
  1. resp, err := client.Index("movies").Search("Batman", &meilisearch.SearchRequest{
  2. FacetsDistribution: []string{
  3. "genres",
  4. },
  5. })
  1. let results: SearchResults<Movie> = movies.search()
  2. .with_query("Batman")
  3. .with_facets_distribution(Selectors::Some(&["genres"]))
  4. .execute()
  5. .await
  6. .unwrap();
  7. let genres: &HashMap<String, usize> = results.facets_distribution.unwrap().get("genres").unwrap();

This query would return not only the matching movies, but also the facetsDistribution key containing all relevant data:

  1. {
  2. "hits": [
  3. ],
  4. "facetsDistribution": {
  5. "genres": {
  6. "action": 273,
  7. "animation": 118,
  8. "adventure": 132,
  9. "fantasy": 67,
  10. "comedy": 475,
  11. "mystery": 70,
  12. "thriller": 217,
  13. }
  14. }
  15. }