Amazon Bedrock Evaluations によるRAGのベクトルDB比較：OpenSearch vs Aurora PostgreSQL

## 要約

* Amazon Bedrock ナレッジベースを2種類構築し、Evaluations を用いてRAGの精度を比較検証
  * ベクトルDBとして OpenSearch Serverless を採用したケース
  * ベクトルDBとして Aurora PostgreSQL Serverless を採用したケース
* 検証の結果、両者の精度にはほぼ差異が見られなかった
* （注：OpenSearch Serverless側のナレッジベースの検索タイプが Default search 設定だったため、Hybrid search とした場合は結果が変わるかも）

## はじめに

Amazon Bedrock ナレッジベースでRAG（Retrieval-Augmented Generation）を実装する際、ベクトルDBの選択は重要な検討事項です。OpenSearchは優れた検索機能を提供する一方で、利用状況に関わらず（割と高めの）固定費用が発生するため、常時使っているわけではない現状では費用対効果の面で課題がありました。

そこで、より柔軟な課金体系を持つAurora PostgreSQLへの移行を検討するために、RAGの精度への影響を検証することにしました。
本記事では、両者の精度比較検証の手順とその結果をご紹介します。

なお、本検証では以下の環境を使用しています：

* 検証機能：Bedrock Evaluations のRAG評価機能（プレビュー版）
* 使用リージョン：us-west-2（オレゴン）

https://aws.amazon.com/jp/blogs/news/new-rag-evaluation-and-llm-as-a-judge-capabilities-in-amazon-bedrock/

## 評価手順

実施した評価手順について説明します。

### データの準備

ナレッジベースの精度比較検証には、以下のオープンデータセットを使用しました。

https://huggingface.co/datasets/allganize/RAG-Evaluation-Dataset-JA

#### RAG用ナレッジデータの準備

RAG-Evaluation-Dataset-JAには、RAG用ナレッジデータのPDFリンク一覧が含まれています。
これらのPDFファイルを一括ダウンロードするため、LLMを利用してPythonスクリプトを作成しました。

```python:download_pdf.py
import pandas as pd
import requests
import os
from tqdm import tqdm
import time

# CSVファイルを読み込む
df = pd.read_csv('.\RAG-Evaluation-Dataset-JA\documents.csv')

# ダウンロード先のディレクトリを作成
download_dir = 'downloaded_pdfs'
os.makedirs(download_dir, exist_ok=True)

# ダウンロード結果を記録するリスト
results = []

# プログレスバーを表示しながら各行のURLからPDFをダウンロード
for index, row in tqdm(df.iterrows(), total=len(df)):
    url = row['url']
    file_name = row['file_name']
    save_path = os.path.join(download_dir, file_name)
    
    # すでにファイルが存在する場合はスキップ
    if os.path.exists(save_path):
        print(f'File already exists: {file_name}')
        results.append({'file_name': file_name, 'status': 'already exists'})
        continue
    
    try:
        response = requests.get(url)
        if response.status_code == 200:
            with open(save_path, 'wb') as file:
                file.write(response.content)
            results.append({'file_name': file_name, 'status': 'success'})
        else:
            results.append({'file_name': file_name, 'status': f'failed (HTTP {response.status_code})'})
        
        # サーバーに負荷をかけないよう少し待機
        time.sleep(1)
    
    except Exception as e:
        results.append({'file_name': file_name, 'status': f'error: {str(e)}'})

# 結果をCSVファイルに出力
results_df = pd.DataFrame(results)
results_df.to_csv('download_results.csv', index=False)

print('Download completed! Check download_results.csv for details.')
```

こちらを実行したところ、リンク切れ等により65ファイル中53ファイルのダウンロードとなりました。ただ、今回の目的は2つのナレッジベース間での精度比較であり、同一データセットでの比較が可能なため、この状態で検証を進めることとしました。


#### 評価用データの準備

RAG-Evaluation-Dataset-JAに含まれる評価用データセットを、Bedrock Evaluationsで利用可能な形式に変換します。

変換後のデータ形式は、AWS公式ドキュメントの[評価用データセット仕様](https://docs.aws.amazon.com/ja_jp/bedrock/latest/userguide/knowledge-base-evaluation-prompt-retrieve-generate.html)に準拠しています。

こちらも、LLMを利用して作成したPythonスクリプトを使用しました。

```python:create_jsonl.py
import csv
import json

def create_jsonl_from_csv(input_csv_path, output_jsonl_path):
    # JSONLファイルを書き込みモードで開く
    with open(output_jsonl_path, 'w', encoding='utf-8') as jsonl_file:
        # CSVファイルを読み込む
        with open(input_csv_path, 'r', encoding='utf-8') as csv_file:
            csv_reader = csv.DictReader(csv_file)
            
            # CSVの各行を処理
            for row in csv_reader:
                # JSON構造を作成
                json_structure = {
                    "conversationTurns": [{
                        "referenceResponses": [{
                            "content": [{
                                "text": row['target_answer']
                            }]
                        }],
                        "prompt": {
                            "content": [{
                                "text": row['question']
                            }]
                        }
                    }]
                }
                
                # JSON構造を文字列に変換してJSONLファイルに書き込む
                jsonl_file.write(json.dumps(json_structure, ensure_ascii=False) + '\n')

# 実行用パラメータ
input_csv_path = './RAG-Evaluation-Dataset-JA/rag_evaluation_result.csv'  # 入力CSVファイルのパス
output_jsonl_path = 'evaluation.jsonl'  # 出力JSONLファイルのパス

create_jsonl_from_csv(input_csv_path, output_jsonl_path)
```

#### S3へのデータ配置

以下の構成でS3バケットを作成し、各種ファイルを格納しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4020656/f52b0c81-e605-4a87-94c4-1b2a982e97f6.png)

```
{データ用Bucket名}
├── data
│   └── (ナレッジベース用PDF × 53ファイル)
├── evaluation-datasets
│   └── evaluation.jsonl
└── evaluation-results
```

### Bedrock ナレッジベースの作成

AWSコンソールから Bedrock ナレッジベースを2つ作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4020656/cc85b935-8415-4b35-995f-e4d9dc742958.png)

細かい手順は割愛しますが、ポイントは以下の通りです。
* データソースは、S3 URI で `{データ用Bucket名}/data` を指定する
* 埋め込みモデルは2つとも同じものを選ぶ（今回は Titan Text Embeddings v2 にしました）
* Vector Storeは、一方のナレッジベースはAmazon OpenSearch Serverless、他方はAmazon Aurora PostgreSQL Serverlessとする

その他については今回はデフォルト設定としました。

### Bedrock Evaluations によるRAG評価

AWSコンソールから Knowledge Base evaluation のジョブを2つ作成し、評価を実施しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4020656/1ee5b7ce-8862-4587-b3d9-8fef8b7067f0.png)

ジョブの設定値は以下のようにしました。（記載のないものはデフォルト値です。）

| 設定項目 | 値 |
|---|---|
| Evaluation name | {わかりやすい名前} |
| Evaluator model | Claude 3.5 Sonnet　※v1が利用されます|
| Choose a Knowledge Base | {作成したナレッジベース} |
| Knowledge Base evaluation type | Retrieval and response generation |
| Response generator model | Claude 3.5 Sonnet v2 (オンデマンド) |
| Metrics | Quality (Helpfulness, Correctness, Logical coherence, Faithfulness, completeness) |
| Dataset for evaluation S3 URI | `s3://{データ用Bucket名}/evaluation-datasets/evaluation.jsonl` |
| Results for evaluation S3 URI | `s3://{データ用Bucket名}/evaluation-results` |
| Amazon Bedrock IAM ロール - 許可 | Create and use a new service role |

※Responsible AIのメトリクスは、本検証の目的（精度比較）に直接関係しないため除外しました。

「作成」を押下するとジョブが走りますが、1ジョブにつき3時間ほどかかりました。。

### 評価の比較

評価が完了したジョブ2つを選択し、「Compare」を押下すると結果を比較して見ることができます。
比較の結果、両ベクトルDBの評価スコアはほぼ差異が見られませんでした。

（「At a glance」のレーダーチャートは重なってしまっている）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4020656/2813509a-222c-42d3-90fd-4e7b95348a37.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4020656/1e9419b2-66c8-4d46-83e5-5ff9b4b4321f.png)

### ちなみに

Opensearchのナレッジベースに対する評価ジョブの詳細設定を見てみると、Search typeが `Default search` となっていました。`Hybrid search` を使用すると結果は変わってくるかもしれません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4020656/f2e825fa-61df-4944-b6fd-300f3796c885.png)

今回は未実施ですが、AWSコンソールではなく、AWS Command や boto3 で実行すると、Search typeを指定した評価が実施できそうです。

https://docs.aws.amazon.com/ja_jp/bedrock/latest/userguide/knowledge-base-evaluation-create.html#kb-eval-create-llm-job