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

# railsで検索機能をつけるには
便利なgemがあります。おそらく検索して上位に出てくるのは[ransak](https://github.com/activerecord-hackery/ransack)ではないでしょうか？しかし、railsで検索機能のついたapiを提供するにあたって、どうしても独自の実装が必要になることもあります。ここではrails wayにのった検索の実装の仕方について説明します。

[こちらのサイト](https://www.justinweiss.com/articles/search-and-filter-rails-models-without-bloating-your-controller/)を大変参考にさせていただきました。

# 完成形
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とかそこらへんです。

```ruby
 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
```



```ruby
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"
``` 

```ruby
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機能のみをもったmodel``SearchForm``も作成。

```ruby
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
```

```ruby
class Genre < ApplicationRecord
    has_many :event_genres, dependent: :destroy
    has_many :events, through: :event_genres
    validates :name, uniqueness: true
    
end
```

```ruby
class EventGenre < ApplicationRecord
  belongs_to :event
  belongs_to :genre
end
```

```ruby
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``で生成したものを少し加工しました。

```ruby: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 

```ruby:index.json.jbuilder
json.array! @events 
```