S3 VectorsからDynamoDBのベクトル検索に寄せられるか実機で確かめてみる
はじめに
データ事業本部のkobayashiです。
Amazon DynamoDBのベクトル検索が2026年8月5日にGAしました。
今の業務では、LLMアプリのデータをDynamoDBに置きつつ、RAGのベクトルストアには2025年12月2日にGAしたAmazon S3 Vectorsを使っています。業務データとベクトルが別々のストアにある構成です。
この形だと気になるのは「この2つを1つにまとめられるのか」でした。そこで同じデータを両方に入れて動かしてみたところ、仕様表を眺めているだけでは気づかない差がいくつか出てきたので、その内容をまとめます。
基本的な使い方は以下の記事にまとまっているので、そちらは扱いません。
検証環境
- リージョン: ap-northeast-1(ローカルのMacから実行)
- ベクトル: 1024次元の正規化した擬似乱数を20,000本
- メタデータ: tenant_id(20値)、doc_type(5値)、chunk_text(非フィルタ
- botocore 1.43.67
SearchVectorsはSDKのサービスモデル更新が必要で、公式チュートリアルの前提条件にあるとおりbotocore 1.43.64以降(AWS CLIなら2.36.16以降)が要ります。これより古いとsearch-vectorsコマンドも--vector-indexesパラメータも存在しません。
埋め込みモデルは使わず擬似乱数を入れているので、検索結果の意味的な妥当性は確認できません。ただしこれから挙げる挙動はデータの分布に依存しないので、実データでも同じ結果になります。
なお料金の単価はすべてus-east-1のものです(2026年9月に料金ページで確認した値)。
何が同じで何が違うか
先に仕様の差をまとめておきます。
| DynamoDB ベクトルインデックス | S3 Vectors | |
|---|---|---|
| 器 | 既存テーブルのインデックス | ベクトルバケット + インデックス |
| 業務データとの関係 | 同じ項目に同居できる | 別ストア |
| 次元数 / 距離関数 | 1〜4,096 / COSINE・EUCLIDEAN・DOT_PRODUCT | 1〜4,096 / cosine・euclidean |
| 絞り込みの演算子 | 等価(=)のみ |
$eq $gt $lt $in $exists $and $or など |
| topKの上限 | 100 | 10,000(1ページ100件) |
| インデックス数 | 1テーブルあたり5(緩和可) | 1バケットあたり10,000 |
| 書き込み一貫性 | インデックスへの反映は結果整合 | 強整合 |
| アクセス制御 | インデックスのARN単位。FGAC非対応 | バケット・インデックス単位でIAM / SCP |
| ストレージ単価 | $0.25 / GB-month(実体テーブル分は別途) | $0.06 / GB-month |
| クエリ課金 | 処理バイト数のみ($0.002 / GB) | 固定費 $2.50 / 100万 + 処理量 + 返却量 |
一番大きな違いは器のほうです。 S3 Vectorsは「ベクトルDBを1つ導入する」意思決定になりますが、DynamoDBのベクトルインデックスは「すでにあるテーブルにインデックスを1本足す」意思決定になります。AWSのベクトルデータベース比較を見ても、pgvectorがRelational DB with vector support、DocumentDBがDocument DB with vector searchと並ぶなかで、S3 VectorsだけがCost-optimized vector storageという別枠にいます。
機能面では絞り込みの演算子が効いてきます。DynamoDB側は等価比較のみで、<> < <= > >= INは「まだ利用できない(not yet available)」とドキュメントに明記されています。なおS3 Vectors側の$gt $gte $lt $lteは数値型専用で、文字列の辞書順比較はできません。
ここで注意したいのは、効いてくるのは範囲フィルタだけではないという点です。「自分の部署のドキュメント または 全社共有のドキュメント」のような選択的な可視範囲や、「コネクタ由来 または 個人資料 または 組織資料」を1クエリで横断する構成も、$orを使っていれば同じように移せません。RAGでは珍しくない形なので、$orと$existsを使っているかは最初に確認しておくとよさそうです。
両方に同じデータを入れてみる
まずS3 Vectors側に20,000本を投入し、同じものをDynamoDB側にも揃えます。使ったスクリプトは以下に置いています。
seed_s3vectors.py: 検証用インデックスを作ってデータを投入s3v_to_ddb.py: S3 Vectorsを読んでDynamoDB側に同じものを揃えるsearch_cost.py: 両者に同じクエリを投げてレイテンシと課金対象バイト数を測るprobe_s3vectors.py: S3 Vectorsの書き込み直後の可視性を確かめる
$ python seed_s3vectors.py --region ap-northeast-1 \
--vector-bucket devio-ddb-s3v-compare --index docs --vectors 20000
...
[done] put=20000
$ python s3v_to_ddb.py --region ap-northeast-1 \
--vector-bucket devio-ddb-s3v-compare --source-index docs \
--table Documents --target-index DocsVectorIndex --create-table \
--hash-key tenant_id --inline-filter doc_type --segments 4
[source] {"dimension": 1024, "distance_function": "COSINE", "non_filterable_keys": ["chunk_text"]}
[schema] {"tenant_id": "S", "doc_type": "S"}
[create-table] Documents / DocsVectorIndex
[wait] IndexStatus=CREATING Backfilling=False
[wait] IndexStatus=ACTIVE Backfilling=False
...
[done] written=20000 skipped=0 failed=0
S3 Vectorsの投入は65秒ほどでした。
コマンドに出てくる--hash-keyと--inline-filterは、DynamoDBのベクトルインデックスに定義するSearchSchemaの指定です。ここが S3 Vectors のメタデータフィルタと考え方が違うところなので、先に整理しておきます。
HASH(ベクトルインデックスのパーティションキー) — 検索範囲を区切る属性です。ここにtenant_idを指定すると、検索は必ず1つのテナントの中だけを見ることになります。1インデックスに1つだけ指定できますINLINE_FILTER— 検索時の絞り込みに使う属性です。こちらは任意で、1インデックスに18個まで指定できます
S3 Vectorsではどのメタデータでも自由に絞り込めますが、DynamoDBはインデックスを作るときに宣言した属性しか絞り込みに使えません。しかも後から変更できないので、最初の設計がそのまま残ります。
ここからが本題です。
実機で分かったこと
件数確認にDescribeTableが使えない
20,000件すべて入っているのにDescribeTableは0を返してきます。
$ aws dynamodb scan --table-name Documents --region ap-northeast-1 \
--select COUNT --query 'Count' --output text | awk '{s+=$1} END {print s}'
20000
$ aws dynamodb describe-table --table-name Documents --region ap-northeast-1 \
--query 'Table.[ItemCount,TableSizeBytes,VectorIndexes[0].ItemCount]' --output text
0 0 0
ドキュメントには「約6時間ごとの更新」とあるので想定内ですが、実際に追いかけてみると対象ごとに更新のタイミングが揃いませんでした。
| 対象 | 値が入るまで |
|---|---|
| テーブル本体 | ロード完了から1時間42分以内 |
DocsVectorIndex(CreateTableで同時作成) |
ロード完了から1時間42分以内 |
DocsNoHashIndex(UpdateTableで後から追加) |
3時間後はまだ0、約23時間後には更新済み |
「0だから失敗」とも「0でないから成功」とも言えない状態が数時間続くので、ロードの検証にはScan --select COUNTを使うことになります。ただしScanで数えられるのは実体テーブルの項目数だけで、ベクトルインデックス側の件数は分かりません。
Backfillingの遷移が思っていたものと違った
既存のデータが入っているテーブルに後からベクトルインデックスを足すと、DynamoDBはそれまでの項目をインデックスに流し込む処理を行います。これがバックフィルで、進行中かどうかはDescribeTableが返すBackfillingフラグで分かります。このフラグがtrueの間はSearchVectorsがエラーになるので、インデックス作成後に検索を始めるコードでは待ち合わせが必要になります。
20,000件入りのテーブルに後からインデックスを追加して、30秒おきにDescribeTableを叩いた推移です。
10:45:03 CREATING False
10:45:34 CREATING True
...
10:53:16 CREATING True
10:53:46 ACTIVE None
ドキュメントには「IndexStatusがACTIVEになりBackfillingがtrueの状態でバックフィルが進む」と書かれているのですが、少なくとも30秒間隔のポーリングではその組み合わせは観測できませんでした。観測できたのはCREATINGかつBackfilling=trueで、ACTIVEになった時点ではnullに戻っています。
実務上は、Backfillingだけを見て待つコードだと即座に抜けてしまう点を押さえておけば十分です。IndexStatusと併せて見るのが安全です。なおCreateTableで同時に作った場合はAWS CLIでnullが返りますが、boto3ではキー自体が省略されます。
HASH属性が欠けた項目は静かに脱落する
ベクトルインデックスにHASHを定義した場合、その属性を持たない項目は実体テーブルへの書き込みは成功するのに、ベクトルインデックスには複製されません。エラーにならないので、「なぜか一部が検索に出てこない」という形で後から発覚します。
同様に、HASHに使うメタデータの型を先頭1件から推測すると危険です。途中で型が混ざっていると、テーブルは作れてしまうのに書き込みの途中で失敗します。移行スクリプトでは先頭200件をサンプリングして、型が割れていたら書き込み前に止めるようにしました。
HASHを定義すると条件式が必須になる
SearchVectorsはパーティションキーの値を必ず要求します。省くとこうなります。
botocore.exceptions.ClientError: An error occurred (ValidationException) when calling
the SearchVectors operation: SearchConditionExpression must be provided when
SearchSchema has a HASH key
S3 Vectors側にはこの制約はなく、フィルタは常に任意です。DynamoDB側はHASHを定義すると、そのインデックスでは全体検索ができなくなります。
SearchVectorsだけ別のエンドポイントに飛ぶ
SearchVectorsは通常のDynamoDBエンドポイントではなく、専用の検索エンドポイント(search-dynamodb.{region}.amazonaws.com)に解決されます。他の操作は従来どおりdynamodb.{region}.amazonaws.comです。
SDKやCLIが自動で振り分けてくれるので普段は意識しませんが、VPCエンドポイントやegressのallowlistで送信先を絞っている環境だと、CreateTableも書き込みも通るのにSearchVectorsだけが接続エラーになります。テーブルが作れてデータも入るぶん原因にたどり着きにくいので、先に許可しておくのが安全です。
S3 Vectors側はどうか
ここまでDynamoDB側の待ちの話が続いたので、S3 Vectors側も同じ観点で確かめてみます。確認用のベクトルを1本だけ書き込んで、その直後に読めるかを見たものです(最後に削除しています)。
$ python probe_s3vectors.py --region ap-northeast-1 \
--vector-bucket devio-ddb-s3v-compare --index docs
=== 1. PUT 直後に読めるか ===
PutVectors 496ms -> GetVectors 167ms : ['probe-readafterwrite']
QueryVectors 211ms : top=probe-readafterwrite distance=0.000000
=== 2. 次元数が違うベクトルは弾かれるか ===
ValidationException: Invalid record for key 'probe-baddim': vector must have length 1024, but has length 512
=== 3. 後片付け ===
DeleteVectors 直後の GetVectors: (消えている)
書き込んだ直後に引けました。バックフィルの待ちも反映待ちも要りません。ここまでのBackfillingやItemCountの話と比べると対照的です。
なお次元不一致はDynamoDB側も拒否するので、この点に差はありません。
課金対象バイト数を測る
DynamoDB側には便利な仕組みがあります。SearchVectorsにReturnConsumedCapacityを付けると、ConsumedCapacity.VectorSearchRequestBytesに課金対象のバイト数がそのまま返ってきます。S3 Vectors側にはこれに相当する値がAPIから返らないので、コストの当たりを事前につけたいならDynamoDB側の利点です。
HASHの有無で測り比べてみました。
| インデックス | 検索対象 | mean VectorSearchRequestBytes |
100万クエリあたり |
|---|---|---|---|
HASHあり(1テナントに限定) |
1,000本 | 24,153 | $0.0450 |
HASHなし |
20,000本 | 65,956 | $0.1229 |
3倍弱の差になりました。倍率そのものより、検索対象が20分の1なのにバイト数は3分の1程度にしかなっていない点が確認できたのが収穫です。少なくとも検索対象の本数に線形比例する課金ではありません。削減幅はデータ分布次第なので、見積もりに使うなら自分のデータで測ることになります。
ちなみに両者に同じクエリを投げた結果も載せておきます。
dynamodb : n=20 min=50.4ms p50=59.5ms p90=81.5ms max=299.6ms mean=75.7ms
s3vectors : n=20 min=75.4ms p50=107.9ms p90=126.2ms max=289.0ms mean=113.6ms
上位2件は両サービスで一致しました。ただしローカルのMacから投げているのでどちらの数字にも往復時間が乗っており、この条件で両者の設計上の差は評価できません。
どちらを選ぶか
- そのデータはすでにDynamoDBにあるか。 チャット履歴やユーザープロファイルへ類似検索を足したいなら、インデックスを1本足すだけで済みます。逆に埋め込みが読み取り専用の派生データなら、テーブルを新設してまで寄せる必然性は薄いです
$orや範囲フィルタに依存していないか。 組織階層のACLや複数情報源の横断検索も含みます。ここが一番引っかかります- topKを100より大きく取っていないか。 リランカーに候補を渡す構成だと設計変更が要ります
- テナントをインデックス単位で分けていないか。 DynamoDBは1テーブルあたり5個(緩和可)なので、S3 Vectorsの10,000個と同じ発想では組めません
- Bedrock Knowledge Basesを使っていないか。 DynamoDBはまだ選択肢になっていません
私の構成は2つ目と4つ目に当たりました。組織階層のアクセス制御を「自分の部署のドキュメント または 全社共有のドキュメント」という形で$orを使って表現しており、さらにテナントごとにインデックスを分けています。どちらもDynamoDB側では同じようには組めないので、今回は動かさないことにしました。
逆に言うと、業務データがDynamoDBにあって、絞り込みが等価比較だけで足りていて、テナント分離をインデックスに頼っていない構成であれば、まとめられる余地は十分にありそうです。
後片付け
検証用リソースはストレージ課金が続くので消しておきます。
$ aws dynamodb delete-table --table-name Documents --region ap-northeast-1
$ aws s3vectors delete-index --vector-bucket-name devio-ddb-s3v-compare \
--index-name docs --region ap-northeast-1
$ aws s3vectors delete-vector-bucket --vector-bucket-name devio-ddb-s3v-compare \
--region ap-northeast-1
まとめ
DynamoDBのベクトル検索とAmazon S3 Vectorsに同じデータを入れて、違いを確かめてみました。
仕様表を眺めているだけでは気づきにくいのは、DynamoDB側が「書いてすぐ検索できるとは限らない」ことでした。Backfillingの待ち、数時間0のままのItemCount、HASH属性が欠けた項目の静かな脱落と、確認の手間がそれなりにかかります。対するS3 Vectorsは書き込んだ直後から引けるので、この差は検証コードを書くときに効いてきます。
一方でDynamoDB側にはVectorSearchRequestBytesがあり、課金対象のバイト数をそのまま実測できます。HASHの設計を変えながら測れるのは便利なので、コストの当たりをつけたい方は試してみてください。
最後まで読んで頂いてありがとうございました。




