5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Amazon DynamoDBのリアルタイムベクトル検索機能をちょっと整理する

5
Posted at

前書き

2026年8月5日、Amazon DynamoDBがリアルタイムベクトル検索に対応しました。

これまでAWSでベクトル検索をやりたい場合、AWS Marketplace利用しない場合、選択肢はだいたい次の5つでした。

  • Amazon OpenSearch Serverless
  • Amazon OpenSearch Service Managed Cluster
  • Amazon Aurora PostgreSQL(pgvector)
  • Amazon Neptune Analytics
  • Amazon S3 Vectors

S3 Vectors以外に安く試せる候補が増えたのは素直にありがたいです:relaxed:
すでに運用データがDynamoDBに入っているアプリケーションなら、別のベクトルDBへ同期するパイプラインを新たに組まずに済むのも大きいポイントです。

早速手を動かして試してみました:point_up_tone1:

結論だけ先に

料金は従量課金で、今回程度の検証(20件のシード+数回の検索)なら1円にも届かない水準でした。

一方で、現時点(2026年8月6日)の制限はいくつか押さえておく必要があります。

  • ベクトル属性として書き込めるのはList型にNumberを並べたもの(Lの中にN)だけです。次元数は最大4,096、精度は32bit浮動小数点(f32)相当までで、それより高精度な値を書き込んでもベクトルインデックス側では丸められます。画像や動画のバイナリそのものを直接保存・検索する機能はありません
  • テーブルはオンデマンドモードのみ対応で、プロビジョンドキャパシティのテーブルでは使えません
  • Amazon Bedrock Knowledge Basesのベクトルストアの選択肢にはまだ入ってありません
  • CloudFormation(CDKのL1コンストラクトCfnTable含む)もVectorIndexesにまだ対応していません

料金詳細 :money_mouth:

DynamoDBの通常課金に加えて、ベクトル検索専用の課金が2つ追加されます。

項目 Standard Standard-IA
ベクトル書き込み(Vector Write) $0.52 / GB $0.65 / GB
ベクトル検索(SearchVectors) $0.002 / GB $0.0025 / GB

いずれも従量課金で、月額固定費や事前コミットメントは不要です。
ただしリクエスト単位では1KB未満のデータでも1KB分として課金される最低課金単位があります。埋め込み生成に使うBedrock Titan Text Embeddings V2は$0.02 / 100万トークンなので、20件のシードと数回の検索クエリ程度なら合計コストは1円にも届きません。

検索コストがインデックスのスキャンデータ量に比例することです。
パーティションキーなしの全件検索構成だと、データ量が増えるほど検索1回あたりのコストも線形に増えます。本番規模で使う場合は、ベクトルインデックスにパーティションキーを設定して検索範囲を絞る設計が公式でも推奨されています。

画像や動画の検索は間接的にできる:clapper:

DynamoDBのベクトル検索が直接扱えるのはList<Number>型の数値ベクトルだけです。
画像や動画のバイナリをそのまま保存したり、埋め込みに変換したりする機能はありません。そもそもDynamoDBのアイテムサイズ上限は400KBなので、画像でもサイズによっては厳しく、動画はまず収まりません。

とはいえ、間接的にはできます。やることは埋め込みの元をテキストから画像/動画に変えるだけです。

  • 画像: BedrockのTitan Multimodal Embeddings G1(モデルIDamazon.titan-embed-image-v1)。画像(最大2048×2048px、25MBまで)とオプションのテキストを渡すと、256/384/1024次元から選べる埋め込みが返ってきます
  • テキスト・画像・動画・音声: BedrockのAmazon Nova Multimodal Embeddings(モデルIDamazon.nova-2-multimodal-embeddings-v1:0)。複数のモダリティを同じベクトル空間に埋め込める統合モデルです

生成した埋め込みベクトルだけをDynamoDBの属性に保存し、画像や動画の実ファイルはS3に置いてそのキーを別属性として持たせておけば、SearchVectorsで検索してヒットしたアイテムからS3参照を取り出す、という構成になります。

機能詳細

DynamoDBには元々、セカンダリインデックスとして次の2種類がありました。

  • グローバルセカンダリインデックス(GSI)
  • ローカルセカンダリインデックス(LSI)

いずれも完全一致・範囲検索のためのものです。
今回追加されたVectorIndexesは、これに続く3つ目のセカンダリインデックスとして新設されたインデックスタイプで、類似度検索専用のSearchVectors APIで検索します。

作り方

VectorIndexesは既存のテーブルにアクション->ベクトルインディックス作成で追加できます、新規の場合は設定をカスタマイズ選択すれば、追加用のオプションが増えます。

作成時に指定するのは次の項目です。

  • インデックス名
  • ベクトルとして扱う属性名
  • 次元数(ディメンション)
  • 距離関数(ユークリッド、コサイン、ドット積から選択)
  • フィルタに使う属性
  • 属性の射影(Attribute projections)

7ED09BD2-5CBA-4BB7-8996-8B532305617B.jpeg

インデックスを追加した時点では、まだどのアイテムにもベクトル属性は入っていません。
その後PutItemで普通のNoSQL属性と同じように埋め込みベクトルを書き込むと、はじめて検索対象になります。

検索の仕方

SearchVectorsはクエリベクトルと返却件数(最大100件)、フィルタ条件を渡すだけで、類似度スコア順にランキングされた結果が返ってきます。
埋め込みの生成自体はAmazon Bedrock Titan Text EmbeddingsやCohere Embedなど好きなものを使え、DynamoDB側は生成済みのベクトルを保存して検索するだけです。

F4CA0F6A-BD40-44D6-89E3-5B96C61DA794.jpeg

検証手順

検証リポジトリはこちら:point_down_tone1:

今回は、コーヒー用品/ランニング用品/オフィス用品/オーディオ用品を数点ずつ用意し、Bedrock Titan Text Embeddings V2で埋め込みを生成してDynamoDBに投入したうえで、最後に自然言語で検索するところまでを一通り試す。
使ったバージョンはCDK CLIが2.1029.3aws-cdk-lib2.217.0です。リージョンはus-east-1に固定します。

まずはプロジェクトを作るところから始めます。

mkdir vector-search-demo && cd vector-search-demo
npx aws-cdk@2.1029.3 init app --language typescript

ベクトルインデックスの操作にはAWS SDK を直接使うので、あわせてインストールしておきます。

npm install @aws-sdk/client-dynamodb @aws-sdk/client-bedrock-runtime
npm install -D ts-node

npm install@aws-sdk/client-dynamodb^3.0.0の範囲で解決するので通常は最新版が入りますが、この機能はリリースから日が浅いぶん、古いバージョンが残っているとVectorIndexUpdatesSearchVectorsCommandを認識してくれません。ParamValidationError: Unknown parameterのようなエラーが出たらnpm update @aws-sdk/client-dynamodbを試してみてください。

続いて、テーブルをCDKで定義します。
ベクトル検索はオンデマンドモードのテーブルでしか使えないので、billingModePAY_PER_REQUESTにしておきます。

lib/vector-search-demo-stack.ts
import * as cdk from 'aws-cdk-lib';
import { CfnTable } from 'aws-cdk-lib/aws-dynamodb';
import { Construct } from 'constructs';

export class VectorSearchDemoStack extends cdk.Stack {
  public readonly table: CfnTable;

  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    this.table = new CfnTable(this, 'ProductsTable', {
      tableName: 'Products',
      attributeDefinitions: [{ attributeName: 'ProductId', attributeType: 'S' }],
      keySchema: [{ attributeName: 'ProductId', keyType: 'HASH' }],
      billingMode: 'PAY_PER_REQUEST',
    });

    new cdk.CfnOutput(this, 'TableName', { value: this.table.ref });
  }
}

あとはデプロイします。

npx cdk deploy

デプロイが終わったら、次にベクトルインデックスを追加します。
ここがCloudFormation未対応の部分なので、UpdateTableをAWS SDKから直接呼ぶスクリプトを書きます。

scripts/create-vector-index.ts
import { DynamoDBClient, UpdateTableCommand } from '@aws-sdk/client-dynamodb';

const TABLE_NAME = 'Products';
const VECTOR_INDEX_NAME = 'DescriptionIndex';

async function main() {
  const client = new DynamoDBClient({ region: 'us-east-1' });

  await client.send(new UpdateTableCommand({
    TableName: TABLE_NAME,
    VectorIndexUpdates: [{
      Create: {
        IndexName: VECTOR_INDEX_NAME,
        VectorAttribute: { AttributeName: 'DescriptionVector' },
        Projection: { ProjectionType: 'ALL' },
        Dimensions: 1024,
        DistanceFunction: 'COSINE',
      },
    }],
  } as any));

  // ACTIVEになるまでDescribeTableでポーリングする
}

main();

package.jsonにスクリプトを登録しておくと呼び出しが楽です。

package.json
"scripts": {
  "create-vector-index": "ts-node scripts/create-vector-index.ts",
  "check-vector-index-status": "ts-node scripts/check-vector-index-status.ts",
  "seed": "ts-node scripts/seed.ts",
  "search": "ts-node scripts/search.ts",
  "teardown-vector-index": "ts-node scripts/teardown-vector-index.ts"
}

UpdateTable実行後、5分でタイムアウトする前提でポーリング処理を書いていたのですが、実際にはACTIVEになるまで20〜30分かかりました。バグを疑って何度か調査しましたが、CloudTrailの履歴を見る限りリクエスト自体は最初から正しく受理されていて、単純にインデックス作成のプロビジョニングがドキュメントの想定より遅かっただけでした。GAから間もない機能を試すときは、時間的な余裕を持っておいたほうがよさそうです。ポーリングのタイムアウトは20分程度に伸ばし、create-vector-indexを再実行するとResourceInUseExceptionになってしまうので、状態だけ確認できるcheck-vector-index-statusを別スクリプトとして用意しておくと安全です。

インデックスがACTIVEになったら、いよいよ商品データを埋め込んでシードします。
埋め込みの生成にはBedrock Titan Text Embeddings V2を使い、出力次元数は1024にしています。

scripts/embed.ts
import { BedrockRuntimeClient, InvokeModelCommand } from '@aws-sdk/client-bedrock-runtime';

export async function embedText(client: BedrockRuntimeClient, text: string): Promise<number[]> {
  const response = await client.send(new InvokeModelCommand({
    modelId: 'amazon.titan-embed-text-v2:0',
    contentType: 'application/json',
    accept: 'application/json',
    body: JSON.stringify({ inputText: text, dimensions: 1024 }),
  }));

  const payload = JSON.parse(new TextDecoder().decode(response.body));
  return payload.embedding as number[];
}

商品ごとに説明文を埋め込みに変換し、BatchWriteItemでDynamoDBへ書き込みます。
ベクトル属性はList型にNumberを並べたものとして渡します。

scripts/seed.ts
function buildPutRequest(product: Product, embedding: number[]) {
  return {
    PutRequest: {
      Item: {
        ProductId: { S: product.productId },
        Name: { S: product.name },
        Description: { S: product.description },
        DescriptionVector: { L: embedding.map((value) => ({ N: value.toString() })) },
      },
    },
  };
}
npm run seed

実行後にDynamoDBコンソールでテーブルの中身を見ると、20件のアイテムそれぞれにDescriptionVector属性が入っているのが確認できます。

5775AC5A-5885-4328-B6EB-6EAD2B654B08.jpeg

DynamoDBはスキーマレスなので、DescriptionVectorを持たないアイテムがあっても壊れません。単にそのアイテムがベクトルインデックスの検索対象に入らないだけです。

データが入ったら、いよいよ検索です。SearchVectorsにクエリベクトルと返却件数を渡すだけですが、ひとつ注意点があります。SearchVectorsは通常のDynamoDB操作とは別の専用エンドポイント(search-dynamodb.<region>.amazonaws.com)を呼びます。

scripts/search.ts
import { DynamoDBClient, SearchVectorsCommand } from '@aws-sdk/client-dynamodb';

const searchClient = new DynamoDBClient({
  region: 'us-east-1',
  endpoint: 'https://search-dynamodb.us-east-1.amazonaws.com',
});

const response = await searchClient.send(new SearchVectorsCommand({
  TableName: 'Products',
  IndexName: 'DescriptionIndex',
  SearchVector: embedding.map((value) => ({ N: value.toString() })),
  TopK: 5,
} as any));

インデックスがACTIVEになった後、実際に日本語の自然文で検索してみました。

$ npm run search -- "12時間保温できる水筒"

1. [coffee] 真空断熱ステンレスマグ (P001)score=0.1458
2. [coffee] ドリップ式コーヒーサーバー (P004)score=0.8259
3. [running] ハイドレーションウエストベルト (P010)score=0.8574
4. [audio] ノイズキャンセリングヘッドホン (P016)score=0.8901
5. [running] トレイルランニングシューズ (P007)score=0.9084

距離関数はコサインなので、スコアは低いほど類似度が高くなります。
1位の「真空断熱ステンレスマグ」がスコア0.1458で、2位以下の0.82〜0.91から明確に離れていました。単語の一致ではなく「保温できる水筒」の意味に近い商品が上位に来ていて、ちゃんと意味検索として機能していることを確認できました。

最後に後片付けです。ベクトルインデックスが残っているとテーブル削除がブロックされるので、先に削除してからcdk destroyします。

npm run teardown-vector-index
npx cdk destroy

最後に

新しいベクトルDBの選択肢が増えたというより、既存のDynamoDBテーブルに検索機能が後付けできるようになった、という捉え方のほうが正確だと思います。運用データが既にDynamoDBにあるアプリケーションほど恩恵が大きいはずです。

CloudFormation対応やBedrock Knowledge Basesとの連携はまだこれからなので、フルに使い込むにはもう少し時間がかかりそうですが、テーブル本体はCDK、ベクトル機能はSDK直接操作という組み合わせなら、機能自体はもう今日から試せます。気になった方は、上の手順をそのまま新規プロジェクトで試してみてください。

参考

5
0
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
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?