書籍管理システムに搭載する予定であるレコメンド機能の実装方法について
解決したいこと(前置き)
現在、プログラミング経験2年ぐらいである私は書籍管理のWebサービスをつくっています。(Webサービスを作るのは今回が初めてです。)そしてそれにいたって後々レコメンド機能も搭載しようと考えました。なので今のうちにレコメンドについて少しchatgptなどを使って調べようと思っていたのですが、レコメンドの方法(コンテンツベースレコメンドや協調フィルタリングなど)ついてはありましたが、私の検索の仕方が悪いのか中々実際の実装方法などについて書いてあるサイトが見つかりませんでした。
解決したいこと(本題)
そこでレコメンド or Web製作の有識者にお聞きしたいのですが、書籍管理システムにおけるレコメンド機能についてどのような実装方法があるのでしょうか? (現時点でchatgptの力も借りて私なりに実装方法を考えてみたのですが、やはり限界があると考えました。)
また、「本のレコメンドに対するアプローチや実装環境などについて具体的に書いてあるサイト」や「レコメンドについて詳しく書いてあるサイト」などがあればぜひ教えていただきたいです。また私のアイデアに対してアドバイスを頂けると大変ありがたいです!!
注意点
今回初めてQiitaを書いたので、何か文章的に意味が伝わりずらかったらすみません。
環境
- フロントエンド:HTML,CSS,bootstrap
- バックエンド:python,Flask
ソースコード(ボタンの設定などが未完成)
フロントエンド
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>サンプルアプリ</title>
<!-- Bootstrap Icons CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
<!-- Bootstrap CSS -->
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
<style>
/* モーダルの表示速度を調整 */
.modal.fade {
transition: transform 0.5s ease-out, opacity 0.5s ease-out;
}
.modal.body{
overflow-y: auto; /* 必要ならモーダル内でスクロール可能に */
max-height: 80vh; /* モーダル内のコンテンツが全画面を超えないように設定 */
}
/* カードの斜線デザインとイラスト(検索) */
.a-card-striped{
position: relative;
}
.a-card-striped::before{
content: "";
position: absolute;
top: 0;
left: 0;
background-color: rgba(255,255,255,0.7);
z-index:0; /* 背景アイコンの上に配置するが、テキストより背面 */
border-top: 100px solid #4c6cb3;
border-right: 100px solid transparent;
}
.a-card-striped::after{
content: "";
position: absolute;
bottom: 0;
right: 0;
border-bottom: 100px solid #4c6cb3;
border-left: 100px solid transparent;
}
.a-icon{
background-image: url('../static/img/musi.png');
background-repeat: no-repeat;
background-position: center;
background-size: 200px 200px;
background-color: rgba(255, 255, 255, 0.7); /* 背景色を追加してテキストが見やすく */
}
.a-icon .card-body{
position:relative;
z-index: 1;/* テキストを前面に表示 */
}
.a-icon::before{
content: "";
position: absolute;
top: 0;
left: 0;
z-index:0; /* 背景アイコンの上に配置するが、テキストより背面 */
}
/*カードの斜線デザインとイラスト(ISBN) */
.b-card-striped{
position: relative;
}
.b-card-striped::before{
content: "";
position: absolute;
top: 0;
left: 0;
background-color: rgba(255,255,255,0.7);
z-index:0; /* 背景アイコンの上に配置するが、テキストより背面 */
border-top: 100px solid #00a381;
border-right: 100px solid transparent;
}
.b-card-striped::after{
content: "";
position: absolute;
bottom: 0;
right: 0;
border-bottom: 100px solid #00a381;
border-left: 100px solid transparent;
}
.b-icon{
background-image: url('../static/img/isbn.png');
background-repeat: no-repeat;
background-position: center;
background-size: 200px 200px;
background-color: rgba(255, 255, 255, 0.7); /* 背景色を追加してテキストが見やすく */
}
.b-icon .card-body{
position:relative;
z-index: 1;/* テキストを前面に表示 */
}
.b-icon::before{
content: "";
position: absolute;
top: 0;
left: 0;
z-index:0; /* 背景アイコンの上に配置するが、テキストより背面 */
}
/* カードの斜線デザインとイラスト(手動) */
.c-card-striped{
position: relative;
}
.c-card-striped::before{
content: "";
position: absolute;
top: 0;
left: 0;
background-color: rgba(255,255,255,0.7);
z-index:0; /* 背景アイコンの上に配置するが、テキストより背面 */
border-top: 100px solid #ff9500;
border-right: 100px solid transparent;
}
.c-card-striped::after{
content: "";
position: absolute;
bottom: 0;
right: 0;
border-bottom: 100px solid #ff9500;
border-left: 100px solid transparent;
}
.c-icon{
background-image: url('../static/img/hand.png');
background-repeat: no-repeat;
background-position: center;
background-size: 200px 200px;
background-color: rgba(255, 255, 255, 0.7); /* 背景色を追加してテキストが見やすく */
}
.c-icon .card-body{
position:relative;
z-index: 1;/* テキストを前面に表示 */
}
.c-icon::before{
content: "";
position: absolute;
top: 0;
left: 0;
z-index:0; /* 背景アイコンの上に配置するが、テキストより背面 */
}
.card{
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container mt-4">
<h1 class="mb-4">サンプル書店</h1>
<br>
<br>
<h2>書籍一覧</h2>
{% if books == [] %}
<p>書籍がありません</p>
{% else %}
<div class="row row-cols-1 row-cols-md-2 g-4">
{% for book in books %}
<div class="col">
<div class="card">
<!-- 画像を表示する場合は下のimgタグを使用 -->
<!-- <img src="path_to_image/{{ book['image'] }}" class="card-img-top" alt="..."> -->
<div class="card-body">
<h5 class="card-title">{{ book['title'] }}</h5>
<p class="card-text"><strong>入荷日:</strong> {{ book['arrival_date'] }}</p>
<p class="card-text"><strong>金額:</strong> {{ book['price'] }}円</p>
<div class="card-footer">
<small class="text-body-secondary"><a href="{{url_for('edit',id=book['id'])}}" class="btn btn-outline-secondary btn-sm"><i class="bi bi-pencil-square">編集(id:{{ book['id']}})</i></a></small>
<small class="text-body-secondary"><a href="{{url_for('view', id=book['id'])}}" class="btn btn-outline-primary btn-sm"><i class="bi bi-info-circle">詳細(id:{{ book['id']}})</i></a></small>
<form method="post" action="{{ url_for('delete', id=book['id']) }}" style="display: inline;">
<button type="submit" class="btn btn-outline-danger btn-sm"><i class="bi bi-trash3-fill">削除(id:{{ book['id']}})</i></button>
</form>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<button type="button" class="btn btn-secondary mt-4" data-toggle="modal" data-target="#bookRegisterModal">
登録フォーム
</button>
</div>
<!-- Modal -->
<div class="modal fade" id="bookRegisterModal" tabindex="-1" aria-labelledby="bookRegisterModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title w-100 text-center fw-bload" id="bookRegisterModalLabel"><div class="font-weight-bold">書籍の登録方法</div></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="position: absolute; right: 15px;">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="row">
<!-- 検索で登録 -->
<div class="col-md-4">
<div class="card a-icon a-card-striped border-primary mb-3"style="height: 600px;">
<div class="card-body text-center">
<h5 class="card-title">検索で登録</h5>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<button type="button" class="btn btn-primary w-50">検索</button>
</div>
</div>
</div>
<!-- ISBNで登録 -->
<div class="col-md-4">
<div class="card b-icon b-card-striped border-success mb-3"style="height: 600px;">
<div class="card-body text-center">
<h5 class="card-title">ISBNで登録</h5>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<button type="button" class="btn btn-success w-50">ISBN</button>
</div>
</div>
</div>
<!-- 手動で登録 -->
<div class="col-md-4">
<div class="card c-icon c-card-striped border-warning mb-3"style="height: 600px;">
<div class="card-body text-center">
<h5 class="card-title">手動で登録</h5>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<button type="button" class="btn btn-warning w-50">手動</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS, Popper.js, and jQuery -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.3/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>サンプルアプリ</title>
<!-- Bootstrap CSS -->
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<h1 class="mb-4">サンプル書店</h1>
<form method="post" action="{{ url_for('register') }}">
<div class="form-group">
<label for="title">タイトル</label>
<input type="text" class="form-control" id="title" name="title">
</div>
<div class="form-group">
<label for="arrival_date">入荷日</label>
<input type="text" class="form-control" id="arrival_date" name="arrival_date">
</div>
<div class="form-group">
<label for="price">金額</label>
<input type="text" class="form-control" id="price" name="price">
</div>
<button type="submit" class="btn btn-primary">登録</button>
<a href="{{url_for('index')}}" class="btn btn-secondary">戻る</a>
</form>
</div>
<!-- Bootstrap JS, Popper.js, and jQuery -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.3/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body>
</html>
バックエンド
from main import app
if __name__ == '__main__':
app.run(debug=True)
import sqlite3
DATA = "data.db"
def create_books_table():
co = sqlite3.connect(DATA)
co.execute("CREATE TABLE IF NOT EXISTS books (id integer primary key,title, price, arrival_date)")
co.close()
from flask import Flask
from flask import render_template,request,redirect,url_for
from db import *
import sqlite3
DATA = "data.db"
app = Flask(__name__)
create_books_table()
@app.route('/')
def index():
co = sqlite3.connect(DATA)
db_books = co.execute("SELECT * FROM books").fetchall()
co.close()
books = [
]
for row in db_books:
books.append({
'id': row[0],
'title': row[1],
'price': row[2],
'arrival_date': row[3]
})
return render_template(
'index.html',
books=books
)
@app.route('/form')
def form():
return render_template('form.html')
@app.route('/register', methods=['POST'])
def register():
id = None
title = request.form['title']
price = request.form['price']
arrival_date = request.form['arrival_date']
co = sqlite3.connect(DATA)
co.execute("INSERT INTO books VALUES (?, ?, ?, ?)", (id,title, price, arrival_date))
co.commit()
co.close()
return redirect(url_for('index'))
@app.route('/edit', methods=['GET'])
def edit():
book_id = request.args.get('id')
if not book_id:
return "IDが指定されていません。", 400
co = sqlite3.connect(DATA)
co.row_factory = sqlite3.Row # 行を辞書形式で返す設定
book = co.execute("SELECT * FROM books WHERE id = ?", (book_id,)).fetchone()
co.close()
if book is None:
return "指定されたIDに対応する本が見つかりませんでした。", 404
return render_template('edit.html', book=book)
@app.route('/update', methods=['POST'])
def update():
book_id = request.args.get('id')
title = request.form['title']
price = request.form['price']
arrival_date = request.form['arrival_date']
co = sqlite3.connect(DATA)
co.execute("UPDATE books SET title = ?, price = ?, arrival_date = ? WHERE id = ?", (title, price, arrival_date, book_id))
co.commit()
co.close()
return redirect(url_for('index'))
@app.route('/delete',methods=['POST'])
def delete():
book_id = request.args.get('id')
co = sqlite3.connect(DATA)
co.execute("DELETE FROM books WHERE id = ?", (book_id,))
co.commit()
co.close()
return redirect(url_for('index'))
@app.route('/view')
def view():
book_id = request.args.get('id')
if not book_id:
return "IDが指定されていません。", 400
co = sqlite3.connect(DATA)
co.row_factory = sqlite3.Row # 行を辞書形式で返す設定
book = co.execute("SELECT * FROM books WHERE id = ?", (book_id,)).fetchone()
co.close()
if book is None:
return "指定されたIDに対応する本が見つかりませんでした。", 404
return render_template('view.html', book=book)
私のアイデア(実装方法){chatgptを活用しているので、多少曖昧な部分があるかもしれないです}
読んだ本のデータベースとレコメンド用のデータベースからそれぞれ本のコンテンツに含まれる要約やあらすじを取ってきて、word2vecで各本のトークンからベクトルを平均化して、一つのベクトルとして表す。そしてcos類似度を使ってレコメンドする。(また補助的にLLM APIを搭載したいです。)
※以下はchatgptに作成させたイメージ図