Google WorkspaceでRAG実装


## 1. はじめに

**社内には大量のドキュメントが存在**し、多くの社員が必要な情報を探すのに時間を浪費しているケースはありませんか？

こうした問題を解決するために、**大規模言語モデル（LLM）** を活用して社内文書に基づいたQA(質問応答)ができるボットを**Google Chat**上で提供する仕組みを構築すると、次のようなメリットがあります。

- **メリット1**: 社員がGoogle Chat上で自然言語で質問するだけで、社内ドキュメントに即した回答が返ってくる  
- **メリット2**: ドキュメントの参照元リンクを付与して回答を補足でき、必要なら原本にすぐアクセス可能  
- **メリット3**: ベクトル検索により、キーワードに依存しない意味ベースのマッチングが可能  
- **メリット4**: 全てクラウド上で完結できるため、オンプレサーバや複雑なインフラ構築が不要  

本記事では、その全体像と設定・実装方法、運用上の留意点を余すところなく解説します。
社内IT担当の方に向けて**理解できる**記事になっていますので、ぜひ最後までお付き合いください。

---

## 2. 課題

**この記事が解決する主なポイント**:  
1. **Google Apps Script**を使うことで、サーバ不要・低コストで実装できる  
2. **Upstash Vector DB**を用いれば手軽にベクトル検索基盤を構築可能  
3. **OpenAI API**や他のLLM APIを組み合わせてRAG（Retrieval-Augmented Generation）を実現  
4. **Google Chat**との連携で、社員が普段使いしているチャットツール上で完結するUXを提供  

---

<a id="全体像とアーキテクチャ"></a>
## 3. 全体像とアーキテクチャ

本ソリューションは大まかに以下のステップで動作します。

1. **Google Drive**にある社内ドキュメントを取得  
2. **OpenAI API**でテキストをEmbeddingし、**Upstash Vector DB**に登録  
3. **Google Chat**からのユーザ質問を**Google Apps Script**（Botのロジック）で受け取り  
4. 質問文をEmbedding化し、**Upstash Vector DB**で類似検索  
5. 検索結果をコンテキストとして**OpenAI API**に投げ、回答を生成  
6. 生成した回答を**Google Chat**に返信  

このやり取りの全体フローを、以下のmermaid図で示します。

```mermaid
flowchart LR
    A((ユーザの質問)) --> B[Google Chat]
    B --> C[Apps Script（Bot）]
    C --> D[OpenAI Embedding API]
    C --> E[Upstash Vector DB]
    D --> E
    E --> C
    C --> F[OpenAI ChatCompletions API]
    F --> C
    C --> B
```

**ポイント**:  
- **GAS**が主要なロジックをすべて管理し、各サービスと連携するハブとして機能  
- **Upstash Vector DB**が文書チャンクを意味空間で管理し、**類似度検索**を高速に実現  
- **OpenAI**などのLLM APIでEmbedding生成と自然言語応答を行う  

---

<a id="各サービスのセットアップ"></a>
## 4. 各サービスのセットアップ

続いて、具体的な設定・構築手順を順を追って見ていきましょう。

<a id="google-driveドキュメント配置権限設定"></a>
### 4.1 Google Drive：ドキュメント配置・権限設定

**ステップ**:  
1. **参照させたいファイル**をまとめるフォルダをGoogle Drive上に作成  
2. Botが利用する**Googleアカウントが閲覧権限**を持つように共有設定  
3. **Apps Script**側で`DriveApp`などの権限承認が必要なので、OAuthスコープに`https://www.googleapis.com/auth/drive.readonly`を含める  

**ポイント**:  
- **Bot**は実行ユーザの権限でファイルにアクセスする形になるため、事前に共有しておく  
- PDFなどバイナリ形式の文書は、必要に応じてOCRやGoogle Docs変換でテキスト化する  

---

<a id="google-apps-scriptプロジェクト作成とスコープ管理"></a>
### 4.2 Google Apps Script：プロジェクト作成とスコープ管理

1. **GASプロジェクト**を作成  
   - <https://script.google.com/> から新規作成  
   - 「Chatアプリ」テンプレートを利用すると便利  
2. **スコープの承認**  
   - Driveへのアクセス権や外部API呼び出し(`UrlFetchApp`)のスコープを追加  
3. **プロジェクト設定でSecrets管理**  
   - **OpenAI APIキー**や**Upstashのトークン**をプロパティストアに保存し、コード上に直書きしない  

**実行フロー**:  

```mermaid
flowchart LR
    A[Apps Scriptプロジェクト作成] --> B[OAuth同意画面]
    B --> C[Drive権限やUrlFetch権限を承認]
    C --> D[スクリプトエディタでBotコード実装]
    D --> E[Botデプロイ（Chatアプリ）]
```

**ポイント**:  
- デプロイ時に「Google Chat アプリ」を選択し、Botとして公開できる  
- 必要なOAuthスコープを**appsscript.json**で明示し、過剰な権限を付与しないよう注意  

---

<a id="upstash-vector-dbインデックス作成apiトークン取得"></a>
### 4.3 Upstash Vector DB：インデックス作成・APIトークン取得

1. **Upstashにアカウント作成**  
2. 管理画面の「Vector」タブから**新しいインデックス(データベース)を作成**  
   - **Dense**を選択し、ベクトル次元数は**OpenAIのembeddingモデル**(text-embedding-ada-002)なら1536  
   - 距離関数は**コサイン類似度**か**内積**を推奨  
3. **RESTエンドポイントURL**と**Bearerトークン**を控える  
4. 後でApps Scriptから`UrlFetchApp.fetch()`で呼び出す際に使用  

**ポイント**:  
- 1つのドキュメントを**複数のチャンク**に分割して保管すると検索精度が上がる  
- 名前空間(namespace)機能でデータを論理的に分割可能  
- **アップサート**により既存データを更新しながら利用できる  

---

<a id="openai-apiapiキー取得モデル選択"></a>
### 4.4 OpenAI API：APIキー取得・モデル選択

1. **OpenAIアカウント**を作成し、**APIキー**を発行  
2. **課金情報**(クレジットカード)を登録して利用上限を設定（コスト管理のため）  
3. Embedding用エンドポイント:  
   - `POST /v1/embeddings`  
   - モデル例: **text-embedding-ada-002**  
4. 回答生成用エンドポイント:  
   - `POST /v1/chat/completions` (GPT-3.5やGPT-4など)  
   - **max_tokens** や **temperature** を調整  

**ポイント**:  
- **機密情報を入力する**場合は社内ポリシーを確認  
- レート制限やトークン単価に注意  
- **Azure OpenAI**など代替も視野に入れる  

---

<a id="google-chatbot作成とスペースへの追加"></a>
### 4.5 Google Chat：Bot作成とスペースへの追加

1. **Apps Script**の「デプロイ > 新しいデプロイ」から種類を「Google Chat アプリ」に指定  
2. Botの名前・説明・アイコンを設定  
3. 社内ドメイン限定かパブリックか選択  
4. デプロイ後、**Google Chat**でBot名を検索してスペースかDMに追加  

**イベント駆動**:  
- `ADDED_TO_SPACE` → Botが追加されたとき  
- `REMOVED_FROM_SPACE` → Botが削除されたとき  
- `MESSAGE` → ユーザメッセージ受信時  
- `CARD_CLICKED` → カードボタンがクリックされたとき  

**onAddToSpace**, **onRemoveFromSpace**, **onMessage**, **onCardClick** といった関数を実装することでBotが動作する。  

---

<a id="apps-scriptによるサービス連携実装"></a>
## 5. Apps Scriptによるサービス連携実装

ここでは、**Google Apps Script**を使って各サービス（Drive、Upstash、OpenAI、Chat）をどう結びつけるかを解説します。大まかな流れは下図の通りです。

```mermaid
flowchart LR
    A[Driveから取得] --> B[テキスト抽出]
    B --> C[OpenAIでEmbedding生成]
    C --> D[Upstashにアップサート]
    E[ユーザ質問] --> F[onMessageで受信]
    F --> G[Embedding生成]
    G --> H[Upstashで類似検索]
    H --> I[OpenAIで回答生成]
    I --> J[Chatに返信]
```

---

<a id="google-driveからのファイル取得テキスト抽出"></a>
### 5.1 Google Driveからのファイル取得・テキスト抽出

**実装例**:  

```javascript
function getTextFromDrive(docId) {
  // DriveAppサービスを使用
  const file = DriveApp.getFileById(docId);
  // Googleドキュメントの場合
  const doc = DocumentApp.openById(docId);
  const text = doc.getBody().getText();
  return text;
}
```

- **textファイル**の場合: `file.getBlob().getDataAsString()`  
- **PDF**の場合: OCRやGoogle Docs変換など追加ステップが必要  

**ポイント**:  
- ドキュメントを**チャンク**（例：500～1000文字程度）に分割し、埋め込み精度を上げる  
- Driveに大きなファイルが多い場合は、**定期バッチ処理**などで一括抽出するとよい  

---

<a id="embedding生成openai-upstashへの登録"></a>
### 5.2 Embedding生成（OpenAI）⇒ Upstashへの登録

**1. Embedding生成**  

```javascript
function generateEmbedding(textContent, openAiApiKey) {
  const url = "https://api.openai.com/v1/embeddings";
  const headers = {
    "Content-Type": "application/json",
    "Authorization": "Bearer " + openAiApiKey
  };
  const payload = {
    model: "text-embedding-ada-002",
    input: textContent
  };
  const response = UrlFetchApp.fetch(url, {
    method: "post",
    headers: headers,
    payload: JSON.stringify(payload)
  });
  const result = JSON.parse(response.getContentText());
  return result.data[0].embedding; // 数値配列
}
```

- `text-embedding-ada-002`は**1536次元**のベクトルを返す  

**2. Upstashへのアップサート**  

```javascript
function upsertToUpstash(vector, docId, chunkIndex, upstashUrl, upstashToken, originalText) {
  const payload = [
    {
      id: `${docId}-${chunkIndex}`,
      vector: vector,
      metadata: {
        filename: docId,
        chunk: chunkIndex
      },
      data: originalText // 必要ならここに抜粋文章など
    }
  ];
  const headers = {
    "Content-Type": "application/json",
    "Authorization": "Bearer " + upstashToken
  };
  const response = UrlFetchApp.fetch(upstashUrl + "/upsert", {
    method: "post",
    headers: headers,
    payload: JSON.stringify(payload)
  });
  // 正常にアップサートされたかログで確認
  Logger.log(response.getContentText());
}
```

- **ポイント**:
  - `id`に一意な識別子（例：`docId-チャンク番号`）を設定  
  - `metadata`に**ファイル名やページ番号**など有用情報を入れておく  
  - 大量のチャンクをまとめて一度にアップサートする場合は配列に複数オブジェクトを格納  

---

<a id="質問受信onmessage-類似検索upstash-回答生成openai"></a>
### 5.3 質問受信（onMessage）⇒ 類似検索（Upstash）⇒ 回答生成（OpenAI）

1. **Chatからのメッセージを受け取る** (onMessage)

```javascript
function onMessage(e) {
  const userMessage = e.message.text; // ユーザの入力
  // 例: "ヘルプ" と打たれたらガイドを返すなど
  if (userMessage.toLowerCase() === "help") {
    return { text: "使い方ガイド: ～" };
  }
  
  // 質問に対する回答を生成
  const answer = handleUserQuery(userMessage);
  return { text: answer };
}
```

2. **handleUserQuery**内で以下を行う:  

   - 質問テキストをOpenAI APIで**Embedding化**  
   - Upstashに**類似検索クエリ**を投げ、関連度の高いチャンクを取得  
   - 得られたチャンクを**コンテキスト**としてLLMに投げ、回答生成  
   - 回答テキストを返す  

```javascript
function handleUserQuery(queryText) {
  // 1) 質問のEmbedding
  const queryEmbedding = generateEmbedding(queryText, OPENAI_API_KEY);

  // 2) Upstashで類似検索
  const similarChunks = queryUpstash(queryEmbedding);

  // 3) LLMへのプロンプトを生成
  const contextText = similarChunks.map(chunk => chunk.data).join("\n");
  const prompt = "以下は社内ドキュメントの抜粋です:\n" + contextText + "\n\nこれに基づいて次の質問に答えてください:\n" + queryText;
  
  // 4) OpenAIで回答生成
  const answer = generateAnswerWithOpenAI(prompt);
  
  return answer;
}
```

**Upstashでの類似検索**(例):

```javascript
function queryUpstash(embedding) {
  const url = UPSTASH_URL + "/query";
  const headers = {
    "Content-Type": "application/json",
    "Authorization": "Bearer " + UPSTASH_TOKEN
  };
  const payload = {
    vector: embedding,
    topK: 3,
    includeMetadata: true,
    includeData: true
  };
  const response = UrlFetchApp.fetch(url, {
    method: "post",
    headers: headers,
    payload: JSON.stringify(payload)
  });
  const result = JSON.parse(response.getContentText());
  return result.result; // 上位3件
}
```

**OpenAI ChatCompletions API**で回答生成 (例):

```javascript
function generateAnswerWithOpenAI(prompt) {
  const url = "https://api.openai.com/v1/chat/completions";
  const headers = {
    "Content-Type": "application/json",
    "Authorization": "Bearer " + OPENAI_API_KEY
  };
  const requestBody = {
    model: "gpt-4o",
    messages: [
      { role: "system", content: "あなたは社内文書に基づいて回答するアシスタントです。" },
      { role: "user", content: prompt }
    ],
    max_tokens: 1000,
    temperature: 0.2
  };
  
  const response = UrlFetchApp.fetch(url, {
    method: "post",
    headers: headers,
    payload: JSON.stringify(requestBody)
  });
  
  const json = JSON.parse(response.getContentText());
  const answerText = json.choices[0].message.content.trim();
  return answerText;
}
```

**ポイント**:  
- **RAG (Retrieval-Augmented Generation)** の流れをGAS内で一括制御  
- 質問Embedding → ベクトル検索 → 回答生成 というパイプライン  
- 返答が長い場合はカードメッセージや折り畳み表示を活用  

---

<a id="運用と管理のポイント"></a>
## 6. 運用と管理のポイント

ここからは実際に運用する段階で留意すべき事項をまとめます。

<a id="データ更新再インデックス化のワークフロー"></a>
### 6.1 データ更新・再インデックス化のワークフロー

- **Google Drive上のドキュメントが更新**された場合、そのままではUpstashのベクトル情報と差が生じる  
- **定期バッチ**: Apps Scriptのトリガー機能で、例えば深夜にDrive内の変更点を確認して埋め込み再生成  
- あるいは**手動トリガー**: 担当者が必要時にスクリプトを実行  

```mermaid
sequenceDiagram
    participant U as User/Editor
    participant GD as Google Drive
    participant AS as Apps Script
    participant V as Upstash Vector
    U->>GD: ドキュメント更新
    AS->>GD: 定期スキャン(変更検知)
    AS->>GD: テキスト抽出
    AS->>AS: Embedding生成
    AS->>V: Upsertで再インデックス
    note over V: データ更新<br/>eventually consistent
```

**ポイント**:  
- **Upsert**を使えば既存のIDに対してベクトルを更新し、最新情報を常に保てる  
- 大量ドキュメントの場合は**バッチ処理**を分割して実行し、API呼び出し上限に注意  

---

<a id="ベクトルdbとllmのコスト管理"></a>
### 6.2 ベクトルDBとLLMのコスト管理

- **Upstash**  
  - Freeプランに**クエリ数制限**がある  
  - 大量問い合わせやベクトル数が増えたら有料プラン検討  
  - 大量アップサート時の**整合性遅延**に注意  
- **OpenAI**  
  - **Embedding**：入力トークン数に応じた課金  
  - **チャット生成**：入力+出力トークンで課金  
  - GPT-4は単価高め、GPT-3.5は低コスト  
  - **レートリミット**: 大規模展開時は要確認  

**対策例**:  
- **キャッシュ**: 同じ質問が頻出する場合は回答をキャッシュ  
- **プロンプト最適化**: 不要文を削り、トークン数を減らす  
- **モデル切り替え**: 通常はgpt-3.5、重要質問のみgpt-4  

---

<a id="エラーハンドリングログ監視"></a>
### 6.3 エラーハンドリング・ログ監視

- **Apps Script**でtry-catchして**ユーザへエラー通知**（「時間を置いて再度お試しください」など）  
- **Logs**: `Logger.log()`や`console.error()`を活用し、[Apps Scriptのログ](https://script.google.com/home/logs)で状況把握  
- **Google Chat**の応答時間が10秒以上になるとタイムアウトの可能性があるため、**効率的な呼び出し**を意識  

---

<a id="精度向上と拡張のヒント"></a>
## 7. 精度向上と拡張のヒント

<a id="プロンプト設計温度パラメータの調整"></a>
### 7.1 プロンプト設計・温度パラメータの調整

- **プロンプト**: 「与えられた情報のみから回答し、分からなければ『分かりません』と答える」など明示的に指示  
- **システムメッセージ**: 「あなたは社内文書に基づいて正確に答えるアシスタントです」などでモデルのロールを設定  
- **温度**: **0～0.3**程度にすることで**幻覚**(事実無根の回答)を抑制  

---

<a id="カードメッセージや追加機能ヘルプコマンド等"></a>
### 7.2 カードメッセージや追加機能（ヘルプコマンド等）

- **カードメッセージ**を使うと、回答をセクションごとに整理できる  
- **「ヘルプ」コマンド**: Botに使い方を聞くと操作ガイドを返す機能  
- **ドキュメント出典リンク**をつける: 「参考：ファイル名やURL」  

```mermaid
flowchart LR
    A[ユーザ入力 'help'] --> B[onMessageで判定]
    B --> C[ヘルプ用のカードメッセージ生成]
    C --> D[ユーザへ返信]
```

---

<a id="スケーラビリティとセキュリティ対策"></a>
### 7.3 スケーラビリティとセキュリティ対策

- **スケールアップ**  
  - Upstashのプラン拡張、あるいは他のベクトルDBに移行も検討可  
  - OpenAIの上位レートリミット申請 or 別のLLMサービスに分散  
- **セキュリティ**  
  - Apps Scriptの**OAuthスコープ**を最小限に  
  - **APIキーやトークン**はプロジェクトのSecretsやVault等で安全管理  
  - 社内機密情報を外部LLMに送るリスクを吟味（エンタープライズプランを使うなど）  

---

<a id="まとめ"></a>
## 8. まとめ

本記事では、**Google Drive**に格納された社内ドキュメントを**Upstash Vector DB**に取り込み、**OpenAIのLLM**を活用して**Google Chatボット**で質問応答を実現する方法を解説しました。  

**重要なポイント**をおさらいします。  

1. **GASとGoogle Chat連携**: イベント駆動でユーザメッセージを受け取り、処理後に返信  
2. **Upstash Vector DB**: 文書チャンクをベクトル化して蓄積し、高速で意味検索ができる  
3. **OpenAI API**: Embeddingと回答生成に活用。モデル選択やプロンプト設計が精度を左右  
4. **運用**: Driveの更新に合わせた再インデックスや、LLMのコスト管理が必須  

**今後の発展例**:  
- **Azure OpenAI**への切り替えでデータガバナンスを強化  
- **Googleフォームやスプレッドシート**と連携し、社内申請・ヘルプデスクなど業務フローに組み込む  
- **社内ポータルサイト**や他チャットツールとも連携し、多チャネルで質問受付  

**本システムを導入**すれば、社員が「〇〇規程はどうなってる？」「休暇手続きの方法は？」といった質問を**自然言語でChatに送るだけ**で、即座に回答が得られるようになります。
**検索効率が飛躍的に高まり、部内の問い合わせ対応負荷も軽減**されることでしょう。

ぜひPoCから始めて、**社内全体の情報活用レベルを一段上へ**引き上げてみてください。

---

<a id="参考文献"></a>
## 9. 参考文献

- [Class DriveApp | Apps Script - Google for Developers](https://developers.google.com/apps-script/reference/drive/drive-app)  
- [Google Chat Bot - Apps Script | Google Cloud Skills Boost](https://www.cloudskillsboost.google/focuses/32756?parent=catalog)  
- [Getting Started - Upstash Documentation](https://upstash.com/docs/vector/overall/getstarted)  
- [Upsert Vectors - Upstash Documentation](https://upstash.com/docs/vector/api/endpoints/upsert)  
- [Query Vectors - Upstash Documentation](https://upstash.com/docs/vector/api/endpoints/query)  
- [What is Upstash Vector? - Upstash Documentation](https://upstash.com/docs/vector/overall/whatisvector)  
- [Build a RAG Chatbot with Upstash, OpenAI, Clerk, and Next.js | Upstash Blog](https://upstash.com/blog/rag-chatbot-upstash-openai-clerk-nextjs)  

上記のリンク先では、Upstash Vector DBの詳細なAPI仕様やOpenAIのAPIドキュメントが掲載されており、さらに深い技術的情報を得ることができます。
もし実装でつまずいたときは、ぜひ参考にしてみてください。

---
### 会社紹介

**株式会社 Mosaica**  
最先端テクノロジーで社会課題を解決し、持続可能な未来を創造する IT カンパニー。
AI ソリューション、クラウド統合、DX 推進、経営コンサルティングなど包括的なサービスでビジネス変革を支援しています。  

詳しくは [公式サイト](https://mosaica.co.jp/) までお気軽にご相談ください。
公式サイト： https://mosaica.co.jp/