Faceted Search

Faceted search is a feature provided out-of-the-box by MeiliSearch. Faceting allows classifying search results into categories that are called facets.

For a movie, its director, or its genre can be used as facets.
A faceted search system provides users with a simple way to narrow down search results by selecting facets. A faceted navigation system is an intuitive interface to display and navigate through content. The facets are placed in the UI as filters which users can apply to refine the results in real-time.
When users perform a search, they are presented with a list of results and a list of facets (i.e., categories) as below:

Faceted navigation on Amazon: facets are displayed on the left column.

Filters or Facets?

Faceted search, also known as faceted navigation, is a technique that combines traditional search with a faceted classification of items.

Setting categorical document attributes as “facet” enables efficient filtering within the different categories. Such categorical attributes are, for example, movie genre, director, or language.

Besides, faceting is a powerful feature that allows building intuitive navigation interfaces.

Both faceting and filtering help drill down into a subset of search results. However, faceting differs from filtering.

  • Filters exclude some results based on criteria. They allow users to narrow down a set of documents to only those matching these chosen criteria. In other words, filtering is used to filter the returned results by adding constraints.
  • Facets, on the other hand, are used to categorize the data into subsets that will be searched upon: they reduce the number of documents to process.

Faceting and filtering aim at being complementary; facets narrows down the set of documents to be searched upon, while filters reduce the number of documents coming out of a search.

Setting Up Facets

The first step in using facets is to choose which will be used as facets. Fields with common values are the best suited for faceting (e.g., genres, color, size ).

For these fields to be used as facets during search, their must have been previously added to the settings. In the settings, the chosen attributes must be added to the attributesForFaceting list.

This step is required because facet needs to be properly processed and prepared by the engine to be usable. This process takes as much time as indexing all your documents.

You can perform faceting on attributes that are either String or [String], and null values are ignored.
If a facet value in a given document is not of type string, or [String], or null, the transaction will stop and raise an error.

References for attributesForFaceting in the settings

Example

Suppose that 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. ...
  14. ]

To be able to facet search on director and genres, you would declare faceted attributes as follows:

  1. curl \
  2. -X POST 'http://localhost:7700/indexes/movies/settings' \
  3. --data '{
  4. "attributesForFaceting": [
  5. "director",
  6. "genres"
  7. ]
  8. }'
  1. client.index('movies')
  2. .updateAttributesForFaceting([
  3. 'director',
  4. 'genres'
  5. ])
  1. client.index('movies').update_attributes_for_faceting([
  2. 'director',
  3. 'genres',
  4. ])
  1. $client->index('movies')->updateAttributesForFaceting(['director', 'genres']);
  1. index.update_attributes_for_faceting([
  2. 'director',
  3. 'genres'
  4. ])
  1. response, error := client.Settings("movies").UpdateAttributesForFaceting([]string{
  2. "director",
  3. "genres",
  4. })
  1. let progress: Progress = movies.set_attributes_for_faceting(["director", "genres"]).await.unwrap();

Using facets

By introducing facets to MeiliSearch, new search query parameters were also added:

  • facetFilters: Narrows the selection on which to search.
  • facetsDistribution: Returns the number of matching documents distributed amongst all the values of a given facet.

The facet filters

Because facets relate specifically to a set of documents, they give an overview of the records and help to understand what kind of information can be searched. Facets then help users navigate through their search results.

You can select multiple categories on Amazon.
They can filter on facets to narrow down their results based on criteria with the facetFilters attribute.

Usage

facetFilters is a query parameter added on search request. It expects a string or an array of strings containing the facetFilter information. Each string is composed of a facetName, a colon, and a facetValue.

facetFilters=["facetName:facetValue"] or facetFilters=[["facetName:facetValue"]]

  • facetName: The name (the attribute) of a field used as a facet (e.g. director, genres). This attribute must be in the attributesForFaceting list.
  • facetValue: The value of the facet used to filter results (e.g. Tim Burton, Mati Diop, Comedy, Romance).

Facet filters can have a maximum array deepness of two.

The following are correct:

good ✅

  1. "genres:horror"

good ✅

  1. ["genres:horror", "genres:thriller"]

good ✅

  1. ["genres:comedy", ["genres:horror", "genres:thiller"]]

When you add one more array deepness, it will raise errors:
error ❌

  1. ["genres:comedy", ["genres:horror", ["genres:romance"]]]

error ❌

  1. [[["genres:romance"]]]

Logical Connectives

Inputting a double dimensional array allows you to use logical connectives.

  • Inner arrays elements are connected by an OR operator (e.g. [["genres:Comedy", "genres:Romance"]]).
  • Outer arrays elements are connected by an AND operator (e.g. ["genres:Romance", "director:Mati Diop"]).

You can mix connectives, for instance, the following array:

  1. [["genres:Comedy", "genres:Romance"], "director:Mati Diop"]

Can be translated as:

  1. ("genres:Comedy" OR "genres:Romance") AND "director:Mati Diop"

Example

Suppose you have declared director and genres as faceted attributes, and you want to get movies matching “thriller” classified as either horror or mystery and directed by Jordan Peele.

  1. ("genres:Horror" OR "genres:Mystery") AND "director:Jordan Peele"

Querying on “thriller”, the above example results in the following CURL command:

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

And you would get the following response:

  1. {
  2. "hits": [
  3. {
  4. "id": 458723,
  5. "title": "Us",
  6. "director": "Jordan Peele",
  7. "genres": [
  8. "Thriller",
  9. "Horror",
  10. "Mystery"
  11. ],
  12. "overview": "Husband and wife Gabe and Adelaide Wilson take their kids to their beach house expecting to unplug and unwind with friends. But as night descends, their serenity turns to tension and chaos when some shocking visitors arrive uninvited.",
  13. },
  14. {
  15. "id": 419430,
  16. "title": "Get Out",
  17. "director": "Jordan Peele",
  18. "genres": [
  19. "Mystery",
  20. "Thriller",
  21. "Horror"
  22. ],
  23. "overview": "Chris and his girlfriend Rose go upstate to visit her parents for the weekend. At first, Chris reads the family's overly accommodating behavior as nervous attempts to deal with their daughter's interracial relationship, but as the weekend progresses, a series of increasingly disturbing discoveries lead him to a truth that he never could have imagined.",
  24. }
  25. ],
  26. ...
  27. "nbHits": 2,
  28. "exhaustiveNbHits": false,
  29. "query": "thriller"
  30. }

The facets distribution

Facet distribution returns the number of matching documents distributed amongst all the values of a given facet.

After a search in a movie dataset, the number of films found in all the different genres is the facet distribution of the genres facet.
In the example below, on IMDbFaceted Search - 图1 (opens new window), the number in parentheses represents the count of search results each facet is associated to.

If a user searches Fantasy, Animation, Adventure movies, and TV shows, they will know there are 826 TV series and 137 video games matching their criteria.
Since the users can have a visual clue about the range of categories available in the UI, they can easily know how many search results are found for each category.

To get the facets distribution, you have to specify a list of facets for which to retrieve the count of matching documents using the facetsDistribution attribute.

The facetsDistribution parameter also introduces exhaustiveFacetsCount in the return object. exhaustiveFacetsCount is a boolean value that informs the user whether or not the facets distribution is matching the reality or if it is an approximation.

The approximative facet count happens when there are too many documents in too many different facet values. In which case, MeiliSearch stops the distribution count to prevent considerably slowing down the request.

Usage

facetsDistribution is a query parameter added on a search request. It expects an array of strings. Each string is an attribute present in the attributesForFiltering list.

Upon search, when using the facetDistribution parameter, there will be a facetDistribution key in the returned object. It contains an object for every facet given. For each of these facets, another object containing all the different values and the count of matching document found with this value. This is called the distribution.

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

Example

Given a movie database, suppose that you want to know what the number of Batman movies per genre is. You would use the following CURL command:

  1. curl --get 'http://localhost:7700/indexes/movies/search' \
  2. --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. index.search('Batman', {
  2. facetsDistribution: ['genres']
  3. })
  1. results, error := client.Search("movies").Search(meilisearch.SearchRequest{
  2. Query: "Batman",
  3. FacetsDistribution: []string{
  4. "genres",
  5. },
  6. })
  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();

And you would get the following response:

  1. {
  2. "hits": [
  3. {
  4. "id": 2661,
  5. "title": "Batman",
  6. "director": "Leslie H. Martinson",
  7. "genres": [
  8. "Adventure",
  9. "Comedy"
  10. ],
  11. "overview": "The Dynamic Duo faces four super-villains who plan to hold the world fo r ransom with the help of a secret invention that instantly dehydrates people.",
  12. },
  13. {
  14. "id": 268,
  15. "title": "Batman",
  16. "director": "Tim Burton",
  17. "genres": [
  18. "Fantasy",
  19. "Action"
  20. ],
  21. "overview": "The Dark Knight of Gotham City begins his war on crime with his first major enemy being the clownishly homicidal Joker, who has seized control of Gotham's underworld."
  22. }
  23. ...
  24. ],
  25. ...
  26. "nbHits": 1684,
  27. "query": "Batman",
  28. "exhaustiveFacetsCount": true,
  29. "facetsDistribution": {
  30. "genres": {
  31. "action": 273,
  32. "animation": 118,
  33. "adventure": 132,
  34. "fantasy": 67,
  35. "comedy": 475,
  36. "mystery": 70,
  37. "thriller": 217,
  38. }
  39. }
  40. }

Walkthrough

With this walkthrough, you will go through each step to successfully add facets and faceted search.

Suppose that you manage a movie database on which you want to search by genres, producer, production_companies and directors.

The first thing to do is to declare faceted attributes as follows:

  1. curl \
  2. -X POST 'http://localhost:7700/indexes/movies/settings' \
  3. --data '{
  4. "attributesForFaceting": [
  5. "director",
  6. "producer",
  7. "genres",
  8. "production_companies"
  9. ]
  10. }'
  1. client.index('movies')
  2. .updateAttributesForFaceting([
  3. 'director',
  4. 'producer',
  5. 'genres',
  6. 'production_companies'
  7. ])
  1. client.index('movies').update_attributes_for_faceting([
  2. 'director',
  3. 'producer',
  4. 'genres',
  5. 'production_companies'
  6. ])
  1. $client->index('movies')->updateAttributesForFaceting([
  2. 'director',
  3. 'producer',
  4. 'genres',
  5. 'production_companies'
  6. ]);
  1. index.update_attributes_for_faceting([
  2. 'director',
  3. 'producer',
  4. 'genres',
  5. 'production_companies'
  6. ])
  1. response, error := client.Settings("movies").UpdateAttributesForFaceting([]string{
  2. "director",
  3. "producer",
  4. "genres",
  5. "production_companies",
  6. })
  1. let attributes_for_faceting = [
  2. "director",
  3. "producer",
  4. "genres",
  5. "production_companies"
  6. ];
  7. let progress: Progress = movies.set_attributes_for_faceting(&attributes_for_faceting).await.unwrap();

In the above example, director, producer, genres and production_companies will be used as facets.

You can now search your documents and use query parameters.

You can filter on facets. For instance, say you want to get movies matching “thriller” classified as either horror or mystery and directed by Jordan Peele. You have to use the following CURL command:

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

You will get the following response:

  1. {
  2. "hits": [
  3. {
  4. "id": 458723,
  5. "title": "Us",
  6. "director": "Jordan Peele",
  7. "genres": [
  8. "Thriller",
  9. "Horror",
  10. "Mystery"
  11. ],
  12. "overview": "Husband and wife Gabe and Adelaide Wilson take their kids to their beach house expecting to unplug and unwind with friends. But as night descends, their serenity turns to tension and chaos when some shocking visitors arrive uninvited."
  13. },
  14. {
  15. "id": 419430,
  16. "title": "Get Out",
  17. "director": "Jordan Peele",
  18. "genres": [
  19. "Mystery",
  20. "Thriller",
  21. "Horror"
  22. ],
  23. "overview": "Chris and his girlfriend Rose go upstate to visit her parents for the weekend. At first, Chris reads the family's overly accommodating behavior as nervous attempts to deal with their daughter's interracial relationship, but as the weekend progresses, a series of increasingly disturbing discoveries lead him to a truth that he never could have imagined."
  24. }
  25. ],
  26. "nbHits": 2,
  27. "exhaustiveNbHits": false,
  28. "query": "thriller"
  29. }

Now, if you want to know what the number of Batman movies per genre is, you have to use the following CURL command:

  1. curl 'http://localhost:7700/indexes/movies/search' \
  2. --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. index.search('Batman', {
  2. facetsDistribution: ['genres']
  3. })
  1. results, error := client.Search("movies").Search(meilisearch.SearchRequest{
  2. Query: "Batman",
  3. FacetsDistribution: []string{
  4. "genres",
  5. },
  6. })
  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();

You will get the following response:

  1. {
  2. "hits": [
  3. ...
  4. ],
  5. ...
  6. "nbHits": 1684,
  7. "query": "Batman",
  8. "exhaustiveFacetsCount": true,
  9. "facetsDistribution": {
  10. "genres": {
  11. "action": 273,
  12. "animation": 118,
  13. "adventure": 132,
  14. "fantasy": 67,
  15. "comedy": 475,
  16. "mystery": 70,
  17. "thriller": 217,
  18. }
  19. }
  20. }

In the above response, you can see a returned object facetsDistribution that contains the count of matching documents for each value of the genres attribute.