1. はじめに
Amazon DynamoDB がネイティブのベクトル検索に対応しました1。テーブルに VectorIndexes を足すと、SearchVectors API で意味の近いアイテムを検索できるようになります。
公式ドキュメントに、この機能が何を解決するのかを一文で示した記述があります2。
This eliminates the need for complex data replication pipelines between DynamoDB and external vector stores.
外部のベクトルストアへ同期するパイプラインが要らなくなる、という話です。DynamoDB を運用データの保存先として使ってきた立場では、この違いが大きくなります。
検証には、以前 OCI の閉域構成で RAG を組んだとき3に用意した Oracle Database のマニュアル抜粋を使いました。合成データではなく実際に運用で参照する文書で、ファイル名から自然にカテゴリが取れるため、後半で扱うパーティションキーの検証にもそのまま使えます。これを分割した 10,946件を投入して検証しました。(たまたま手元にあったデータを利用しているだけで、RAGの検証などでやっていたうまく回答できるか?とかを比較しているわけではありません)
1.1. 結論(先出し)
- 外部ストアも同期処理も無しに、テーブル 1 つでセマンティック検索ができる
- 書き込んだ内容はすぐに検索結果へ出てくる。ただしインデックスの
ItemCountは更新が遅れ、反映済みかの判定には使えない - HNSW のような調整用のパラメータは必要なくなるが、代わりにパーティションキー設計という判断が新しく必要になる
- パーティションキーは検索する量を減らすが、選び方を間違えるとエラーも警告も出ないまま返るスコアが悪くなる
1.2. 検証ゴール
| # | 確かめること | 確認できれば OK の条件 |
|---|---|---|
| 1 | テーブル 1 つでセマンティック検索ができる | 外部ストアを作らずに、内容の合う箇所が上位に返る |
| 2 | Tool Use から呼べる | LLM が検索ツールを選び、取得結果をもとに回答を組み立てる |
| 3 | パーティションキーが検索する範囲を決める | 条件の有無と、指定した値が内容と合っているかで挙動が変わることを数値で示せる |
2. 検証環境
| 項目 | 内容 |
|---|---|
| DynamoDB | ap-northeast-1・オンデマンド |
| ベクトルインデックス | 1024 次元・COSINE |
| 埋め込みモデル | Amazon Titan Text Embeddings v2(amazon.titan-embed-text-v2:0・normalize=true) |
| 生成モデル | Amazon Nova Lite(amazon.nova-lite-v1:0) |
| AWS CLI | 2.36.19 |
| boto3 / botocore | 1.43.67 |
| Python | 3.12.3(WSL Ubuntu 24.04) |
| コーパス | Oracle Database 26ai のマニュアル抜粋と自作ガイド 86 ファイル・20 MB |
コーパスは 2,000 文字を目安に分割して 10,946 チャンクになります。ファイル名のプレフィックスが 5 種類あり、これがそのままカテゴリになります。
| カテゴリ | チャンク数 | 中身 |
|---|---|---|
| DBRef | 3,013 | Database Reference |
| ADB | 2,947 | Autonomous Database |
| SQLRef | 2,595 | SQL Language Reference |
| Adm | 1,661 | Administrator's Guide |
| GUIDE | 730 | 自作の運用ガイドと日英対訳用語集 |
使用した生成モデルは今回の主題ではありません。手元のアカウントで使えたものを選んでいます。
ベクトルインデックスはオンデマンドキャパシティ専用です。プロビジョンドのテーブルには作れません4。
3. これまでの困りごと
DynamoDB のクエリは完全一致と範囲に限られます。「意味が近いものを探す」には、外部のベクトルストアが必要でした。
AWS 自身も、ネイティブ対応の前は OpenSearch との zero-ETL 連携を案内していました5。運用データは DynamoDB、ベクトルは OpenSearch という二重持ちになり、間をつなぐ同期を用意することになります。
面倒なのはパイプラインを作ること自体より、そのあと運用し続けることです。同期の失敗を検知して再送する、初期データを流し込む、片方だけ更新されてずれた状態を直す、といった作業が付いてきます。検索のために別サービスを 1 つ運用する形になります。
ネイティブ対応後はこうなります。
同期は DynamoDB 側で行われます。GSI を追加するのと同じ手順で、テーブルにインデックスを 1本足すだけです。
4. テーブルにベクトルインデックスを足す
4.1. 作り方
CreateTable に VectorIndexes を渡します。GSI の定義と並びが似ています。
client.create_table(
TableName="oracle-docs-vec",
AttributeDefinitions=[{"AttributeName": "DocId", "AttributeType": "S"}],
KeySchema=[{"AttributeName": "DocId", "KeyType": "HASH"}],
BillingMode="PAY_PER_REQUEST", # ベクトルインデックスはオンデマンド専用
VectorIndexes=[
{
"IndexName": "VectorIndexNoPk",
"VectorAttribute": {"AttributeName": "Vector"},
"Dimensions": 1024,
"DistanceFunction": "COSINE",
"Projection": {
"ProjectionType": "INCLUDE",
"NonKeyAttributes": ["Text", "SourceFile", "Category"],
},
}
],
)
調整用のパラメータはありません。指定するのは次元数と距離関数、そして何を投影するかだけです。pgvector で HNSW を作るときに指定する m や ef_construction に相当するものは公開されていません。
Projection を ALL にしなかったのは、レスポンスに 16 MB の上限があってページネーションが無いためです4。TopK を上げると上限に達しやすくなるので、必要な属性だけ投影しています。
インデックスが ACTIVE になるまでは 15 秒前後でした。3回試して 14.4 秒・20.1 秒・20.2 秒です。このときテーブルは空なので、データ量に依存しない立ち上げ時間だと考えられます。
作成したインデックスは、マネジメントコンソールの「インデックス」タブに「ベクトルインデックス」として並びます。
一覧の列がそのまま設定項目です。Partition key と インラインフィルター属性 が - なのは、SearchSchema を指定していないためです。ここに値が入ると挙動が変わります(6 章)。
なお、この画面の「サイズ」と「項目数」は 0 と表示されています。この時点で 10,946件を投入済みで、検索も返ってくる状態です。 統計値の更新が遅れているだけで、インデックスが空なわけではありません。
4.2. 投入
ベクトル属性は数値のリストとして渡します。
"Vector": {"L": [{"N": "{:.7g}".format(float(x))} for x in embedding]}
7 桁に丸めているのは、インデックス内では 32 bit 浮動小数点で保存されるためです4。丸めずに渡しても動きます6。
BatchWriteItem で 25件ずつ、10,946件の投入に 77.6 秒かかりました。未処理アイテムの再送は 1件も発生していません。
1 件あたりの保存サイズは 6,206 バイトで、400 KB の上限に対して 1.5% です。送信した JSON のバイト数(24,442 バイト)とは 4 倍違いました。DynamoDB は数値を 10 進文字列のまま持たず、有効桁 2 桁につき約 1 バイトで数えるためです7。
なお、元ファイルのうち 1 つは 423 KB あり、分割しなければ 1 アイテムに収まりません。RAG の文脈ではどのみちチャンク分割するので実害はありませんが、大きな文書をそのまま入れる想定なら事前に確認しておく必要があります。
投入した内容は待たずに検索へ出てきます。投入完了の 0.4 秒後に、コーパス全体から等間隔に抜いた 20件をそれ自身のベクトルで検索したところ、20件すべてが最上位で返りました。0.4 秒というのは最初に確認した時点で既に終わっていたという意味で、反映にかかる時間そのものではありません。
5. 実際に検索する
5.1. SearchVectors を直接呼び出す
Oracle の運用でよく参照する内容で検索しました。クエリは英語です。コーパスが英語のマニュアルなので合わせました。
日英の対訳用語集もコーパスに入れてありますが、1 回のベクトル検索では用語集のページが返るだけで、英語の本文には届きません。用語集を活かすなら「用語集を引く」「得られた英語で検索し直す」の 2 段階が要ります。日本語のまま検索しても上位 10件まで取れば結果はほぼ同じでしたが、上位 3件に絞ると英語のほうが当たりが多くなりました。
| クエリ | 検索時間 | 上位に返ったもの |
|---|---|---|
| how to find sessions using temporary tablespace | 20.9 ms | 一時表領域の説明を含む Adm のチャンク 3件 |
| change AWR snapshot retention period | 19.9 ms | AWR 関連パラメータの記述を含む DBRef のチャンク |
| Autonomous Database scaling | 16.2 ms | ADB のスケーリングに関するチャンク |
検索そのものは 16〜21 ms です。初回だけ 209 ms かかりました。
クエリの埋め込み生成のほうが時間がかかりました。Titan で 115〜194 ms で、検索の 6〜10倍です。RAG の応答時間を詰めたいなら、まず見るべきは検索側ではありません。
検索結果が的確かどうかをファイル名で判断すると誤読します。今回のコーパスは 1 ファイルが 360 KB あって複数章にまたがり、ファイル名は中の最初の節を指しているだけです。「一時表領域」の質問に Managing_Archived_Redo_Log_Files というファイルが返りますが、本文を見ると一時表領域の説明そのものでした。
5.2. 近似検索の取りこぼしを確かめる
SearchVectors は近似最近傍検索です2。取りこぼしがあるのかを見るため、埋め込みは手元にもあるので、全 10,946件との距離を総当たりで計算して突き合わせました。
6 クエリすべてで上位 5件のアイテムも順序も一致し、スコアの差は最大 1.2e-07 でした。この規模では総当たりと同じ結果が返っています。
ただし 10,946件は近似検索にとって小さい部類です。公式が挙げている高いリコール率を確認した、とまでは言えません。
5.3. Tool Use から呼ぶ
Bedrock の Converse API に検索ツールを 1 つだけ持たせて、LLM に使わせました。以前 Aurora の pgvector で組んだループ8の、検索する部分だけを SearchVectors に差し替えています。
resp = ddb.search_vectors(
TableName=TABLE,
IndexName=INDEX,
# 検索ベクトルの要素は bare float ではなく AttributeValue
SearchVector=[{"N": "{:.7g}".format(float(x))} for x in vec],
TopK=top_k,
)
for r in resp["SearchResults"]: # キーは Items ではない
item, score = r["Item"], r["Score"]
日本語で 3問聞いたところ、3問とも 1 ターン目で検索ツールが選ばれ、2 ターンで回答まで到達しました。所要は 1.45〜1.79 秒です。
LLM は日本語の質問を英語のクエリに直して呼び出していました。
| 質問 | LLM が組み立てたクエリ |
|---|---|
| 一時表領域を大量に使っているセッションを特定するには… | identifying sessions using a lot of temporary tablespace |
| AWR スナップショットの保存期間を変更する方法… | AWR snapshot retention period change |
| Autonomous Database で CPU をスケールさせるとき… | Autonomous Database CPU scaling connection |
指示していないのに英語へ寄せています。これは DynamoDB の機能ではなく LLM の判断なので、別のモデルや別の指示文なら変わります。
回答の裏も取りました。1問目が挙げた V$TEMPSEG_USAGE は、取得した 3件のチャンクに実在していました。2問目は「マニュアルからは確認できませんでした」と答えていて、こちらも正しい振る舞いです。正解の MODIFY_SNAPSHOT_SETTINGS はコーパスにありますが総当たりでも 10 位で、LLM が指定した top_k=5 では届きません。近似検索の取りこぼしではなく TopK と順位の問題で、取れなかったときに推測で補わなかったのは指示文でそう書いたためです。3問目は照合に使った語の選び方が不適切で、裏を取れていません。
6. 代わりに考えることが増える
ここまでは「楽になった」話です。ただし、すべてが簡単になるわけではありません。
6.1. パーティションキーという新しい判断
インデックス作成時に SearchSchema を定義すると、そのアイテムをパーティションに分けられます。
"SearchSchema": [
{"AttributeName": "Category", "SearchSchemaElementType": "HASH"}
],
これを付けると、1回の検索は 1 つのパーティションキー値の範囲だけが検索対象になります2。付けなければ全件を横断できますが、水平にスケールしません。
先ほどのテーブルに 2本目のインデックスを足して、挙動を比べました。
| 試したこと | 結果 |
|---|---|
| パーティションキーありのインデックスに条件を付けずに検索 | SearchConditionExpression must be provided when SearchSchema has a HASH key |
Category = 'ADB' などを指定(3 クエリ × 5 カテゴリ) |
15通りすべてで、指定外のカテゴリの混入は 0件 |
begins_with(Category, 'A') を指定 |
Invalid operator used in SearchConditionExpression |
インラインフィルタが等価のみという制約2は、実際に前方一致を指定するとエラーになります。範囲で絞りたい要件があるなら、この時点で設計を考え直すことになります。
6.2. パーティションを絞ると、エラーが出ないまま結果が悪くなる
同じクエリを、全件を対象にするインデックスと、Category が ADB のアイテムだけを対象にするインデックスの両方で検索した結果です。ADB はコーパスのカテゴリの 1 つで、Autonomous Database のマニュアルにあたります(2 章)。全体の 27% を占めます。
| クエリ | 全件を対象 | ADB カテゴリに限定 | 答えのある場所 |
|---|---|---|---|
| 一時表領域を使っているセッション | 0.348 | 0.584 | Adm |
| AWR スナップショットの保存期間 | 0.400 | 0.489 | DBRef |
| Autonomous Database のスケーリング | 0.209 | 0.209 | ADB |
3 つ目は上位 5件が完全に同じでした。もともと答えが ADB にあるので、絞っても何も失いません。
問題は 1 つ目と 2 つ目です。答えが別のカテゴリにあるため、絞った側では取得できていません。そして返ってくるのは「見つかりません」ではなく、そのパーティション内で最も距離が近いアイテムです。エラーも警告も出ず、スコアが悪くなるだけです。
スコアの閾値で足切りしていない実装では、この悪化を検出できません。公式が「クエリパターンに合うキーを選べ」と書いている9のは、性能の話であると同時に、外すと気づかないまま結果が悪くなるという話でもあります。
6.3. 後から変えるのは難しい
パーティションの切り方を後から変えられるかというと、制約がいくつかあります。
まず、次元数と距離関数はインデックス作成後に変更できません9。作り直しになります。
次に、SearchSchema に使う属性は AttributeDefinitions への宣言が必要です。宣言せずに UpdateTable を呼び出すと、GSI と同じようにエラーになります。
ValidationException: One or more parameter values were invalid:
One element in SearchSchema is not defined in attribute definitions
宣言だけの問題なら足せばよいのですが、その属性の値がアイテムに入っていなければバックフィルされません。「あとでカテゴリ別に分けよう」と思っても、分割の軸になる属性を最初からアイテムに持たせていなければ、全件の書き直しが要ります。
そして時間がかかります。既存 10,946件へのバックフィルは 521 秒でした。空のテーブルに作ったときの 15 秒に対して 35倍です。
t=0.0 秒 IndexStatus=CREATING Backfilling=false
t=130.3 秒 IndexStatus=CREATING Backfilling=true ← ここから始まる
t=521.1 秒 IndexStatus=ACTIVE
UpdateTable の呼び出し自体は 0.26 秒で返りますが、呼び出しからバックフィルが始まるまでに 130 秒かかります。この間 Backfilling は false のままなので、失敗したと勘違いしないよう注意が要ります。
HNSW の調整用パラメータが無くなった代わりに、後から変えにくい判断が 1 つ増えました。
6.4. 絞ると読む量は減る
パーティションキーが検索内容と合っている場合の効果も測りました。ReturnConsumedCapacity を付けると VectorSearchRequestBytes という値が返ります。
| クエリ | 全件を対象 | ADB カテゴリに限定 |
|---|---|---|
| 一時表領域 | 65,367 | 29,631 |
| AWR 保存期間 | 65,640 | 29,316 |
| ADB スケーリング | 66,408 | 30,284 |
対象を ADB カテゴリに絞ると、アイテム数は全体の 27% になりますが、消費量は 45% までしか下がりません。件数に比例しない固定分があるように見えますが、内訳は公開されていないので断定はできません。
なお、この値は読み取りキャパシティユニットではありません。ConsumedCapacity に CapacityUnits は返らず、この専用の単位だけが返ります。
7. 現時点の制約
本文で扱わなかったものも含めて、公式ドキュメント249に載っている制約をまとめます。
以下はいずれも本記事の執筆時点(2026 年 8 月)の値です。GA 直後の機能なので、上限が引き上げられたり、対応していない操作が使えるようになったりする可能性があります。実際に設計へ落とすときは公式ドキュメントで最新の値を確認してください。
| 項目 | 内容 |
|---|---|
| キャパシティモード | オンデマンド専用 |
| 最大次元数 | 4,096 |
TopK の上限 |
100 |
| 1 テーブルあたりのベクトルインデックス | 5 |
| 1 インデックスあたりのインラインフィルタ | 18 |
| インラインフィルタの演算子 | 等価のみ |
| レスポンスサイズ | 16 MB・ページネーション非対応 |
| ベクトルの精度 | 32 bit 浮動小数点 |
| 使えない操作 |
Query / Scan / PartiQL / DAX |
| きめ細かなアクセス制御 |
SearchVectors では非対応 |
| パーティションキーあたりの検索レート | 1 GB/秒 |
実装で引っかかった点も 2 つ挙げておきます。どちらも今回発生しました。
検索ベクトルは、CLI のヘルプには「32 bit 浮動小数点数のリスト」と書かれていますが、SDK のレベル API は AttributeValue のリストを要求します。[0.019, -0.001, ...] ではなく [{"N": "0.019"}, {"N": "-0.001"}, ...] です。CLI は内部で変換してくれますが、SDK は変換しません。
レスポンスのキーは SearchResults です。Query や Scan と同じつもりで Items を読むと空になります。
もう 1 つ、DescribeTable が返す ItemCount と IndexSizeBytes は統計値で、更新が遅れます。投入直後は 0 のままですし、後から足したインデックスは、バックフィルが終わって検索が正しく返る状態になってもなお 0 のままでした。インデックスが使えるかどうかの判定には使えません。実際に SearchVectors を実行して確かめる必要があります。
8. まとめ
DynamoDB を使いながらセマンティック検索が欲しくなったとき、これまでは外部のベクトルストアと同期のパイプラインが付いてきました。それがテーブルにインデックスを 1本足すだけになります。同期は DynamoDB 側で行われ、書き込んだ内容は待たずに検索へ出てきます。
一方で、すべてが簡単になるわけではありません。インデックスのチューニングは無くなりましたが、パーティションキーをどの軸で切るかという判断が新しく必要になります。しかもこれは後から変えにくく、選び方を外したときはエラーにならず、返るスコアだけが悪くなります。
全件を対象にしてよければ SearchSchema を付けなければ済みますし、1 万件程度なら 20 ms 弱で返ってきます。パーティションに分けるのは、クエリが必ずそのパーティションを対象にすると言い切れるときに限るのが良さそうです。
参考
-
Amazon DynamoDB now supports real-time vector search at any scale(AWS News Blog) ↩
-
Using vector indexes in DynamoDB(Amazon DynamoDB Developer Guide) ↩ ↩2 ↩3 ↩4 ↩5
-
Requirements and limitations と Quotas in Amazon DynamoDB の Vector indexes ↩ ↩2 ↩3 ↩4
-
Vector search for Amazon DynamoDB with zero ETL for Amazon OpenSearch Service(AWS Database Blog) ↩
-
Build semantic search with native vector support in Amazon DynamoDB(AWS Database Blog) ↩
-
DynamoDB item sizes and formats(Amazon DynamoDB Developer Guide) ↩
-
Bedrock Tool Use から Aurora pgvector を直接叩く — Knowledge Bases を使わない RAG の組み立て ↩
-
Best practices for vector indexes(Amazon DynamoDB Developer Guide) ↩ ↩2 ↩3
