Google Colabで購入履歴RAGシステムを構築【OpenAI API + FAISS】

## はじめに
購入履歴を検索・分析できるRAG（Retrieval-Augmented Generation）システムをGoogle Colabで構築しました。
Udemyの[RAG実装講座](https://www.udemy.com/course/rag-deploy/?srsltid=AfmBOoqGcEqMq3YeI7j-jBZUHGukqNHS_AaADo4VPT-I5pwTyeSMPX9f&couponCode=V2JPLETSLEARN "【初心者向け】大規模言語モデルにおけるRAGを実装できるようになろう！Webページの情報を元に回答できるAIを作ろう！")をベースに発展させたものです。

## RAGとは
生成AI(chatGPTなど)にGoogle検索を追加したようなもので、
汎用的なAIを自社専用のAIに変えるもの
#### 従来のAI
```
質問：弊社の売上を教えて
回答：御社の具体的な売上データは分かりません
```
#### RAG
```
質問：弊社の売上を教えて
↓
[システムが社内DBを検索]
↓
回答：2025年Q1の売上は1000万円です
```

### アーキテクチャ
```
購入履歴データ → ベクトル化 → FAISS検索 → OpenAI API → 自然言語回答
```

## 必要な環境・ライブラリ

```bash
!pip install openai==1.25.1 httpx==0.27.0 faiss-cpu pandas numpy
```
**注意:** OpenAI 1.25.1を用いる場合はhttpx 0.27.0の組み合わせを推奨

## コード実装
以下でproxiesのエラーが出る場合は[解消方法](https://qiita.com/kira33/items/bd9ded342eb85b881646 "Google Colab】OpenAI APIのTypeError: Client.init() got an unexpected keyword argument 'proxies'解消方法")参照
```pythonあ
client = OpenAI()
```
### 1. 基本設定
```python
import pandas as pd
import numpy as np
import faiss
from openai import OpenAI
import os
from google.colab import userdata
import pickle

# OpenAI APIキーを設定
os.environ["OPENAI_API_KEY"] = userdata.get('OPENAI_API_KEY')
client = OpenAI()
```
### 2. サンプルデータ
切手の種類の質問を想定しているため複数追加
```python
def generate_sample_data():   
    # 20件データ（日付、商品名、カテゴリ、価格、説明）
    data = {
        'date': [
            '2023-12-01', '2023-12-15', '2024-01-03', '2024-01-20', '2024-02-05',
            '2024-02-18', '2024-03-02', '2024-03-15', '2024-03-28', '2024-04-10',
            '2024-04-22', '2024-05-05', '2024-05-18', '2024-05-30', '2024-06-12',
            '2024-06-25', '2024-07-08', '2024-07-20', '2024-08-02', '2024-08-15'
        ],
        'product_name': [
            '50円普通切手 10枚セット',
            'iPhone 15 Pro Max 256GB ナチュラルチタニウム',
            'ソニー ワイヤレスイヤホン WF-1000XM4',
            '80円普通切手 5枚セット',
            'プログラミング学習書 Python実践入門',
            'Apple MacBook Air M2 13インチ',
            'コーヒー豆 エチオピア 500g スペシャルティ',
            '100円普通切手 15枚セット',
            'ワイヤレス充電器 Qi対応 15W',
            'ビジネス書 7つの習慣',
            'ブルートゥーススピーカー JBL FLIP6',
            '50円切手 20枚 + 80円切手 10枚 組み合わせセット',
            '有機野菜セット 旬の野菜 8品目',
            'USB-Cハブ 7in1 多機能',
            'プロテイン ホエイ チョコレート味 1kg',
            '100円記念切手 桜シリーズ 8枚セット',
            'ノートPCスタンド アルミ製 調整可能',
            '緑茶 静岡産 煎茶 100g×3袋',
            'デスクライト LED 調光調色',
            'マウスパッド ゲーミング 大型'
        ],
        'category': [
            'Office Products', 'Electronics', 'Electronics', 'Office Products', 'Books',
            'Electronics', 'Food & Beverages', 'Office Products', 'Electronics', 'Books',
            'Electronics', 'Office Products', 'Food & Beverages', 'Electronics', 'Health & Personal Care',
            'Office Products', 'Office Products', 'Food & Beverages', 'Home & Kitchen', 'Electronics'
        ],
        'price': [
            500, 159800, 28000, 400, 3200,
            148800, 2400, 1500, 3500, 2200,
            12800, 1800, 1800, 4200, 3800,
            800, 2500, 1200, 5600, 1500
        ],
        'description': [
            '50円普通切手 10枚 郵便局正規品 手紙やはがきの郵送に最適',
            'Apple iPhone 15 Pro Max 256GB チタニウム製 A17 Proチップ搭載 カメラ性能大幅向上',
            'ソニー 完全ワイヤレス ノイズキャンセリング機能付き 音質最高クラス',
            '80円普通切手 5枚 郵便局正規品 封書の郵送に使用',
            'Python プログラミング 実践的な学習書 初心者から中級者向け データサイエンス対応',
            'Apple MacBook Air M2チップ 13インチ 256GB SSD 軽量薄型 高性能',
            'エチオピア産 スペシャルティコーヒー豆 シングルオリジン フルーティーな酸味',
            '100円普通切手 15枚 郵便局正規品 速達や重量のある郵便物に',
            'Qi規格対応 ワイヤレス充電パッド 15W高速充電 iPhone Android対応',
            '自己啓発書の名著 7つの習慣 人格主義の回復 ビジネスパーソン必読',
            'JBL FLIP6 ポータブルBluetoothスピーカー 防水機能付き 高音質',
            '50円切手20枚と80円切手10枚のお得なセット 様々な郵便料金に対応',
            '有機栽培 旬の野菜セット 農薬不使用 産地直送 新鮮野菜8品目',
            'USB-C ハブ 7ポート HDMI SD カードリーダー 高速データ転送',
            'ホエイプロテイン チョコレート味 1kg 高タンパク質 筋トレ ダイエットサポート',
            '100円記念切手 桜デザイン 8枚 限定デザイン 収集にも最適',
            'ノートパソコンスタンド アルミ合金製 角度調整可能 放熱効果 姿勢改善',
            '静岡産 煎茶 100g×3袋セット 深蒸し茶 香り高い上級茶葉',
            'LED デスクライト 調光調色機能 目に優しい 読書 作業用 USB給電',
            'ゲーミングマウスパッド 大型サイズ 滑らかな表面 ゲーム 仕事用'
        ]
    }
    
    return pd.DataFrame(data)
```
### 3.  OpenAI API関数(ベクトル化)
サンプルデータの各々をEmbeddingsモデル(text-embedding-3-small)を用いてベクトル化(文章や単語を数値配列で表現したもの)
```python
def get_embedding(text, model="text-embedding-3-small"):
    """
    テキストをOpenAI Embeddings APIでベクトル化
    
    Args:
        text (str): ベクトル化するテキスト
        model (str): 使用するEmbeddingsモデル（text-embedding-3-small）
    
    Returns:
        list: 1536次元のベクトル（埋め込み表現）
    """
    response = client.embeddings.create(
        input=text,
        model=model
    )
    return response.data[0].embedding
```
### 4. ベクトルデータベース構築
購入履歴のベクトル化したデータをもとにFAISSデータベースを構築
一度作成したベクトルはファイルに保存して、次回実行時に再利用(実行費用の削減)
#### FAISS(Facebook AI Similarity Search)
・Meta(旧Facebook)が開発
・ベクトル検索に特化した高速ライブラリ
```python
def create_purchase_embeddings(df, save_path='embeddings.pkl'):
    """
    購入履歴をベクトル化してFAISSデータベースを構築
    一度作成したベクトルはファイルに保存し、次回実行時に再利用してコスト削減
    
    Args:
        df (pd.DataFrame): 購入履歴データ
        save_path (str): ベクトルデータの保存先ファイルパス
    
    Returns:
        tuple: (FAISSインデックス, ドキュメント一覧, 埋め込みベクトル配列)
    """
    # 既存のベクトルファイルがあるかチェック（コスト削減のため）
    if os.path.exists(save_path):
        try:
            with open(save_path, 'rb') as f:
                index, documents, embeddings_array = pickle.load(f)
            print(f"既存ベクトルデータ読み込み完了: {len(documents)}件")
            return index, documents, embeddings_array
        except:
            print("既存データ読み込み失敗。新規作成します。")
    
    # 各商品情報を検索用テキストに変換
    # 商品名、カテゴリ、価格、購入日、説明を組み合わせて1つのドキュメントに
    documents = []
    for _, row in df.iterrows():
        doc_text = f"商品名: {row['product_name']}\n"
        doc_text += f"カテゴリ: {row['category']}\n"
        doc_text += f"価格: {row['price']}円\n"
        doc_text += f"購入日: {row['date']}\n"
        doc_text += f"説明: {row['description']}"
        documents.append(doc_text)
    
    # 各ドキュメントをOpenAI APIでベクトル化
    embeddings = []
    for i, doc in enumerate(documents):      
        # OpenAI APIでベクトル化
        embedding = get_embedding(doc)
        embeddings.append(embedding)
    
    # FAISSを使った高速類似度検索のためのインデックス作成
    embeddings_array = np.array(embeddings).astype('float32')  # FAISS用の型変換
    dimension = embeddings_array.shape[1]  # ベクトルの次元数（通常1536）
    index = faiss.IndexFlatIP(dimension)   # 内積による類似度検索インデックス
    index.add(embeddings_array)            # ベクトルをインデックスに登録
    
    # 次回実行時のためベクトルデータを保存
    try:
        with open(save_path, 'wb') as f:
            pickle.dump((index, documents, embeddings_array), f)
        print(f"ベクトルデータ保存完了: {save_path}")
    except Exception as e:
        print(f"保存失敗（処理続行）: {e}")
    
    return index, documents, embeddings_array
```
### 5. セマンティック(意味)検索
「意味」を理解する検索
#### 従来の検索(キーワード検索)
```
検索語："切手"
データ："50円切手を購入" →　〇("切手"が含まれる)
データ："郵便料金分の印紙" →　×（"切手"が含まれない)
```
#### セマンティック検索(意味検索)
```
検索語："切手"
データ："50円切手を購入" →　〇（直接的）
データ："郵便料金分の印紙" →　〇（意味が近い）
データ："封書用の料金証紙" →　〇（同じ用途）
```
#### RAGシステムでのセマンティック検索
1.ベクトル化で意味を数値化
```
# 意味が近い言葉は似たベクトルになる
"切手"     → [0.2, -0.1, 0.8, 0.5, ...]
"郵便"     → [0.3, -0.2, 0.7, 0.6, ...]  # 似ている
"iPhone"   → [0.9, 0.5, -0.3, -0.8, ...] # 全然違う
```
2.質問と商品の意味マッチング
```
質問: "郵便に使うものを買いましたか？"
↓ ベクトル化
[0.25, -0.15, 0.75, 0.55, ...]

商品A: "50円切手 10枚セット"
↓ ベクトル化  
[0.2, -0.1, 0.8, 0.5, ...]

# ベクトルの近さ = 意味の近さ
類似度: 0.89 → 関連性が高い！
```
まとめると、セマンティック＝文字ではなく「意味」で理解するもの
```python
def search_similar_purchases(query, index, documents, top_k=3):
    """
    ユーザーの質問に類似する商品をベクトル検索で発見
    
    Args:
        query (str): ユーザーの質問文
        index: FAISSインデックス
        documents (list): 商品ドキュメント一覧
        top_k (int): 返す類似商品の数
    
    Returns:
        list: 類似度順にソートされた検索結果
    """
    
    # ユーザーの質問をベクトル化（検索クエリとして使用）
    query_embedding = np.array([get_embedding(query)]).astype('float32')
    
    # FAISSで類似度検索実行（内積による類似度計算）
    scores, indices = index.search(query_embedding, top_k)
    
    # 検索結果を整理（ランキング、スコア、ドキュメント内容）
    results = []
    for i, (score, idx) in enumerate(zip(scores[0], indices[0])):
        results.append({
            'rank': i + 1,           # 順位
            'score': float(score),   # 類似度スコア（高いほど関連性が高い）
            'document': documents[idx]  # 関連商品の詳細情報
        })
    
    return results
```
### 6. RAG回答生成システム
プロンプト(AIへの指示文)の作成と使用するAPIモデル、
temperature(創造性と一貫性のバランス制御)とmax_token(生成される回答最大文字数)を設定
#### temperature
```
# 同じ質問には毎回同じ回答
temperature = 0.0
# 安定した回答、わずかにバリエーション
temperature = 0.3
# バランス型(今回の設定) - 自然で適度に多様な回答
temperature = 0.7
# 創造性重視 - 多様だが時々予測不能な回答
temperature = 1.0
```
#### max_token
日本語の場合は設定した半分くらいの数値が最大文字数として設定される
数値が低いほどコストが抑えられる
```python
def generate_rag_response(query, search_results):
    """
    検索結果を基にOpenAI APIで自然言語回答を生成
    RAG（Retrieval-Augmented Generation）のG部分
    
    Args:
        query (str): ユーザーの質問
        search_results (list): セマンティック検索の結果
    
    Returns:
        str: AIが生成した回答
    """
    
    # 検索結果を回答生成用のコンテキストに整形
    context = "以下は関連する購入履歴です:\n\n"
    for result in search_results:
        context += f"[関連度スコア: {result['score']:.3f}]\n"
        context += result['document'] + "\n" + "-"*50 + "\n\n"
    
    # AIへの指示プロンプト作成（システムメッセージ + ユーザーの質問 + 検索結果）
    prompt = f"""購入履歴を参考に、簡潔に回答してください。

{context}

質問: {query}

回答:"""

    # OpenAI APIで回答生成（gpt-3.5-turbo使用、コスト重視）
    response = client.chat.completions.create(
        model="gpt-3.5-turbo",              # 精度は最新のgpt-4より劣るが早くて安い
        messages=[
            {"role": "system", "content": "あなたは購入履歴分析の専門家です。簡潔で有用な情報を提供してください。"},
            {"role": "user", "content": prompt}
        ],
        temperature=0.7,  # 適度な創造性（0=一貫性重視、1=創造性重視）
        max_tokens=80     # 短い回答でコスト削減
    )
    
    return response.choices[0].message.content
```
### 7. RAGシステム統合クラス
```python
class PurchaseRAG:
    """
    購入履歴を対象とした質問応答システム
    セマンティック検索 + AI回答生成を組み合わせたRAGシステム
    """
    
    def __init__(self, df):
        """
        RAGシステムの初期化
        
        Args:
            df (pd.DataFrame): 購入履歴データ
        """
        self.df = df
        # ベクトルデータベース構築（初回時はOpenAI APIコストが発生）
        self.index, self.documents, self.embeddings = create_purchase_embeddings(df)
    
    def query(self, question, top_k=3):
        """
        質問に対してRAG検索と回答生成を実行
        
        Args:
            question (str): ユーザーの質問
            top_k (int): 検索する関連商品数
        
        Returns:
            dict: 質問、検索結果、AI回答を含む辞書
        """
        
        # 1. セマンティック検索で関連商品を発見
        search_results = search_similar_purchases(question, self.index, self.documents, top_k)
        
        # 2. 検索結果を基にAI回答生成
        answer = generate_rag_response(question, search_results)
        
        # 3. 結果表示
        print(f"\n質問: {question}")
        print(f"回答: {answer}")
        
        return {
            'question': question,
            'search_results': search_results,
            'answer': answer
        }
```
### 8. システム初期化と実行
```python
# サンプルデータ生成
df = generate_sample_data()

# RAGシステム初期化
rag_system = PurchaseRAG(df)
```
## 質問してみる
```python
rag_system.query("最も新しい日付で購入した切手は何ですか？")
rag_system.query("2024年3月に購入した切手を教えて")
```
```
# 回答
質問: 最も新しい日付で購入した切手は何ですか？
回答: 2024年6月25日に購入した「100円記念切手 桜シリーズ 8枚セット」が最も新しい切手です。

質問: 2024年3月に購入した切手を教えて
回答: 2024年3月に購入した切手は、100円普通切手 15枚セットです。
```

## まとめ
今回は購買データをソースとしましたが、Udemy講座ではウェブサイトをソースとした説明もあり
GWS(Google Workspace)を社内で利用している場合であれば応用できるのではないかと思いました。

OpenAI APIは有料ですが、安価なベクトル化モデル(text-embedding-3-small)と
精度は最新(gpt4.x)より劣るが早くて安価な回答生成モデル(gpt-3.5-turbo)を用いているため
初回ベクトル化に約0.4円、１質問あたり約0.1円と学習には最適だと思いますので、
ぜひ動かしてみてください。

## 最後に
本来は購買データをサンプルではなく、Amazonの購入履歴のCSV化したデータをソースに
「〇〇いつ買ったっけ？」「過去に買ったとき何円だった？」などと回答を求めるものを作りたかったのですが
購入履歴にある個人情報をマスクする処理が面倒で、サンプルデータにしました。
いつか自分用に購入履歴をもとに作りたいと思っています。