Introduction

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

We are going to create a very basic application, our main focus will be the search, therefore we won't go into much details about Rails or React.

Prerequisites

To follow this tutorial you need to have installed the latest stable version of:

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 to install MeiliSearch. The easiest is to use cURL, a tool that allows you to make http requests/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. Create and set up your Rails app

Now that you've got MeiliSearch up and running, let's create our RoR app.
We have decided to create a very simple recipe app named delicious_meals. Run the following command on the terminal:

rails new delicious_meals

Once the command is done running, you can go into the project folder:

cd delicious_meals

Let's generate our model Recipe, it will have 4 attributes

  • title
  • ingredients
  • directions
  • diet
 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 can be saved to the database if a field remains empty.

class CreateRecipes < ActiveRecord::Migration[6.1]
  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

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

# Creates the database
rails db:create

# Runs the migration
rails db:migrate

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

rails g controller Recipes index

We are going to 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, it would exceed the purpose of this tutorial.

Once the controller has been created, you can go check the config/routes.rb file. Modify it so it looks 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

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

rails server

Open your browser window and navigate to http://127.0.0.1:3000, you should see your index view, which should display a message such as:

Recipes#index

Find me in app/views/recipes/index.html.erb

To stop the webserver press CTRL+C.

Step 3. Add MeiliSearch to your app

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

Open your Gemfile and add the following line:

gem 'meilisearch-rails', '~> 0.3.0'

Note: 0.3.0 is the latest version of the gem at the time this tutorial is being written. You can check the latest version in the meilisearch-rails GitHub repository or on MeiliSearch finds rubygems.

Save your file and, run bundle install in the command line.

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

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 can leave the field meilisearch_api_key empty:

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

Note: a master or a private key will be required in production, you can learn more about it here.

If your credentials are different, you have to update your configuration accordingly before running MeiliSearch (see step 1)

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

include MeiliSearch

We also need to add a meilisearch block:

class Recipe < ApplicationRecord
    include MeiliSearch
    
    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

Note that the settings inside the MeiliSearch block are not mandatory.

Let's break down each line of code:

displayed_attributes ['id', 'title', 'ingredients', 'directions', 'diet']

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

You can learn more here.

searchable_attributes ['title', 'ingredients', 'directions', 'diet']

With the above line of code we are doing 2 things:

  1. We are first telling 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 matching query word found in the title of a document makes that document more relevant than a document with a matching query word found in directions. The first document being more relevant, it is returned first in the search results.

Learn more about the searchable_attributes here.

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.

If you want to know more about filtering and faceted search, it's over here.

Step 3. Seeding the database

To test our application, we need some data in our database. The quickest way is to populate the database using dummy data. For this purpose, we are going to use a gem called faker, very helpful to have real-looking test data.

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:

# Loads the faker library
require 'faker'

# 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 rails db:seed in the command line.

Step 4. Testing the search with the mini-dashboard

MeiliSearch delivers an out-of-the-box web interface to test it interactively. Just 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.

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 until the updates are processed. Learn more about asynchronous updates here

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

ror-minidashboard

As you can see, the data has been automatically added to our MeiliSearch instance and the only visible and searchable attributes are the ones specified in our model file inside the meilisearch block.

This is great to test MeiliSearch and some of its features, but it doesn't showcase the filterable_attributes we specified in our block. We need a custom UI we can use in production.

Step 5. Add React to the Rails app

There are several ways of using React with Rails. We have chosen to use react-rails which is the official React community gem for integrating React with Rails.

Start by adding the gem to your Gemfile

gem 'react-rails'

Now run the installers:

bundle install
rails webpacker:install
rails webpacker:install:react
rails generate react:install

It generates:

  • a app/javascript/components/ directory for your React components,
  • a app/javascript/packs/application.js file with ReactRailsUJS setup in,
  • a app/javascript/packs/server_rendering.js file for server-side rendering,
  • and a app/javascript/packs/hello_react.jsx example file, that you can delete.

⚠️ If you have any errors during the installation, check the react-rails gem common errors and their fixes here

Step 6. Integrate a front-end search bar with a search-as-you-type experience

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

  • the open-source React InstantSearch library powered by Algolia that provides all the front-end tools you need to highly customize your search bar environment.
  • the MeiliSearch client instant-meilisearch to establish the communication between your MeiliSearch instance and the React InstantSearch library.
npm install react-instantsearch-dom @meilisearch/instant-meilisearch

Let's create our first component Recipes, which will be added to app/javascript/components/ by default. Run the following command:

rails g react:component Recipes

You can now open your app/javascript/components/Recipes.js file and replace the existing code with the code from the getting started guide of the meilisearch-react repository. We only need to modify the searchClientwith 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 Recipes = () => (
  <InstantSearch
    indexName="Recipe" // Change your index name here
    searchClient={searchClient}
  >
    <SearchBox />
    <div><Hits hitComponent={Hit} /></div>
  </InstantSearch>
);

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

export default Recipes

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

<%= react_component("Recipes") %>

Now you can run the rails server command, open your browser and navigate to http://127.0.0.1:3000 and see the result:

ror-no-css-2

You probably have a speed badge displayed at the top left corner of the page. It's the rack-mini-profiler, included by default in all Rails apps. If you want to get rid of it, you just have to comment out or remove the appropriate line in the Gemfile (you should find it in the development group), run bundle install and restart the server.

Well, the search works, but it's not really pretty.

Luckily, InstantSearch provides a CSS 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/[email protected]/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.

Let's check the rendering:

ror-css

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

Step 7. Adding faceted search

It's as simple as importing the RefinementList widget in your Recipes.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 on the right the search bar and the results.

You can also add a title and the ClearRefinements widget, which 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 Recipes = () => (
  <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 Recipes

For this to work we need to add some simple styling in the app/assets/stylesheets/recipes.scss file:

.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.scss */

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

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

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

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

ror-final

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 title of the recipes the ingredients, the instruction and the diet type are not necessarily consistent with each other.