In this guide, we’ll delve into the concept of faceting in Meilisearch and how to use it to display facet names while filtering by IDs.

What is faceting?

Faceting is a technique used in search engines to classify search results into multiple categories or “facets”. These facets can be anything from categories, tags, price ranges, to even colors. This makes it easier for the user to navigate and filter through the results, providing a more refined and efficient search experience.

Why use the ID as a facet filter?

For most applications and users, filtering by facet names, such as the genre of movies, is often sufficient and intuitive. Facet names are human-readable and provide a clear idea of what the filter does. However, in some use cases, it is preferable to filter by ID for a couple of key reasons:

  • Less prone to errors: IDs are usually simpler and standardized, making them less susceptible to typos or inconsistencies that might be present in facet names.
  • Unique identifiers: In some databases, facet names might be repeated with slightly different characteristics, whereas IDs are always unique. This is especially useful in cases where you have items with similar or identical names but different properties.

By using IDs for filtering but displaying the corresponding facet names to the users, you can achieve the best of both worlds: efficiency and usability. This way, you are leveraging the strengths of both IDs and names, making your application robust and user-friendly.

The ID-to-name challenge

In Meilisearch, facets are a specialized use-case of filters. Essentially, you can use any attribute added to the filterableAttributes list as a facet. When you add a facet parameter to your search query, Meilisearch will return a facetDistribution object. This object provides the number of matching documents distributed among the values of the given facet.

 "facetDistribution":{
    "genres":{
      "Classics":6,
      "Comedy":1,
      "Coming-of-Age":1,
      "Fantasy":2,
      "Fiction":8,
      …
    }
  }

However, if the field you’ve added to the filterableAttributes list is an ID, the facetDistribution object will return these IDs.

 "facetDistribution":{
    "genres":{
      "5":6,
      "6":1,
      "8":1,
      "13":2,
      "16":8,
      …
    }
  }

While IDs are great for back-end operations, they are not necessarily user-friendly or meaningful on the front-end. This is why you might want to map these IDs back to their corresponding facet names when displaying them in your user interface.

ID-to-name mapping in the frontend

Suppose you have a dataset of movies following the structure below:

{
    "id": 5,
    "title": "Four Rooms",
    "overview": "It's Ted the Bellhop's first night on the job....",
    "genres": [
        {
            "id": 7,
            "name": "Crime"
        },
        {
            "id": 6,
            "name": "Comedy"
        },
    ],
    "release_date": 818467200,
}

You want a faceted search based on the movies genres. So you add genres.id to the filterable attributes list. Of course, in the UI you want to display genres.name, so that the user will see “Crime” on the UI instead of “7”.

Let’s dive into how you can accomplish this with InstantSearch and instant-meilisearch.

Using InstantSearch and instant-meilisearch

InstantSearch is an open-source front-end library to build search UIs. Instant-meilisearch is the go-to search client for integrating InstantSearch with Meilisearch.

To use instant-meilisearch with InstantSearch, you need to:

1. Import the required modules, including instantMeiliSearch

import { instantMeiliSearch } from '@meilisearch/instant-meilisearch'
import instantsearch from 'instantsearch.js'
import { searchBox, infiniteHits,refinementList } from 'instantsearch.js/es/widgets'


2. Establish a Meilisearch client with your Meilisearch host and search API key

const searchClient = instantMeiliSearch(
  'https://ms-7053a8dd7c09-72.lon.meilisearch.io',
  'meilisearchApiKey'
)

3. Set up instantsearch with your Meilisearch index name and the search client

const searchIndex = instantsearch({
  indexName: 'movies',
  searchClient
})

In this guide, we’ll use InstantSearch’s refinementList widget to map IDs to user-friendly names.

This widget comes with an optional parameter called transformItems. This function receives the items (or facets) and allows you to transform them before they are displayed on the UI. It also includes the full results data in its parameters.

Each item—or facet—includes the following properties:

  • count: the number of occurrences of the facet in the result set
  • value: the value used for refining (in our case it would be genres.id)
  • label: the label to display
  • highlighted: the highlighted label. This value is displayed in the default template

As you can see highlighted is the label used by default in the refinementlList widget.

With this information, we can use the transformItems function to display the genres.name instead of genres.id.

transformItems(items, { results }) {
    // The 'results' parameter contains the full results data
    return items.map(item => {
      // Initialize genreName with the existing highlighted label
      let genreName = item.highlighted;
      
      // Loop through results.hits to find a matching genre
      for (let hit of results.hits) {
        const matchedGenre = hit.genres.find(genre => genre.id.toString() === item.value);
        
        // Update genreName if a match is found
        if (matchedGenre) {
          genreName = matchedGenre.name;
          break;
        }
      }
      
      // Return the updated item with the new tagName as the highlighted value
      return {
        ...item,
        highlighted: genreName
      };
    });
  }

With this configuration, you’ll efficiently map IDs to more user-friendly names, enhancing the search experience for your users.

The full code should look like this:

import { instantMeiliSearch } from '@meilisearch/instant-meilisearch'
import instantsearch from 'instantsearch.js'
import { searchBox, infiniteHits,refinementList } from 'instantsearch.js/es/widgets'

const searchClient = instantMeiliSearch(
  'https://ms-7053a8dd7c09-72.lon.meilisearch.io',
  'meilisearchApiKey'
)
  
const searchIndex = instantsearch({
  indexName: 'movies',
  searchClient
})

const searchBox = instantsearch.widgets.searchBox({
  // ...
});

const hits = instantsearch.widgets.hits({
  // ...
});

const refinementList = instantsearch.widgets.refinementList({
  container: '#facets',
  attribute: 'genres.id',
  transformItems(items, { results }) {
    return items.map(item => {
      let genreName = item.highlighted; 
      for (let hit of results.hits) {
        const matchedGenre = hit.genres.find(genre => genre.id.toString() === item.value);
        if (matchedGenre) {
          genreName = matchedGenre.name;
          break;
        }
      }
      return {
        ...item,
        highlighted: genreName
      };
    });
  }
})

searchIndex.addWidgets([searchBox, infiniteHits, refinementList]);

searchIndex.start()

This example shows how to implement ID-to-name mapping with vanilla JavaScript, but you can achieve similar results with your preferred frontend framework. Checkout the respective documentation for React and Vue.

For more things Meilisearch, you can subscribe to our newsletter. You can learn more about our product by checking out the roadmap and participating in our product discussions.

For anything else, join our developers community on Discord.