Railsで簡易的なリコメンド機能を実装する

はじめに

こんにちは。今回はRailsで簡易的なリコメンド機能を実装したのでその内容を紹介しようと思います。

実行環境

この記事は以下の動作環境で動作確認しています。

  • ruby (2.6.4)
  • rails (5.2)

概要

ユーザー(User)に対して動画(Video)を勧める機能を作成します。

Youtubeのような動画サイトをイメージしてもらえると良いかもしれません。

なお、UsersテーブルとVideosテーブルは既に存在しているものとします。

全体のロジック

下記が全体のロジックになります。

  1. 自分と似たUserを取得(自分が見た動画を見たUser)
  2. 1で取得したUserが見たVideoのidと視聴回数を取得(video_id, watch_count)
  3. 2で取得したVideoを重み付け(current_userが一回見たVideoは視聴回数を0.1倍する)
  4. 2で取得したVideoのidを使ってVideoを取得
  5. ページネーション&レスポンスを返却
class Api::V1::Videos::RecommendsController < ApplicationController
  before_action :logged_in_user, only: %i(index)
  WEIGHTING_COEFFICIENT = 0.1.freeze

  def index
    video_info = get_video_ids_and_watch_counts_by(similar_users)

    weighted_video_info = weight(video_info)

    recommend_videos = find_videos_by(weighted_video_info)

    render_response(Kaminari.paginate_array(recommend_videos)
        .page(params[:page]).per(params[:limit]))
  end
end

それでは一つずつ紹介していきます。

準備. 中間テーブルを作成する

まずは、推薦に必要なデータ(視聴回数)を持っておくために中間テーブルを作成します。

class CreateVideoWatchCounts < ActiveRecord::Migration[5.2]
  def change
    create_table :video_watch_counts do |t|
      t.integer :user_id, null: false
      t.integer :video_id, null: false
      t.integer :watch_count, default: 1,  null: false

      t.timestamps
    end
    add_index :video_watch_counts, :user_id
    add_index :video_watch_counts, :video_id
    add_index :video_watch_counts, [:user_id, :video_id], unique: true
  end
end

自分と似たUserを取得する(自分が見たVideoを見たUserを取得する)

ここでは自分が見たVideoを見たUserを取得しています。

今回はこのUsersを自分と似たUserとして扱っていきます。

    def similar_users
      similar_users = []

      current_user.watch_videos.each do |video|
        video.watch_users.each do |user|
          similar_users.push(user)
        end
      end
      similar_users.uniq
    end

Userが見たVideoを取得する

ここで先ほど作成したsimilar_usersを引数として使用することで、自分と似たUserが見たVideoの情報を取得することができます。

ここでいう情報はvideoのIDと合計視聴回数を指しています。

    def get_video_ids_and_watch_counts_by(users)
      recommend_videos_info = {}

      users.each do |user|
        user.watch_videos.each do |video|
          if recommend_videos_info.key?(video.id)
            recommend_videos_info[video.id] += user.video_watch_counts.find_by(video_id: video.id).watch_count
          else
            recommend_videos_info.store(video.id, user.video_watch_counts.find_by(video_id: video.id).watch_count)
          end
        end
      end
      recommend_videos_info
    end

先ほど取得したVideoを重み付けする

先ほどの実装で自分が見たVideoからsimilar_usersを取得しているので、自然と自分の見たVideoが再度リコメンドされやすくなります。

そこで一度自分が見ているVideoに関しては重み付けをしています。

具体的には視聴回数を0.1倍しています。(最終的に視聴回数の降順でソートするため、そのような処理にしています。)

    def weight(videos)
      videos.each do |key, value|
        videos[key] = value * WEIGHTING_COEFFICIENT if current_user.watch_videos.ids.include?(key)
      end
      videos.sort_by{ |_, v| -v }.to_h
    end

4. Videoのidを使ってVideoを取得する

ここで推薦されるVideoのObjectを取得しています。

    def find_videos_by(video_info)
      videos = []

      video_info.each do |key, _|
        videos.push(Video.find(key))
      end
      videos
    end

おわりに

今回はUserにVideoを勧める機能を作成しました。

最適化などは行っておらず、User数が増えると簡単に破綻してしまう所が問題だと思いますし、リコメンド自体も単純な機能のみで意外性等考慮できていない部分が多くあると感じています。

ですが、ざっと推薦機能の全体を見渡すことができて良かったかなというお気持ちです。

最後まで読んでいただきありがとうございました。

スライドもありますので良ければ下記からどうぞ。

https://docs.google.com/presentation/d/1JvupV5CDoMBnH2YvmAbIga5pMs095aazklLVvTUUQ2A/edit?usp=sharing