人気ベース推薦リストの作成
バックエンド
モデル
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にアクセスしてください。
- http://localhost:8000/api/online/movies_popularity/?target_genre_id=1
- http://localhost:8000/api/online/movies_popularity/?target_genre_id=1&user_id=【ユーザID】
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件提示されます。ブラウザを更新する度に、ジャンルがランダムに切り替わります。