Ransackのススメ

Railsアプリで検索機能を実装するケースは非常に多いと思います。
簡単な検索であればwhereとLIKEを使って書けますし、やや複雑なものも[everywhere](https://github.com/amatsuda/everywhere)が便利ですが、ここではもっと複雑な条件の組み合わせを実装する時に便利な[ransack](https://github.com/ernie/ransack)を紹介します。

## 基本
searchメソッドで条件を指定し、resultメソッドで結果を返します。

```Ruby
Item.search(:name_cont => 'ほげ').result
```

resutはActiveRecord::Relationを返すので、SQLは普通のActiveRecord同様遅延評価されますし、さらにwhereを繋げたり、kaminariでページングしたりすることもできます。また、to_sqlで発行されるSQLを確認することもできます。

もう少し詳しく書くと、searchはModelまたはActiveRecord::RelationをレシーバにしてRansack:Searchを返し、Ransack:search#resultはActiveRecord::Relationを返します。

## searchの書き方
searchメソッドは一種のDSLになっています。
属性をpredicate（述語）で繋いだ文字列のシンボルをキーに、検索対象を値にして書いていきます。
前述の例だと「name属性に'ほげ'という文字列を含む（countain）」対象を検索します。以下のようなSQLが発行されます。

```Ruby
Item.search(:name_cont => 'ほげ').result.to_sql
# => "SELECT `items`.* FROM `items` WHERE `items`.`name` LIKE '%ほげ%')"
```

以下のようなpredicateがあります。

### eq
等しいものにマッチします。

```Ruby
Item.search(:name_eq => 'ほげ').result.to_sql
# => "SELECT `items`.* FROM `items` WHERE `items`.`name` = 'ほげ')"
```

単体だとwhereで十分なので意味がありませんが、複数の条件を動的に組み合わせる際に便利です。
not_eqで「等しくないもの」になります。

### lt
ある値より小さいものにマッチします。

```Ruby
Item.search(:price_lt => 1000).result.to_sql
# => "SELECT `items`.* FROM `items` WHERE `items`.`price` < 1000)"
```

lteqだと「以下」になります。

### gt
ある値より大きいものにマッチします。

```Ruby
Item.search(:price_gt => 1000).result.to_sql
# => "SELECT `items`.* FROM `items` WHERE `items`.`price` > 1000)"
```

gteqだと「以上」になります。

### in
SQLのinです。含まれるものにマッチします。

```Ruby
Item.search(:category_id_in => [5,10,15,20]).result.to_sql
# => "SELECT `items`.* FROM `items` WHERE `items`.`category_id` IN (5,10,15,20))"
```

not_inで「含まれないもの」になります。

### cont
前述したように「文字列が含まれるもの」にマッチします。
not_contで「文字列が含まれないもの」になります。

### start
「特定の文字列が先頭のもの」にマッチします。

```Ruby
Item.search(:name_start => 'ほげ').result.to_sql
# => "SELECT `items`.* FROM `items` WHERE `items`.`name` LIKE 'ほげ%')"
```

endで「特定の文字列が末尾のもの」になります。

predicateはこの他にもあるので、[wiki](https://github.com/ernie/ransack/wiki/Basic-Searching)を参考にするといいでしょう。

## 組み合わせ
複数の属性を組み合わせることができます。
その場合、andかorで繋ぎます。

```Ruby
Item.search(:name_and_description_cont => 'ほげ').result.to_sql
# => "SELECT `items`.* FROM `items` WHERE (((`items`.`name` LIKE '%ほげ%') AND (`items`.`description` LIKE '%ほげ%')))"
Item.search(:name_or_description_cont => 'ほげ').result.to_sql
# => "SELECT `items`.* FROM `items` WHERE (((`items`.`name` LIKE '%ほげ%') OR (`items`.`description` LIKE '%ほげ%')))"
```

## 複数条件
条件を組み合わせることができます。
末尾に_allを付けると全ての条件にマッチ、_anyだといずれかにマッチするものを検索します。

```Ruby
Item.search(:name_cont_all => ['ほげ', 'ひげ']).result.to_sql
# => "SELECT `items`.* FROM `items` WHERE (((`items`.`name` LIKE '%ほげ%') AND (`items`.`name` LIKE '%ひげ%')))"
Item.search(:name_cont_any => ['ほげ', 'ひげ']).result.to_sql
# => "SELECT `items`.* FROM `items` WHERE (((`items`.`name` LIKE '%ほげ%') OR (`items`.`name` LIKE '%ほげ%')))"
```

## 関連モデルの検索
関連先を条件に含めることができます。

```Ruby
Item.search(:comments_body_cont => 'ほげ').result.to_sql
# => "SELECT `items`.* FROM `items` LEFT OUTER JOIN `item_comments` ON `item_comments`.`item_id` = `items`.`id` WHERE `item_comments`.`body` LIKE '%ほげ%'"
```

## 注意点
存在しないカラムを指定すると例外を返さず無視されます。

```Ruby
Item.search(:unknownattribute_cont => 'ほげ').result.to_sql
# => "SELECT `items`.* FROM `items`"
```

動的に組み立てる際には便利ですが、実装せずにテストが通ってしまったりするのでテストコードにマッチ・アンマッチ双方のテストを書くなど工夫しましょう。