3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[AWS]DynamoDBをセマンティックキャッシュとして使ってみた

3
Last updated at Posted at 2026-08-14

TL:DR

  • ベクトル検索対応したDynamoDBでセマンティックキャッシュは機能した
  • コストは実質「ミス率 × クエリ数」で決まる。間欠トラフィックならDynamoDBが圧倒的に安い
  • サーバーレス構成ならDynamoDB、常時高トラフィックや最速レイテンシ狙いならValkeyという使い分け

📚はじめに

DynamoDBでベクトル検索対応しましたね。
※機能自体の解説は本記事の主旨ではないのでこちらの素晴らしい記事をご参照ください。

ということは"セマンティックキャッシュ"としても使えるんじゃね?ということでやってみました。

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

普通のキャッシュと何が違うの?

普通のキャッシュはキーの完全一致で動きます。例えば「返品したい」というキーで回答を保存しても「商品を送り返したいんですが」ではヒットしません。人間にとっては同じ質問でも文字列としては別物だからですね。

セマンティックキャッシュ(意味的キャッシュ)はこの判定を「文字が同じか」ではなく「意味が近いか?」で行います。言い回しが違っても意図が同じなら保存済みの回答を返せるので「同じことを違う言葉で聞かれる」のがLLMアプリと相性が良いのです。

仕組み(ざっくり)

予め、質問文のベクトルと回答文をキャッシュとして保持しておいて。質問する際に質問を埋め込みモデルでベクトル化し、キャッシュ側のベクトルと類似検索します。十分に近いものが見つかれば(ヒット)保存済みの回答を返し、見つからなければ(ミス)通常のRAG検索を行った後に、質問と回答のペアをキャッシュに書き戻します。

キャッシュ検索時に、あらかじめ設定したスコアの範囲に満たないものはミスとなります。

ポイントは「十分に近い」の線引きをする類似度閾値です。緩すぎると別の質問に別人の回答を返す事故が起き、厳しすぎると誰もキャッシュにヒットしません。

何が嬉しいの?

RAG検索のLLM呼び出しは数秒かかるうえ、従量課金です。同じような質問が繰り返し来るユースケースなら、2回目以降をキャッシュから返すことで応答は数秒から数百ミリ秒になり、LLM代もまるごと浮きます。カスタマーサポートのFAQのように「聞かれることが大体決まっている」領域とは特に相性が良いのです。

👑定番はElastiCache Valkey

2025年10月にElastiCacheのベクトル検索がGAしてからは、AWSでセマンティックキャッシュといえばValkeyが定番ですね。

ベクトル検索はサーバーレス非対応という罠

ところがElastiCacheにはサーバーレスオプションがあるのにベクトル検索が使えるのはノードベースクラスタだけ(2026年8月時点)。つまりキャッシュのために24時間365日起動しておく必要があります。最小の cache.t4g.micro でも月10ドル前後、公式検証で使われている cache.r7g.large だと月130ドル前後です(US East基準)。誰も質問していない深夜3時もノードは元気に稼働しています。:basketball_player:

アクセスが疎らなら従量課金で組みたいっ(願望)

常時トラフィックのあるサービスなら固定費で全然ペイします。悩ましいのは社内ツールやFAQ、検証段階のLLMアプリのような間欠的なワークロードで、1日数百クエリのためにノードを立てるのはどうにも気が引けますよね。使った分だけ課金のDynamoDBがベクトル検索に対応した今、ここを置き換えられないか?というのが本記事の主旨になります。

⛏️DynamoDBでセマンティックキャッシュを作ってみた

全体構成

題材はカスタマーサポートにしました。テーブルは2つ作りました(ナレッジ用/キャッシュ用)。

テーブル 役割
cs-support-manual(ナレッジ層) 応対マニュアル10件。キャッシュミス時のRAG検索対象
cs-semantic-cache(キャッシュ層) 質問文とそのベクトル + LLMが生成した回答文 + 出典マニュアルID

顧客の質問が来たら、まずキャッシュ層を類似検索します。閾値内にヒットすれば保存済みの回答と出典を即返し、ミスならナレッジ層をRAG検索してLLM(Claude Haiku 4.5)に回答を作らせ、質問+回答+出典をキャッシュ層に書き戻す構成です。

大事なのは「キャッシュ層に埋め込むのは質問文」という点です。似ているかどうかを判定したい相手は過去の質問だからです。回答文はベクトル化せずヒットしたら返すだけです。

テーブルとベクトルインデックスを作る

テーブル作成時に --vector-indexes を指定します。

aws dynamodb create-table \
  --region ap-northeast-1 \
  --table-name cs-support-manual \
  --attribute-definitions AttributeName=ManualId,AttributeType=S \
  --key-schema AttributeName=ManualId,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST \
  --vector-indexes '[
    {
      "IndexName": "ManualVectorIndex",
      "VectorAttribute": { "AttributeName": "ContentVector" },
      "Dimensions": 1024,
      "DistanceFunction": "COSINE",
      "Projection": { "ProjectionType": "ALL" }
    }
  ]'

ナレッジを書き込む

応対マニュアルを10件用意しました。

ManualId Category Title Content(先頭のみ)
MANUAL-001 返品・交換 商品の返品および交換の受付手順 お客様から返品のお申し出があった場合、まず注文番号と...
MANUAL-002 配送遅延 配送の遅れに関するお問い合わせ対応 配送状況のお問い合わせを受けたら、注文番号から配送業...
MANUAL-003 初期不良 初期不良品の交換対応フロー 商品が動作しない、破損しているとのご連絡を受けた場合...
MANUAL-004 パスワードリセット ログインできない場合のパスワード再設定案内 ログイン不可のお問い合わせでは、まず登録メールアドレ...
MANUAL-005 請求・支払い 請求金額に関するお問い合わせ対応 請求金額に相違があるとのご指摘を受けたら、該当月の注...
MANUAL-006 解約手続き 有料会員プランの解約受付手順 解約のお申し出があった場合、まず解約理由を任意でお伺...
MANUAL-007 クーポン利用 クーポンコードが使用できない場合の対応 クーポンが適用できないとのお問い合わせでは、まずコー...
MANUAL-008 会員情報変更 登録情報の変更手続き案内 住所や電話番号の変更はマイページの「会員情報編集」か...
MANUAL-009 領収書発行 領収書および適格請求書の発行対応 領収書のご依頼を受けたら、マイページの注文履歴からP...
MANUAL-010 営業時間・店舗案内 営業時間と実店舗に関する問い合わせ対応 カスタマーサポートの受付時間は平日の午前9時から午後...

Titan Text Embeddings V2 で Title + Content を1024次元にベクトル化して PutItem します。

まずナレッジ検索を単体で試す

キャッシュ層を作る前に、ナレッジ検索がちゃんと「意味」で拾えるかを確認しておきます。マニュアル本文とは違う言い回しの顧客質問を5件投げてみました(TopK=3)。

# 顧客質問 期待 1位 判定 1位スコア(距離)
1 買った商品が壊れてたんですけど 初期不良 初期不良 OK 0.694855
2 引き落とし金額がおかしい気がします 請求・支払い 請求・支払い OK 0.806849
3 サイズが合わなかったので別のものと取り替えたいです 返品・交換 請求・支払い NG 0.806700
4 ログインのパスワードを忘れてしまいました パスワードリセット パスワードリセット OK 0.529474
5 来週で月額プランをやめたいのですがどうすればいいですか 解約手続き 解約手続き OK 0.604598

3問目だけNGでしたが、1位とのスコア差はわずか0.011の接戦で、正解も2位にちゃんと入っています。RAGでは上位3件をLLMに投げるので実害はない、という判断です。

キャッシュテーブルを作る

キャッシュ層は別のテーブル cs-semantic-cache にしました。
持たせる属性はこちらです。

属性 中身
CacheId (PK) エントリのID
QueryText 質問文そのもの
QueryVector 質問文の埋め込み。ここにベクトルインデックスを張る
Answer LLMが生成した回答文
SourceManualIds / SourceTitles 回答生成時にRAGで使ったマニュアルの出典
ExpireAt TTL用のepoch秒

ヒット判定と類似度閾値

回答フローの本体がこちらです。

THRESHOLD = 0.7 

def answer(question: str):
    vector = embed(question)                      # Titanで質問を埋め込み
    hit = search_cache(vector)                    # キャッシュ層をSearchVectors(TopK=1)
    if hit and hit["score"] <= THRESHOLD:
        return hit["answer"], hit["sources"]      # ヒット: 保存済み回答+出典を即返す
    manuals = search_manuals(vector)              # ミス: ナレッジ層をRAG検索(TopK=3)
    result = generate_answer(question, manuals)   # LLM(Haiku 4.5)で回答生成
    put_cache(question, vector, result, manuals)  # 質問+回答+出典を書き戻し
    return result, manuals

キャッシュ側の検索は最類似の過去質問1件だけ見ればよいので TopK=1 で十分です。

動かしてみる

まずキャッシュを空にして顧客質問5件を投げると想定どおり5/5がミスになり、RAG検索+LLM生成が走って回答と出典がキャッシュに書き戻されました。所要は1件あたり2.7〜3.4秒です。(haikuだとやはり速いな...)

続いてウォームラン(キャッシュあり状態)。同じ意図の言い換え質問5件を投げます。

# 言い換え質問 判定 スコア(距離) ターンアラウンド
1 届いた品物が不良品でした HIT 0.644852 490.5ms
2 カード引き落としの金額に誤りがあるようです HIT 0.513193 190.0ms
3 注文したサイズが違ったので交換をお願いしたいです HIT 0.443936 201.0ms
4 アカウントにサインインできず、パスワードが分かりません HIT 0.508777 202.1ms
5 有料会員プランを今月中に解約したいです HIT 0.404004 184.4ms

全件(5/5)ヒットです。回答文と出典は保存済みのものがそのまま返り、LLMは一度も呼ばれていませんでした。Haikuの出番はゼロです。

無関係な質問を投げてみる

「今日の東京の天気は?」はスコア0.920、「おすすめのラーメン屋さんはありますか?」は0.887で、どちらも閾値0.7に遠く及ばず正しくミス。無関係なキャッシュを拾って答えることはなく通常のRAG検索処理を行いました。

今回登場した計12テキストの全ペア間距離を計測して、分布を見てみました。

ペアの種類 n min max avg
同じ意図の言い換え同士 5 0.4040 0.6449 0.5030
意図が違うサポート質問同士 30 0.7569 0.9798 0.8764
無関係な質問 vs サポート質問 20 0.8682 0.9837 0.9298

同じ意図の最大値0.6449と、違う意図の最小値0.7569の間。この間あたりがキャッシュヒット/ミスを切り分ける閾値になってきます。今回はとりあえず0.7を閾値に設定しました。とはいえ件数はまだ少なく、境界事例を増やせば分布は変わりうるので実運用ではしっかりとしたチューニングする必要があります。

ヒット時と未ヒット時を測り比べる

ヒット時と未ヒット時をそれぞれ20回ずつ、3つの観点で測りました(ネットワーク往復込み)。

指標 ヒット時 p50 ヒット時 p95 未ヒット時 p50 未ヒット時 p95
ベクトルデータ抽出時間(小計) 185.6ms 218.2ms 191.4ms 236.4ms
├ 埋め込み生成(Titan V2) 146.9ms 178.3ms 115.3ms 155.3ms
├ キャッシュ検索(SearchVectors) 38.7ms 39.9ms 38.2ms 40.1ms
└ ナレッジRAG検索(SearchVectors) なし なし 37.9ms 41.1ms
LLM生成時間(Haiku 4.5) 0ms(発生せず) 0ms(発生せず) 2258.5ms 2832.2ms
ターンアラウンドタイム 185.7ms 214.8ms 2542.7ms 3097.9ms

差を生んでいるのはほぼLLM生成時間です。セマンティックキャッシュが削っているのは「LLM呼び出し」という一番重い処理そのもので、DynamoDBのベクトル検索はどちらの経路でも誤差レベルの脇役でした。

未ヒット時にはキャッシュへの書き戻し(約48ms)もターンアラウンドに含めています。回答を先に返して書き戻しを非同期にすれば、ここはさらに削れます。

:fish:キャッシュの鮮度管理

キャッシュエントリに出典( SourceManualIds )を持たせたのには、表示以外にもう一つ狙いがあります。マニュアルが改訂されたとき「そのマニュアルを出典に持つキャッシュだけ」を狙い撃ちで消すことが可能になります。マニュアルが変更されたのに同じ質問をして古いマニュアルの結果がキャッシュヒットしたら嫌ですよね?キャッシュを削除することで次回の検索では通常のRAG検索とLLM回答生成が行われ、最新ナレッジで回答を生成できます。

古いキャッシュの削除

古いキャッシュはTTLで自動削除できます。DynamoDBの特性として削除までのラグはあリマスがこのワークロードではデメリットになりません。

よくヒットするキャッシュは残したいっ!

ヒットするキャッシュを残したい場合はTTL対象のExpireAtを更新すれば良いです。(寿命を伸ばす)
Valkeyの Least Recently Used (LRU) アルゴリズムにちょっと似てますね。

⚖️Valkeyとの比較

では気になる王道ValkeyとDynamoDBのセマンティックキャッシュを比べてみましょう。

応答速度

ElastiCache for Valkey(cache.t4g.micro、Valkey 9.0)を実際に立てて同じ計測をしました。キャッシュデータは cs-semantic-cache の5件を保存済みベクトルごと移植(埋め込みの再生成なし)、ミス時のRAG+LLM生成もDynamoDB版と同一です。

ElastiCacheはVPC内からしか繋がらないため、Valkey側の計測は踏み台EC2経由のSSHトンネルを掘って、DynamoDB側と同じくローカルPCから行っています(計測位置を揃えるため)。

3指標の実測結果(p50、各20回)を並べます。

指標 DynamoDB ヒット Valkey ヒット DynamoDB 未ヒット Valkey 未ヒット
ベクトルデータ抽出時間(小計) 185.6ms 170.5ms 191.4ms 197.3ms
├ 埋め込み生成(Titan V2) 146.9ms 142.8ms 115.3ms 130.1ms
├ キャッシュ検索 38.7ms 27.7ms 38.2ms 27.5ms
└ ナレッジRAG検索 なし なし 37.9ms 39.7ms
LLM生成時間 0ms 0ms 2258.5ms 2381.0ms
ターンアラウンドタイム 185.7ms 170.8ms 2542.7ms 2638.7ms

ターンアラウンドはどちらの構成でも未ヒット時2.5〜2.6秒がヒット時0.17〜0.19秒になっており、「LLMを呼ばない分だけ速くなる」という同じ結論に落ち着きました。

コスト構造と損益分岐点

単価を並べても構造が違いすぎるのでまず構造から簡単に整理します。Valkeyはノード時間課金で使っても使わなくても毎月同じ額。代わりに検索は叩き放題です。DynamoDBは完全従量でちょっと複雑なので棚卸しするとこうなります。

処理 課金されるもの 実測値
キャッシュ検索(SearchVectors) ベクトル検索 $0.002/GB 28.3KB/回(数件時)〜54.5KB/回(1万件時)
ナレッジRAG検索(ミス時のSearchVectors) 同上 29.5KB/回
書き戻し(ミス時のPutItem) 通常のWRU + ベクトル書き込み$0.52/GBの二階建て 5WRU + 4,822バイト/回
ストレージ テーブル+ベクトルインデックス 各$0.25/GB-月 数十KB(誤差)

ナレッジRAG検索はProjection ALLで返るアイテム本体もベクトル検索課金の内数なので、ヒット時に別途 GetItem で引き直す必要がなく、読み取り側はベクトル検索課金だけ見ればOKです。一方の書き戻しはアイテム本体の通常WRU(実測5WRU)とベクトルインデックスへの書き込み(実測4,822バイト)の両方がかかります。これを全部積んで試算します。前提は、WRU単価$0.625/100万WRU(US East)ストレージは無視できる規模、埋め込み生成コストは両構成共通のため除外。キャッシュ件数は実運用を想定して1万件とします。

キャッシュ検索の単価は、質問文を実際に1万件投入して実測した54.5KB/回(TopK=1)を使っています。

キャッシュミス率は読みにくいので、30% / 50% / 70% の3パターンで試算します。

月間クエリ数 DynamoDB(ミス30%) DynamoDB(ミス50%) DynamoDB(ミス70%) Valkey (t4g.micro) Valkey (r7g.large)
1万 約$0.02 約$0.03 約$0.04 約$9.3 約$128
100万 約$1.8 約$3.0 約$4.1 約$9.3 約$128
1000万 約$18 約$30 約$41 約$9.3 約$128
1億 約$182 約$296 約$410 約$9.3 約$128

DynamoDBセマンティックキャッシュのコストは実質「ミス率 × クエリ数」で決まり、ヒット率が上がるほど安くなる、キャッシュらしい性質を持っています。損益分岐に直すとこうなります。

『1億クエリはt4g.microじゃ捌けんやろ!』というツッコミが聞こえてきそうですが...無視します:hear_no_evil:

損益分岐

ミス率 100万クエリあたり t4g.microとの損益分岐 r7g.largeとの損益分岐
30% 約$1.8 月510万クエリ 月7,000万クエリ
50% 約$3.0 月310万クエリ 月4,300万クエリ
70% 約$4.1 月230万クエリ 月3,100万クエリ

運用と機能

速度とコスト以外の差も表にまとめます。

観点 DynamoDB ElastiCache Valkey
課金 従量(GB単位) 固定(起動時間)
トラフィックゼロ時 ストレージ代のみ ノード代が満額
接続経路 VPC設計不要 VPC内限定
検索レイテンシ 1桁ms(公称)、実測38〜41ms(往復込み) 実測27.7ms(往復込み)、VPC内からは0.31ms
エビクション TTLのみ TTLに加えLRU等
検索時フィルタ 等価条件のみ 豊富
運用 なし ノード管理、バージョンアップ

ElastiCacheにはサーバーレスオプションもありますが、ベクトル検索が非対応のためセマンティックキャッシュ用途では使えません。そのため接続経路もVPC内に限定されます。また上記の試算には表れていませんが、アーキテクチャ構成によってはVPC関連のコストや運用の手間も考慮する必要が出てきます。

🧭使い分け指針

DynamoDBが向くのは、トラフィックが間欠的なアプリです。社内ツール、PoC、夜間はほぼ無風のサービスなど、「ノードを24時間立てるほどではない」規模なら従量課金が素直に効きます。すでにDynamoDBで会話履歴などを管理していてキャッシュを同居させたい場合や、キャッシュのためだけに運用対象を増やしたくない場合も有効な選択肢となります。Lambda中心のサーバーレス構成なら相性は抜群です。VPC設計なしのLambdaからHTTPSで呼ぶだけでセマンティックキャッシュが完結します。埋め込みのBedrockも回答生成のLLMも同じくHTTPSで呼べるので、キャッシュを足してもアーキテクチャに「VPC」が登場しません。ここはVPC住まいのValkeyには真似できないところですね。

Valkeyが向くのはその逆です。常時高トラフィックで検索単価を固定費に押し込んだほうが安くつく規模やLRU戦略/ハイブリッド検索(全文 + ベクトル)が必要な要件なら素直にValkeyを選ぶのが良いと思います。

ちなみに...ManagedKBのセマンティックキャッシュとしても使えます。ManagedKBのemmbeddingモデルは公開されてませんが、セマンティックキャッシュのemmbeddingモデルと合わせる必要はない為実現可能です。ただManagedKBを使うモチベーションとして多いのが「手放しで手軽にRAG検索したい!」という部分だと思われます。セマンティックキャッシュのようなアプリ側でゴリゴリ制御するのはちょっと相性悪いかなと思ったりしました。

🍵締め

DynamoDBのベクトル検索対応でアーキテクチャの選択肢がいろいろ広がってきたように思います。今後のDynamoDBのアップデートやもしかしたらValkeyサーバーレスのベクトル検索対応もあるかもしれないので動向をチェックしていきたいですね。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?