15
loading...
This website collects cookies to deliver better user experience
rails new rails-search-across-multiple-models
rails g scaffold Post title body:text
rails g scaffold User name biography:text
rails g model SearchEntry title body:text searchable:references{polymorphic}
rails db:migrate
# app/models/search_entry.rb
class SearchEntry < ApplicationRecord
delegated_type :searchable, types: %w[ Post User ]
end
What's Going On Here?
# app/models/concerns/searchable.rb
module Searchable
extend ActiveSupport::Concern
included do
has_one :search_entry, as: :searchable, touch: true
end
end
# app/models/post.rb
class Post < ApplicationRecord
include Searchable
end
# app/models/user.rb
class User < ApplicationRecord
include Searchable
end
What's Going On Here?
updated_at
column updated. This is because we're calling touch: true
. That part is not required, but helps keep things consistent between models. # app/models/search_entry.rb
class SearchEntry < ApplicationRecord
delegated_type :searchable, types: %w[ Post User ]
validates :searchable_type, uniqueness: { scope: :searchable_id }
end
What's Going On Here?
# app/models/post.rb
class Post < ApplicationRecord
include Searchable
after_commit :create_search_entry, on: :create
after_commit :update_search_entry, on: :update
after_commit :destroy_search_entry, on: :destroy
private
def create_search_entry
SearchEntry.create(title: self.title, body: self.body, searchable: self)
end
def update_search_entry
self.search_entry.update(title: self.title, body: self.body) if self.search_entry.present?
end
def destroy_search_entry
self.search_entry.destroy if self.search_entry.present?
end
end
# app/models/user.rb
class User < ApplicationRecord
include Searchable
after_commit :create_search_entry, on: :create
after_commit :update_search_entry, on: :update
after_commit :destroy_search_entry, on: :destroy
private
def create_search_entry
SearchEntry.create(title: self.name, body: self.biography, searchable: self)
end
def update_search_entry
self.search_entry.update(title: self.name, body: self.biography) if self.search_entry.present?
end
def destroy_search_entry
self.search_entry.destroy if self.search_entry.present?
end
end
What's Going On Here?
title
and body
columns on the SearchEntry to whatever values make most sense. This allows us to have full control over what will be able to be searched. Note that we can pass whatever we want into the title
and body
columns.rails g controller SearchEntries index
# config/routes.rb
Rails.application.routes.draw do
root to: 'search_entries#index'
get 'search_entries/index', as: 'search'
...
end
# app/controllers/search_entries_controller.rb
class SearchEntriesController < ApplicationController
def index
@search_entries = SearchEntry.where("title LIKE ? OR body LIKE ?", "%#{params[:query]}%", "%#{params[:query]}%") if params[:query]
end
end
What's Going On Here?
if params[:query]
conditional to prevent any results from being rendered until a user makes a search query. This is optional.# app/views/search_entries/index.html.erb
<%= form_with url: :search, method: :get do |form| %>
<%= form.label :query, "Search for:" %>
<%= form.text_field :query %>
<%= form.submit "Search" %>
<%= link_to "Reset", search_path %>
<% end %>
<%= render partial: "search_entries/search_entry", collection: @search_entries %>
# app/views/search_entries/_search_entry.html.erb
<%= link_to polymorphic_path search_entry.searchable do %>
<h2><%= highlight search_entry.title, params[:query] %></h2>
<span><strong><%= search_entry.searchable_type %></strong></span>
<p><%= highlight search_entry.body, params[:query] %></p>
<hr/>
<% end %>
What's Going On Here?
search_entries#index
. The form.text_field :query
field simply passes the correct parameter into the URL.