AWS MemoryDB のセマンティックキャッシュによる LLM アプリケーションの高速化検証

AWS MemoryDB のセマンティックキャッシュによる LLM アプリケーションの高速化検証

AWS MemoryDB のセマンティックキャッシュ機能を使って、LLM の応答速度の高速化を検証しました。意味的に類似するクエリでキャッシュ化し、レスポンス時間を平均 97.6% 短縮しました。実際の定量的なパフォーマンス測定結果とともに、生成 AI アプリケーションの高速化とコスト削減を実現する方法を詳しく解説します。

はじめに

本記事では、AWS MemoryDB のセマンティックキャッシュ機能の効果を体感するための実装を行います。特に、LLM への問い合わせにおいて、意味的に類似するクエリに対してキャッシュを返すことで、レスポンス時間の大幅な短縮とコスト削減を実現します。LLM には Amazon Bedrock の Claude Sonnet 4 を使用し、セマンティックキャッシュのヒット時とミス時のパフォーマンス差を定量的に測定します。

Amazon MemoryDB とは

Amazon MemoryDB は、Redis 互換のインメモリデータベースサービスです。従来の Redis と異なり、データの永続化とマルチ AZ での高可用性を提供しながらも、マイクロ秒レベルの低レイテンシーを両立します。

MemoryDB のセマンティックキャッシュとは

MemoryDB のセマンティックキャッシュ は、従来の完全一致キャッシュとは異なり、意味的に類似する質問に対してもキャッシュされた回答を返す仕組みです。

例:
■ 従来のキャッシュ

  • JavaScript とは何ですか? → キャッシュヒット
  • JS って何? → キャッシュミス (完全に異なる文字列として扱われる)

■ セマンティックキャッシュ

  • JavaScript とは何ですか? → キャッシュヒット
  • JS って何? → キャッシュヒット (意味的に類似していると判定)

対象読者

  • AWS を活用した生成 AI アプリ開発に興味がある方
  • LLM を使ったアプリのパフォーマンス最適化やコスト削減を検討している方
  • MemoryDB のセマンティックキャッシュの実用性を確認したい方

参考

実装

MemoryDB のセマンティックキャッシュ機能を使った最小限の実装を行います。MemoryDB の「ベクトル検索」機能を活用することで、複雑な類似度計算を自前で実装する必要がなく、非常にシンプルなコードで実現できます。

[ユーザー] 
    ↓ 1. 質問送信
[Amazon API Gateway] 
    ↓ 2. Lambda呼び出し
[AWS Lambda] 
    ↓ 3. 埋め込み生成
[Amazon Bedrock - Titan Embeddings V2]
    ↓ 4. ベクトル検索
[Amazon MemoryDB]
    ↓ 5. 回答生成 (キャッシュミス時)
[Amazon Bedrock - Claude Sonnet 4]

Amazon Bedrock の設定

Amazon Bedrock コンソール の Model catalog で Titan Text Embeddings V2Claude Sonnet 4 のアクセス許可を取得します。

Model catalog でアクセス許可を取得

MemoryDB クラスターの作成

MemoryDB コンソール で MemoryDB クラスターを作成します。

  • 作成方法:
    • クラスターのタイプ: シングルリージョンクラスター
    • クラスターの作成方法: 簡易作成
  • 設定: デモ
  • クラスター情報:
    • クラスター名: 任意 (例: semantic-cache-cluster)
    • エンジン: Valkey
  • 接続性:
    • ネットワークタイプ: IPv4
    • サブネットグループ: 新しいサブネットグループを作成
    • 名前: 任意 (例: semantic-cache-subnet-group)
    • VPC ID: デフォルト VPC または既存の VPC を選択
    • サブネット選択済み: 「管理」ボタンをクリックして最低 1 つのアベイラビリティーゾーンを選択
  • ベクトル検索: 「ベクトル検索を有効にする」にチェック (← 今回の検証ではこれが重要です!)
    ベクトル検索の検索

作成したら、クラスターエンドポイント (例: clustercfg.semantic-cache-cluster.xxxxxx.memorydb.ap-northeast-1.amazonaws.com) を控えておきます。

Lambda の実装

プロジェクトの初期化

ローカル環境で Node.js プロジェクトを作成します。

mkdir semantic-cache-lambda
cd semantic-cache-lambda
npm init -y

依存関係のインストール

npm install @aws-sdk/client-bedrock-runtime iovalkey

プロジェクト構成

semantic-cache-lambda/
├── index.js              # Lambda ハンドラー
├── semantic-cache.js     # セマンティックキャッシュクラス
├── package.json
└── node_modules/

セマンティックキャッシュクラスの実装

// semantic-cache.js - セマンティックキャッシュクラス
import { BedrockRuntimeClient, InvokeModelCommand } from '@aws-sdk/client-bedrock-runtime';
import Redis from 'iovalkey';

export class SemanticCache {
    constructor() {
        this.client = null;
        this.bedrockClient = new BedrockRuntimeClient({
            region: process.env.AWS_REGION || 'ap-northeast-1'
        });
        this.threshold = 0.4; // 類似度の閾値 (0.4 = 40% 以上の類似度でキャッシュヒット)
        this.indexName = 'semantic_cache_idx';
    }

    async connect() {
        if (!this.client) {
            // MemoryDB に接続
            this.client = new Redis({
                host: process.env.MEMORYDB_ENDPOINT,
                port: 6379,
                tls: { rejectUnauthorized: false }
            });
            await this.setupIndex();
        }
    }

    // ベクトル検索用のインデックスを作成
    async setupIndex() {
        try {
            await this.client.call('FT.INFO', this.indexName);
            console.log('✅ ベクトルインデックスは既に存在します');
        } catch {
            console.log('🔧 ベクトルインデックスを作成中...');
            await this.client.call(
                'FT.CREATE', this.indexName,
                'ON', 'HASH',
                'PREFIX', '1', 'cache:',
                'SCHEMA',
                'embed', 'VECTOR', 'FLAT', '6',
                'TYPE', 'FLOAT32',
                'DIM', '1024',
                'DISTANCE_METRIC', 'COSINE',
                'question', 'TEXT',
                'answer', 'TEXT'
            );
            console.log('✅ ベクトルインデックスを作成しました');
        }
    }

    // Titan Embeddings V2 でテキストをベクトル化
    async generateEmbedding(text) {
        const command = new InvokeModelCommand({
            modelId: 'amazon.titan-embed-text-v2:0',
            body: JSON.stringify({
                inputText: text,
                dimensions: 1024
            })
        });
        const response = await this.bedrockClient.send(command);
        const result = JSON.parse(new TextDecoder().decode(response.body));
        return result.embedding;
    }

    // MemoryDB の KNN 検索でセマンティック検索
    async searchCache(queryEmbedding) {
        const queryVector = Buffer.from(new Float32Array(queryEmbedding).buffer);

        // KNN 検索で類似するベクトルを検索
        const results = await this.client.call(
            'FT.SEARCH', this.indexName,
            `*=>[KNN 1 @embed $vec AS score]`,
            'PARAMS', '2', 'vec', queryVector,
            'RETURN', '3', 'question', 'answer', 'score',
            'SORTBY', 'score',
            'DIALECT', '2'
        );

        // 検索結果を解析
        if (results[0] > 0) {
            const fields = results[2];
            const doc = {};
            for (let i = 0; i < fields.length; i += 2) {
                doc[fields[i]] = fields[i + 1];
            }

            // 類似度の閾値チェック
            const similarity = 1 - parseFloat(doc.score);
            if (similarity >= this.threshold) {
                return doc;
            }
        }
        return null;
    }

    // 質問と回答をキャッシュに保存
    async saveToCache(question, answer, embedding) {
        const key = `cache:${Date.now()}`;
        const vectorBytes = Buffer.from(new Float32Array(embedding).buffer);

        // ハッシュとして保存
        await this.client.hset(key, {
            question,
            answer,
            embed: vectorBytes
        });

        // 24時間の TTL を設定
        await this.client.expire(key, 86400);
    }
}

Lambda ハンドラーの実装

// index.js - Lambda ハンドラー
import { BedrockRuntimeClient, InvokeModelCommand } from '@aws-sdk/client-bedrock-runtime';
import { SemanticCache } from './semantic-cache.js';

// Lambda コンテナ再利用のためにグローバルで初期化
const cache = new SemanticCache();
const bedrockClient = new BedrockRuntimeClient({
    region: process.env.AWS_REGION || 'ap-northeast-1'
});

let isInitialized = false;

// Claude Sonnet 4 で回答を生成
async function generateAnswer(question) {
    const command = new InvokeModelCommand({
        modelId: 'apac.anthropic.claude-sonnet-4-20250514-v1:0',
        body: JSON.stringify({
            messages: [{
                role: "user",
                content: `簡潔で分かりやすく答えてください: ${question}`
            }],
            max_tokens: 300,
            anthropic_version: "bedrock-2023-05-31"
        })
    });

    const response = await bedrockClient.send(command);
    const result = JSON.parse(new TextDecoder().decode(response.body));
    return result.content[0].text;
}

export const handler = async (event) => {
    try {
        // 初回実行時のみ MemoryDB に接続
        if (!isInitialized) {
            console.log('🚀 セマンティックキャッシュを初期化中...');
            await cache.connect();
            isInitialized = true;
            console.log('✅ 初期化完了');
        }

        // リクエストから質問を取得
        const body = typeof event.body === 'string' ? JSON.parse(event.body) : event.body;
        const { question } = body;

        if (!question) {
            return {
                statusCode: 400,
                body: JSON.stringify({ error: 'question パラメータが必要です' })
            };
        }

        const startTime = Date.now();
        console.log(`❓ 質問: ${question}`);

        // 1. Titan Embeddings V2 で質問をベクトル化
        const embedding = await cache.generateEmbedding(question);

        // 2. MemoryDB でセマンティック検索
        const cached = await cache.searchCache(embedding);

        let answer, isFromCache = false, llmTime = 0;

        if (cached) {
            // キャッシュヒット - 既存の回答を返す
            answer = cached.answer;
            isFromCache = true;
            const similarity = 1 - parseFloat(cached.score);
            console.log(`✅ キャッシュヒット (類似度: ${similarity.toFixed(3)})`);
            console.log(`   類似質問: ${cached.question}`);
        } else {
            // キャッシュミス - Claude で新しい回答を生成
            console.log('❌ キャッシュミス - Claude で回答生成中...');
            const llmStart = Date.now();
            answer = await generateAnswer(question);
            llmTime = Date.now() - llmStart;

            // 新しい回答をキャッシュに保存
            await cache.saveToCache(question, answer, embedding);
        }

        const totalTime = Date.now() - startTime;
        console.log(`💬 回答: ${answer}`);
        console.log(`⏱️  レスポンス時間: ${totalTime}ms (キャッシュ: ${isFromCache ? 'ヒット' : 'ミス'})`);

        return {
            statusCode: 200,
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                success: true,
                data: {
                    question,
                    answer,
                    isFromCache,
                    responseTime: totalTime,
                    llmTime
                }
            })
        };

    } catch (error) {
        console.error('❌ エラー:', error);
        return {
            statusCode: 500,
            body: JSON.stringify({ success: false, error: error.message })
        };
    }
};

package.json

"type": "module" 行を追加します。

{
  "name": "semantic-cache-lambda",
  "version": "1.0.0",
  "type": "module",
  "main": "index.js",
  "dependencies": {
    "@aws-sdk/client-bedrock-runtime": "^3.x.x",
    "iovalkey": "^5.x.x"
  }
}

デプロイ用 ZIP ファイルの作成

zip -r function.zip .

Lambda 関数の作成

Lambda コンソール で関数を作成します。

  • 関数名: 任意 (例: semantic-cache-function)
  • ランタイム: Node.js 22.x
  • アーキテクチャ: x86_64

関数を作成後、先に作成した ZIP ファイルをアップロードします。

環境変数

ENVIRONMENT VARIABLES を設定します。MEMORYDB_ENDPOINT には先に控えたエンドポイント URL を設定します。

キー
MEMORYDB_ENDPOINT clustercfg.semantic-cache-cluster.xxxxxx.memorydb.ap-northeast-1.amazonaws.com

一般設定

LLM への問合せにおいて動作が安定するようメモリとタイムアウトを設定します。

  • メモリ: 512 MB
  • タイムアウト: 30 秒

IAM 権限の設定

下記の IAM ポリシーを Lambda の実行ロールにアタッチします。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ec2:CreateNetworkInterface",
                "ec2:DescribeNetworkInterfaces",
                "ec2:DeleteNetworkInterface"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "bedrock:InvokeModel"
            ],
            "Resource": [
                "arn:aws:bedrock:*:*:foundation-model/amazon.titan-embed-text-v2:0",
                "arn:aws:bedrock:*:*:foundation-model/anthropic.claude-sonnet-4-20250514-v1:0",
                "arn:aws:bedrock:*:*:foundation-model/apac.anthropic.claude-sonnet-4-20250514-v1:0",
                "arn:aws:bedrock:*:*:inference-profile/apac.anthropic.claude-sonnet-4-20250514-v1:0"
            ]
        }
    ]
}

ネットワーク設定

Lambda から Bedrock API にアクセスするため、VPC エンドポイントを作成します。まずは、VPC コンソール でエンドポイントにアタッチするセキュリティグループを作成します。

  • インバウンドルール:
    • 名前: 任意 (例: bedrock-endpoint-sg)
    • タイプ: HTTPS
    • ソース: semantic-cache-function-sg (後の手順で作成します。いったんインバウンドルールを空にして作成し、あとで編集してください。)

Lambda から Bedrock API にアクセスするため、VPC コンソール でエンドポイントを作成します。

  • 名前: 任意 (例: bedrock-runtime-endpoint)
  • サービス名: com.amazonaws.ap-northeast-1.bedrock-runtime
  • VPC: MemoryDB と同じ VPC を選択
  • サブネット: MemmoryDB と同じサブネットを選択
  • セキュリティグループ: bedrock-endpoint-sg

Lambda 用にセキュリティグループを作成します。

  • 名前: 任意 (例: semantic-cache-function-sg):
  • アウトバウンドルール:
    • タイプ: HTTPS
    • 宛先: bedrock-endpoint-sg
    • タイプ: カスタム TCP
    • ポート: 6379
    • ソール: MemoryDB のセキュリティグループ

先に作成した VPC エンドポイント用セキュリティグループのインバウンドに、 semantic-cache-function-sg を追加します。

MemoryDB のセキュリティグループに Lambda 用セキュリティグループのインバウンドを追加します。

  • インバウンドルール:
    • タイプ: カスタム TCP
    • ポート: 6379
    • ソース: semantic-cache-function-sg

Lambda 関数の設定ページに戻り、VPC を設定します。

  • VPC: MemoryDB と同じ VPC を選択
  • サブネット: MemoryDB と同じサブネットを選択
  • セキュリティグループ: semantic-cache-function-sg

動作確認

Lambda 関数のテストを実行して、セマンティックキャッシュの効果を確認します。

テスト質問 1

{"body": "{\"question\": \"Python とは何ですか?\"}"}

キャッシュ: ミス
所要時間: 5010.65 ms

{"body": "{\"question\": \"Pythonって何?\"}"}

キャッシュ: ヒット
所要時間: 188.53 ms

テスト質問 2

{"body": "{\"question\": \"MySQL について教えて\"}"}

キャッシュ: ミス
所要時間: 6459.03 ms

{"body": "{\"question\": \"mysqlとは?\"}"}

キャッシュ: ヒット
所要時間: 104.30 ms

テスト質問 3

{"body": "{\"question\": \"AWS とは何ですか?\"}"}

キャッシュ: ミス
所要時間: 5391.64 ms

{"body": "{\"question\": \"AmazonWebServiceって何?\"}"}

キャッシュ: ヒット
所要時間: 91.40 ms

テスト質問 4

{"body": "{\"question\": \"機械学習とは何ですか?\"}"}

キャッシュ: ミス
所要時間: 5098.60 ms

{"body": "{\"question\": \"ML って何?\"}"}

キャッシュ: ミス
所要時間: 4550.20 ms

テスト質問 5

{"body": "{\"question\": \"HTML とは何ですか?\"}"}

キャッシュ: ミス
所要時間: 4977.28 ms

{"body": "{\"question\": \"エイチティーエムエルって何?\"}"}

キャッシュ: ミス
所要時間: 3599.95 ms

キャッシュヒット時には、平均 97.6% のレスポンス時間短縮を実現できていることが分かります。

まとめ

AWS MemoryDB のセマンティックキャッシュ機能を実装し、実際のパフォーマンス測定を行いました。キャッシュヒット時、平均 97.6% のレスポンス時間の短縮を実現できました。AWS から提供されている仕組みを利用することで、クエリのベクトルエンベディングおよび意味的な類似度計算を自前で実装することなく、高速なキャッシュ応答の導入が可能になります。クエリを自然言語で行う生成 AI アプリケーションにおいて、パフォーマンス向上とコスト削減を両立する有効な手段として活用できます。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.