Integrating Elasticsearch with Ruby on Rails

Wind turbine

Diving into the world of Elasticsearch in your Rails app? It’s exciting but can be a little intimidating at first. Here are 10 things you should do to integrate Elasticsearch smoothly with your Ruby on Rails app (don’t worry—we’ll keep it light and fun!).

Schema from Blog Post Project used as example:
CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(255) NOT NULL,
  email VARCHAR(255) NOT NULL UNIQUE,
  password_digest VARCHAR(255) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

CREATE TABLE posts (
  id INT AUTO_INCREMENT PRIMARY KEY,
  user_id INT NOT NULL,
  title VARCHAR(255) NOT NULL,
  content TEXT NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE TABLE tags (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(50) NOT NULL UNIQUE
);

CREATE TABLE post_tags (
  post_id INT NOT NULL,
  tag_id INT NOT NULL,
  PRIMARY KEY (post_id, tag_id),
  FOREIGN KEY (post_id) REFERENCES posts(id),
  FOREIGN KEY (tag_id) REFERENCES tags(id)
);

CREATE TABLE comments (
  id INT AUTO_INCREMENT PRIMARY KEY,
  post_id INT NOT NULL,
  user_id INT NOT NULL,
  content TEXT NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  FOREIGN KEY (post_id) REFERENCES posts(id),
  FOREIGN KEY (user_id) REFERENCES users(id)
);

1. Add Elasticsearch Gems

First things first, you need the right gems. Add elasticsearch and elasticsearch-model gems to your Gemfile. These two will be your best friends throughout the integration.

# Gemfile
gem 'elasticsearch'
gem 'elasticsearch-model'

Run bundle install to get things rolling.

2. Install Elasticsearch (Obviously)

You’ll need Elasticsearch running somewhere. Use Docker to simplify this. If Docker’s not your jam, just download it locally. With Docker:

docker run -d -p 9200:9200 elasticsearch:7.17.0

Optional: Adjust the version to whatever’s hip right now.

3. Model Integration

Now, you’ll need to integrate Elasticsearch with your models. Let’s use Post as an example:

class Post < ApplicationRecord
  include Elasticsearch::Model
  include Elasticsearch::Model::Callbacks

  # Elasticsearch indexing config, if you need custom mapping
  settings do
    mappings dynamic: false do
      indexes :title, analyzer: 'english'
      indexes :content, analyzer: 'english'
      indexes :tags, type: 'nested' do
        indexes :name, analyzer: 'english'
      end
    end
  end

  belongs_to :user
  has_many :comments, dependent: :destroy
  has_many :post_tags, dependent: :destroy
  has_many :tags, through: :post_tags

  def as_indexed_json(options = {})
    self.as_json(
      include: { tags: { only: :name } }
    )
  end
end

class Comment < ApplicationRecord
  belongs_to :post
  belongs_to :user
end

class Tag < ApplicationRecord
  has_many :post_tags, dependent: :destroy
  has_many :posts, through: :post_tags
end

class PostTag < ApplicationRecord
  belongs_to :post
  belongs_to :tag
end

class User < ApplicationRecord
  has_many :posts, dependent: :destroy
  has_many :comments, dependent: :destroy

  # Add bcrypt for password encryption
  has_secure_password
end

This hooks up your Post model to Elasticsearch—the Callbacks module makes sure the data stays in sync whenever a post is created, updated, or deleted.

4. Create the Index

After setting up your model, you need to create the corresponding index in Elasticsearch. It’s like setting up a database table but for searching purposes:

Post.__elasticsearch__.create_index! force: true
Post.import # This will index all your existing posts

Tip: Do this in a Rails console, but don’t do it every day—you’ll overwhelm Elasticsearch.

5. Don’t Forget to Configure Elasticsearch Host

Add your Elasticsearch server host to the config. You can keep it simple in config/initializers/elasticsearch.rb:

Elasticsearch::Model.client = Elasticsearch::Client.new host: 'http://localhost:9200'

Optional: Make this more dynamic by fetching the host from environment variables if deploying elsewhere.

6. Customize Search Method

Create a custom search method in your model to make things cleaner:

class Post < ApplicationRecord
  include Elasticsearch::Model
  include Elasticsearch::Model::Callbacks
  def self.search(query)
    __elasticsearch__.search(
      {
        query: {
          multi_match: {
            query: query,
            fields: ['title^5', 'content', 'tags.name']
          }
        }
      }
    )
  end
end

That’s it—searching should be painless now.

7. Handle Search Results in Controllers

Remember: In your controller, process the search results and render them appropriately:

class PostsController < ApplicationController
  def index
    if params[:query].present?
      @posts = Post.search(params[:query]).records
    else
      @posts = Post.all
    end
  end
end

8. Data Sync is a Must

Remember: Elasticsearch won’t magically stay in sync with your MySQL database unless you tell it to. elasticsearch-model helps, but if you’re bulk importing or manually changing data, always run Post.import to re-index.

9. Consider Pagination with Kaminari

Using Kaminari? It works smoothly with Elasticsearch results, which is pretty cool. Example:


# In your Gemfile
gem 'kaminari'

# Run bundle install
bundle install

# In your controller
class PostsController < ApplicationController
  def index
    if params[:query].present?
      @posts = Post.search(params[:query]).page(params[:page]).records
    else
      @posts = Post.page(params[:page])
    end
  end
end

Optional: You can also use will_paginate if that’s your preference.

10. Handle Failures Gracefully

Elasticsearch sometimes throws fits—network issues, server down, you name it. Wrap your queries in begin...rescue blocks:

begin
  @results = Post.search('Elasticsearch')
rescue Faraday::ConnectionFailed
  @results = []
  flash[:alert] = "Search is temporarily unavailable. Please try again later."
end

Optional: Set up monitoring to make sure Elasticsearch is up.

Wrapping Up

And there you have it! These tips should make Elasticsearch feel less like a dark art and more like a powerful ally in your Rails journey. Take it step by step, and remember—it’s all about making your app’s search smooth and efficient.

Happy coding, and may your searches be fast and relevant!