Closed13

100日チャレンジ day24 (簡易RAGシステム)

riddle_tecriddle_tec

昨日
https://zenn.dev/gin_nazo/scraps/c38d986ba9c0eb


https://blog.framinal.life/entry/2025/04/14/154104

100日チャレンジに感化されたので、アレンジして自分でもやってみます。

やりたいこと

  • 世の中のさまざまなドメインの簡易実装をつくり、バックエンドの実装に慣れる(dbスキーマ設計や、関数の分割、使いやすいインターフェイスの切り方に慣れる
  • 設計力(これはシステムのオーバービューを先に自分で作ってaiに依頼できるようにする
  • 生成aiをつかったバイブコーティングになれる
  • 実際にやったことはzennのスクラップにまとめ、成果はzennのブログにまとめる(アプリ自体の公開は必須ではないかコードはgithubにおく)

できたもの

https://github.com/lirlia/100day_challenge_backend/tree/main/day24_simple_rag

riddle_tecriddle_tec

アプリケーション仕様:

  1. 目的: 特定のWikipediaページ(名探偵コナン関連)を知識ソースとして、ユーザーの質問に関連する情報を検索し、その情報に基づいて簡易的な応答を生成するRAG (Retrieval-Augmented Generation) システムを構築する。
  2. 知識ソース:
  3. 主要機能:
    • RAG質問インターフェース: ユーザーが自然言語で質問を入力できるUIを提供する。
    • 関連ドキュメント検索: 入力された質問からキーワードを抽出し、SQLiteデータベース内の Document テーブルの content フィールドを検索する。
      • 検索ロジック: 質問文をスペースで分割し、各単語で contentcontains 検索 (OR 条件)。最も多くのキーワードにマッチしたドキュメントを最優先とする (簡易実装)。
    • 応答生成 (シミュレーション): 検索で見つかったドキュメントの情報を使って、固定テンプレートに基づいた応答文を生成する。実際のLLMは使用しない。
      • 応答テンプレート例 (見つかった場合): 「${ドキュメントタイトル}」の情報によると、あなたの質問「${質問文}」に関連すると思われる記述は以下の通りです。「${ドキュメント本文の冒頭200文字}...」
      • 応答テンプレート例 (見つからない場合): 申し訳ありませんが、「${質問文}」に関連する明確な情報は知識ベースの中に見つかりませんでした。
  4. データモデル:
    model Document {
      id        Int      @id @default(autoincrement())
      title     String   // 例: "名探偵コナン (Wikipedia)"
      content   String   @db.Text // Wikipediaページのテキスト本文
      sourceUrl String   // 例: "https://ja.wikipedia.org/wiki/名探偵コナン"
      createdAt DateTime @default(now())
      updatedAt DateTime @updatedAt
    }
    
  5. APIエンドポイント:
    • POST /api/rag
      • リクエスト: { "query": "ユーザーの質問文字列" }
      • レスポンス: { "response": "生成された応答文字列", "sources": [{ "id": number, "title": string, "url": string }] } (応答の根拠となったドキュメント情報のリスト)
  6. UI:
    • 質問入力用のテキストエリア
    • 実行ボタン
    • 生成された応答と、根拠となったドキュメントへのリンク(タイトルとWikipedia URL)を表示するエリア
riddle_tecriddle_tec

はい、承知いたしました。day24「簡易RAGシステム」の実装に向けた作業計画は以下の通りです。

  1. プロジェクト設定:
    • 作業ディレクトリ day24_simple_rag に移動します。
    • day24_simple_rag/package.jsonname フィールドを day24-simple-rag に変更します。
    • day24_simple_rag/package.jsonscriptsdb:seed を追加します (prisma db seed)。 seed スクリプトは TypeScript で記述するため、実行用に tsconfig.seed.json も作成し、scripts.db:seedprisma db seed -- --tsconfig tsconfig.seed.json に修正します。
  2. データベース設定:
    • day24_simple_rag/prisma/schema.prismaDocument モデル (id, title, content, sourceUrl, createdAt, updatedAt) を定義します。
    • npx prisma migrate deploy --schema=./day24_simple_rag/prisma/schema.prisma を実行してデータベーススキーマを適用します。
  3. 初期データ準備 (Seed):
    • day24_simple_rag/prisma/seed.ts を作成します。
    • seed.ts 内に、指定された2つのWikipediaページのコンテンツを取得し(※取得処理は簡易的に、直接テキストを埋め込むか、別途取得したテキストファイルを読み込む形とします。Webスクレイピングは実装しません)、Document テーブルに登録する処理を実装します。
    • npx prisma db seed -- --tsconfig day24_simple_rag/tsconfig.seed.json を実行して初期データを投入します。
  4. API 実装:
    • day24_simple_rag/app/api/rag/route.ts を作成します。
    • POST リクエストハンドラを実装します。
      • リクエストボディから query を受け取ります。
      • query を単語に分割します。
      • Prisma Client を使用して、Document テーブルの content を検索します (分割した単語で contains OR 検索)。
      • 検索結果から関連性の高いドキュメントを選択します (今回は最初に見つかった1件とします)。
      • 選択されたドキュメント情報(タイトル、本文冒頭、URL)と質問文を使って、テンプレートに基づいた応答文字列 (response) を生成します。
      • 応答文字列と、根拠となったドキュメントの情報 (sources) をJSON形式で返します。
  5. UI 実装:
    • day24_simple_rag/app/page.tsx を作成 (または編集) します。
    • useState を用いて、質問入力 (query)、ローディング状態 (isLoading)、APIからの応答 (response)、参照元 (sources) を管理します。
    • 質問入力用の <textarea> と送信ボタンを配置します。
    • ボタンクリック時に /api/rag へ POST リクエストを送信する関数を実装します (fetch を使用)。
    • ローディング状態に応じて表示を切り替えます。
    • APIからの応答 (responsesources) を画面に表示します。sources はリンク付きで表示します。
    • Tailwind CSS で基本的なスタイリングを適用します。
  6. 動作確認:
    • npm run dev で開発サーバーを起動します。
    • ブラウザで http://localhost:3001 にアクセスします。
    • 名探偵コナンに関する様々な質問を入力し、期待される応答(関連情報を含む応答 or 情報が見つからない旨のメッセージ)が表示されるか確認します。
  7. ドキュメント整備:
    • day24_simple_rag/README.md の内容を最終確認し、必要に応じて追記・修正します。
    • .cursor/rules/knowledge.md に Day 24 のエントリを追加します。

この計画に沿って、まずはプロジェクト設定から進めていきます。

riddle_tecriddle_tec

んーーー、vertor db 使って欲しい。そうじゃないとただ単語ぶんかつしてるだけでは?


ご指摘ありがとうございます。確かに、単なるキーワード検索では RAG の「賢い検索」部分が十分に表現できませんね。ベクトル検索の導入は良いアイデアです。

ただし、本格的なベクトルデータベース (Pinecone, Weaviate, Qdrant 等) を導入するのは、今回の1日チャレンジの範囲と制約 (SQLite利用) を考えると難しいです。セットアップや外部サービス依存が発生してしまいます。

そこで、より RAG のコンセプトに近づけるための代替案として、以下を提案します。

提案: transformers.js を利用したインメモリでの簡易ベクトル検索

  1. ライブラリ導入:
    • @xenova/transformers パッケージを導入します。これは、JavaScript (Node.js やブラウザ) で Hugging Face の Transformer モデル(テキストのベクトル化/Embedding を含む)を利用可能にするライブラリです。
  2. ベクトル化 (Embedding) 処理:
    • 知識ソースのベクトル化: アプリケーション起動時、または最初の API リクエスト時に、SQLite から全ての Document (id, content) を読み込みます。
    • transformers.js の Embedding モデル (例: Xenova/multilingual-e5-small) を使用して、各ドキュメントの content をベクトル(数値の配列)に変換します。
    • これらのドキュメントIDとベクトルのペアをメモリ上にキャッシュします。(DBにはベクトル自体は保存しません)
  3. 質問のベクトル化と類似度検索:
    • ユーザーからの質問 (query) も、同じ Embedding モデルを使ってベクトル化します。
    • メモリ上にキャッシュされたドキュメントベクトル群と、質問ベクトルのコサイン類似度を計算します。
    • 最も類似度が高いドキュメントを検索結果として特定します。
  4. 応答生成:
    • 特定されたドキュメントの情報を使って、従来通りテンプレートベースで応答を生成します。

このアプローチのメリット:

  • セマンティック検索: 単なるキーワードマッチではなく、意味的に類似したドキュメントを検索できます。RAG の「賢い検索」に近づきます。
  • SQLite 利用の維持: ベクトルデータ自体はメモリ上に持つため、SQLite のスキーマは変更不要(Document モデルはそのまま)で、Prisma でのメタデータ管理も継続できます。
  • ローカル完結: 外部のベクトルDBサービスは不要です。

注意点・デメリット:

  • メモリ消費: 全ドキュメントのベクトルをメモリ上に保持するため、ドキュメント数や内容によってはメモリを消費します。(今回の Wikipedia 2ページ程度なら問題ない範囲と考えられます)
  • 初回処理時間: 初めて Embedding モデルを使う際や、アプリケーション起動時に全ドキュメントをベクトル化する際に、モデルのダウンロードと計算処理が発生し、時間がかかる可能性があります。
  • transformers.js の学習: このライブラリの使い方を学ぶ必要があります。
  • 検索精度: 使用する Embedding モデルに依存します。

作業計画の変更点:

  • package.json@xenova/transformers を追加。
  • API (/api/rag) または共通ライブラリ (lib/) に、ベクトル化と類似度検索を行うロジックを追加。
  • アプリケーション起動時 or 初回アクセス時にドキュメントベクトルを計算し、メモリにキャッシュする仕組みを実装。 (簡易的には、api/rag が最初に呼ばれた時にキャッシュを生成するのでも良いかもしれません)

この transformers.js を利用したインメモリベクトル検索 のアプローチで進めるのはいかがでしょうか? より挑戦的で RAG らしい実装になるかと思います。

riddle_tecriddle_tec

承知いたしました。transformers.js を利用したインメモリベクトル検索を導入する方向で、修正した作業計画をお伝えします。

修正版 作業計画:

  1. プロジェクト設定:
    • 作業ディレクトリ day24_simple_rag に移動します。 (cd day24_simple_rag)
    • package.jsonname フィールドを day24-simple-rag に変更します。
    • package.jsondependencies@xenova/transformers を追加します (npm install @xenova/transformers)。
    • package.jsonscriptsdb:seed を追加します ("db:seed": "npx prisma db seed -- --tsconfig tsconfig.seed.json" など)。
    • Seed スクリプト用の tsconfig.seed.json を作成します。
  2. データベース設定:
    • prisma/schema.prismaDocument モデル (id, title, content, sourceUrl, createdAt, updatedAt) を定義します。(これは変更ありません)
    • npx prisma migrate deploy --schema=./day24_simple_rag/prisma/schema.prisma を実行してデータベーススキーマを適用します。
  3. 初期データ準備 (Seed):
    • prisma/seed.ts を作成します。
    • seed.ts 内に、指定されたWikipediaページのテキストデータを Document テーブルに登録する処理を実装します。(テキストデータは直接埋め込むかファイル読み込みとします)
    • npx prisma db seed -- --tsconfig day24_simple_rag/tsconfig.seed.json を実行して初期データを投入します。
  4. ベクトル化・検索ロジック実装:
    • day24_simple_rag/lib/vectorStore.ts (または類似のパス) を作成します。
    • このファイルで以下の機能を実装します:
      • Embeddingモデルの準備: transformers.jspipeline 関数を使って、Embeddingモデル (例: Xenova/multilingual-e5-small) のインスタンスを作成する関数を定義します。(モデルロードは非同期なので注意)
      • インメモリキャッシュ: ドキュメントIDと対応するベクトルを格納するキャッシュ(例: Map<number, number[]>)をモジュールスコープで定義します。キャッシュが初期化されたかどうかのフラグも用意します。
      • キャッシュ初期化関数 (initializeVectorCache):
        • キャッシュが未初期化の場合のみ実行します。
        • Prisma Client を使ってDBから全ドキュメント (id, content) を取得します。
        • 取得した各 content を準備したEmbeddingモデルでベクトル化します。
        • { documentId: vector } の形式でキャッシュに格納します。
        • 初期化フラグを true にします。
      • 類似度計算関数 (cosineSimilarity): 2つのベクトル間のコサイン類似度を計算するヘルパー関数。
      • 検索関数 (findMostSimilarDocument):
        • 入力された質問文字列 (query) をEmbeddingモデルでベクトル化します。
        • キャッシュ内の全てのドキュメントベクトルと質問ベクトルのコサイン類似度を計算します。
        • 最も類似度が高いドキュメントの id を返します。
  5. API 実装 (app/api/rag/route.ts):
    • POST リクエストハンドラを実装します。
    • 最初に initializeVectorCacheawait 付きで呼び出し、キャッシュが準備されるのを待ちます。(一度初期化されれば、以降の呼び出しはすぐ完了する想定)
    • リクエストボディから query を受け取ります。
    • findMostSimilarDocument 関数を呼び出して、最も関連性の高いドキュメントIDを取得します。
    • 取得したIDを使って、Prismaで該当ドキュメントの詳細 (id, title, content, sourceUrl) をDBから取得します。
    • 見つかったドキュメント情報に基づいて、テンプレート応答文 (response) を生成します。
    • 応答 (response) とソース情報 (sources) をJSON形式で返します。
  6. UI 実装 (app/page.tsx):
    • (基本的な構成は変更なし)質問入力、実行ボタン、結果表示エリアを設けます。
    • API (/api/rag) 呼び出し処理を実装します。初回API呼び出し時にモデルのロード等で時間がかかる可能性があるため、ローディング表示を適切に行います。
    • APIからの応答を表示します。
    • Tailwind CSS でスタイリングします。
  7. 動作確認:
    • npm run dev で起動し、ブラウザで動作確認します。
    • 初回アクセス時に時間がかかること、2回目以降は比較的速いことを確認します。
    • 様々な質問(キーワードが直接一致しないものも含む)で、意味的に関連すると思われるドキュメントが検索され、応答が生成されるかを確認します。
  8. ドキュメント整備:
    • README.mdtransformers.js の利用、インメモリベクトル検索、使用モデル、注意点などを追記・修正します。
    • .cursor/rules/knowledge.md を更新します。

この計画で進めていきます。まずはプロジェクト設定のステップ1から開始します。

riddle_tecriddle_tec

こういうざっくりDocumentDBをつくって対応している

そしてtransfer.js が一番一致率が高いものを返している

Searching for query: "黒の組織は?"
Similarity with doc 3: 0.7822222866720302
Similarity with doc 4: 0.8467446724048368
Similarity with doc 5: 0.7907871183658612
Similarity with doc 6: 0.78245901433795
Similarity with doc 7: 0.836542908913885
Similarity with doc 8: 0.7712599015458526
Similarity with doc 9: 0.7996219390536028
Similarity with doc 10: 0.7844713683406721
Similarity with doc 11: 0.7812669525229684
Similarity with doc 12: 0.817501108462392
Best match: doc 4 with similarity 0.8467446724048368
Found most similar document ID: 4
Sending response.
riddle_tecriddle_tec

本来のデータの取り方

Wikipedia のデータをベクトルデータベース (Vector DB) に入れて RAG システムで活用するための、より一般的で本格的な手順は以下のようになります。

  1. データ取得 (Data Acquisition):

    • 目標: 必要な Wikipedia ページのコンテンツを取得します。
    • 方法:
      • Wikipedia API / ライブラリ: Python の wikipedia ライブラリや、MediaWiki API を直接叩くなどして、指定したページのコンテンツ(通常は HTML または Wiki マークアップ形式)を取得します。これにより、手動コピーよりも効率的かつ網羅的にデータを取得できます。
      • Web スクレイピング: 上記が難しい場合、requestsBeautifulSoup (Python) などのライブラリで直接ページをスクレイピングしますが、Wikipedia の利用規約や負荷に配慮が必要です。
  2. データ抽出・整形 (Data Extraction & Cleaning):

    • 目標: 取得した生データ(HTML や Wiki マークアップ)から、RAG に必要な本文テキストだけを抽出し、不要な情報(ナビゲーションメニュー、サイドバー、情報ボックス、編集リンク、参考文献リスト、カテゴリなど)を除去します。
    • 方法:
      • HTML パーサー (BeautifulSoup, lxml など) を使って HTML 構造を解析し、本文が含まれる主要なタグ (<p>, <h1>, <h2> など) の内容を抽出します。
      • 正規表現などを用いて、不要な定型句やマークアップの残骸をクリーニングします。
      • Wiki マークアップの場合は、専用のパーサーライブラリを利用することもあります。
  3. チャンキング (Chunking):

    • 目標: 整形された長い本文テキストを、ベクトル化に適した、意味的にまとまりのある短い単位(チャンク)に分割します。
    • 方法:
      • 単純な分割: 固定文字数、固定トークン数、文(.)ごとなどで分割します。シンプルですが、意味的な区切りとずれる可能性があります。
      • 再帰的文字分割 (Recursive Character Text Splitting): LangChain などのフレームワークでよく使われる手法。まず大きな区切り文字(例: \n\n 段落)で分割し、チャンクが長すぎる場合は次に小さな区切り文字(例: \n 改行)、さらに文 (, .)、単語 ( ) へと再帰的に分割していきます。意味的なまとまりを維持しやすいとされます。
      • 意味的チャンキング (Semantic Chunking): Embedding モデルを使って文や段落間の意味的な類似度を計算し、意味が変わる箇所で分割する高度な手法もあります。
    • 考慮点: チャンクのサイズ(大きすぎると情報が薄まり、小さすぎると文脈が失われる)や、チャンク間のオーバーラップ(文脈維持のためにチャンクの境界を少し重ねる)を調整します。
  4. ベクトル化 (Embedding):

    • 目標: 分割した各チャンクを、意味情報を保持した数値ベクトル(Embedding)に変換します。
    • 方法:
      • Hugging Face Hub で公開されているような事前学習済みの Embedding モデル(例: sentence-transformers/all-MiniLM-L6-v2 (英語)、intfloat/multilingual-e5-large (多言語)、cl-tohoku/bert-base-japanese-whole-word-masking (日本語、ファインチューニングが必要な場合も) など)を使用します。
      • transformers.js (今回使用)、sentence-transformers (Python)、transformers (Python) などのライブラリを使って、各チャンクテキストを入力し、ベクトル出力を得ます。
  5. ベクトルデータベースへの格納 (Storing in Vector DB):

    • 目標: 生成したベクトルと、それに対応するチャンクテキスト、およびメタデータをベクトルデータベースに効率的に格納し、高速な類似度検索を可能にします。
    • 方法:
      • Vector DB の選択: 用途や規模に応じて適切なベクトルデータベースを選びます (例: Pinecone, Weaviate, Qdrant, Milvus, Chroma DB など)。ローカルで試せるものから、クラウドサービスまで様々です。
      • データの登録: 各 Vector DB が提供するクライアントライブラリや API を使用して、以下の情報をセットで登録します。
        • ベクトル: Embedding モデルから得られた数値配列。
        • コンテンツ (ペイロード): 対応するチャンクの本文テキスト。検索後に内容を確認したり、LLM に渡したりするために必要です。
        • メタデータ: 元のドキュメント名、URL、チャンク番号、タイトル、カテゴリなど、検索結果のフィルタリングや情報源の提示に役立つ情報。
      • インデックス作成: Vector DB は登録されたベクトルに対して、効率的な近似最近傍探索 (ANN) を行うためのインデックスを内部で構築します。

この一連のパイプラインを経ることで、Wikipedia のような大規模な非構造化テキストデータから、質問応答に適した形式で情報を抽出し、ベクトルデータベースに格納することができます。今回の実装では、特にチャンキングと Vector DB への格納部分を大幅に簡略化(ドキュメント単位、インメモリキャッシュ)した形になります。

riddle_tecriddle_tec

はい、ご指摘の通り Redis と PostgreSQL にもベクトル検索機能を追加するための拡張機能やモジュールが存在します。これらを利用して今回のデータを保存・検索する方法の概要は以下のようになります。

1. PostgreSQL と pgvector を使う場合:

  • 準備:
    • PostgreSQL サーバーを用意し、pgvector 拡張機能をインストール・有効化します (CREATE EXTENSION vector;)。
    • pgvector に対応した Node.js クライアント (例: pg) や、Prisma のプレビュー機能などを利用します。(Prisma の pgvector ネイティブサポートはまだ進化途上かもしれません)
  • テーブル定義:
    • ドキュメントチャンクとベクトルを格納するテーブルを作成します。スキーマ例:
      CREATE TABLE wiki_docs (
          id SERIAL PRIMARY KEY,
          chunk_text TEXT,
          doc_title TEXT,
          doc_url TEXT,
          chunk_order INTEGER,
          embedding vector(384) -- ベクトルの次元数を指定 (e.g., multilingual-e5-small は 384次元)
      );
      
  • データ投入:
    1. Wikipedia データを取得し、意味のある単位にチャンキングします (今回の手動分割よりさらに細かく、自動化するのが理想)。
    2. 各チャンクテキストを transformers.jsベクトル化します。
    3. PostgreSQL クライアントを使って、チャンクテキスト、メタデータ (タイトル、URL、順序など)、そして計算したベクトルwiki_docs テーブルに INSERT します。ベクトルは pgvector が期待する形式(例: '[0.1, 0.2, ...]' という文字列や配列)で渡します。
  • インデックス作成 (性能向上のため推奨):
    • ベクトル列に対して pgvector が提供するインデックスを作成します (例: HNSWIVFFlat)。距離計算方法 (コサイン類似度など) も指定します。
    CREATE INDEX ON wiki_docs USING hnsw (embedding vector_cosine_ops);
    
  • 検索:
    1. ユーザーの質問を同じモデルでベクトル化します。
    2. SQL クエリで pgvector の類似度検索演算子 (<=> はコサイン距離) を使って検索します。
    SELECT id, chunk_text, doc_title, embedding <=> '[query_vector]'::vector AS distance
    FROM wiki_docs
    ORDER BY distance ASC -- コサイン距離なので昇順 (類似度は 1 - distance)
    LIMIT 5;
    
    これにより、質問ベクトルに最も近いチャンクが取得できます。

2. Redis と RediSearch (Vector Search) を使う場合:

  • 準備:
    • Redis サーバーを用意し、RediSearch モジュールをロードします。
    • Node.js 用の Redis クライアント (例: redis) を利用します。
  • インデックス定義:
    • FT.CREATE コマンドでインデックススキーマを定義します。メタデータ用の TEXTTAG フィールド、そしてベクトル用の VECTOR フィールドを指定します。VECTOR フィールドには、アルゴリズム (FLAT, HNSW)、次元数、データ型 (FLOAT32)、距離計量 (COSINE) を指定します。
  • データ投入:
    1. データをチャンキングし、各チャンクをベクトル化します。
    2. 各チャンクについて、Redis の Hash データ構造を使ってメタデータ、チャンクテキスト、そしてベクトルデータを格納します。ベクトルデータは Buffer (Float32Arrayなどから変換) として格納する必要があります。
    3. HSET コマンドなどでデータを登録します。
  • 検索:
    1. 質問をベクトル化します。
    2. FT.SEARCH コマンドと KNN (K-Nearest Neighbors) クエリを使って類似ベクトルを検索します。
    FT.SEARCH idx:wiki_docs "*=>[KNN 5 @embedding $query_vec AS score]" PARAMS 2 query_vec <query_vector_buffer> RETURN 3 chunk_text doc_title score
    
    これにより、指定したベクトル ($query_vec) に最も近い5つのドキュメント(のチャンクテキストやタイトル、スコア)が返されます。

現在の実装との主な違い:

  • 専用機能の利用: pgvectorRediSearch といった専用の拡張/モジュールが必要です。
  • データ構造: メモリ上の Map ではなく、PostgreSQL のテーブルや Redis の Hash に、ベクトルデータを含めて永続化します。
  • インデックス: パフォーマンスのために専用のベクトルインデックスを利用します。
  • クエリ方法: SQL の特殊な演算子や Redis の専用コマンドを使います。
  • 複雑性: SQLite とインメモリキャッシュに比べて、セットアップや管理の複雑性が増します。

今回のプロジェクトではこれらの導入はスコープ外ですが、より本格的な RAG システムを構築する際には、このようなベクトルデータベース機能を持つシステムを利用するのが一般的です。

このスクラップは4ヶ月前にクローズされました