映画類似度ベース推薦システム
バックエンド
モデル
src/backend/api/online/models.py
...(略)...
# ↓追加
class ReclistMovieSimilarity(models.Model):
"""
映画類似度ベース推薦システムによる推薦リストモデル
Attributes
----------
id : CharField
推薦リストID
base_movie : ForeignKey[Movie]
ベース映画
rank : IntegerField
推薦順位
movie : ForeignKey[Movie]
推薦映画
score : FloatField
推薦スコア
"""
id = models.CharField(primary_key=True, max_length=9)
base_movie = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name='base_movie_movies')
rank = models.IntegerField(blank=False, null=False)
movie = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name='movie_base_movies')
score = models.FloatField()
class Meta:
managed = True
db_table = 'r_reclist_movie_similarity'
def __str__(self):
return f'ReclistMovieSimilarity {self.id}: BaseMovie {self.base_movie.title} - RecMovie {self.rank}: {self.movie.title}'
# ↑追加
マイグレーション
(recsys_full) backend$ python manage.py makemigrations online
Migrations for 'online':
api/online/migrations/0005_reclistmoviesimilarity.py
+ Create model ReclistMovieSimilarity
(recsys_full) backend$ python manage.py migrate
Operations to perform:
Apply all migrations: accounts, admin, auth, contenttypes, online, sessions, token_blacklist
Running migrations:
Applying online.0005_reclistmoviesimilarity... OK
データの登録
offline$
psql recsys_full -U postgres -c "\copy r_reclist_movie_similarity (id, base_movie_id, rank, movie_id, score) from 'data/reclist_movie_similarity.csv' with delimiter E'\t' csv header encoding 'UTF8'"
データの確認
recsys_full=#
SELECT * FROM r_reclist_movie_similarity;
ビュー
src/backend/api/online/views.py
...(略)...
from .models import Movie, UserMovieRating
from .models import ReclistPopularity, ReclistMovieSimilarity # <- ReclistMovieSimilarityを追加
from .mappers import MovieMapper, RatingMapper
...(略)...
# ↓追加
class MoviesMovieSimilarityView(APIView):
"""映画類似度ベース推薦システムによる映画リストビュー"""
permission_classes = (AllowAny,)
def get(self, request, format=None):
"""
ベース映画に対する映画類似度ベース推薦リストを取得する。
Requests
--------
user : User
ユーザ
base_movie_id : int
ベース映画ID
Responses
---------
movies : json
推薦映画リスト
"""
# ユーザ認証
user = _get_user(request)
# リクエストパラメタの取得
base_movie_id = request.GET.get('base_movie_id')
# オブジェクトの取得
movies = ReclistMovieSimilarity.objects\
.select_related('movie')\
.prefetch_related('movie__movie_genres__genre')\
.filter(base_movie_id=base_movie_id)
rating_map = {}
if user:
rating_map = {
rating.movie_id: rating for rating in UserMovieRating.objects.filter(user=user)
}
# レスポンス
movies_dict = [
MovieMapper(
reclist.movie,
rating=rating_map.get(reclist.movie_id)
).as_dict() for reclist in movies
]
data = {
'movies': movies_dict,
}
return Response(data, status.HTTP_200_OK)
# ↑追加
URL マッピング
src/backend/api/online/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('movies/', views.MoviesView.as_view()),
path('movies/<int:movie_id>/', views.MovieView.as_view()),
path('movies/popularity/', views.MoviesPopularityView.as_view()),
path('movies/movie_similarity/', views.MoviesMovieSimilarityView.as_view()), # <- 追加
path('ratings/', views.RatingView.as_view()),
]
実行確認
ブラウザで下記 URL にアクセスしてください。
base_movie_idを変えてアクセスすると、ベース映画ごとに類似映画が推薦リストとして表示されます。
サインインビューの作成の手順にしたがってサインインし、アクセストークンを取得してください。取得したトークンを用いて、バックエンドサーバが起動している状態で、下記コマンドを実行してください。
$
curl -X GET http://localhost:8000/api/online/movies/movie_similarity/?base_movie_id=1 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <ACCESS_TOKEN>"
ユーザによる評価値が付与された映画リストが取得できます。
フロントエンド
定数の定義
src/frontend/src/constants/styles.ts
/**
* スタイル関連の定数
*/
export const STYLES = {
...(略)...
// 人気ベース映画推薦リスト関連
LIST_MOVIE_POPULARITY_PHRASE: '人気の映画',
LIST_MOVIE_POPULARITY_COUNT: 3,
// ↓追加
// 映画類似度ベース映画推薦リスト関連
LIST_MOVIE_MOVIE_SIMILARITY_PHRASE: 'この映画が好きな人はこんな映画も好んでいます',
// ↑追加
...(略)...
} as const;
API
src/frontend/src/api/movies/getMoviesMovieSimilarity.ts
import { ApiContext, Movie } from "@/types/data";
import { fetcher } from "@/utils";
const context: ApiContext = {
apiRootUrl: process.env.NEXT_PUBLIC_API_BASE_URL,
};
/**
* 映画類似度ベース推薦システムによる推薦映画リスト取得API
* @param baseMovieId - ベース映画ID
* @returns movies - 推薦映画リスト
*/
const getMoviesMovieSimilarity = async (
baseMovieId: number,
): Promise<{ movies: Movie[] }> => {
const access =
typeof window !== "undefined" ? localStorage.getItem("access") : null;
const url = `${context.apiRootUrl?.replace(/\/$/g, "")}/online/movies/movie_similarity/?base_movie_id=${baseMovieId}`;
const headers: Record<string, string> = {
Accept: "application/json",
"Content-Type": "application/json",
...(access ? { Authorization: `Bearer ${access}` } : {}),
};
return await fetcher(url, {
method: "GET",
headers: headers,
cache: "no-store",
});
};
export default getMoviesMovieSimilarity;
コンポーネント
src/frontend/src/app/components/ui/list/ListMovieMovieSimilarity.tsx
"use client";
import { useEffect, useState } from "react";
import { Movie, User } from "@/types/data";
import { STYLES, ERROR_MESSAGES } from "@/constants";
import getMoviesMovieSimilarity from "@/api/movies/getMoviesMovieSimilarity";
import getMyAccount from "@/api/auth/getMyAccount";
import ListMovie from "./ListMovie";
import Loading from "../Loading";
interface Props {
baseMovieId: number;
}
/**
* 映画類似度ベース推薦システムによる推薦映画リストコンポーネント
*/
const ListMovieMovieSimilarity = (props: Props) => {
const [loading, setLoading] = useState(true);
const [loadingUser, setLoadingUser] = useState(true);
const [user, setUser] = useState<User | null>(null);
const [movies, setMovies] = useState<Movie[]>([]);
const phrase = STYLES.LIST_MOVIE_MOVIE_SIMILARITY_PHRASE;
useEffect(() => {
const load = async () => {
try {
const user_ = await getMyAccount();
setUser(user_);
} catch (e) {
setUser(null);
} finally {
setLoadingUser(false);
}
try {
const { movies: movies_ } = await getMoviesMovieSimilarity(
props.baseMovieId,
);
setMovies(movies_);
} catch (e) {
console.error(ERROR_MESSAGES.MOVIE_GET_FAILED, e);
setMovies([]);
} finally {
setLoading(false);
}
};
load();
}, [props.baseMovieId]);
if (loading) {
return (
<div className={`${STYLES.LOADING_INLINE}`}>
<Loading />
</div>
);
}
return (
<>
<ListMovie
phrase={phrase}
user={loadingUser ? null : user}
movies={movies}
/>
</>
);
};
export default ListMovieMovieSimilarity;
src/frontend/src/app/components/ui/card/CardMovieDetail.tsx
...(略)...
import Loading from '../Loading';
import StarRating from '../rating/StarRating';
import ListMovieMovieSimilarity from '../list/ListMovieMovieSimilarity'; // <- 追加
...(略)...
/**
* 映画(詳細)カードコンポーネント
*/
const CardMovieDetail = (props: Props) => {
...(略)...
return (
<div className={`${STYLES.MOVIE_DETAIL_PAGE}`}>
<article className={`${STYLES.MOVIE_DETAIL_CARD}`} key={movie.movie_id}>
...(略)...
<ListMovieMovieSimilarity baseMovieId={movie.movie_id} /> {/* <- 追加 */}
</article>
</div>
);
};
export default CardMovieDetail;
実行確認
ブラウザで下記 URL にアクセスしてください。
対象映画の類似映画が推薦リストとして提示されます。