Integrate Rails, React and MeiliSearch using Docker
Rodrigo Odhin
Posted on April 7, 2022
Introduction
In this post, you'll learn how to integrate MeiliSearch with your Rails application database and how to create a front-end search bar with a search-as-you-type experience using React.
This is a very basic application, my focus will be the search, therefore I don't go into much details about Rails or React.
1- Create Docker Containers
To create our application, I will use a docker image (rodrigoodhin/rails:6:0:0) with Rails and Node.js. In addition, I will use the image (getmeili/meilisearch:v0.25.0) to create a MeiliSearch container.
Let's create the docker-compose.yml
file and add the following code to create the containers:
version: '3'
services:
odhin_rails:
image: rodrigoodhin/rails:6.0.0
container_name: odhin_rails_app
restart: always
volumes:
- .:/projects
ports:
- "14880:22"
- "14881:3000"
depends_on:
- odhin_meilisearch
extra_hosts:
- 'host.docker.internal:host-gateway'
networks:
odhin_network:
odhin_meilisearch:
image: "getmeili/meilisearch:v0.25.0"
container_name: odhin_meilisearch
restart: always
command: ./meilisearch --master-key=A53451A4E5A5C21D265944AB8654984016199CCA362F2CA25B3CCD4DF821993B
ports:
- "14883:7700"
volumes:
- "/tmp/data.ms:/data.ms"
networks:
odhin_network:
networks:
odhin_network:
You can generate and set a different
master-key
for your MeiliSearch or leave it empty. For security reasons, do not leave themaster-key
empty in production.
Run the command below to create and start the containers:
docker-compose up -d
Now we have an environment ready to start creating your Rails application.
2- Create and set up your Rails app
Now that we've got everything up and running, let's create our RoR app.
Before start, we need to access the Rails container with this command:
ssh app@localhost -p 14880
Type
app
for the password.
Go to the projects folders with the command:
cd /projects
I have decided to create a very simple collection app named games_collection
. Run the following command on the terminal:
rails new games_collection
Go to the games collection project folder:
cd games_collection
Let's generate our model Game, it will have 4 attributes
- cover
- title
- genre
- platform
rails g model Game cover:string title:string genre:string platform:string
Let's create the database and run the migration with the following commands:
rails db:create
rails db:migrate
Next, we need to generate the controller with its index action:
rails g controller Games index
We are going to use the index view to show our games and search through them with our search bar.
We won't generate the rest of the CRUD actions, that's not the purpose of this post.
Once the controller has been created, we have to modify config/routes.rb
file to looks like this:
Rails.application.routes.draw do
root "games#index"
end
Add the following code to the config/puma.rb
file, above the port
information:
set_default_host '0.0.0.0'
This will specifies the
ip
that Puma will listen on to receive requests
Our config/puma.rb
file will look like this:
# Puma can serve each request in a thread from an internal thread pool.
# The `threads` method setting takes two numbers: a minimum and maximum.
# Any libraries that use thread pools should be configured to match
# the maximum value specified for Puma. Default is set to 5 threads for minimum
# and maximum; this matches the default thread size of Active Record.
#
max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
threads min_threads_count, max_threads_count
# Specifies the `ip` that Puma will listen on to receive requests.
#
set_default_host '0.0.0.0'
# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
#
port ENV.fetch("PORT") { 3000 }
# Specifies the `environment` that Puma will run in.
#
environment ENV.fetch("RAILS_ENV") { "development" }
# Specifies the `pidfile` that Puma will use.
pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
# Specifies the number of `workers` to boot in clustered mode.
# Workers are forked web server processes. If using threads and workers together
# the concurrency of the application would be max `threads` * `workers`.
# Workers do not work on JRuby or Windows (both of which do not support
# processes).
#
# workers ENV.fetch("WEB_CONCURRENCY") { 2 }
# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
# before forking the application. This takes advantage of Copy On Write
# process behavior so workers use less memory.
#
# preload_app!
# Allow puma to be restarted by `rails restart` command.
plugin :tmp_restart
3- Integrate MeiliSearch to our Rails 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.
Add the following line to your Gemfile:
gem 'meilisearch-rails', '~> 0.5.0'
Create a file named meilisearch.rb
inside the config/initializers/
folder to setup your MEILISEARCH_HOST
and MEILISEARCH_API_KEY
:
To configure the MeiliSearch host, run the following command to get the MeiliSearch container ip:
docker inspect -f \
'{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \
$(docker container ls -aq --filter "name=odhin_meilisearch" | awk '{print $1;}')
Create the config/initializers/meilisearch.rb
file with the following code:
MeiliSearch::Rails.configuration = {
meilisearch_host: 'http://192.168.80.2:7700',
meilisearch_api_key: 'A53451A4E5A5C21D265944AB8654984016199CCA362F2CA25B3CCD4DF821993B',
}
The host must be configured with your MeiliSearch container IP. The API Key must be the same as in docker-compose.yml
Let's open the app/models/game.rb
file and add the following line inside the Class declaration:
include MeiliSearch
We also need to add a MeiliSearch block:
class Game < ApplicationRecord
include MeiliSearch::Rails
meilisearch force_utf8_encoding: true, primary_key: :id do
# all attributes will be sent to MeiliSearch if block is left empty
displayed_attributes ['id', 'cover', 'title', 'genre', 'platform']
searchable_attributes ['cover', 'title', 'genre', 'platform']
filterable_attributes ['genre', 'platform']
end
end
Click here to learn more about displayed and searchable attributes.
4- 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'
Add the following code to db/seeds.rb
file to populate our database with 1000 games:
# Loads the faker library
require 'faker'
# Deletes existing games, useful if you seed several times
Game.destroy_all
# Creates 1000 fake games
1000.times do
Game.create!(
cover: "assets/games/#{rand(1..26)}.jpeg",
title: Faker::Game.title,
genre: Faker::Game.genre,
platform: Faker::Game.platform
)
end
# Displays the following message in the console once the seeding is done
puts 'Games created'
To populate the database, we must run the command:
rails db:seed
5- Add React to the Rails app
Start by adding the React gem to your Gemfile:
gem 'react-rails'
Now run the installers:
bundle install
rails webpacker:install
rails webpacker:install:react
rails generate react:install
If you have any errors during the installation, check the react-rails gem common errors and their fixes here
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 Games, which will be added to app/javascript/components/
by default. Run the following command:
rails g react:component Games
We can now open your app/javascript/components/Games.js
file and replace with the following code. 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,
RefinementList,
Pagination,
ClearRefinements,
} from "react-instantsearch-dom";
import { instantMeiliSearch } from "@meilisearch/instant-meilisearch";
const searchClient = instantMeiliSearch(
"http://localhost:14883", // Your MeiliSearch host
"A53451A4E5A5C21D265944AB8654984016199CCA362F2CA25B3CCD4DF821993B" // Your MeiliSearch API key, if you have set one
);
const Games = () => (
<InstantSearch
indexName="Game" // Change your index name here
searchClient={searchClient}
>
<div className="left-panel">
<ClearRefinements />
<h2>Genre</h2>
<RefinementList attribute="genre" />
<h2>Platform</h2>
<RefinementList attribute="platform" />
</div>
<div className="right-panel">
<SearchBox />
<Hits hitComponent={Hit} />
<div className="pagination">
<Pagination />
</div>
</div>
</InstantSearch>
);
//const Hit = ({ hit }) => <Highlight attribute="title" hit={hit} />
const Hit = (hit) => {
const { cover, genre, platform } = hit.hit;
return (
<div className="hit media">
<div className="media-left">
<img className="media-object" src={`http://localhost:14881/${cover}`} />
</div>
<div className="media-body">
<h4 className="media-heading">
<Highlight attribute="title" hit={hit.hit} />
</h4>
<p className="genre">{genre}</p>
<p className="platform">{platform}</p>
</div>
</div>
);
};
export default Games;
Now, go to your views folder and replace the content of the app/views/games/index.html.erb
with the code below:
<%= react_component("Games") %>
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.
For this to work we need to add some simple styling in the app/assets/stylesheets/games.scss
file:
body {
font-family: sans-serif;
padding: 1em;
}
.ais-SearchBox {
margin: 1em 0;
}
.right-panel {
margin-left: 210px;
}
.left-panel {
float: left;
width: 200px;
}
.hit.media {
width: 100%;
}
.hit .media-object {
height: 200px;
width: auto;
float: right;
}
.hit .media-heading {
color: #167ac6;
font-weight: normal;
font-size: 18px;
}
.hit .media-left {
width: calc(20% - 20px);
max-width: 220px;
padding: 10px;
float: left;
}
.hit .media-body {
width: calc(80% - 20px);
padding: 10px;
float: left;
display: block;
}
Update Yarn packages with the command:
yarn install --check-files
I have compressed games cover images to use in this post. Download this file and unzip all images at folder app/assets/images/games/
.
Now starting the application with the following command:
rails server
Check that everything is working properly at http://localhost:14881
Yeah... Looks Fine!!
I hope you enjoy! 😉
Get the full code at Rails + React + MeiliSearch gitlab repository.
References:
Posted on April 7, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024