【Rails6】ちょっとだけややこしかったタグ検索機能の実装

# はじめに
商品に紐づけるタグの検索機能を作成しました。

その際に、商品との関連付けや公開/非公開への配慮など、考えることが多くて頭がこんがらがってしまったので、実装した内容を覚えているうちにまとめておこうと思います。

# 前提
ショップが商品を持っていいて、商品には複数のタグを登録できるという形になっています。

![2024-06-06タグ検索.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3762011/fd2b0d97-fedb-f5fe-28bb-103f52ae6257.png)

:::note warn
このER図に乗せるのはうっかり忘れていましたが、ショップ(Shop)は<b>退会機能</b>があり、商品(Item)は<b>公開/非公開</b>を指定する機能があります。
ここが、今回作成するタグ検索機能においても、少しだけややこしくなるポイントになってきます。
:::

ちなみに、タグのイメージとしてはこんな感じです。

![2024-06-06タグ検索2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3762011/e197ee79-2431-03a1-f042-553e7b7ae11a.png)

この「# 花束」などのリンクを押したりタグ名から検索した時に、タグに紐づいた商品の一覧を表示するための検索機能を作成していきます。

# 実装
実際のコントローラでは他にも商品検索などもまとめて記載していますが、タグ検索に関係している部分だけピックアップしています。

## 検索フォーム

```html:検索フォームの部分テンプレート
<%= form_with url: search_path, method: :get do |f| %>
  <div>
    <%= f.hidden_field :model, value: 'tag' %>
    <div>
      <%= f.text_field :content, placeholder: 'タグから探す' %>
    </div>
    <div>
    <%= button_tag type: 'submit', name: '' do %>
      <i class="fa-solid fa-magnifying-glass"></i>
    <% end %>
   </div>
  </div>
<% end %>
```

検索フォームではhidden_fieldから検索方法を指定しています。
こちらはタグ検索なのでvalue: 'tag'を指定。

## コントローラ

```ruby:コントローラ
def search
    active_shops = Shop.where(is_active: true)    
    @content = params[:content]
    @search = if @model.in?(['shop', 'address', 'tag'])
                { model: @model }
              else
                { model: 'item' }
              end
:
:
    elsif @search[:model] == 'tag'
          active_items = Item.where(is_active: true, shop_id: active_shops)
          tag_items = Tag.search_items(@content, active_items)
          @records = Kaminari.paginate_array(tag_items).page(params[:page])
    end
:
end
```

まず、私の想定しているショップには<b>退会機能</b>があるので、退会済みのショップが検索結果に反映されないようにis_activeカラムでショップが在籍しているかどうかを判定しています。
trueが在籍中のショップなので、is_activeがtrueのショップのみをピックアップします。

ちなみに、上の<b>@&#8203;search = if @model.in?</b>の部分では、検索後の画面でラジオボタンでチェックを入れておくための設定をしていますが、今回は本題と少しずれた部分になるので詳しい説明は省きます。

ラジオボタンのチェックについてはこちら↓↓

https://qiita.com/plant0322/items/ff19815402aa8e02bfcc#%E6%A4%9C%E7%B4%A2%E5%BE%8C%E3%81%AB%E3%83%A9%E3%82%B8%E3%82%AA%E3%83%9C%E3%82%BF%E3%83%B3%E3%81%AE%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF%E3%81%8C%E5%A4%96%E3%82%8C%E3%81%AA%E3%81%84%E3%82%88%E3%81%86%E3%81%AB%E3%81%99%E3%82%8B

<b>「elsif @&#8203;search[:model] == 'tag'」</b>の部分が検索のための記述です。
公開中の商品で尚且つ、在籍中のショップが関連付けられた商品(Item)を呼び出します。

<b>tag_items = Tag.search_items(@&#8203;content, active_items)のsearch_items</b>メソッドはtagモデルに記述しています。

## モデル

```ruby:app/models/tag.rb
  def self.search_items(content, active_items)
    tags = Tag.where('name LIKE?', '%'+content+'%')
    tags.inject([]) do |result, tag|
      result + tag.items.where(id: active_items.ids)
                        .order(updated_at: 'DESC')
    end.uniq
  end
```
ここでは、普通の検索と同じように、Tagテーブルから、名前にcontentが部分一致するタグを検索しLIKE演算子を使用して、タグ名の部分一致検索を行っています。

:::note info
<b>injectメソッドとは？</b>
injectはRubyのメソッドで、リスト（配列）の中のすべての数やアイテムを一つずつ順番に取り出して、計算や操作をして、最終的に一つの答えを作るための方法です。

まず、最初の値を設定し、リストの中のアイテムを一つずつ見て、計算や操作をします。
（たとえば、数字を1つずつ順番に足して合計値を出したいときなどに使われます。）
:::

https://qiita.com/wangqijiangjun/items/9e56a5313ae780ec7f11

今回の記述で言うと、検索で絞り込んだタグを1つずつ順番に取り出して、タグの紐づいた商品が在籍中のショップの商品で尚且つ公開中のものだけをピックアップしていく作業を繰り返している…ということになります。

細かく手順で並べてみると…、

* <b>inject([])</b>を使って、結果を格納する空の配列resultを初期化

* 各tagに関連する商品 (tag.items)の中から、active_itemsに含まれる商品のみをフィルタリング
（このactive_itemsは先ほどコントローラに記載したactive_items = Item.where(is_active: true, shop_id: active_shops)の部分ですね）

* フィルタリングされた商品をupdated_atを基準に<b>降順(DESC)</b>で並び替え

* 商品をresultに追加

* これをtagsの数だけ繰り返して、最後に、<b>uniq</b>メソッドで重複するアイテムを取り除く

という流れになっています。

私はinjectメソッドに馴染みがなさ過ぎて、何を言っているんだ…とここでの理解にすごく時間がかかりました…。

さあ、山場は越えました。
いよいよ、ビューで検索結果を呼び出してみましょう。
ここからはもう簡単です。

## 検索画面のビュー

```html:searches/search.html.erb
<% elsif @model == 'tag' %>
<!--タグ検索-->
    <h2>「<%= @content %>」<span>を含むタグのついた商品の検索結果</span><span>（全<%= @records.count %>件）</span></h2>
    <hr>
    <% if @records.blank? %>
        <p>
            該当する商品はありません<br>
            <%= link_to '◀ TOPへ戻る', root_path %>
        </p>
    <% else %>
        <%= render 'public/items/index', items: @records %>
    <% end %>
<% end %>
```
<%= render 'public/items/index', items: @&#8203;records %>の部分で検索結果として表示したい情報の一覧を部分テンプレートで渡しています。

こちらの検索結果一覧のビューは検索機能には直接関係がないので省いています。

## ちなみに…タグをリンクとして表示する方法

タグの検索結果をリンクとして表示したいなら例えば、

```ruby:コントローラ
@tags = Tag.all
```
```html:タグのリンク
<div>
  <% @tags.each do |tag| %>
    <div>
      <%= link_to "/search?model=tag&content=#{tag.name}" do %>
        # <%= tag.name %>
      <% end %>
    </div>
  <% end %>
</div>
```
こんな風に記述することで、全てのタグをリンクボタンとして呼び出すことができます。
コントローラでの呼び出し方を変えたら、1つの商品に紐づいたタグを呼び出して、商品詳細に表示したりすることも可能です。

タグでのリンクがあるだけで回遊率も上がりそうですし、何よりサイト感が増して達成感があります…！

# おわりに
文章にまとめたことでやっと理解出来たような気がします。

injectメソッドだけでなく、商品が公開してないといけないから…とか、退会したショップのデータが出てくるのはよくないな…とか、後で追加したりもしたので余計に頭が混乱してしまっていました。
まとめてみると単純なことだったんだなとホッとしました。

ただ、また新たに管理者側の公開承認設定なるものを追加したため、この設定も付け加えなければいけないのでは…と今気づいてまた頭を抱え始めています…。