はじめに
こんばんは、mirukyです。
DynamoDBにネイティブのベクトル検索が加わりました。別の検索基盤へデータを複製せず、通常の属性とベクトルを1レコードへ格納して近傍検索できる機能です。
今回は仕組みがAWSマネコン(マネジメントコンソール)から伝わるように、3次元の小さなデータを手で登録し、埋め込みモデルは使いません。テーブル作成から検索、次元数が合わない入力、お片付けまでマネジメントコンソールだけで進めます。
目次
1. DynamoDBのベクトル検索の仕組み
ベクトル検索は、レコードが持つ数値の並びを距離で比べ、検索ベクトルに近いレコードを返す機能です。商品説明が近い商品を探す、問い合わせ文に近いFAQを選ぶ、閲覧した商品に近い候補を出す、といった処理に使えます。
DynamoDBではベクトルを数値のリストとしてレコード内へ格納し、その属性を対象にベクトルインデックスを作ります。距離関数はユークリッド距離、コサイン距離、ドット積の3種類です。今回はベクトルの向きを比べるコサイン距離を使います。
DynamoDBが担当するのは、作成済みのベクトルを保存し、近いベクトルを検索するところまでです。文章や画像からベクトルを生成する機能ではないため、実際の構成ではAmazon Bedrockなどの埋め込みモデルで作った値をDynamoDBへ保存します。
通常の主キー検索では、指定したキーとの一致が取得条件です。一方、ベクトル検索では検索ベクトルとの距離を計算し、近いデータを近似最近傍探索で返します。同じテーブルにあるデータでも、item_idによる取得と、内容の近さによる検索では利用するインデックスが異なります。
ベクトルインデックスを持てるのはオンデマンドテーブルです。1ベクトルは最大4,096次元、1回の検索で返せる件数は最大100件で、検索結果は結果整合性です。インデックス作成後に次元数や距離関数は変更できないため、変更するときは新しいインデックスを作ります。
詳しいRAG周りの知識については、下記記事で解説しています。
2. ハンズオンの準備
操作先はアジアパシフィック(東京)、リソース名はqiita-vector-search-20260812です。コンソールへサインインしたIAMプリンシパルには、DynamoDBのテーブル作成、項目登録、ベクトル検索、テーブル削除を行える権限が必要です。
ベクトル検索では通常のオンデマンド料金に加え、ベクトル書き込み、ベクトル検索、ベクトルインデックス保存が個別に計測されます。今回は4件だけを短時間保存しますが、実行前にDynamoDBの料金ページを確認してください。
登録する値は、コーヒー方向を[1, 0, 0]、お茶方向を[0, 1, 0]、食べ物方向を[0, 0, 1]とした手作りのベクトルです。実際のアプリケーションでは、埋め込みモデルが出した次元数とインデックスの次元数を一致させます。
今回は3個の数値を、[コーヒーらしさ, お茶らしさ, 食べ物らしさ]と考えます。たとえばCoffeeは[1, 0, 0]、Latteはコーヒー寄りで少しだけお茶方向を含む[0.9, 0.1, 0]です。後ほど[1, 0, 0]で検索し、CoffeeとLatteが近くなる様子を確かめます。
これは距離の変化を目で追うための簡略化です。実際の埋め込みベクトルは数百から数千次元になることがあり、各次元へ人が意味を割り当てるとは限りません。ただし、保存するベクトルと検索に使うベクトルを同じモデルで生成し、次元数を一致させるという条件は同じです。
3. テーブルとベクトルインデックスの作成
この時点では、対象のテーブルもベクトルインデックスもありません。ここでは通常データを保存するテーブルと、embedding属性を距離で探すベクトルインデックスを一緒に作ります。
DynamoDBコンソールで新しいテーブルを用意し、今回だけ使う名前を入力します。
画面のテーブル名へqiita-vector-search-20260812を入力します。既存テーブルと区別できる名前にしておくと、最後の削除対象を特定できます。
主キーには各商品を一意に表す文字列を使います。
パーティションキーへitem_idを入力し、データ型は文字列のままにします。ソートキーは追加しません。
item_idはCoffeeやTeaを1件ずつ識別するための主キーです。後で追加するembeddingは近さを比べるための属性なので、項目そのものを一意に識別する役割はitem_idに残します。
続いて、ベクトルインデックスを利用できるキャパシティーモードへ変更します。
カスタム設定へ切り替え、キャパシティーモードをオンデマンドにします。テーブルクラスは初期値を変更しません。
ここでオンデマンドを選ぶのは、ベクトルインデックスを利用できるのがオンデマンドテーブルに限られるためです。アクセス数に応じて自動で処理容量が割り当てられるという特徴もありますが、今回の選択はベクトル検索の前提条件を満たすことが目的です。
同じページのベクトルインデックス欄で、検索対象と距離関数を指定します。
値は表の内容にします。
| 項目 | 値 |
|---|---|
| インデックス名 | embedding-index |
| ベクトル属性 | embedding |
| ディメンション | 3 |
| 距離関数 | コサイン |
| ルーティングキー | 入力しない |
| 属性の射影 | All |
embedding-indexは、後ほど検索先として選ぶインデックス名です。ベクトル属性をembedding、ディメンションを3にすると、各項目のembeddingには3個の数値が必要になります。この対応は検索ベクトルにも適用されます。
コサイン距離はベクトルの大きさより向きを比べる距離関数で、小さい値ほど向きが近いことを表します。今回はテナントやカテゴリ単位で検索範囲を分けないため、ルーティングキーは入力しません。属性の射影をAllにすると、検索結果でtitleやcategoryも一緒に確かめられます。
モーダル下部のインデックスの作成を押すと、テーブル作成内容へ設定が追加されます。まだAWS上のテーブルは作られていません。
追加後の行にはembedding-index、ベクトル属性embedding、ディメンション3、距離関数コサイン、射影すべてが表示されます。内容を確認し、ページ最下部の作成操作を確定します。
ここで確定すると、通常データを受け持つテーブルと、距離検索を受け持つベクトルインデックスの作成が始まります。設定欄へインデックスを追加しただけではAWS上のリソースは作られないため、ページ全体の作成操作まで進めてください。
作成には少し時間がかかります。テーブル一覧で対象名だけを検索し、利用可能になるまで待ちます。
対象行の状態がアクティブになりました。インデックスは1です。テーブル名を選び、インデックスの詳細へ進みます。
テーブルがアクティブならデータを保存できますが、ベクトル検索にはインデックス側の準備も必要です。作成処理は別々に進むため、次の画面でembedding-indexの状態も確かめます。
ベクトルインデックスのステータスもアクティブです。ここまで進んでから商品データを登録します。
これで保存先と検索経路の両方が利用可能になりました。次は空のテーブルへ4件の商品を入れ、検索結果の差が分かる小さなデータセットを作ります。
4. 検索用データの登録
現在はテーブルとインデックスだけがあり、商品データはまだありません。ここではitem_idやtitleなどの通常属性と、検索に使うembeddingを同じレコードへ登録します。
対象テーブルへ1件目のデータを加えます。
対象テーブルの項目一覧で項目を作成、JSON ビューの順に進みます。DynamoDB JSON の表示をオンにすると、文字列をS、数値をN、リストをLで入力できます。p1はこのJSONです。
{"item_id":{"S":"p1"},"title":{"S":"Coffee"},"category":{"S":"drink"},"embedding":{"L":[{"N":"1"},{"N":"0"},{"N":"0"}]}}
embeddingの外側にあるLはリスト、その中のNは各数値を表します。p1の値は[1, 0, 0]なので、このハンズオンでは検索の基準となるコーヒー方向そのものです。
貼り付けたらページ下部の項目を作成を押します。これで基準となるCoffeeが1件入りました。次はCoffeeに少し近いLatteを登録します。
{"item_id":{"S":"p2"},"title":{"S":"Latte"},"category":{"S":"drink"},"embedding":{"L":[{"N":"0.9"},{"N":"0.1"},{"N":"0"}]}}
Latteの[0.9, 0.1, 0]はCoffeeの[1, 0, 0]とほぼ同じ方向ですが、完全には一致しません。検索結果ではCoffeeの次に近く、距離は0より少し大きくなるはずです。JSONを保存したら、3件目のTeaへ進みます。
{"item_id":{"S":"p3"},"title":{"S":"Tea"},"category":{"S":"drink"},"embedding":{"L":[{"N":"0"},{"N":"1"},{"N":"0"}]}}
Teaは2番目の数値だけが1で、Coffeeとは直交する方向です。同じdrinkカテゴリでも、カテゴリ名ではなくベクトル同士の向きによって距離が決まることを確かめるために入れます。保存後は最後のCakeを登録します。
{"item_id":{"S":"p4"},"title":{"S":"Cake"},"category":{"S":"food"},"embedding":{"L":[{"N":"0"},{"N":"0"},{"N":"1"}]}}
Cakeは3番目の数値だけが1で、CoffeeともTeaとも別の方向です。Cakeを保存すると、距離の違いを比べる4件がテーブルに入ります。
一覧へ戻ると、4件をまとめて確認できます。
一覧にはp1からp4までが表示され、categoryとtitleも同じ項目に入っています。ベクトル専用ストアへ別データを作ったのではなく、通常のDynamoDB項目へembeddingを追加した形です。
ベクトルインデックスへの反映は非同期で、検索結果は結果整合性です。次の検索で4件が返らない場合は、データを作り直さず、少し待ってから同じ検索をもう一度実行してください。
5. 近い項目の検索
現在はアクティブなテーブルとインデックスがあり、3次元ベクトルを持つ4件の商品が登録されています。ここから主キーを指定せず、コーヒー方向に近いデータをベクトルインデックスから探します。
検索条件として、検索ベクトルと返却件数を用意します。
検索を選び、ベクトルインデックスはembedding-index、ベクトルを検索は[1, 0, 0]、結果の数 (トップ K)は4にします。属性の射影はすべての属性のままにします。
検索ベクトル[1, 0, 0]は、登録時と同じ3次元の座標系でコーヒー方向を表します。トップKは距離が近い順に何件返すかという指定で、今回は登録した4件すべてを比較するため4です。Allで射影した属性も返るため、距離値と商品名を同じ結果で見比べられます。
入力内容を確認したら、検索の実行へ進みます。
実行するを押し、検索完了を待ちます。
この操作ではitem_idの一致を探すのではなく、指定したベクトルと各embeddingのコサイン距離をベクトルインデックスで評価します。結果の距離値を見れば、登録時に意図した向きの差が反映されたか判断できます。
p1の距離値は0、p2は約0.0061でした。この列はコサイン距離なので、小さい値ほど検索ベクトルに近い結果です。直交するp3とp4はどちらも1になりました。
Coffeeは検索ベクトルと同じ向きなので距離が0です。Latteはわずかに傾いているため次に並び、TeaとCakeはコーヒー方向との共通成分がないため1になりました。商品名やカテゴリの文字列ではなく、embeddingの向きによって順位が決まったことが分かります。
6. 次元数が合わない入力
3次元の検索は成功し、インデックスから4件を距離順に取得できました。次は検索ベクトルだけを2次元にし、インデックス作成時のディメンションが入力値の契約として扱われることを確かめます。
インデックスは3次元で作ったため、検索側も3個の数値が必要です。検索ベクトルを[1, 0]へ変え、同じ条件で実行します。足りない値が自動で補われるのではなく、次元数が異なる入力として拒否されます。
画面にはInput search vector dimension 2 does not match vector index dimension 3と表示され、返された項目は0です。埋め込みモデルを変更すると出力次元数が変わる場合があるため、モデルとインデックスを組み合わせて管理する必要があります。
このエラーはデータ内容の近さを計算する前に、入力形式がインデックスと一致しないことを示しています。本番で埋め込みモデルを切り替える場合は、出力次元数を確認し、必要なら対応する新しいベクトルインデックスを用意します。
7. 後片付け
現在残っているのは、qiita-vector-search-20260812テーブル、embedding-index、4件の商品データです。テーブルを削除すると配下のインデックスと項目も失われるため、対象名を確かめてから後片付けを進めます。
対象テーブルの詳細へ戻ります。
アクションを開いてテーブルの削除を選ぶと、対象名を確かめる画面へ進みます。
確認欄へ確認と入力し、オンデマンドバックアップは作らずに削除を押します。テーブルを削除すると、今回作ったベクトルインデックスと4件の項目も削除されます。
削除を確定した時点で、インデックスや商品データを個別に消す操作は不要です。次は一覧を名前で検索し、削除対象が残っていないことを確かめます。
削除処理が終わったら、テーブル一覧でqiita-vector-search-20260812を名前検索します。
一致した結果は0 件で、一覧にもテーブルが見つかりませんと表示されました。これで今回のAWSリソースは残っていません。
おわりに
ここまでお付き合いいただきありがとうございます。
DynamoDBの通常属性とベクトルを1レコードへ格納し、コサイン距離で近い商品を返せました。手で作った3次元ベクトルを埋め込みモデルの出力へ置き換えれば、商品検索や文書検索も叶いますね。
ではまた、お会いしましょう。
















