How to integrate an extremely fast and relevant search into your Rails app using Meilisearch and React
Tags: tutorial, meilisearch, react, rails
Published: 2022-09-01 12:12:34

## Introduction

In this tutorial, you'll learn how to integrate Meilisearch with your Rails application database and quickly create a front-end search bar with a search-as-you-type experience using React.

We will create a very basic application; our main focus will be the search. Therefore we won't go into much detail about Rails or React.
Prerequisites

To follow this tutorial, you need:

-     [Node.js](https://nodejs.org/) >=16.10
-     [yarn](https://yarnpkg.com/) 1
-     [Ruby](https://www.ruby-lang.org/) >= 2.7
-     [Ruby on Rails](https://guides.rubyonrails.org/getting_started.html#creating-a-new-rails-project-installing-rails) 7.0

Ideally, you are familiar with Ruby on Rails and have already created a simple RoR app. If that's not the case, you can still follow this tutorial, but as we stated in the introduction, explanations will focus on the search.

## Step 1. Installing Meilisearch

There are [several ways](https://docs.meilisearch.com/learn/getting_started/installation.html#download-and-launch) to install Meilisearch. The easiest is [cURL](https://curl.se/), a tool that allows you to make HTTP requests and transfer data from the command line.

Open your terminal and paste the following lines of code:

```
# Install Meilisearch
curl -L https://install.meilisearch.com | sh

# Launch Meilisearch
./meilisearch
```

## Step 2. Creating and setting up your Rails app

Now that you've got Meilisearch up and running, let's create our RoR app.‌‌ We will create a simple recipe app named `delicious_meals`. Run the following command on the terminal:

```
rails new delicious_meals -j esbuild
```

‌Let's generate our model `Recipe`. It will have four attributes:

1.     `title`
2.     `ingredients`
3.     `directions`
4.     `diet`

Go into the project folder and run the following command:

```
bin/rails g model Recipe title:string ingredients:text directions:text diet:string
```

This command also generates the migration file inside the `db/migrate` directory. Let's add the `null: false` option next to each column of the table so that no recipe is saved to the database if a field is empty.

```
class CreateRecipes < ActiveRecord::Migration[7.0]
  def change
    create_table :recipes do |t|
      t.string :title, null: false
      t.text :ingredients, null: false
      t.text :directions, null: false
      t.string :diet, null: false

      t.timestamps
    end
  end
end
```

The `timestamps` column method adds two extra fields to the table: `created_at` and `updated_at`.

You can now create the database and run the migration above with the following commands:

```
# Creates the database 
bin/rails db:create 

# Runs the migration 
bin/rails db:migrate
```

Next, you need to generate the controller with its `index` action.

```
bin/rails g controller Recipes index
```

We will use the `index` view to show our recipes and search through them with our search bar. We won't generate the rest of the CRUD actions, as it would exceed the purpose of this tutorial.

Once the controller is created, modify the `config/routes.rb` file to look like this:

```
Rails.application.routes.draw do
  # Maps requests to the root of the application to the index action of the 'Recipes controller'
  root "recipes#index"
end
```

Now, the `root` route is mapped to the `index` action of `RecipesController`. That way, the content of `app/views/recipes/index.html.erb` will be rendered at the root of your application.

You can check that everything is working as intended by starting the application with the following command:

```
bin/dev
```

Open your browser window and navigate to `http://127.0.0.1:3000`. You should see your index view displaying a message such as:

<h1 style="margin-left:6rem;">Recipes#index</h1>
<p style="margin-left:6rem;">Find me in app/views/recipes/index.html.erb</p>

## Step 3. Adding Meilisearch to your app

Now that we have the back-end basics of our application, let's connect it to our running Meilisearch instance using the `meilisearch-rails` gem.

Install it by running the following command:

```
bundle add meilisearch-rails
```

👉 When writing this tutorial, the latest version of the gem was 0.7.1. You can check the latest version in the [meilisearch-rails GitHub repository](https://github.com/meilisearch/meilisearch-rails) or on [Meilisearch finds rubygems](https://rubygems.meilisearch.com/?q=meilisearch-rails).

Create a file named `meilisearch.rb` inside the `config/initializers/` folder to setup your `MEILISEARCH_HOST` and `MEILISEARCH_API_KEY`:

```
touch config/initializers/meilisearch.rb
```

If you have followed step 1 to the letter, your Meilisearch host should be `http://127.0.0.1:7700`. Since we did not set any API key, we will comment out the line with the `meilisearch_api_key field`:

```
MeiliSearch::Rails.configuration = {
    meilisearch_host: 'http://127.0.0.1:7700',
    # meilisearch_api_key: ''
}
```

👉 You will need a master or a private key in production, you can learn more about it [here.](https://docs.meilisearch.com/reference/features/authentication.html)

If you did set a master key, you have to update your configuration accordingly before running Meilisearch (see step 1).

Let's open our `app/models/recipe.rb` file and add the following line inside the `Class` declaration:

```
include MeiliSearch::Rails
```

We also need to add a meilisearch block. Note that the settings inside the Meilisearch block are not mandatory.

```
class Recipe < ApplicationRecord
    include MeiliSearch::Rails
    
    meilisearch do
        # all attributes will be sent to Meilisearch if block is left empty
        displayed_attributes [:id, :title, :ingredients, :directions, :diet]
        searchable_attributes [:title, :ingredients, :directions, :diet]
        filterable_attributes [:diet]
    end
end
```

Let's break down each line of code:

### Setting displayed attributes

```
displayed_attributes [:id, :title, :ingredients, :directions, :diet]
```

By default, Meilisearch displays all the attributes. Here, we are instructing Meilisearch to only display the specified attributes in the search response, this setting prevents Meilisearch from displaying the `created_at` and `updated_at` fields.

👉 You can learn more about `displayed attributes` in our [documentation](https://docs.meilisearch.com/reference/features/field_properties.html#displayed-fields).

### Setting searchable attributes

```
searchable_attributes [:title, :ingredients, :directions, :diet]
```

With the above line of code, we are doing two things:

    

1. We first tell Meilisearch to only search among the specified attributes when performing a search query. So it won't try to find matches in the `id`, `created_at` and `updated_at` fields.
2. We are also specifying the order of importance of the attributes. We are telling Meilisearch that a document with a matching query word found in its `title` is more relevant than a document with a matching query word found in `directions`. The first document is more relevant and is returned first in the search results.

👉 Learn more about `searchable fields` in our [documentation](https://docs.meilisearch.com/learn/configuration/displayed_searchable_attributes.html#searchable-fields).

### Setting filterable attributes

```
filterable_attributes [:diet]
```
 

Finally, we are telling Meilisearch that we want to be able to **refine our search results** based on the `diet` type. That will allow us to search only for vegetarian recipes, for instance.

👉 Visit our [documentation](https://docs.meilisearch.com/reference/features/filtering_and_faceted_search.html) to know more about filtering.

## Step 4. Seeding the database

To test our application, we need some data in our database. The quickest way is to populate the database with dummy data using a gem called _[faker](https://github.com/faker-ruby/faker)_.

Add the following line to your `Gemfile` inside the development group, save and run `bundle install`:

```
gem 'faker', :git => 'https://github.com/faker-ruby/faker.git', :branch => 'master' 
```

Then open the `./db/seeds.rb` file and add the following code to populate your database with 1000 recipes:

```
# Deletes existing recipes, useful if you seed several times
Recipe.destroy_all

# Creates 1000 fake recipes
1000.times do
    Recipe.create!(
        title: "#{Faker::Food.dish} by #{Faker::Name.unique.name}",
        ingredients: "#{Faker::Food.ingredient}, #{Faker::Food.ingredient}, #{Faker::Food.ingredient}",
        directions: Faker::Food.description,
        diet: ['omnivore', 'pescetarian', 'vegetarian', 'vegan'].sample
    )
end 

# Displays the following message in the console once the seeding is done
puts 'Recipes created'
```

Now, run `bin/rails db:seed` in the command line.

## Step 5. Testing search with the search preview

Meilisearch delivers an out-of-the-box web interface to test it interactively. Open your browser and go to the Meilisearch HTTP address, which should be `http://127.0.0.1:7700`, unless you specified it otherwise [at launch](https://docs.meilisearch.com/learn/configuration/instance_options.html#http-address-port-binding).

👉 Adding documents to an index is an asynchronous operation, don't worry if you don't see the 1000 documents right away. It might take some time for the updates to process. Learn more about asynchronous updates [here](https://docs.meilisearch.com/learn/advanced/asynchronous_updates.html).

Make sure the `Recipe` index is selected in the menu located at the top right, next to the search bar.

![Meilisearch mini dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/94bg2j6d1yvty9xkroag.gif)


As you can see, the data has been automatically added to our Meilisearch instance. The only visible and searchable attributes are the ones [we specified in our model file](https://blog.meilisearch.com/how-to-integrate-an-extremely-fast-and-relevant-search-into-your-rails-app-using-meilisearch-and-react/#setting-displayed-attributes) inside the `meilisearch block`. Please note that your search results may be different from those shown in the GIF, since faker randomly generates data.

This is great for testing Meilisearch and some of its features, but it doesn't showcase the `filterable_attributes` we specified in our block. We need a custom UI for production.

## Step 6. Adding React to the Rails app

There are several ways of using ReactJS with Rails. We have chosen the most straightforward one: installing it as a JavaScript dependency in our Rails application.

Run the following command to install **ReactJS** and its **react-dom** package for working with the DOM:

```
yarn add react react-dom
```

Let's create the folders and files for our React code.

```
mkdir app/javascript/recipes 
touch app/javascript/recipes/index.jsx 
touch app/javascript/recipes/App.jsx
```

Let's open `app/javascript/recipes/index.jsx` and add the required code to render our React elements:

```
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

const container = document.getElementById('app');
const root = createRoot(container); 
root.render(<App/>);
```

Open the `app/javascript/application.js` and import the file we just created:

```
import "./recipes"
```

## Step 7. Integrating a front-end search bar

To integrate a front-end search bar, you need to install two packages:

-   **[React InstantSearch](https://github.com/algolia/react-instantsearch)**: an open-source library that provides all the front-end tools you need to customize your search bar environment
-     **[Instant Meilisearch](https://github.com/meilisearch/instant-meilisearch)**: the Meilisearch client to establish the communication between your Meilisearch instance and the React InstantSearch library

```
yarn add react react-dom react-instantsearch-dom @meilisearch/instant-meilisearch
```

You can now open your `app/javascript/recipes/App.jsx` file and replace the existing code with the code from the `meilisearch-react` [Getting Started](https://github.com/meilisearch/meilisearch-react/#getting-started) guide. We only need to modify the `searchClient` with our Meilisearch host and Meilisearch API key, as well as the `indexName`. It should look like this:

```
import React from "react"
import { InstantSearch, Highlight, SearchBox, Hits } from 'react-instantsearch-dom';
import { instantMeiliSearch } from '@meilisearch/instant-meilisearch';

const searchClient = instantMeiliSearch(
  "http://127.0.0.1:7700", // Your Meilisearch host
  "" // Your Meilisearch API key, if you have set one
);

const App = () => (
  <InstantSearch
    indexName="Recipe" // Change your index name here
    searchClient={searchClient}
  >
    <SearchBox />
    <Hits hitComponent={Hit} />
  </InstantSearch>
);

const Hit = ({ hit }) => <Highlight attribute="title" hit={hit} />

export default App

```
Now, go to your `views` folder and replace the content of the `app/views/recipes/index.html.erb` with the code below:

```
<div id="app"></div>
```

Now you can run the `bin/dev` command, open your browser and navigate to `http://127.0.0.1:3000` and see the result:‌

![Typing salad in an unstyled search bar, the results are updated as you type](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1oivxbr4czw4v6yj5upf.gif)

Well, the search works, but it's not very pretty. Luckily, InstantSearch provides a [CSS theme](https://www.algolia.com/doc/guides/building-search-ui/widgets/customize-an-existing-widget/react/#loading-the-theme) you can add by inserting the following link into the `<head>` element of your `app/views/layouts/application.html.erb`:

```
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/instantsearch.css@7.4.5/themes/satellite-min.css" integrity="sha256-TehzF/2QvNKhGQrrNpoOb2Ck4iGZ1J/DI4pkd2oUsBc=" crossorigin="anonymous">
```

You can also customize the widgets or create your own if you want to. Check the [React InstantSearch documentation](https://www.algolia.com/doc/guides/building-search-ui/widgets/customize-an-existing-widget/react/) for more details.

Let's check the rendering:

![Typing chicken in a styled search bar, the results are updated as you type](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xnvmi7f1h2u3xnl5qpax.gif)

Not bad, right? But once again we lack the possibility of filtering the results by type of diet.

## Step 8. Adding faceted search

It's as simple as importing the `RefinementList` widget in your `index.js` file:

```
import { InstantSearch, Highlight, SearchBox, Hits, RefinementList } from 'react-instantsearch-dom';
```

and adding it inside our `InstantSearch` widget, specifying the attribute we want to filter by:

```
<RefinementList attribute="diet" />
```

To make it more aesthetic and practical, let's create two `<div>` elements to divide our components. On the left, we'll find the filters, and the search bar and results on the right.

You can also add a "Type of diet" heading along with the `ClearRefinements` widget. It allows you to clear all the filters just by clicking on it, instead of having to uncheck them one by one.

The file should now look like this:

```
import React from "react"
import { InstantSearch, Highlight, SearchBox, Hits, RefinementList, ClearRefinements } from 'react-instantsearch-dom';
import { instantMeiliSearch } from '@meilisearch/instant-meilisearch';

const searchClient = instantMeiliSearch(
  "http://127.0.0.1:7700",
  ""
);

const App = () => (
  <InstantSearch
    indexName="Recipe" // Change your index name here
    searchClient={searchClient}
  >
    <div className="left-panel">
      <ClearRefinements />
      <h2>Type of diet</h2>
      <RefinementList attribute="diet" />
    </div>
    <div className="right-panel">
      <SearchBox />
      <Hits hitComponent={Hit} />
    </div>

  </InstantSearch>
);

const Hit = ({ hit }) => <Highlight attribute="title" hit={hit} />

export default App
```

For this to work we need to add some CSS. Let's create a `app/assets/stylesheets/recipes.css` file and add the following lines of code:

```
.right-panel {
    margin-left: 210px;
}
  
.left-panel {
    float: left;
    width: 200px;
}
```

And just to make it even prettier, let's add some padding and margin to the body and the search bar, and change the font:

```
/* app/assets/stylesheets/recipes.css */

body { 
    font-family: sans-serif; 
    padding: 1em; 
}

.ais-SearchBox { 
    margin: 1em 0; 
}

.right-panel {
    margin-left: 210px;
}

.left-panel {
    float: left;
    width: 200px;
}
```

![Typing salad in an styled search bar with type of diet filters on the left, the results are updated as you type](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lh69wqlmrgzd9pf1cf5m.gif)

And tada! 🎉  You have a nice search bar with a search-as-you-type experience ready! 🥳

⚠️ Because we used fake data to seed our database, the recipes' `title`s, `ingredients`, `directions`, and `diet` type are not necessarily consistent.
Conclusion

We have learned how to synchronize our Ruby on Rails database with Meilisearch and customize our search settings directly on our Rails app, allowing us to search through our data in milliseconds. To top it all off, we have also created a faceted search interface with a search-as-you-type experience using React.

We have achieved all this seamlessly, thanks to [Meilisearch Rails](https://github.com/meilisearch/meilisearch-rails) and [Instant Meilisearch](https://github.com/meilisearch/instant-meilisearch). Meilisearch has integrations for almost every popular language or framework. Take a look at the complete list in the [Meilisearch integration guides](https://github.com/meilisearch/integration-guides/).

If you have any questions, please join us on [Slack](https://slack.meilisearch.com/); we are always happy to hear from you. For more information on Meilisearch, check out our [Github repository](https://github.com/meilisearch/meilisearch/) and [official documentation](https://docs.meilisearch.com/).