Posted at

railsで独自の検索機能をつける


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

    最大何件取得するかを指定。



実装

今回はeventgenreが多対多なので、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で生成したものを少し加工しました。


events_controller.rb

 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


index.json.jbuilder

json.array! @events