中級読了 7 分

DynamoDB のベクトル検索

2026 年 8 月 5 日より前は、DynamoDB のデータに対する類似検索といえば、テーブルを OpenSearch や別のベクトルデータベースへ複製し、同期に保つことを意味しました。多くのワークロードでは、そのパイプラインはもう不要です。

DynamoDB はベクトル検索に対応していますか?

はい。2026 年 8 月 5 日からネイティブに対応しています。埋め込みを Number 値の普通の List として保存し、ベクトルインデックスを追加して、SearchVectors API で近似最近傍(ANN)クエリを実行します。

対応はオンデマンドテーブルのみ、最大 4,096 次元で、書き込み・検索・保存したバイト数に応じて課金されます。

  • 第 3 のインデックスファミリー: ベクトルインデックスは と LSI の隣に並び、独自の読み取り API(SearchVectors)を持ち、ANN のみに対応します。
  • 速い、しかも計測済み: us-east-1 のライブな 1024 次元インデックス(2026 年 8 月のプローブ)で、SearchVectors は同じクライアントからの GetItem と遜色ない速さで応答しました(p50 39 ms 対 44 ms、各 30 回)。
  • 計測はバイト単位: インデックスへの書き込みは GB あたり $0.52、検索が調べて返すデータは GB あたり $0.002、保存は GB-月あたり $0.25(us-east-1 の定価)。インデックス化された埋め込みは、テーブルの書き込みが次元あたり 4 バイトで課金されます(計測値)。インデックスなしではその約 1.9 倍です。
  • バルクストアは依然として S3 Vectors: 保存は少なくとも約 8 分の 1 の安さで、バッチロードもはるかに安価です。DynamoDB が勝つのは、ミリ秒の読み取り、ストリーミング書き込み、そしてベクトルがその説明するアイテムのすぐ隣にあることです。

ベクトルインデックスの仕組み

新しい属性型はありません。埋め込みはアイテム上の普通の数値のリストで、ワイヤ上では {"L": [{"N": "0.0132"}, {"N": "-0.0475"}, …]} となり、すでに使っている同じ PutItem と UpdateItem で書き込まれます。

インデックスは別個の構造です。DynamoDB はベクトルを 32 ビット float 精度で、射影またはフィルターに使う属性とともに、そこへ非同期にレプリケートします。検索結果は の読み取りと同じく結果整合性です。

この遅延は、実際には小さいものです。計測した 1 回の実行では、PutItem の後に最初に送った検索がすでにそのベクトルを見つけており、それは書き込みを送ってから 136 ms 後でした(両方の往復を含みます。同じ実行で別に行った単発の PutItem は 99 ms でした)。

小さいとはいえゼロではないので、書き込みの確認に検索を使わないでください。ユーザーが保存したばかりのものを見る必要があるときは、GetItem と ConsistentRead: true で読み戻して自分で結果にマージするか、そのキーが現れるまで検索をリトライしましょう。

非同期レプリケーション、f32PutItem / UpdateItemベーステーブルNumber の List としての埋め込みベクトルインデックスベクトル + フィルター属性SearchVectorsTopK + 等価フィルタースコア付きの Top-K アイテム

既知のインデックスタイプの隣にどう位置づくか:

ベクトルインデックスGSILSI
テーブルあたりの最大数5(デフォルトクォータ)20(デフォルトクォータ)5
読み取り APISearchVectorsQuery、ScanQuery、Scan
PartiQLいいえはいはい
キャパシティモードオンデマンドのみ両方両方
整合性結果整合性結果整合性強力な整合性が可能
テーブル作成後の追加はいはいいいえ

各インデックスは、作成時に次元数と距離関数を固定します(置き換え方は落とし穴で扱います)。COSINE と EUCLIDEAN はスコアが低いほど類似、DOT_PRODUCT は高いほど類似で、負にもなりえます。

インデックスは f32 のコピーを保持します。より高精度の値も受け入れられ、取り込み時に丸められるので、リコールは float64 の元の値ではなく f32 に対して測ってください。

作成して検索する

サポートチケットに対するセマンティック検索を運用していて、エージェントがキーワード一致なしで「以前これに遭遇したユーザー」を見つけられるようにしたいとします。各チケットアイテムは件名と本文の埋め込みを持ち、それは好きなモデルで生成できます。

たとえば Titan Text Embeddings V2 は、Bedrock で入力 100 万トークンあたり $0.02 です(us-east-1、オンデマンド)。

テーブルはオンデマンドでなければなりません。プロビジョンドのテーブルは aws dynamodb update-table --table-name SupportTickets --billing-mode PAY_PER_REQUEST で切り替えます。AWS がこの切り替えを許すのはローリング 24 時間あたり 4 回までで、それぞれ数分かかります。

コスト面でこの移行が心配なら、上限としてオンデマンドの最大スループットを設定しましょう。トレードオフはオンデマンドとプロビジョンドの比較で扱っています。

次にインデックスを追加します。HASH 要素はすべての検索を 1 つの service 値にスコープし、INLINE_FILTER 属性(最大 18 個)は検索時の等価フィルターを可能にします:

aws dynamodb update-table \
  --table-name SupportTickets \
  --attribute-definitions AttributeName=service,AttributeType=S \
                          AttributeName=severity,AttributeType=S \
  --vector-index-updates '[{"Create": {
    "IndexName": "TicketEmbeddings",
    "VectorAttribute": {"AttributeName": "embedding"},
    "SearchSchema": [
      {"AttributeName": "service", "SearchSchemaElementType": "HASH"},
      {"AttributeName": "severity", "SearchSchemaElementType": "INLINE_FILTER"}
    ],
    "Projection": {"ProjectionType": "KEYS_ONLY"},
    "Dimensions": 1024,
    "DistanceFunction": "COSINE"
  }}]'

各 HASH 値には、インデックス書き込み 10 MBps と、検索が調べるデータ 1 GBps の上限があります(デフォルトクォータで、AWS サポート経由で引き上げ可能)。スループットは値をまたいでスケールするので、値の種類が多い属性でインデックスをキー付けしましょう。

AWS 自身の例はアイテムサイズで割っています。アイテムあたり約 4 KB(1024 次元のベクトルだけで 4,161 B)なら、値あたり毎秒およそ 2,500 件の書き込みで、アイテムが大きければそれより少なくなります。それより忙しい値には、より細かい HASH(たとえば service#region)を使い、値ごとに 1 回検索して Score でマージします。検索スキーマは構築後に固定されるので、新しい HASH には古いインデックスと並べて置き換え用のインデックスを構築する必要があります。

既存のテーブルにインデックスを追加するとバックフィルが走り、それが終わるまで SearchVectors は ValidationException を返し、部分的な結果はありません。

AWS はバックフィルが "can take a substantial amount of time even when the base table holds very few items" と警告しているので、数分は見込んでください。

DescribeTable が ACTIVE と言った後も、検索エンドポイントはしばらく拒否し続けることがあります。ウェイターはありません。リトライループの中で実際の検索を投げて探ってください。

新しい空のテーブルで CreateTable の中でインデックスを作成すると、バックフィルを省けます。2026 年 8 月のプローブ(テーブル 1 つ、DescribeTable を 5 秒ごとにポーリング)では、呼び出しから約 26 秒で ACTIVE になり、最初の検索が成功しました。

検索は、クエリ埋め込みを {"N": …} 値の裸の JSON 配列として受け取ります。DynamoDB の L で包んではいけません。保存された属性はリスト型を使い、リクエストパラメータは使いません。この 2 つの取り違えは、ありがちな最初のミスです:

aws dynamodb search-vectors \
  --table-name SupportTickets \
  --index-name TicketEmbeddings \
  --search-vector file://query-embedding.json \
  --top-k 5 \
  --search-condition-expression "service = :svc AND severity = :sev" \
  --expression-attribute-values '{":svc": {"S": "billing"}, ":sev": {"S": "high"}}'

最も類似したものから順にソートされた最大 TopK 件のアイテムが、それぞれ Score 付きで返り、求めれば ConsumedCapacity も付いてきます。TopK の上限は 100 で、ページネーションはなく、レスポンスの上限は 16 MB です。

100 件を超える近傍が必要ですか? S3 Vectors(TopK は最大 10,000、ページネーションあり)を使うか、HASH やフィルターの値で検索を分割して Score でマージしましょう。ただし、分割した各検索が返すのはやはり最大 100 件です。

埋め込み自体は、射影したうえでリクエストしない限り結果から除外されます。含めないでおきましょう。ベクトルを返すと、レスポンスと検索の請求の両方が膨らみます。

SearchConditionExpression は等価(=)のみを受け付けます。AWS は <>、<、<=、>、>=、IN をまだ利用できないものとして挙げています。インデックスが HASH 属性を定義している場合、すべての検索はそれにちょうど 1 つの値を固定しなければなりません。それが変わるまでは:

  • IN、または複数の HASH 値: 値ごとに 1 回検索し、結果を Score でマージします。同じインデックス、同じ距離関数なので、スコアは比較できます。
  • 範囲(日付、価格): month のような粗いバケット属性を書き込み、それを INLINE_FILTER として宣言して、バケットに = でフィルターします。検索スキーマは固定なので、稼働中のインデックスには置き換え用を並べて構築する必要があり、複数のバケットにまたがる範囲ではバケットごとに 1 回検索して Score でマージします。
  • それ以外: 多めに取得して(TopK は最大 100)クライアント側でフィルターします。返ってくるのは射影した属性だけなので、フィルターに使う属性を射影しておきましょう。

SearchVectors には、インデックスの ARN に対する独自の IAM アクション dynamodb:SearchVectors が必要です。AWS は、DynamoDB の読み取りアクセスを付与する既存のポリシーにはそれが含まれないと注記しています。

また、独自のホスト名も使います。認証情報にアカウント ID が含まれていれば、AWS SDK for JavaScript v3 は検索を {account-id}.search-ddb.{region}.amazonaws.com へ送り、そうでなければ search-dynamodb.{region}.amazonaws.com へ送ります。

VPC エンドポイント、プロキシ、エグレス許可リストが dynamodb.{region} だけをカバーしている場所では、その両方と、デュアルスタックの search-dynamodb.{region}.api.aws を許可してください。そうしないと、書き込みは成功するのに、検索は理由を決して語らない接続エラーで失敗します。

--endpoint-url も渡さないでください。1 つの上書きでは両方のホスト名をまかなえません。

DynamoDB Local はベクトルに対応していません。上の update-table には ValidationException: Nothing to update を、SearchVectors には UnknownOperationException を返すので、開発アカウントの小さなオンデマンドテーブルに対してテストしてください。

ベクトルインデックスの課金

新しいバイト単位のメーターが 2 つ(ベクトル書き込み、ベクトル検索)と、インデックスのコピーに対する通常のストレージがあり、書き込みと検索リクエストごとに 1 KB の最低課金があって、通常のテーブル料金の上に載ります(us-east-1 の定価、AWS 料金 API より、2026-10-09 に再確認):

メーター標準Standard-IA
ベクトル書き込み$0.52/GB$0.65/GB
検索が調べる/返すベクトルデータ$0.002/GB$0.0025/GB
ストレージ(テーブルとインデックス)$0.25/GB-月$0.10/GB-月

AWS のチュートリアルは、List の中に 10 進数の桁として保存される埋め込みのベーステーブル側のコピーが、インデックスの f32 コピーより "can be considerably larger" と警告しています。インデックス化されると、ライブで計測した書き込みユニットは別の結果を示します。

インデックスなしでは、埋め込みは 10 進数の桁で課金されます。有効数字 9 桁で f32 サイズの約 1.9 倍です(桁が少なければ小さくなります)。その同じ属性にベクトルインデックスを向けると、ベーステーブルの書き込み課金は次元あたりちょうど 4 バイトまで下がります:

次元数インデックスなしの List 属性(課金)同じ属性、ベクトルインデックスあり(課金)
2561,914 B1,024 B
7685,760 B3,072 B
1,0247,653 B4,096 B
1,53611,501 B6,144 B
3,07222,957 B12,288 B

パディング属性を使って書き込みユニットの境界を二分探索し、書き込みごとに新しいアイテムキーを使って計測しました。プローブのフル 1024 次元のチケットアイテムは 5 書き込みユニットで課金され、embedding にインデックスがなければ 8 でした。ストレージの課金は計測していません。

同じ計測の中で、ベクトル書き込みのメーターは f32 サイズを忠実に追跡しました。VectorWriteRequestBytes は、裸のインデックスでは次元あたり 4 バイトにキーのオーバーヘッド 11 B を加えた値、プローブのインデックス(検索スキーマで短い文字列属性 service と severity の 2 つを追加)では 65 B を加えた値で返ってきました。フィルター属性が違えば、この値も変わると考えてください。

検索課金は、事前に計算できないメーターです。VectorSearchRequestBytes は検索が調べるベクトルデータに、返すデータを加えた量を数えるので、TopK と射影する内容に応じて増えます。

AWS のガイダンスは、次元数から見積もるのではなく、代表的なデータセットで ReturnConsumedCapacity を使って測ることです。

テーブル側のコストは料金計算ツールでモデル化してください。ベクトルのメーターは、そこですでに計算される書き込みユニットの上に積み重なります。

DynamoDB ベクトル検索 vs S3 Vectors

AWS は 2 つのサーバーレスなベクトルストアを売っており、両者は正反対のアクセスパターンのために作られています。

DynamoDB ベクトル検索S3 Vectors
GA2026 年 8 月2025 年 12 月
レイテンシー階級一桁ミリ秒(AWS の主張)頻繁アクセスで約 100 ms、低頻度で 1 秒未満(AWS の主張)
スケール上限ベクトル数の明示された上限なし。インデックス作成には 600 GB のテーブル上限(ソフト)インデックスあたり 20 億ベクトル
最大次元数4,0964,096
距離関数コサイン、ユークリッド、ドット積コサイン、ユークリッド
インデックス書き込みテーブルから非同期(結果整合性)強力な整合性
フィルタリング等価のみ、属性 18 個以下 + パーティションキー 1 個リッチなメタデータフィルター、ベクトルあたりフィルター可能 2 KB 上限
TopK100、ページネーションなし10,000、ページネーションあり
ストレージ$0.25/GB-月、2 回分(テーブル + インデックス)$0.06/GB-月、1 回分
書き込み$0.52/GB、リクエストあたり最低 1 KB$0.20/GB、PUT あたり最低 128 KB
クエリ調べて返した分に対して $0.002/GBリクエスト 100 万件あたり $2.50 + インデックス全体の処理バイト課金

ストリーミングのケースを決めるのは書き込みの最低課金です。1024 次元のベクトルを 1 件ずつ書き込む場合の、書き込み 100 万件あたりのコスト(定価と、計測したアイテムあたり 5 書き込みユニット + 4,161 ベクトル書き込みバイトから算出):

書き込みパターンDynamoDBS3 Vectors
ベクトル 1 件ずつの書き込み約 $5.14/M約 $24.41/M
バッチ(PutVectors あたり 500 件)n/a(書き込みはアイテム単位)約 $0.78/M

S3 の PUT あたり 128 KB の最低課金のため、1 件ずつの書き込みでは S3 が高価な選択肢になります。

1024 次元のコーパスについて、月間ストレージと、月 100 万クエリでの S3 Vectors のクエリ課金を us-east-1 の定価で示します。S3 Vectors のクエリ課金は公開された式、すなわちインデックス全体のサイズ × 段階制の料率に従います。

ここでの DynamoDB のストレージは、テーブルのコピーも f32 で保存されると仮定しており、これは計測していません。インデックスなしの 10 進数のサイズでは約 1.4 倍高くなります。

コーパスDynamoDB ストレージS3 Vectors ストレージS3 Vectors クエリ
100 万ベクトル約 $1.95約 $0.23約 $11
1,000 万ベクトル約 $19.50約 $2.35約 $80
1 億ベクトル約 $195約 $23.50約 $217

DynamoDB のクエリ課金は、各検索で計測されるバイト数に従います。AWS によれば、その値はアイテムを追加しても対数的にしか増えず("doubling the number of items in an index adds a small, constant amount")、HASH キーがそれをさらに縮めます。

月 100 万検索あたり、us-east-1 の定価で:

検索あたりの計測バイト数100 万検索あたりのコスト
約 22 KB(計測値: TopK 10、50 ベクトルのパーティション、キーのみのインデックス)約 $0.04
400 KB(仮定)約 $0.76
4 MB(仮定)約 $7.81
40 MB(仮定)約 $78

1 ベクトルのパーティションに対する同じ検索でも 21.4 KB が計測され、1 KB の最低課金をはるかに上回りました。AWS のチュートリアルは、自社の 50 アイテムの例について 31,449 バイトを報告しています。

計測値の行は小さなパーティションから得たものなので、どの行を信頼するにしても、その前に代表的なパーティションで自分の検索を計測してください。

どちらを使うべきか

  • ベクトルが、すでに DynamoDB に保持しているライブなアイテムを説明する(チケット、ナレッジベースの記事、ユーザープロフィール、エージェントメモリ): ベクトルインデックスを使いましょう。ベクトルはアイテムと一緒に書き込まれるので、ドリフトする同期パイプラインはありません。
  • 数百万の埋め込みを、たまにクエリする(ドキュメントに対する RAG、アーカイブ、夜間ジョブ): S3 Vectors を使いましょう。安価にバッチロードし、保存には $0.06/GB を支払い、数百ミリ秒を許容します。
  • ハイブリッドランキングを伴う高 QPS(テキスト + ベクトルの関連度、ファセット、集計): OpenSearch を使いましょう。Classic のサーバーレスコレクションは最低 2 OCU が課金されます(us-east-1 の定価 $0.24/OCU 時間で月約 $350)。NextGen には最低額がなく、ゼロまでスケールします。
  • リレーショナルデータに結合されるベクトル: pgvector 付きの Aurora PostgreSQL で、ゼロまでスケールします。定価での見積もりでは、小さな RAG ワークロードは月約 $50 未満に収まります。

テーブルから OpenSearch へデータを流すには、DynamoDB と Amazon OpenSearch Service のゼロ ETL 統合(OpenSearch Ingestion)を使います。ポイントインタイムリカバリと、新旧両方のイメージを有効にしたストリームが必要です。

S3 エクスポートを通じてスナップショットを読み込み、その後ストリームを追いかけます。テーブルの読み取り・書き込みキャパシティーは使いません。24 時間を超える中断があると更新が失われ、インデックスを一から構築し直すことになりかねません。

ほとんどの DynamoDB ショップにとって、答えは両方です。ホットでフィルター可能なベクトルは、書き込みがアイテムとアトミックになるテーブル上に保ち、ロングテールは S3 Vectors へアーカイブしましょう。強力な整合性を持つバッチ書き込みが、S3 Vectors をきれいなシンクにしてくれます。

DynoTable でのベクトル検索

DynoTable はインデックスを作成し、そこに埋め込みモデルを紐付けます。テーブルのインデックスペインでは、インデックスを作成が GSI と並んでベクトルインデックスを提示し、どのモデルがそれを満たすのか、そしてテキストをどの属性から取るのかを尋ねます。

インデックスの形(次元数、距離関数、検索スキーマ)は DynamoDB によって固定されます。モデルとソース属性は、あとからインデックスの行で編集できます。

アイテムエディターでの保存は、ソーステキストが変わっていれば、AI がオンでライセンスがあり設定済みである場合にアイテムを再埋め込みします。そうでなければ、ベクトルが古いテキストを表しているという通知とともに保存が行われます。

一括編集のコミット、PartiQL の書き込み、直接実行では再埋め込みされません。

文章を入力するか、生のベクトルを貼り付けるか、行の類似を検索で検索します。結果にはスコア列が付きます。完全な手順はベクトル検索のドキュメントにあります。

落とし穴

  • 静かな脱インデックス: インデックスの HASH 属性を欠くアイテムは問題なく書き込まれ、ベクトルインデックスには決して入りません。ライブで再現しました。PutItem はエラーなく成功し、15 秒後もベクトルはどのパーティションの結果にもありませんでした。

    書き込みパスでこの属性を必須にしましょう。ConditionExpression は保存済みのアイテムを検査するので、これには使えません。取り残されたアイテムは attribute_exists(embedding) AND attribute_not_exists(service) でフィルターした Scan で見つけ、書き直します。

  • 次元数の違う書き込みは拒否される: モデルを切り替えると、すべての書き込みが、インデックスとベクトル属性を名指しし、期待される次元数と実際の次元数(1024 に対して 512)を示す ValidationException で失敗します。

    モデルを切り替えるには、新しいベクトルを 2 つ目の属性に書き込み、その上に 2 つ目のベクトルインデックスを構築します。バックフィルし、関連性を比較し、読み取りを移してから、古いインデックスを削除しましょう。

  • 古くなった埋め込み: DynamoDB がベクトルを再計算することはありません。embedding を書き直さずにチケットのテキストを編集すると、検索は静かに古い内容に一致し、どのアイテムがずれたのかはどこにも記録されません。

    テキストを変える書き込みと同じ書き込みでベクトルを作り直し、自分で制御できない書き込み元についてはから再生成コンシューマーへ流しましょう。

  • TopK には関連性のカットオフがない: パーティションとフィルターに一致するベクトルが K 件以上あれば、どれほど質が低くても K 件返ってきます。良い一致が 3 件しかなくても 10 件を求めれば 10 件返り、それより少なく返るのは一致するベクトルがそれより少ないときだけです。

    自分のデータから Score のしきい値を決め、それを超えた結果は捨てましょう。スコアの向きは距離関数によって反転します。

  • 1 KB の最低課金: 低次元のベクトルは、書き込みでも検索でも、比例して安くは計測されません。約 250 次元(f32 で 1 KB)未満では、小さなモデルにしてもベクトルのメーターでは何も節約できません。

  • インデックスの形は固定: 次元数、距離関数、検索スキーマ、INCLUDE 射影の属性セットは変更できません。その場で削除して再作成すると、バックフィルの間ずっと検索が失敗します。

    代わりに、古いインデックスの隣に置き換え用を構築しましょう(AWS 自身の例でも、同じ属性に距離関数の異なる 2 つ目のインデックスを置いています)。それが応答するようになったら読み取りを切り替え、古いほうを削除します。

  • インデックスは 1 つずつ構築される: 1 つの構築中に同じテーブルで 2 つ目の作成や削除を行うと LimitExceededException で失敗し、置き換えには 5 インデックスのデフォルトクォータの中に空き枠が必要です。

    インデックスのストレージは、クエリされるか否かによらず、インデックスの生涯全体にわたって課金されるので、使わなくなったものは削除しましょう。

自分のテーブルで試す

採用を決める前に埋め込みのサイズを測りましょう。アイテムサイズ制限は依然として適用され、インデックス化された 3072 次元の埋め込みは、そのアイテムのすべての書き込みに 12 KB を上乗せし、さらにベクトル自体を書き込むたびに、ベクトル書き込みのメーターでほぼ同じだけ上乗せします。

アイテムサイズ計算ツールは埋め込みのインデックスなしのサイズを示し、検索の遅延は GSI の遅延と同じように振る舞います。

それから DynoTable を試してインデックスを作成し、モデルを紐付け、アイテムエディターで保存するたびにアイテムを再埋め込みしてみましょう。一括編集、PartiQL の書き込み、自分のコードからの書き込みには、上の再生成パスが引き続き必要です。

著者: Dmitriy · 更新日