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:
- Node.js
- npm (comes with Node.js)
- Ruby
- Ruby on Rails
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:
- 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
andupdated_at
fields. - 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 indirections
. 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
andfaceted 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.
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 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 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:
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 thedevelopment
group), runbundle 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/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.
Let's check the rendering:
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;
}
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.