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.

NOTE

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.

WARNING

MeiliSearch does not support filtering on nested arrays and objects yetFiltering and faceted search - 图1 (opens new window). Therefore, fields containing nested arrays and objects will be silently ignored.

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

JS

Python

PHP

Java

Ruby

Go

Rust

Swift

Dart

  1. curl \
  2. -X POST 'http://localhost:7700/indexes/movies/settings' \
  3. -H 'Content-Type: application/json' \
  4. --data-binary '{
  5. "filterableAttributes": [
  6. "director",
  7. "genres"
  8. ]
  9. }'
  1. client.index('movies')
  2. .updateFilterableAttributes([
  3. 'director',
  4. 'genres'
  5. ])
  1. client.index('movies').update_filterable_attributes([
  2. 'director',
  3. 'genres',
  4. ])
  1. $client->index('movies')->updateFilterableAttributes(['director', 'genres']);
  1. Settings settings = new Settings();
  2. settings.setFilterableAttributes(new String[] {"director", "genres"});
  3. client.index("movies").updateSettings(settings);
  1. client.index('movies').update_filterable_attributes([
  2. 'director',
  3. 'genres'
  4. ])
  1. resp, err := client.Index("movies").UpdateFilterableAttributes(&[]string{
  2. "director",
  3. "genres",
  4. })
  1. let progress: Progress = movies.set_filterable_attributes(["director", "genres"]).await.unwrap();
  1. client.index("movies").updateFilterableAttributes(["genre", "director"]) { (result: Result<Update, Swift.Error>) in
  2. switch result {
  3. case .success(let update):
  4. print(update)
  5. case .failure(let error):
  6. print(error)
  7. }
  8. }
  1. await client.index('movies').updateFilterableAttributes([
  2. 'director',
  3. 'genres',
  4. ]);

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 the following filter operators 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
  • TO is equivalent to >= AND <=. The expression release_date 795484800 TO 972129600 translates to release_date >= 795484800 AND release_date <= 972129600

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

JS

Python

PHP

Java

Ruby

Go

Rust

Swift

Dart

  1. curl \
  2. -X POST 'http://localhost:7700/indexes/movies/search' \
  3. -H 'Content-Type: application/json' \
  4. --data-binary '{ "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. SearchRequest searchRequest =
  2. new SearchRequest("Avengers").setFilter(new String[] {"release_date > \"795484800\""});
  3. client.index("movies").search(searchRequest);
  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();
  1. let searchParameters = SearchParameters(
  2. query: "Avengers",
  3. filter: "release_date > 795484800"
  4. )
  5. client.index("movies").search(searchParameters) { (result: Result<SearchResult<Movie>, Swift.Error>) in
  6. switch result {
  7. case .success(let searchResult):
  8. print(searchResult)
  9. case .failure(let error):
  10. print(error)
  11. }
  12. }
  1. await await client
  2. .index('movies')
  3. .search('Avengers', filter: 'release_date > 795484800');

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

JS

Python

PHP

Java

Ruby

Go

Rust

Swift

Dart

  1. curl \
  2. -X POST 'http://localhost:7700/indexes/movies/search' \
  3. -H 'Content-Type: application/json' \
  4. --data-binary '{ "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. SearchRequest searchRequest =
  2. new SearchRequest("Batman").setFilter(new String[] {"release_date > 795484800 AND (director = \"Tim Burton\" OR director = \"Christopher Nolan\")"});
  3. client.index("movies").search(searchRequest);
  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();
  1. let searchParameters = SearchParameters(
  2. query: "Batman",
  3. filter: "release_date > 795484800 AND (director = \"Tim Burton\" OR director = \"Christopher Nolan\"")
  4. client.index("movies").search(searchParameters) { (result: Result<SearchResult<Movie>, Swift.Error>) in
  5. switch result {
  6. case .success(let searchResult):
  7. print(searchResult)
  8. case .failure(let error):
  9. print(error)
  10. }
  11. }
  1. await client.index('movies').search('Batman',
  2. filter:
  3. 'release_date > 795484800 AND (director = "Tim Burton" OR director = "Christopher Nolan")');

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

JS

Python

PHP

Java

Ruby

Go

Rust

Swift

Dart

  1. curl \
  2. -X POST 'http://localhost:7700/indexes/movies/search' \
  3. -H 'Content-Type: application/json' \
  4. --data-binary '{ "q": "Planet of the Apes", "filter": "rating >= 3 AND (NOT director = \"Tim Burton\")" }' \
  1. client.index('movies').search('horror', {
  2. filter: 'director = "Jordan Peele"'
  3. })
  1. client.index('movies').search('horror', {
  2. 'filter': 'director = "Jordan Peele"'
  3. })
  1. $client->index('movies')->search('horror', ['filter' => 'director = "Jordan Peele"']);
  1. SearchRequest searchRequest =
  2. new SearchRequest("horror").setFilter(new String[] {"director = \"Jordan Peele\""});
  3. client.index("movies").search(searchRequest);
  1. client.index('movies').search('horror', { filter: 'director = "Jordan Peele"' })
  1. resp, err := client.Index("movies").Search("horror", &meilisearch.SearchRequest{
  2. Filter: "director = \"Jordan Peele\"",
  3. })
  1. let results: SearchResults<Movie> = movies.search()
  2. .with_query("horror")
  3. .with_filter(r#"director = "Jordan Peele""#)
  4. .execute()
  5. .await
  6. .unwrap();
  1. let searchParameters = SearchParameters(
  2. query: "horror",
  3. filter: "director = \"Jordan Peele\"")
  4. client.index("movies").search(searchParameters) { (result: Result<SearchResult<Movie>, Swift.Error>) in
  5. switch result {
  6. case .success(let searchResult):
  7. print(searchResult)
  8. case .failure(let error):
  9. print(error)
  10. }
  11. }
  1. await client
  2. .index('movies')
  3. .search('horror', filter: 'director = "Jordan Peele"');

Filtering with _geoRadius

If your documents contain _geo data, you can use the _geoRadius built-in filter rule to filter results according to their geographic position.

_geoRadius establishes a circular area based on a central point and a radius. Results beyond this area will be excluded from your search. This filter rule requires three parameters: lat, lng and distance_in_meters.

  1. _geoRadius(lat, lng, distance_in_meters)

lat and lng must be floating point numbers indicating a geographic position. distance_in_meters must be an integer indicating the radius covered by the _geoRadius filter.

Example

When using a dataset of restaurants containing geopositioning data, we can filter our search so it only includes places within two kilometres of our location:

<>

cURL

JS

Python

PHP

Java

Ruby

Go

Rust

Swift

Dart

  1. curl -X POST 'http://localhost:7700/indexes/restaurants/search' \
  2. -H 'Content-type:application/json' \
  3. --data-binary '{ "filter": "_geoRadius(45.4628328, 9.1076931, 2000)" }'
  1. client.index('restaurants').search('', {
  2. filter: ['_geoRadius(45.4628328, 9.1076931, 2000)'],
  3. })
  1. client.index('restaurants').search('', {
  2. 'filter': '_geoRadius(45.4628328, 9.1076931, 2000)'
  3. })
  1. $client->index('restaurants')->search('', ['filter' => '_geoRadius(45.4628328, 9.1076931, 2000)']);
  1. SearchRequest searchRequest =
  2. new SearchRequest("").setFilter(new String[] {"_geoRadius(45.4628328, 9.1076931, 2000)"});
  3. client.index("restaurants").search(searchRequest);
  1. client.index('restaurants').search('', { filter: '_geoRadius(45.4628328, 9.1076931, 2000)' })
  1. resp, err := client.Index("restaurants").Search("", &meilisearch.SearchRequest{
  2. Filter: "_geoRadius(45.4628328, 9.1076931, 2000)",
  3. })
  1. let results: SearchResults<Restaurant> = restaurants.search()
  2. .with_filter("_geoRadius(45.4628328, 9.1076931, 2000)")
  3. .execute()
  4. .await
  5. .unwrap();
  1. let searchParameters = SearchParameters(
  2. filter: ["_geoRadius(45.4628328, 9.1076931, 2000)"]
  3. )
  4. client.index("restaurants").search(searchParameters) { (result: Result<SearchResult<Movie>, Swift.Error>) in
  5. switch result {
  6. case .success(let searchResult):
  7. print(searchResult)
  8. case .failure(let error):
  9. print(error)
  10. }
  11. }
  1. await await client
  2. .index('restaurants')
  3. .search('', filter: '_geoRadius(45.4628328, 9.1076931, 2000)');

You can read more about filtering results with _geoRadius in our geosearch guide.

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

JS

Python

PHP

Java

Ruby

Go

Rust

Swift

Dart

  1. curl \
  2. -X POST 'http://localhost:7700/indexes/movies/search' \
  3. -H 'Content-Type: application/json' \
  4. --data-binary '{ "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. SearchRequest searchRequest =
  2. new SearchRequest("thriller").setFilterArray(new String[][] {
  3. new String[] {"genres = Horror", "genres = Mystery"},
  4. new String[] {"director = \"Jordan Peele\""}});
  5. client.index("movies").search(searchRequest);
  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();
  1. let searchParameters = SearchParameters(
  2. query: "thriller",
  3. filter: [["genres = Horror", "genres = Mystery"], ["director = \"Jordan Peele\""]])
  4. client.index("movies").search(searchParameters) { (result: Result<SearchResult<Movie>, Swift.Error>) in
  5. switch result {
  6. case .success(let searchResult):
  7. print(searchResult)
  8. case .failure(let error):
  9. print(error)
  10. }
  11. }
  1. await client.index('movies').search('thriller', filter: [
  2. ['genres = Horror', 'genres = Mystery'],
  3. 'director = "Jordan Peele"'
  4. ]);

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 - 图3 (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

JS

Python

PHP

Java

Ruby

Go

Rust

Swift

Dart

  1. curl \
  2. -X POST 'http://localhost:7700/indexes/movies/search' \
  3. -H 'Content-Type: application/json' \
  4. --data-binary '{ "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. SearchRequest searchRequest =
  2. new SearchRequest("Batman").setFacetsDistribution(new String[] {"genres"});
  3. client.index("movies").search(searchRequest);
  1. client.index('movies').search('Batman', {
  2. facets_distribution: ['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();
  1. let searchParameters = SearchParameters(
  2. query: "Batman",
  3. facetsDistribution: ["genres"]])
  4. client.index("movies").search(searchParameters) { (result: Result<SearchResult<Movie>, Swift.Error>) in
  5. switch result {
  6. case .success(let searchResult):
  7. print(searchResult)
  8. case .failure(let error):
  9. print(error)
  10. }
  11. }
  1. await client.index('movies').search('Batman', facetsDistribution: ['genres']);

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. }