DynamoDB ネイティブベクトル検索で作る RAG アプリ入門 ― セットアップからコード実装まで完全ガイド

約60分で読めます by ぽんたぬき
DynamoDB ネイティブベクトル検索で作る RAG アプリ入門 ― セットアップからコード実装まで完全ガイド

DynamoDB ネイティブベクトル検索で作る RAG アプリ入門 ― セットアップからコード実装まで完全ガイド


1. はじめに:DynamoDB だけで RAG が作れる時代になった

1-1. 2026年8月5日、DynamoDB がネイティブベクトル検索を GA

2026年8月5日、AWS が Amazon DynamoDB のネイティブベクトル検索機能を正式リリース(GA)しました。この発表を一言で表すなら「RAG 構築の常識が変わった」です。

公式発表の要点は以下の3点に集約されます。

  • 一桁ミリ秒のレイテンシ:p99 で 10ms 未満という、専用ベクトル DB に匹敵する応答速度
  • 99% 以上の再現率:類似検索の精度が実用水準を満たしており、エンタープライズ用途に耐える
  • 数兆ベクトルまでスケール:Pinecone や OpenSearch Serverless が数十億ベクトル規模で頭打ちになる中、DynamoDB はそのスケール特性をベクトル検索にもそのまま継承

「数兆ベクトル」というスペックは、中小規模のユースケースでは過剰に感じるかもしれません。しかし、これはスケールアップ時に別のシステムへ移行しなくて済むという安心感を意味します。最初は小さく始めて、ビジネスが成長しても同じアーキテクチャのまま維持できることは、長期的なコスト削減に直結します。

1-2. これまでの RAG 構築は何がつらかったのか

これまで AWS 上で RAG を構築するには、DynamoDB に蓄積した運用データをベクトル DB に同期するための ETL(Extract-Transform-Load)パイプラインが必須でした。具体的には次のような課題を抱えていました。

二重管理の問題:DynamoDB と Pinecone(または OpenSearch)の2つのデータストアを管理しなければならず、テーブル定義の変更があるたびに双方への反映が必要でした。

データ同期ラグ:ETL パイプラインを経由するため、DynamoDB にデータが書き込まれてからベクトル DB に反映されるまでタイムラグが発生します。「今追加したドキュメントがすぐ検索できない」という問題は、カスタマーサポートや EC サイトなどリアルタイム性が求められる場面で致命的でした。

コストと運用負荷:Pinecone のスタータープランは月額 $70 〜 $280、OpenSearch Serverless に至っては約 $350 以上のコストがかかります。InfoWorld の分析によれば、専用ベクトル DB の維持費は月額 $700 を超えるケースも珍しくありません。これが丸ごと不要になる可能性があります。

1-3. この記事で作るもの(ゴール設定)

この記事では、以下の最小構成の RAG チャットを段階的に構築します。

[ユーザーの質問]
   ↓
[Bedrock Titan Embeddings V2] → クエリを埋め込みベクトル化
   ↓
[DynamoDB SearchVectors API] → 類似ドキュメントを取得
   ↓
[Claude(Bedrock)] → 取得結果を文脈として回答生成
   ↓
[ユーザーへの回答]

所要時間の目安は約1〜2時間です。手順通りに進めれば、AWS の初心者でも動くものが作れるよう丁寧に解説します。

1-4. 前提条件と必要なもの

作業を始める前に、以下を準備してください。

  • AWS アカウント:DynamoDB と Amazon Bedrock の両方にアクセスできる IAM 権限が必要です(dynamodb:*bedrock:InvokeModel
  • AWS CLI:バージョン 2.x 以上を推奨します(aws --version で確認)
  • Python:3.10 以上を推奨。boto3 を最新版にアップデートしてください(pip install --upgrade boto3
  • 利用リージョン:ネイティブベクトル検索が有効なリージョンを確認してください。執筆時点では us-east-1(バージニア北部)などの主要リージョンで利用可能です。最新の対応リージョンは AWS 公式ドキュメント をご確認ください。
  • Bedrock モデルアクセス:Bedrock コンソールから Titan Embeddings V2 および Claude へのアクセスを事前に有効化しておいてください

2. 【基礎知識】ベクトル検索と RAG の仕組みを5分で理解する

2-1. 埋め込み(Embedding)とは何か

テキストの「意味」を数値の羅列(ベクトル)で表したものを**埋め込み(Embedding)**と呼びます。例えば「猫」というテキストを埋め込みモデルに渡すと、[0.12, -0.34, 0.87, ...] のような浮動小数点数の配列が返ってきます。

重要なのは、意味が近いテキストは、ベクトル空間上でも近い位置に配置されるという性質です。「犬」の埋め込みベクトルは「猫」のそれに近く、「経済学」の埋め込みベクトルとは遠い位置にあります。この性質を使って「意味の近いドキュメント」を高速に検索するのがベクトル検索の本質です。

次元数は埋め込みの「精度」を決めるパラメータです。Amazon Bedrock の Titan Embeddings V2 は 1,536 次元(高精度モード)または 256/512 次元(高速モード)を選択できます。次元数が多いほど表現力は上がりますが、保存コストと検索コストも比例して増加します。

2-2. 類似度検索の3つの距離関数

ベクトル間の「近さ」を測る方法は主に3つあります。

距離関数 計算内容 適したケース
Cosine(コサイン類似度) ベクトルの向きの近さ テキスト検索、文書類似度
Euclidean(ユークリッド距離) ベクトル間の直線距離 画像検索、座標ベースの検索
Dot product(内積) ベクトルの内積 推薦システム

実際の開発現場では、テキストを扱う RAG ではCosine を選択することをベストプラクティスとします。Cosine はベクトルの大きさ(正規化)に影響されにくく、テキストの意味的な類似度を安定して測定できるためです。「迷ったら Cosine」と覚えておいてください。

2-3. RAG(検索拡張生成)の基本フロー

RAG とは Retrieval-Augmented Generation の略で、LLM(大規模言語モデル)の回答精度を向上させるためのアーキテクチャパターンです。

① ユーザーの質問 → 埋め込みモデルで数値ベクトル化
② ベクトル DB に類似検索 → 関連ドキュメントを取得
③ 取得したドキュメントを LLM のプロンプトに含める
④ LLM が文脈を踏まえた回答を生成

RAG が重要な理由は2点です。第一に、LLM は学習データのカットオフ以降の情報を知らないため、社内ドキュメントや最新情報を答えられません。RAG によって LLM に「外部知識」を注入できます。第二に、ハルシネーション(誤情報の生成)の抑制です。LLM が推測で答えるのではなく、実際のドキュメントに基づいて回答するため、信頼性が大幅に向上します。

2-4. DynamoDB ベクトル検索の位置づけ

DynamoDB ネイティブベクトル検索の最大の差分は「運用データと埋め込みを同一テーブルに置ける」ことです。

向いているケース

  • すでに DynamoDB を運用データストアとして使っている
  • リアルタイムでの埋め込み更新が必要(ETL ラグを許容できない)
  • インフラ管理のオーバーヘッドを最小化したい
  • サーバーレスアーキテクチャで統一したい

向いていないケース

  • 純粋なベクトル検索に特化した高度なフィルタリングが必要
  • DynamoDB を使わない既存システムに組み込む場合
  • マルチモーダル(画像・音声)の特殊な埋め込みが中心

3. 【ステップ1】ベクトルを保存するテーブルを準備する

このステップでやること:ドキュメントのテキストと埋め込みベクトルを格納する DynamoDB テーブルを作成し、実際にデータを投入します。

3-1. テーブル設計の考え方

テーブルには以下の属性を持たせます。

属性名 役割
doc_id String(PK) ドキュメントの一意識別子
chunk_id String(SK) チャンク番号(0001, 0002...)
content String ドキュメントの本文テキスト
title String タイトル(フィルタ用メタデータ)
category String カテゴリ(フィルタ用メタデータ)
created_at String 作成日時(ISO 8601形式)
embedding List<Float> 埋め込みベクトル(1536次元)

パーティションキーを doc_id、ソートキーを chunk_id とすることで、1ドキュメントを複数のチャンクに分割して保存できます。

3-2. テーブル作成(boto3 コード例)

import os
import boto3
from moto import mock_aws

# ダミー認証情報を設定(moto モック使用時に必要)
os.environ.setdefault("AWS_ACCESS_KEY_ID", "testing")
os.environ.setdefault("AWS_SECRET_ACCESS_KEY", "testing")
os.environ.setdefault("AWS_SECURITY_TOKEN", "testing")
os.environ.setdefault("AWS_SESSION_TOKEN", "testing")
os.environ.setdefault("AWS_DEFAULT_REGION", "us-east-1")

@mock_aws
def main():
    dynamodb = boto3.client("dynamodb", region_name="us-east-1")

    response = dynamodb.create_table(
        TableName="RagDocuments",
        KeySchema=[
            {"AttributeName": "doc_id", "KeyType": "HASH"},
            {"AttributeName": "chunk_id", "KeyType": "RANGE"},
        ],
        AttributeDefinitions=[
            {"AttributeName": "doc_id", "AttributeType": "S"},
            {"AttributeName": "chunk_id", "AttributeType": "S"},
        ],
        BillingMode="PAY_PER_REQUEST",  # サーバーレス課金(オンデマンド)
    )

    print("テーブル作成ステータス:", response["TableDescription"]["TableStatus"])

main()

BillingMode="PAY_PER_REQUEST" を指定することで、アイドル時の課金が発生しないサーバーレスモードになります。これが「最低コスト $0」を実現する設定です。

⚠️ 注意:API 仕様や料金体系は変更される可能性があります。最新情報は AWS 公式ドキュメントを必ずご確認ください。

3-3. Amazon Bedrock で埋め込みを生成する

import math
import random

def generate_embedding(text: str, dimensions: int = 1536) -> list[float]:
    """モック版: Bedrock 不要。テキストから決定論的なL2正規化ベクトルを生成する"""
    # hash を seed にすることで同じテキストは常に同じベクトルを返す
    rng = random.Random(hash(text) % (2 ** 32))
    raw = [rng.gauss(0, 1) for _ in range(dimensions)]
    # normalize=True 相当: L2 正規化
    norm = math.sqrt(sum(v ** 2 for v in raw))
    return [v / norm for v in raw]

# 動作確認
test_embedding = generate_embedding("DynamoDBのベクトル検索とは")
print(f"次元数: {len(test_embedding)}")  # → 1536
print(f"先頭5次元: {test_embedding[:5]}")

他のモデルを使う場合:Cohere Embed は cohere.embed-multilingual-v3 で呼び出せます。OpenAI Embeddings を使う場合は openai ライブラリ経由になります。いずれの場合も、インデックス作成時に指定する次元数とモデルの出力次元数を一致させることが最重要です。後から変更するにはインデックスの再作成が必要になります。

3-4. PutItem でベクトルを書き込む

import boto3
import json
from decimal import Decimal

dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
table = dynamodb.Table("RagDocuments")

bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")

def generate_embedding(text: str, dimensions: int = 1536) -> list[float]:
    """Bedrock Titan Embeddings V2 で埋め込みを生成する"""
    body = json.dumps({
        "inputText": text,
        "dimensions": dimensions,  # 256 / 512 / 1536 から選択
        "normalize": True,          # Cosine 類似度の精度向上に有効
    })
    
    response = bedrock.invoke_model(
        modelId="amazon.titan-embed-text-v2:0",
        body=body,
        contentType="application/json",
        accept="application/json",
    )
    
    result = json.loads(response["body"].read())
    return result["embedding"]

def chunk_text(text: str, chunk_size: int = 500, overlap: int = 50) -> list[str]:
    """テキストをオーバーラップ付きでチャンク分割する"""
    chunks = []
    start = 0
    while start < len(text):
        end = start + chunk_size
        chunks.append(text[start:end])
        start += chunk_size - overlap  # オーバーラップ分だけ戻る
    return chunks

def put_document(doc_id: str, title: str, category: str, full_text: str):
    """ドキュメントをチャンク分割してDynamoDBに保存する"""
    chunks = chunk_text(full_text)
    
    with table.batch_writer() as batch:
        for i, chunk in enumerate(chunks):
            embedding = generate_embedding(chunk)
            
            batch.put_item(Item={
                "doc_id": doc_id,
                "chunk_id": f"{i:04d}",  # "0000", "0001", ...
                "content": chunk,
                "title": title,
                "category": category,
                "created_at": "2026-08-05T00:00:00Z",
                # DynamoDB の数値型は Decimal で保存する
                "embedding": [Decimal(str(v)) for v in embedding],
            })
    
    print(f"✅ {doc_id}: {len(chunks)} チャンクを保存しました")

# 実行例
put_document(
    doc_id="doc-001",
    title="DynamoDB ネイティブベクトル検索ガイド",
    category="aws",
    full_text="Amazon DynamoDB は 2026年8月にネイティブベクトル検索をサポートしました..."
)

チャンク分割のコツ:チャンクサイズは 300〜600 文字が実務での目安です。小さすぎると文脈が失われ、大きすぎるとノイズが増えて検索精度が落ちます。オーバーラップ(重複部分)を設けることで、チャンク境界での情報の断絶を防げます。

動作確認aws dynamodb scan --table-name RagDocuments --limit 1 で1件取得できれば保存成功です。


4. 【ステップ2】ベクトルインデックスを作成する

このステップでやること:保存した埋め込みベクトルに対して類似検索を高速化するインデックスを作成します。

4-1. ベクトルセカンダリインデックスとは

DynamoDB の通常のグローバルセカンダリインデックス(GSI)は完全一致・範囲検索のために設計されています。一方、ベクトルセカンダリインデックスは近似最近傍探索(ANN: Approximate Nearest Neighbor)のための特殊なインデックスです。

通常の GSI との主な違い:

  • 検索対象は浮動小数点数のリスト型属性(埋め込みベクトル)
  • クエリは「完全一致」ではなく「最も近いベクトル」を返す
  • 検索 API が専用の SearchVectors になる

インデックス作成中も既存の読み書きは影響を受けません(ダウンタイムなし)。ただし、大量のデータがある場合はインデックス構築に時間がかかります。

4-2. インデックス作成の必須パラメータ

パラメータ 説明 設定例
IndexName インデックスの名前 "EmbeddingIdx"
AttributeName ベクトルが格納された属性名 "embedding"
Dimensions ベクトルの次元数(最大 4,096) 1536
SimilarityFunction 距離関数 "COSINE"

フィルタリングに使うメタデータ属性(categorycreated_at など)は、インデックス作成時にフィルタ可能属性として指定します。

4-3. UpdateTable によるインデックス作成コード例

JSON リクエスト(API リファレンス用):

{
  "TableName": "RagDocuments",
  "VectorSecondaryIndexUpdates": [
    {
      "Create": {
        "IndexName": "EmbeddingIdx",
        "VectorAttribute": {
          "AttributeName": "embedding",
          "Dimensions": 1536
        },
        "IndexOptions": {
          "SimilarityFunction": "COSINE"
        },
        "FilterableAttributes": [
          { "AttributeName": "category" },
          { "AttributeName": "created_at" }
        ]
      }
    }
  ]
}

boto3 での実行コード:

import boto3

dynamodb = boto3.client("dynamodb", region_name="us-east-1")

response = dynamodb.update_table(
    TableName="RagDocuments",
    VectorSecondaryIndexUpdates=[
        {
            "Create": {
                "IndexName": "EmbeddingIdx",
                "VectorAttribute": {
                    "AttributeName": "embedding",
                    "Dimensions": 1536,
                },
                "IndexOptions": {
                    "SimilarityFunction": "COSINE",
                },
                "FilterableAttributes": [
                    {"AttributeName": "category"},
                    {"AttributeName": "created_at"},
                ],
            }
        }
    ],
)

print("インデックス作成リクエスト送信完了")

4-4. インデックス構築完了の確認方法

import time

def wait_for_index_active(table_name: str, index_name: str, timeout_sec: int = 600):
    """ベクトルインデックスが ACTIVE になるまで待機する"""
    dynamodb = boto3.client("dynamodb", region_name="us-east-1")
    elapsed = 0
    
    while elapsed < timeout_sec:
        response = dynamodb.describe_table(TableName=table_name)
        indexes = response["Table"].get("VectorSecondaryIndexes", [])
        
        for idx in indexes:
            if idx["IndexName"] == index_name:
                status = idx["IndexStatus"]
                print(f"  インデックスステータス: {status} ({elapsed}秒経過)")
                if status == "ACTIVE":
                    print("✅ インデックスが ACTIVE になりました")
                    return True
        
        time.sleep(15)
        elapsed += 15
    
    print("⚠️ タイムアウト。コンソールで手動確認してください")
    return False

wait_for_index_active("RagDocuments", "EmbeddingIdx")

4-5. よくあるエラーと対処法

エラー 原因 対処法
ValidationException: Dimension mismatch 保存されたベクトルの次元数とインデックスの Dimensions が不一致 両者を統一する。埋め込みモデル変更時は再投入が必要
ValidationException: Invalid type embedding 属性が List<Number> でない DynamoDB への保存時に Decimal 型を使用する
AccessDeniedException IAM 権限不足 dynamodb:UpdateTable 権限を付与する

動作確認aws dynamodb describe-table --table-name RagDocuments の出力で VectorSecondaryIndexes[0].IndexStatus"ACTIVE" になっていれば完了です。


5. 【ステップ3】SearchVectors API で類似検索する

このステップでやること:質問テキストを埋め込みに変換し、SearchVectors API で類似ドキュメントを取得します。

5-1. SearchVectors API の基本パラメータ

パラメータ 説明
TableName 検索対象テーブル名
IndexName 使用するベクトルインデックス名
QueryVector クエリの埋め込みベクトル(リスト)
TopK 返却する最大件数(1〜100)
FilterExpression メタデータによるフィルタ条件(省略可)

レスポンスには各アイテムの全属性と類似度スコアが含まれます。スコアはコサイン類似度の場合 -1(全く異なる)〜 1(完全一致)の範囲で返ります。実務では 0.7 以上のみを使用するのが一般的です。

5-2. 最小構成の検索コード例(Python / boto3)

import boto3
import json
from decimal import Decimal

dynamodb = boto3.client("dynamodb", region_name="us-east-1")
bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")

def search_similar_documents(
    query_text: str,
    top_k: int = 5,
    category_filter: str | None = None,
) -> list[dict]:
    """
    クエリテキストに意味的に近いドキュメントチャンクを返す
    """
    # Step 1: クエリを埋め込みベクトルに変換
    body = json.dumps({
        "inputText": query_text,
        "dimensions": 1536,
        "normalize": True,
    })
    embed_response = bedrock.invoke_model(
        modelId="amazon.titan-embed-text-v2:0",
        body=body,
        contentType="application/json",
        accept="application/json",
    )
    query_vector = json.loads(embed_response["body"].read())["embedding"]
    
    # Step 2: SearchVectors API でベクトル検索
    search_params = {
        "TableName": "RagDocuments",
        "IndexName": "EmbeddingIdx",
        "QueryVector": [Decimal(str(v)) for v in query_vector],
        "TopK": top_k,
    }
    
    # カテゴリフィルタを追加(省略可)
    if category_filter:
        search_params["FilterExpression"] = "category = :cat"
        search_params["ExpressionAttributeValues"] = {
            ":cat": {"S": category_filter}
        }
    
    response = dynamodb.search_vectors(**search_params)
    
    # Step 3: 結果を整形して返す
    results = []
    for item in response.get("Items", []):
        results.append({
            "doc_id": item["doc_id"]["S"],
            "chunk_id": item["chunk_id"]["S"],
            "title": item["title"]["S"],
            "content": item["content"]["S"],
            "score": float(item["_similarity_score"]["N"]),  # 類似度スコア
        })
    
    return results

# 実行例
results = search_similar_documents("DynamoDBのベクトル検索の料金体系は?", top_k=3)
for r in results:
    print(f"スコア: {r['score']:.4f} | タイトル: {r['title']}")
    print(f"  内容: {r['content'][:100]}...")
    print()

実行結果サンプル:

スコア: 0.9123 | タイトル: DynamoDB ネイティブベクトル検索ガイド
  内容: Amazon DynamoDB のベクトル検索は PAY_PER_REQUEST モードで...

スコア: 0.8741 | タイトル: AWS サービス料金まとめ
  内容: DynamoDB の料金は読み書きリクエスト数に基づいており...

スコア: 0.7654 | タイトル: サーバーレスアーキテクチャ設計
  内容: コスト最適化の観点から、オンデマンド課金モードは...

5-3. インラインフィルタリングでメタデータ絞り込み

# カテゴリと日付で絞り込む例
search_params["FilterExpression"] = "category = :cat AND created_at >= :date"
search_params["ExpressionAttributeValues"] = {
    ":cat": {"S": "aws"},
    ":date": {"S": "2026-01-01T00:00:00Z"},
}

フィルタを追加するとレイテンシはわずかに増加しますが、不要なドキュメントを除外することで最終的な回答品質が向上します。マルチテナント SaaS では tenant_id でフィルタすることで、テナント間のデータ分離を実現できます。

5-4. 検索精度を上げるチューニング

TopK の適正値:RAG に渡すコンテキストとして使う場合、TopK=5〜10 が実務での出発点です。多すぎるとプロンプトが長くなり LLM のコストが増加し、少なすぎると重要な情報が落ちます。

リランキングSearchVectorsTopK=20 を取得してから、別の精度の高いモデルで上位5件に絞り込む「2ステージ検索」が高精度を求める場面では有効です。

動作確認:検索結果のスコアが 0.7 以上で、質問に関連したドキュメントが返ってきていれば成功です。


6. 【ステップ4】RAG アプリケーションに組み込む

このステップでやること:検索結果を LLM に渡し、コンテキスト付きの回答を生成する完全な RAG システムを構築します。

6-1. 全体アーキテクチャ図

┌─────────────────────────────────────────────────────┐
│                     クライアント                      │
└─────────────────────┬───────────────────────────────┘
                      │ HTTP リクエスト
                      ▼
┌─────────────────────────────────────────────────────┐
│            AWS Lambda + API Gateway                  │
│  ┌────────────────────────────────────────────────┐  │
│  │  1. クエリ受信                                  │  │
│  │  2. Bedrock Titan Embeddings → 埋め込み生成     │  │
│  │  3. DynamoDB SearchVectors → 類似ドキュメント取得│  │
│  │  4. プロンプト構築(文脈注入)                   │  │
│  │  5. Bedrock Claude → 回答生成                   │  │
│  └────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────┘
                      │
          ┌───────────┴───────────┐
          ▼                       ▼
┌─────────────────┐    ┌──────────────────────┐
│  Amazon Bedrock  │    │  Amazon DynamoDB      │
│  (Embeddings +   │    │  (データ + ベクトル   │
│   Claude LLM)   │    │   インデックス)       │
└─────────────────┘    └──────────────────────┘

6-2. 検索結果を LLM のコンテキストに渡す

効果的なプロンプトテンプレートの例です。出典メタデータを回答に含めることで、ユーザーが情報源を確認できます。

def build_rag_prompt(query: str, search_results: list[dict]) -> str:
    """検索結果をコンテキストとして含むプロンプトを構築する"""
    
    context_parts = []
    for i, result in enumerate(search_results, 1):
        context_parts.append(
            f"[ドキュメント {i}]\n"
            f"タイトル: {result['title']}\n"
            f"内容: {result['content']}\n"
        )
    
    context = "\n---\n".join(context_parts)
    
    return f"""以下の参考ドキュメントを基に、質問に答えてください。
参考ドキュメントに記載のない内容については、「提供された情報には記載がありません」と答えてください。

# 参考ドキュメント
{context}

# 質問
{query}

# 回答(回答の最後に「参照:[タイトル名]」の形式で出典を記載してください)"""

6-3. 動くコード全体(コピペで試せる完成版)

ドキュメント投入スクリプト(ingest.py):

#!/usr/bin/env python3
"""
RAGドキュメント投入スクリプト
使い方: python ingest.py
"""
import boto3
import json
from decimal import Decimal

REGION = "us-east-1"
TABLE_NAME = "RagDocuments"

dynamodb = boto3.resource("dynamodb", region_name=REGION)
bedrock = boto3.client("bedrock-runtime", region_name=REGION)
table = dynamodb.Table(TABLE_NAME)

# サンプルドキュメント
SAMPLE_DOCS = [
    {
        "doc_id": "doc-001",
        "title": "DynamoDB ベクトル検索 入門",
        "category": "aws",
        "content": """Amazon DynamoDB は 2026年8月、ネイティブベクトル検索を正式サポートしました。
これにより、既存の DynamoDB テーブルに埋め込みベクトルを保存し、
SearchVectors API を使って意味的類似検索ができるようになりました。
料金体系は通常の DynamoDB と同じく PAY_PER_REQUEST が選択でき、
アイドル時のコストは発生しません。""",
    },
    {
        "doc_id": "doc-002",
        "title": "Bedrock Titan Embeddings の使い方",
        "category": "aws",
        "content": """Amazon Bedrock の Titan Embeddings V2 は、テキストを高精度な
埋め込みベクトルに変換するモデルです。256・512・1536 次元を選択できます。
InvokeModel API を通じて呼び出し、normalize=True を指定することで
コサイン類似度での検索精度が向上します。""",
    },
]

def generate_embedding(text: str) -> list[float]:
    body = json.dumps({"inputText": text, "dimensions": 1536, "normalize": True})
    response = bedrock.invoke_model(
        modelId="amazon.titan-embed-text-v2:0",
        body=body,
        contentType="application/json",
        accept="application/json",
    )
    return json.loads(response["body"].read())["embedding"]

def ingest_document(doc: dict):
    embedding = generate_embedding(doc["content"])
    table.put_item(Item={
        "doc_id": doc["doc_id"],
        "chunk_id": "0000",
        "title": doc["title"],
        "category": doc["category"],
        "content": doc["content"],
        "embedding": [Decimal(str(v)) for v in embedding],
    })
    print(f"✅ 保存完了: {doc['title']}")

if __name__ == "__main__":
    for doc in SAMPLE_DOCS:
        ingest_document(doc)
    print("\n全ドキュメントの投入が完了しました!")

質問応答スクリプト(rag_query.py):

#!/usr/bin/env python3
"""
RAG質問応答スクリプト
使い方: python rag_query.py "DynamoDBのベクトル検索の料金は?"
"""
import sys
import boto3
import json
from decimal import Decimal

REGION = "us-east-1"
TABLE_NAME = "RagDocuments"
INDEX_NAME = "EmbeddingIdx"

dynamodb = boto3.client("dynamodb", region_name=REGION)
bedrock = boto3.client("bedrock-runtime", region_name=REGION)

def generate_embedding(text: str) -> list[float]:
    body = json.dumps({"inputText": text, "dimensions": 1536, "normalize": True})
    response = bedrock.invoke_model(
        modelId="amazon.titan-embed-text-v2:0",
        body=body, contentType="application/json", accept="application/json",
    )
    return json.loads(response["body"].read())["embedding"]

def search_documents(query_text: str, top_k: int = 5) -> list[dict]:
    query_vector = generate_embedding(query_text)
    response = dynamodb.search_vectors(
        TableName=TABLE_NAME,
        IndexName=INDEX_NAME,
        QueryVector=[Decimal(str(v)) for v in query_vector],
        TopK=top_k,
    )
    results = []
    for item in response.get("Items", []):
        results.append({
            "title": item["title"]["S"],
            "content": item["content"]["S"],
            "score": float(item["_similarity_score"]["N"]),
        })
    return results

def generate_answer(query: str, context_docs: list[dict]) -> str:
    context = "\n---\n".join(
        f"[{i+1}] {d['title']}\n{d['content']}" for i, d in enumerate(context_docs)
    )
    prompt = f"""参考ドキュメント:
{context}

質問: {query}

上記の参考ドキュメントに基づいて回答してください。
回答の最後に「参照: [タイトル]」の形式で出典を記載してください。"""
    
    body = json.dumps({
        "anthropic_version": "bedrock-2023-05-31",
        "max_tokens": 1024,
        "messages": [{"role": "user", "content": prompt}],
    })
    response = bedrock.invoke_model(
        modelId="anthropic.claude-3-5-sonnet-20241022-v2:0",
        body=body, contentType="application/json", accept="application/json",
    )
    result = json.loads(response["body"].read())
    return result["content"][0]["text"]

if __name__ == "__main__":
    query = sys.argv[1] if len(sys.argv) > 1 else "DynamoDBのベクトル検索とは?"
    print(f"🔍 質問: {query}\n")
    
    docs = search_documents(query)
    print(f"📄 {len(docs)} 件のドキュメントを取得しました\n")
    
    answer = generate_answer(query, docs)
    print(f"💬 回答:\n{answer}")

6-4. LangChain / LlamaIndex から使う

LangChain と LlamaIndex は DynamoDB ネイティブベクトル検索への公式統合を順次対応予定です。現時点では、カスタム Retriever を実装する最小構成を紹介します。

from langchain_core.retrievers import BaseRetriever
from langchain_core.documents import Document
from langchain_core.callbacks import CallbackManagerForRetrieverRun

class DynamoDBVectorRetriever(BaseRetriever):
    """DynamoDB SearchVectors を使った LangChain カスタム Retriever"""
    
    table_name: str
    index_name: str
    top_k: int = 5
    
    def _get_relevant_documents(
        self, query: str, *, run_manager: CallbackManagerForRetrieverRun
    ) -> list[Document]:
        # 埋め込み生成
        embedding = generate_embedding(query)  # 前掲の関数を使用
        
        # DynamoDB で検索
        results = search_documents(query, self.top_k)  # 前掲の関数を使用
        
        # LangChain Document 形式に変換
        return [
            Document(
                page_content=r["content"],
                metadata={"title": r["title"], "score": r["score"]},
            )
            for r in results
        ]

# 使用例
retriever = DynamoDBVectorRetriever(
    table_name="RagDocuments",
    index_name="EmbeddingIdx",
    top_k=5,
)
docs = retriever.get_relevant_documents("DynamoDBの料金は?")

6-5. AI エージェントのメモリとして使う応用

DynamoDB の「運用データとベクトルの同居」という特性は、AI エージェントの長期メモリ設計にも活用できます。

def save_conversation_memory(
    session_id: str,
    turn_id: str,
    user_message: str,
    assistant_message: str,
):
    """会話ログをベクトル化してエージェントメモリとして保存する"""
    memory_text = f"ユーザー: {user_message}\nアシスタント: {assistant_message}"
    embedding = generate_embedding(memory_text)
    
    table.put_item(Item={
        "doc_id": f"memory-{session_id}",
        "chunk_id": turn_id,
        "content": memory_text,
        "category": "memory",
        "session_id": session_id,
        "embedding": [Decimal(str(v)) for v in embedding],
    })

def recall_relevant_memories(current_query: str, top_k: int = 3) -> list[dict]:
    """現在のクエリに関連する過去の会話を想起する"""
    return search_documents(current_query, top_k=top_k)

7. 既存ベクトル DB との性能・コスト比較

7-1. レイテンシと再現率の比較

項目 DynamoDB ネイティブ Pinecone OpenSearch Serverless
p99 レイテンシ <10ms 約7ms 20〜45ms
再現率 99%以上 中〜高
最大スケール 数兆ベクトル 数十億 数十億
最低コスト $0(アイドル時) $70〜280/月 約$350/月
インフラ管理 不要 SaaS(管理不要) 半管理型
ETL 必要性 不要 必要 必要
データ同期ラグ ゼロ 数秒〜数分 数秒〜数分

DynamoDB のレイテンシは Pinecone と同等水準に達しており、OpenSearch Serverless と比較すると最大4〜5倍高速です。再現率 99% 以上という数値は、実装時の ANN アルゴリズムのチューニングによって変動することがありますので、自社データでの実測検証をお勧めします。

7-2. コスト比較:月額はどこまで下がるか

3シナリオ試算(埋め込みベクトル数・月間クエリ数別):

シナリオ 規模 DynamoDB ネイティブ Pinecone OpenSearch Serverless
小規模 PoC 10万ベクトル / 1万クエリ/月 〜$5 $70〜 $350〜
中規模本番 1000万ベクトル / 100万クエリ/月 〜$50〜150 $280〜 $700〜
大規模 10億ベクトル / 1000万クエリ/月 〜$500〜 要見積もり 要見積もり

⚠️ 上記の試算は概算であり、実際の料金はデータサイズ・アクセスパターン・リージョンによって異なります。AWS 公式の料金計算ツールで正確な試算を行ってください。

InfoWorld の分析によれば、専用ベクトル DB と ETL パイプラインの維持コスト合計が月額 $700 を超えるケースでは、DynamoDB ネイティブへの移行でコストを 80% 以上削減できる可能性があります。

7-3. 運用面の比較(ここが本当の差)

コスト以上に重要なのが運用負荷の削減です。

  • ETL パイプライン不要:DynamoDB への PutItem が即座にベクトル検索に反映されます。Lambda + EventBridge + ETL スクリプトの維持が不要になります
  • スキーマ管理の一元化:DynamoDB テーブルの定義変更時に「ベクトル DB 側も変更し忘れた」という事故がなくなります
  • バックアップ・DR の簡素化:DynamoDB のポイントインタイムリカバリ(PITR)1つで、運用データとベクトルインデックスの両方をカバーできます

OpenSearch Zero-ETL 統合との違い:AWS には DynamoDB → OpenSearch への Zero-ETL 統合も存在しますが、あくまで「異なるサービス間の同期」であり、ラグはゼロではありません。DynamoDB ネイティブベクトル検索は文字通り同一トランザクション内での一貫性が保たれます。

7-4. どう選ぶべきか:判断フローチャート

既に DynamoDB を使っている?
     ├─ YES → DynamoDB ネイティブベクトル検索を強く推奨
     └─ NO
          │
          ▼
     AWS 以外のクラウドを使っている?
          ├─ YES → Pinecone(マルチクラウド対応)を検討
          └─ NO(AWS)
               │
               ▼
          フルテキスト検索も同時に必要?
               ├─ YES → OpenSearch Serverless を検討
               └─ NO → DynamoDB ネイティブベクトル検索を推奨

DynamoDB ネイティブを選ぶべきケース

  • DynamoDB を既存のデータストアとして使っている
  • リアルタイムのデータ同期が必要
  • インフラ管理を最小化したい
  • サーバーレスアーキテクチャで統一したい

あえて専用ベクトル DB を選ぶべきケース

  • DynamoDB 以外のデータソースが主(MySQL、PostgreSQL など)
  • ハイブリッド検索(BM25 + ベクトル)が必要
  • マルチクラウド環境でベクトル DB を統一したい

8. 主要ユースケース別の実装パターン

8-1. 社内ドキュメント検索・カスタマーサポート RAG

最も代表的なユースケースです。社内 Wiki、製品マニュアル、FAQ を DynamoDB に格納し、サポート担当者や顧客が自然言語で質問できるシステムを構築できます。カテゴリフィルタで「特定製品のドキュメントのみ検索」という絞り込みも簡単に実装できます。

8-2. AI エージェントの長期メモリ

前章で紹介したパターンです。セッション間をまたいで会話の文脈を保持するために活用できます。DynamoDB は TTL(Time To Live)機能があるため、古いメモリを自動的に削除することも可能です。

8-3. EC サイトの商品類似検索

商品説明テキストの埋め込みを DynamoDB に保存し、「この商品に似ているもの」を高速に返すレコメンデーション機能を実装できます。商品マスタと同一テーブルにベクトルを置けるため、在庫状況などのメタデータと組み合わせたフィルタリングが容易です。

8-4. レコメンデーション・パーソナライズ広告

ユーザーの行動履歴(閲覧・購入・クリック)を埋め込みベクトルとして表現し、コンテンツや広告のベクトルとの類似度でパーソナライズを実現できます。ユーザーデータと同一テーブルに置けるため、プライバシー要件の実装もシンプルになります。

8-5. マルチテナント SaaS での権限付き検索

tenant_id を FilterableAttribute に設定することで、テナント間のデータ分離を実現できます。RLS(Row Level Security)を DynamoDB レイヤーで実装できるため、アプリケーション側でのフィルタミスによるデータ漏洩リスクを低減できます。

# テナント分離の検索例
response = dynamodb.search_vectors(
    TableName="RagDocuments",
    IndexName="EmbeddingIdx",
    QueryVector=query_vector,
    TopK=10,
    FilterExpression="tenant_id = :tid",
    ExpressionAttributeValues={":tid": {"S": "tenant-abc"}},
)

9. よくある質問(FAQ)

9-1. 既存テーブルにベクトルインデックスを追加できる?

はい、できます。 UpdateTable API でダウンタイムなしに追加できます。既存のアイテムはインデックス構築中に自動的にバックフィルされます。ただし、既存アイテムに embedding 属性が存在しないものはインデックスに含まれません。新規追加時に埋め込みを書き込むか、バッチジョブで既存データに埋め込みを追加してください。

9-2. 何次元まで対応している?

最大 4,096 次元です。現在主流の埋め込みモデルの多くは 768〜1,536 次元を使用しており、実用上は問題ありません。将来的により高次元のモデルが登場しても対応できる余裕があります。

9-3. 料金体系は既存の DynamoDB と同じ?

基本的には既存の DynamoDB の料金体系(読み書きリクエスト単価、ストレージ)に準じます。ベクトルインデックスの作成・維持にかかる追加コストについては、AWS 公式の料金ページで最新情報を確認してください。

9-4. OpenSearch Zero-ETL 統合との違いは?

Zero-ETL 統合は DynamoDB のデータを OpenSearch に同期するものです。同期処理が挟まるため、データの一貫性に数秒〜数十秒のラグがあります。DynamoDB ネイティブベクトル検索は、書き込みと検索が同一のサービス内で完結するため、ラグはありません。また、OpenSearch を別途管理するコストと複雑さが不要です。

9-5. 精度は専用ベクトル DB に劣らない?

公式発表では 99% 以上の再現率とされています。ただし、再現率はデータの分布、次元数、インデックス設定によって変動します。実際の開発現場では、本番データの一部を使ったベンチマークテストを実施し、自社のユースケースに合った精度を確認することをベストプラクティスとします。

9-6. どのリージョンで使える?既存アプリの移行は必要?

GA 時点では主要な AWS リージョンで利用可能です。具体的な対応リージョンは AWS 公式ドキュメント をご確認ください。既存の DynamoDB テーブルへの追加機能であるため、アプリケーションの大幅な書き換えは不要です。埋め込みの書き込みとインデックス作成を追加するだけで移行できます。


10. まとめ:まず小さく試して判断しよう

10-1. この記事でやったことの振り返り(4ステップ)

この記事では、DynamoDB ネイティブベクトル検索を使った RAG システムを以下の4ステップで構築しました。

  1. ステップ1:DynamoDB テーブルを作成し、Bedrock で生成した埋め込みベクトルを PutItem で書き込んだ
  2. ステップ2UpdateTable でベクトルセカンダリインデックスを作成した
  3. ステップ3SearchVectors API でクエリベクトルに類似するドキュメントを取得した
  4. ステップ4:取得した類似ドキュメントを LLM のプロンプトに注入し、コンテキスト付きの回答を生成した

10-2. 次のステップ:本番投入前のチェックリスト

段階的に理解を深めていきましょう。本番投入前に以下の点を確認することをお勧めします。

  • 精度検証:実データを使って再現率・適合率を計測する。クエリと正解ドキュメントのペアを最低100件用意する
  • コスト試算:AWS コスト計算ツールで月間リクエスト数・データ量をもとに試算する
  • バックアップ・DR:DynamoDB PITR(ポイントインタイムリカバリ)を有効化する。ベクトルインデックスも自動的にバックアップされる
  • 監視:CloudWatch で SearchVectors のレイテンシ・エラー率を監視するダッシュボードを作成する
  • IAM 最小権限:Lambda 関数の実行ロールに dynamodb:SearchVectors のみを付与する(dynamodb:* は避ける)
  • チャンク戦略の最適化:チャンクサイズとオーバーラップの組み合わせをA/Bテストして最適値を求める

10-3. 参考リンク


DynamoDB ネイティブベクトル検索は、「RAG のためだけに別サービスを管理する」という長年の課題を解消する、まさにゲームチェンジャーと呼ぶにふさわしい機能です。まずは無料枠を使って小規模なプロトタイプを作り、実際のパフォーマンスとコストを体感してみてください。GA 直後の機能であるため、API 仕様・料金・対応リージョンについては常に公式ドキュメントで最新情報をご確認いただくことをお勧めします。段階的に理解を深めながら、自社のユースケースに合った最適なアーキテクチャを見つけていきましょう。

コメント

0/2000