Integrate Rails, React and MeiliSearch using Docker

rodrigoodhin

Rodrigo Odhin

Posted on April 7, 2022

Integrate Rails, React and MeiliSearch using Docker

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: 
Enter fullscreen mode Exit fullscreen mode

You can generate and set a different master-key for your MeiliSearch or leave it empty. For security reasons, do not leave the master-key empty in production.

Run the command below to create and start the containers:

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Type app for the password.

Go to the projects folders with the command:

cd /projects
Enter fullscreen mode Exit fullscreen mode

I have decided to create a very simple collection app named games_collection. Run the following command on the terminal:

rails new games_collection
Enter fullscreen mode Exit fullscreen mode

Go to the games collection project folder:

cd games_collection
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Let's create the database and run the migration with the following commands:

rails db:create
Enter fullscreen mode Exit fullscreen mode
rails db:migrate
Enter fullscreen mode Exit fullscreen mode

Next, we need to generate the controller with its index action:

rails g controller Games index
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Add the following code to the config/puma.rb file, above the port information:

set_default_host '0.0.0.0'
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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;}')
Enter fullscreen mode Exit fullscreen mode

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',
} 
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

To populate the database, we must run the command:

rails db:seed
Enter fullscreen mode Exit fullscreen mode

5- Add React to the Rails app

Start by adding the React gem to your Gemfile:

gem 'react-rails'
Enter fullscreen mode Exit fullscreen mode

Now run the installers:

bundle install
rails webpacker:install
rails webpacker:install:react
rails generate react:install
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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") %>
Enter fullscreen mode Exit fullscreen mode

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">
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Update Yarn packages with the command:

yarn install --check-files
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Check that everything is working properly at http://localhost:14881

example

Yeah... Looks Fine!!

I hope you enjoy! 😉

Get the full code at Rails + React + MeiliSearch gitlab repository.

References:

💖 💪 🙅 🚩
rodrigoodhin
Rodrigo Odhin

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