View on GitHub

人気ベース推薦リストの作成

Home

人気ベース推薦リストの作成

バックエンド

モデル

backend/api/online/models.py

......
class ReclistPopularity(models.Model):
    """人気ベース推薦システムによる推薦リストモデル

    Attributes
    ----------
    id : IntegerField
        推薦リストID
    target_genre : ForeignKey[Genre]
        対象ジャンル
    rank : IntegerField
        推薦順位
    movie : ForeignKey[Movie]
        推薦映画
    score : FloatField
        推薦スコア
    """
    id = models.TextField(primary_key=True, max_length=5)
    target_genre = models.ForeignKey(Genre, on_delete=models.CASCADE)
    rank = models.IntegerField(blank=False, null=False)
    movie = models.ForeignKey(Movie, on_delete=models.CASCADE)
    score = models.FloatField()

    class Meta:
        managed = True
        db_table = 'reclist_popularity'

    def __str__(self):
        return '{}:{}:{}({})'.format(self.target_genre.id, self.rank, self.movie.id, self.score)

マイグレーション

(recsys_full) backend$ python manage.py makemigrations online --settings config.settings.development
Migrations for 'online':
  api/online/migrations/0004_reclistpopularity.py
    + Create model ReclistPopularity
(recsys_full) backend$ python manage.py migrate --settings config.settings.development

データの登録

offline/data$ psql recsys_full -U postgres -c "\copy reclist_popularity (id, target_genre_id, rank, movie_id, score) from 'reclist_popularity.csv' with delimiter E'\t' csv header encoding 'UTF8'"

データの確認

recsys_full=# SELECT * FROM reclist_popularity;

ビュー

backend/api/online/views.py

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import User, Movie, Rating
from .models import ReclistPopularity  # <- 追加
from .mappers import UserMapper, MovieMapper, RatingMapper
from .utils import hash
import uuid
from django.db.models import Prefetch
......
# ↓追加
class MoviesPopularityView(APIView):
    """人気ベース推薦システムによる映画リストビュークラス
    """
    def get(self, request, format=None):
        """対象ジャンルの人気ベース推薦リストを取得する。

        Requests
        --------
        target_genre_id : int
            対象ジャンルID
        user_id : str
            ユーザID

        Response
        --------
        movies : json
            映画リスト
        """
        # ユーザ認証
        user_id = request.GET.get('user_id') if 'user_id' in request.GET else None

        # リクエストパラメタの取得
        target_genre_id = request.GET.get('target_genre_id')

        # オブジェクトの取得
        reclist = []
        if user_id:
            reclist = ReclistPopularity.objects.filter(target_genre_id=target_genre_id)\
                .select_related('movie')\
                .prefetch_related('movie__genres')\
                .prefetch_related(
                    Prefetch(
                        'movie__movie_ratings',
                        queryset=Rating.objects.filter(user_id=user_id),
                        to_attr='user_ratings',
                    )
                )
        else:
            reclist = ReclistPopularity.objects.filter(target_genre_id=target_genre_id)\
                .select_related('movie')\
                .prefetch_related('movie__genres')

        # レスポンス
        movies = [rec.movie for rec in reclist]
        movies_dict = [MovieMapper(movie).as_dict(user_id) for movie in movies]
        data = {
            'movies': movies_dict,
        }
        return Response(data, status.HTTP_200_OK)   
# ↑追加

URLマッピング

backend/api/online/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('users/', views.UsersView.as_view()),
    path('movies/', views.MoviesView.as_view()),
    path('movies/<int:id>/', views.MovieView.as_view()),
    path('ratings/', views.RatingsView.as_view()),
    path('movies_popularity/', views.MoviesPopularityView.as_view()),  # <- 追加
]

ブラウザで下記それぞれのURLにアクセスしてください。

target_genre_idを変えてアクセスすると、ジャンル別に推薦リストが表示されます。

フロントエンド

API

frontend/src/services/movies/getMoviesPopularity.ts

import { ApiContext, Movie, User } from '@/types/data';
import { fetcher } from '@/utils';

const context: ApiContext = {
  apiRootUrl: process.env.NEXT_PUBLIC_API_BASE_URL,
};

/**
 * 人気ベース推薦システムによる映画リスト取得API
 * @param targetGenreId - 対象ジャンルID
 * @param user - ユーザ
 * @returns 映画リスト
 */
const getMoviesPopularity = async (
  targetGenreId: number,
  user?: User
): Promise<{ movies: Movie[] }> => {
  const userParam = user ? `&user_id=${user.id}` : '';
  return await fetcher(
    `${context.apiRootUrl?.replace(/\/$/g, '')}/movies_popularity/?target_genre_id=${targetGenreId}${userParam}`,
    {
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json',
      },
      cache: 'no-store',
    }
  );
};

export default getMoviesPopularity;

コンポーネント

frontend/src/app/components/movie/MovieListPopularity.tsx

import { User } from '@/types/data';
import React from 'react';
import getMoviesPopularity from '@/services/movies/getMoviesPopularity';
import MovieList from './MovieList';

const GENRES = [
  'Action',
  'Adventure',
  'Animation',
  'Children',
  'Comedy',
  'Crime',
  'Documentary',
  'Drama',
  'Fantasy',
  'Film-Noir',
  'Horror',
  'Musical',
  'Mystery',
  'Romance',
  'Sci-Fi',
  'Thriller',
  'War',
  'Western',
  'IMAX',
];

type Props = {
  targetGenreId: number;
  perPage: number;
  user: User;
};

const MovieListPopularity = async (props: Props) => {
  const phrase = GENRES[props.targetGenreId - 1] + 'で人気の映画';
  const { movies } = await getMoviesPopularity(props.targetGenreId, props.user);

  return (
    <>
      <MovieList phrase={phrase} movies={movies} perPage={props.perPage} user={props.user} />
    </>
  );
};

export default MovieListPopularity;

ページ

frontend/src/app/page.tsx

import { SessionProvider } from 'next-auth/react';
import React from 'react';
import HelloAccount from './components/HelloAccount';
import connectUser from '@/services/users/connectUser';
import MovieList from './components/movie/MovieList';
import getMovies from '@/services/movies/getMovies';
import { auth } from '@/auth';
import getUser from '@/services/users/getUser';
import MovieListPopularity from './components/movie/MovieListPopularity'; // <- 追加

const PER_PAGE = 5;
const N_GENRES = 19; // <- 追加
const N_MOVIE_LISTS_POPULARITY = 3; // <- 追加

const Index = async () => {
  await connectUser();
  const session = await auth();
  const user = session ? await getUser(session?.user?.email!) : null;
  const { movies } = await getMovies(user!);

  return (
    <>
      <section>
        <SessionProvider>
          <HelloAccount />
        </SessionProvider>
        <MovieList phrase="本日のおすすめ" movies={movies} perPage={PER_PAGE} user={user!} />
        {/* ↓追加 */}
        {(function () {
          // ジャンル配列をシャッフルする。
          const genres = [...Array(N_GENRES)].map((_, i) => i + 1);
          genres.sort((a, b) => 0.5 - Math.random());

          const movieListsByPopularityRecommender = [];
          for (let i = 0; i < N_MOVIE_LISTS_POPULARITY; i++) {
            movieListsByPopularityRecommender.push(
              <MovieListPopularity
                targetGenreId={genres[i]}
                perPage={PER_PAGE}
                user={user!}
                key={i}
              />
            );
          }
          return <div>{movieListsByPopularityRecommender}</div>;
        })()}
        {/* ↑追加 */}
      </section>
    </>
  );
};

export default Index;

ブラウザで下記URLにアクセスしてください。

ジャンル別推薦リストが3件提示されます。ブラウザを更新する度に、ジャンルがランダムに切り替わります。