railsで検索機能をつけるには
便利なgemがあります。おそらく検索して上位に出てくるのはransakではないでしょうか?しかし、railsで検索機能のついたapiを提供するにあたって、どうしても独自の実装が必要になることもあります。ここではrails wayにのった検索の実装の仕方について説明します。
こちらのサイトを大変参考にさせていただきました。
完成形
urlで次のようにたたくと条件を満たすevent
が返ってくる.
http://example.com/event?capacity=10&location=0&genre[]=1人でもOK&genre[]=おしゃれなカフェ&order=popular&offset=3&limit=3
この場合、最大人数10人以上で。場所が梅田、ジャンルが「1人でもOK」もしくは「おしゃれなカフェ」、人気順に表示、最初から3番目のデータで、全部で3件取得という意味になる。
説明
-
capacity
イベントの最大人数を指定。例えばこの値が10なら最大人数が10人以上のものが返ってくる。 -
location
開催場所。0:梅田 1:なんば 2:神戸 -
genre
イベントの種類。例えば「女性多め」「年上男性多め」「おしゃれなカフェ」など。 -
order
返ってくる順番。popular: 見られた数が多い順 new:更新日が新しい順 -
offset
データベースで初めから何番目のデータかを指定。pagenationとかに使う。 -
limit
最大何件取得するかを指定。
実装
今回はevent
とgenre
が多対多なので、events
テーブルとgenres
テーブルとevent_geres
テーブルの中間テーブルを作る。
schema
rails g model
をした結果のschema.rb
配下の通り。アソシエーションの作り方はqiitaの他の記事を参照してください。referencesとかそこらへんです。
create_table "events", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
t.string "name" #イベント名
t.string "days" #イベントが実施される曜日
t.text "description" #イベント詳細
t.integer "capacity" #イベントに来れる最大人数
t.integer "location" #イベント開催場所 0:梅田 1:なんば 2:神戸
t.integer "views" #みられた数
t.datetime "created_at", null: false #作成日
t.datetime "updated_at", null: false #更新日
end
create_table "genres", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
t.string "name", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_foreign_key "event_genres", "events"
add_foreign_key "event_genres", "genres"
create_table "event_genres", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
t.bigint "event_id"
t.bigint "genre_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["event_id"], name: "index_event_genres_on_event_id"
t.index ["genre_id"], name: "index_event_genres_on_genre_id"
end
model
1番のポイント。
検索機能自体はモデルに直接書く。なおvalidation機能のみをもったmodelSearchForm
も作成。
class Event < ApplicationRecord
has_many :event_genres, dependent: :destroy
has_many :genres, through: :event_genres
scope :regexp_days, -> (pattern){ where("events.days REGEXP ?", pattern)}
def self.search(params)
# order is determined when order is new or popular
order = nil
puts params
if params[:order] == "new"
order = "events.updated_at ASC"
elsif params[:order] == "popular"
order = "events.views DESC"
end
events = Event.where(nil)
events = events.where('capacity > ?', params[:capacity]) if params[:capacity].present?
events = events.where(location: params[:location]) if params[:location].present?
if params[:days].present?
days = params[:days].split(//).join("|") # 1文字ずつばらばらにする "013" => "0|1|3"
events = events.regexp_days(days)
end
events = events.includes(:genres).where(genres: {name: params[:genre]}) if params[:genre].present?
events = events.offset(params[:min]) if params[:min].present?
events = events.limit(params[:max]) if params[:max].present?
events = events.order(order) if order.present?
events
end
end
class Genre < ApplicationRecord
has_many :event_genres, dependent: :destroy
has_many :events, through: :event_genres
validates :name, uniqueness: true
end
class EventGenre < ApplicationRecord
belongs_to :event
belongs_to :genre
end
class SearchForm
include ActiveModel::Model, ActiveModel::Serialization, ActiveModel::Validations
attr_accessor :days, :min, :max, :order, :genre, :location, :capacity
# 以下にvalidationを書いて行く。
validates :order, inclusion: { in: %w(new popular)}, allow_nil: true
# デフォルトの値
def attributes
{days: nil, frequency: nil, min: nil, max: nil, order: "new", genre: nil, fee: nil, location: nil }
end
end
controller
こちらは
rails g scaffold_controller envent
で生成したものを少し加工しました。
def index
search_word = SearchForm.new(event_params)
if search_word.valid?
@events = Event.search(search_word.serializable_hash)
puts "event is #{@events}"
@events
else
render json: search_word.errors, status: :unprocessable_entity
end
end
#以下略
private
# Never trust parameters from the scary internet, only allow the white list through.
def event_params
params.permit({genre: []}, :order, :min, :max, :capacity, :days, :location)
end
view
json.array! @events