Rails レコメンド機能実装（協調フィルタリング）

Railsでアプリを作成した際に、ユーザーベースの協調フィルタリングに基づく、独自アルゴリズムでレコメンド機能を実装しましたので、学習メモとして紹介させていただこうと思います。

簡単な処理の流れとしまして、　

「ユーザーがブックマークしたクイズAと、他ユーザーがブックマークしたクイズAが一致したとき、他ユーザーがブックマークしている他のクイズをレコメンドする」という流れになります

現在、初学者で学習中のため、内容に誤りがある場合がございます！
その際は、コメント等で教えていただけると幸いです！

## レコメンド機能
#### レコメンド
投稿されたまたは事前に用意されたコンテンツと類似したコンテンツを推奨する機能をレコメンドと呼びます。代表的なアルゴリズムとして、協調フィルタリングとコンテンツフィルタリングがあります。
#### 協調フィルタリング
協調フィルタリングは、多くのユーザの嗜好情報を蓄積し、あるユーザと嗜好の類似した他のユーザの情報を用いて自動的に推論を行う方法論です。趣味の似た人からの意見を参考にするという口コミの原理に例えられることが多いです。

#### コンテンツフィルタリング（内容ベース)
ユーザーではなく商品側に何かしらの特徴量を付与し、特徴が似ている商品を推薦するレコメンドアルゴリズムです。 対象ユーザーのデータさえあれば推薦を行うことができるので、コールドスタート問題を回避することが出来るという特徴があります。

## 協調フィルタリング
今回の実装は、ユーザーベースの協調フィルタリングに基づくように実装していきます
参考：

https://qiita.com/haminiku/items/f5008a57a870e0188f63

## 前提条件
今回は、Webアプリで作成した際の実装になっております

このようなテーブル構成となっており、必要なテーブルは赤枠になります
主となるテーブルは、Usersテーブル・Resultsテーブル・Bookmarksテーブル・Questionsテーブルです

![SCR-20231018-icll 2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3514733/6db929c3-59ec-9713-02d9-599280b3568b.png)

### モデル（必要コードのみ記載）
```
class User < ApplicationRecord
  authenticates_with_sorcery!
  has_many :results, dependent: :destroy
  has_many :questions, through: :results
  has_many :bookmarks, dependent: :destroy
  has_many :bookmarks_questions, through: :bookmarks, source: :question
  has_one_attached :avatar
end
```

```
class Result < ApplicationRecord
  belongs_to :question
  belongs_to :user
end
```

```
class Question < ApplicationRecord
  has_many :results, dependent: :destroy
  has_many :bookmarks, dependent: :destroy
  has_many :users, through: :bookmarks
end
```

```
class Bookmark < ApplicationRecord
  belongs_to :user
  belongs_to :question

  validates :user_id, uniqueness: { scope: :question_id }
end
```

## コード
実際にコードを見ていきます

```
before_action :set_recommend_questions, only: %i[index recommend_explanation]

def index; end

def recommend_explanation; end


private

# レコメンドクイズを設定します
    def set_recommend_questions   
      @recommend_questions = recommend_questions
    end

#類似ユーザーを探す
    def similar_users
    bookmarked_question_ids = current_user.bookmarks_questions.pluck(:question_id)
    similar_user_ids = Bookmark.where(question_id: bookmarked_question_ids)
                               .where.not(user_id: current_user.id)
                               .distinct.pluck(:user_id)
    User.where(id: similar_user_ids)
  end

#類似ユーザーのブックマークからクイズを提案する 
    def recommend_questions
    bookmarked_question_ids = current_user.bookmarks_questions.pluck(:question_id)
    similar_users = similar_users
    similar_user_question_ids = Bookmark.where(user_id: similar_users.ids)
                                        .where.not(question_id: bookmarked_question_ids)
                                        .distinct.pluck(:question_id)
    recommended_question_ids = Question.where(id: similar_user_question_ids)
                                        .where.not(id: bookmarked_question_ids)
                                        .where.not(id: Result.where(user_id: current_user)
                                                              .distinct.pluck(:question_id)).pluck(:id)
    Question.where(id: recommended_question_ids).sample(3)
```
## コードを紐解く
まず、類似しているユーザーを探します

```
  def find_similar_users
#current_userがブックマークしたクイズのquestion_idをbookmarked_question_idsに定義
    bookmarked_question_ids = current_user.bookmarks_questions.pluck(:question_id)

#ブックマークテーブルから、ブックマークしたquestion_idから、そのquestion_idを同様にブックマークしたユーザーを取得します（この時、自分のuser_idが入らないようにしています）
    similar_user_ids = Bookmark.where(question_id: bookmarked_question_ids)
                               .where.not(user_id: current_user.id)
                               .distinct.pluck(:user_id)

#類似しているユーザーを取得します
    User.where(id: similar_user_ids)
  end
```

次に、類似しているユーザーから、おすすめのクイズを探して提供します
```
#近しいユーザーのブックマークからクイズを提案する 
  def recommend_questions
    bookmarked_question_ids = current_user.bookmarks_questions.pluck(:question_id)

#find_similar_usersで取得したユーザーをsimilar_usersで定義します
    similar_users = find_similar_users

#定義したユーザーがブックマークしているクイズを取得します
    similar_user_question_ids = Bookmark.where(user_id: similar_users.ids)
                                        #ユーザーAがブックマークした以外のクイズを取得
                                        .where.not(question_id: bookmarked_question_ids)
                                        .distinct.pluck(:question_id)

#取得したクイズの中で、今までクイズに挑戦していないクイズのみ取得します
    recommended_question_ids = Question.where(id: similar_user_question_ids)
                                       .where.not(id: bookmarked_question_ids)
                                       .where.not(id: Result.where(user_id: current_user)
                                                          .distinct.pluck(:question_id))
                                       .pluck(:id)

#取得したクイズ3問を出力します
    Question.where(id: recommended_question_ids).sample(3)
```

## 最後に
今回のレコメンド機能は、高度なアルゴリズムではなく、簡単なアルゴリズムで書いています。
これから、高度なアルゴリズムでレコメンド機能を実装できるように頑張ろうと思います。
最後まで読んでいただき、ありがとうございます!