Rails

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