niwatori-rookie
@niwatori-rookie

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

書籍管理システムに搭載する予定であるレコメンド機能の実装方法について

解決したいこと(前置き)

現在、プログラミング経験2年ぐらいである私は書籍管理のWebサービスをつくっています。(Webサービスを作るのは今回が初めてです。)そしてそれにいたって後々レコメンド機能も搭載しようと考えました。なので今のうちにレコメンドについて少しchatgptなどを使って調べようと思っていたのですが、レコメンドの方法(コンテンツベースレコメンドや協調フィルタリングなど)ついてはありましたが、私の検索の仕方が悪いのか中々実際の実装方法などについて書いてあるサイトが見つかりませんでした。

解決したいこと(本題)

そこでレコメンド or Web製作の有識者にお聞きしたいのですが、書籍管理システムにおけるレコメンド機能についてどのような実装方法があるのでしょうか? (現時点でchatgptの力も借りて私なりに実装方法を考えてみたのですが、やはり限界があると考えました。)

また、「本のレコメンドに対するアプローチや実装環境などについて具体的に書いてあるサイト」や「レコメンドについて詳しく書いてあるサイト」などがあればぜひ教えていただきたいです。また私のアイデアに対してアドバイスを頂けると大変ありがたいです!!

注意点
今回初めてQiitaを書いたので、何か文章的に意味が伝わりずらかったらすみません。

環境

  • フロントエンド:HTML,CSS,bootstrap
  • バックエンド:python,Flask

ソースコード(ボタンの設定などが未完成)

フロントエンド

index.html
<!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">&times;</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>




form.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>

バックエンド

__init__.py
from main import app
if __name__ == '__main__':
    app.run(debug=True)
db.py
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()

main.py
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に作成させたイメージ図
image.png

0

書籍管理システムに搭載する予定であるレコメンド機能の実装方法について

「書籍管理」と「レコメンド機能」の立ち位置が分からないのですが、質問者の言う「書籍管理」とはどういうものでしょうか?書籍管理システムのターゲットユーザーはどんな人でどのような使われ方を想定していますか?

私の検索の仕方が悪いのか中々実際の実装方法などについて書いてあるサイトが見つかりませんでした。

プログラミング言語を指定して生成AIに質問したりネット検索すれば情報が見つかるかと思います。

1Like

@megchandesu さん、返信ありがとうございます。

書籍管理について

大学の図書館を利用している人や自分もそうなのですが、図書館で読んだ本に関するメモや分からない語句などを個人的に記録はできません。ですので今のところ「書籍管理」は、本に関するメモや分からない語句などを本と一緒に残しておきたい人をターゲットとして、非共有の上で簡単に本および本の関連内容の記録ができることを想定しています。「レコメンド機能」については、この頃興味を持てる本と出合うというのは中々難しいと友人から聞いていたことやリリースされている本管理アプリの中で「レコメンド機能」があるアプリがkindleなどのごく少数だったことから、導入を考えました。

調べ方について

調べ方については今までプログラミング言語を指定せずに調べていたので、次からは指定した上で調べてみます。

0Like

大学の図書館を利用している人や自分もそうなのですが、図書館で読んだ本に関するメモや分からない語句などを個人的に記録はできません。ですので今のところ「書籍管理」は、本に関するメモや分からない語句などを本と一緒に残しておきたい人をターゲットとして、非共有の上で簡単に本および本の関連内容の記録ができることを想定しています。

ターゲットは個人の読書家で機能は読んだ本(+読んでいる本)の情報の管理ってところでしょうか。
そうなると「レコメンド機能」というのは何の中からのレコメンドなのでしょうか?ネットショッピングサイトであればそのサイトで買える商品を通常はレコメンドしますよね。質問者さんの場合は図書館の蔵書ということでしょうか?蔵書のデータベースは入手可能ですか?

0Like

Your answer might help someone💌