Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Elasticsearch with elasticsearch-rails #2

Open
wants to merge 1 commit into
base: before-elastic
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ gem "rails", github: "rails/rails", branch: "main"

gem "bootsnap", require: false
gem "dotenv-rails"
gem "elasticsearch-model"
gem "elasticsearch-rails"
gem "importmap-rails"
gem "opengraph_parser"
gem "pg"
Expand Down
42 changes: 42 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,46 @@ GEM
dotenv-rails (2.8.1)
dotenv (= 2.8.1)
railties (>= 3.2)
elasticsearch (7.17.7)
elasticsearch-api (= 7.17.7)
elasticsearch-transport (= 7.17.7)
elasticsearch-api (7.17.7)
multi_json
elasticsearch-model (7.2.1)
activesupport (> 3)
elasticsearch (~> 7)
hashie
elasticsearch-rails (7.2.1)
elasticsearch-transport (7.17.7)
faraday (~> 1)
multi_json
erubi (1.12.0)
faraday (1.10.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
globalid (1.1.0)
activesupport (>= 5.0)
hashie (5.0.0)
i18n (1.12.0)
concurrent-ruby (~> 1.0)
importmap-rails (1.1.5)
Expand All @@ -134,6 +171,8 @@ GEM
mini_mime (1.1.2)
minitest (5.18.0)
msgpack (1.7.0)
multi_json (1.15.0)
multipart-post (2.3.0)
net-imap (0.3.4)
date
net-protocol
Expand Down Expand Up @@ -183,6 +222,7 @@ GEM
connection_pool
reline (0.3.3)
io-console (~> 0.5)
ruby2_keywords (0.0.5)
sidekiq (7.0.7)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0)
Expand Down Expand Up @@ -217,6 +257,8 @@ PLATFORMS
DEPENDENCIES
bootsnap
dotenv-rails
elasticsearch-model
elasticsearch-rails
importmap-rails
opengraph_parser
pg
Expand Down
5 changes: 5 additions & 0 deletions app/controllers/search_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class SearchController < ApplicationController
def index
@results = Searchable.search(params[:query])
end
end
25 changes: 25 additions & 0 deletions app/jobs/indexer_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class IndexerJob < ApplicationJob
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: The implementation of this Job was based on the implementation suggested in the elasticsearch-model gem: https://github.com/elastic/elasticsearch-rails/tree/main/elasticsearch-model#asynchronous-callbacks

I would not have written this with an operation argument and case statement if I did it from scratch.

def perform(operation, klass, id)
klass = klass.constantize

case operation
when "create"
model = klass.find_by_id(id)
return unless model

model.__elasticsearch__.index_document
when /update/
model = klass.find_by_id(id)
return unless model

model.__elasticsearch__.update_document
when /delete/
begin
klass.__elasticsearch__.client.delete(index: klass.index_name, id: id)
rescue Elasticsearch::Transport::Transport::Errors::NotFound # rubocop:disable Lint/SuppressedException
end
else
raise ArgumentError, "Unknown operation '#{operation}'"
end
end
end
70 changes: 70 additions & 0 deletions app/models/concerns/searchable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Description:
# Include this module in models that should be searchable.
#
# Models including the concern are expected to implement 'title' and 'searchable_content'
# for indexing. Title will be given higher priority compared to content.
#
# After adding and indexing your models. Search across all models can be performed via
# the Searchable.search method.

module Searchable
extend ActiveSupport::Concern

mattr_accessor :models
self.models = []

def self.search(query)
return [] if query.blank?

search_definition = {
query: {
multi_match: {
query: query,
fields: ["title^2", "content"],
},
},
}

Elasticsearch::Model.search(search_definition, models).records
end

def as_indexed_json(_options = {})
{
title: title,
content: searchable_content,
}
end

def should_index?
true
end

def searchable_content
raise NotImplementedError
end

included do
include Elasticsearch::Model

Searchable.models << self

after_commit on: :create, if: :should_index? do
IndexerJob.perform_later("create", self.class.name, id)
end

after_commit on: :update, if: :should_index? do
IndexerJob.perform_later("update", self.class.name, id)
end

after_commit on: :destroy do
IndexerJob.perform_later("delete", self.class.name, id)
end

settings index: { number_of_shards: 1 } do
mappings dynamic: "false" do
indexes :title, analyzer: "english", boost: 2
indexes :content, analyzer: "english"
end
end
end
end
10 changes: 10 additions & 0 deletions app/models/link.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class Link < ApplicationRecord
include Searchable

validates_presence_of :url
validate :validate_format_of_url
validates_inclusion_of :state, in: %w[pending success error]
Expand All @@ -8,6 +10,14 @@ class Link < ApplicationRecord

private

def should_index?
status == "success"
end

def searchable_content
description
end

def enqueue_crawl_job
CrawlLinkJob.perform_later(id)
end
Expand Down
8 changes: 8 additions & 0 deletions app/models/post.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
class Post < ApplicationRecord
include Searchable

validates_presence_of :title
validates_presence_of :body

private

def searchable_content
body
end
end
1 change: 1 addition & 0 deletions app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<nav style="font-size: 1.5rem;">
<%= link_to "Posts", posts_path %>
<%= link_to "Links", links_path %>
<%= link_to "Search", search_path %>
</nav>
<%= yield %>
</body>
Expand Down
16 changes: 16 additions & 0 deletions app/views/search/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<h1>Search</h1>

<%= form_with url: search_path, method: :get do |form| %>
<%= form.label :query %>
<%= form.text_field :query, value: params[:query], autofocus: true %>
<%= form.submit "Search" %>
<% end %>

<% if @results.present? %>
<h2>Results</h2>
<ul>
<% @results.each do |result| %>
<li><%= link_to result.title, result %></li>
<% end %>
</ul>
<% end %>
2 changes: 1 addition & 1 deletion config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
config.enable_reloading = true

# Do not eager load code on boot.
config.eager_load = false
config.eager_load = true # necessary for inclusion tracking of Searchable concern

# Show full error reports.
config.consider_all_requests_local = true
Expand Down
2 changes: 1 addition & 1 deletion config/environments/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# this is usually not necessary, and can slow down your test suite. However, it's
# recommended that you enable it in continuous integration systems to ensure eager
# loading is working properly before deploying your code.
config.eager_load = ENV["CI"].present?
config.eager_load = true # necessary for inclusion tracking of Searchable concern

# Configure public file server for tests with Cache-Control for performance.
config.public_file_server.enabled = true
Expand Down
8 changes: 8 additions & 0 deletions config/initializers/elasticsearch.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# https://github.com/elastic/elasticsearch-ruby/issues/1429#issuecomment-958162468
module Elasticsearch
class Client
def verify_with_version_or_header(*_args)
@verified = true
end
end
end
3 changes: 2 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
Rails.application.routes.draw do
resources :links
resources :posts
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

get "search" => "search#index", as: :search

# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
# Can be used by load balancers and uptime monitors to verify that the app is live.
Expand Down