はじめに
DynamoDB に埋め込みベクトルを保存して、そのまま類似度検索までできるようになったようです。
この記事では、Terraform でベーステーブルを作り、Terraform 未対応の部分(ベクトルインデックス)は AWS CLI で追加する形で、実際にデータを入れて検索するところまで試してみます。
DynamoDBベクトル検索とは
ベクトルインデックスは、テーブルの属性に埋め込みベクトル(数値の配列)を持たせておくと、そのベクトルに対して近似最近傍検索(ANN)ができる新しいインデックス種別です。既存の GSI/LSI とは別枠で、テーブルごとに最大 5 個まで作成できます。
検索は専用の SearchVectors API を使います。
距離関数は
-
COSINE(方向のみ比較、テキスト埋め込みの定番) -
DOT_PRODUCT(大きさも考慮、レコメンドで人気度を重みづけしたい場合など) -
EUCLIDEAN(空間的な距離、画像の近似重複検出など)
の 3 種類から選び、インデックス作成後は変更できません。
最大 4,096 次元まで対応しています。
RAG での関連ドキュメント検索、AI エージェントの記憶、類似商品のレコメンド、異常検知などが主なユースケースとして挙げられています。操作データとベクトルを同じテーブルに置けるので、別ストアとの整合性を気にしなくてよくなるのが一番のうれしさかと思います。
やってみた
前提
- AWS CLI 2.36.16 以降
- Terraform 1.5.0 以降
1. Terraform でベーステーブルを作る
Terraform の AWS providerは 6.60.0(本記事執筆時点の最新)でも aws_dynamodb_table に vector_index 系の引数がありません。レジストリのリソースドキュメントを確認しましたが該当パラメータは存在しないため、ベクトルインデックスは AWS CLI 側で追加する構成にしました。
main.tf
# main.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.60"
}
}
}
provider "aws" {
region = "ap-northeast-1"
}
resource "aws_dynamodb_table" "products" {
name = "vector-search-demo-products"
billing_mode = "PAY_PER_REQUEST" # ベクトルインデックスはオンデマンドモード限定
hash_key = "ProductId"
attribute {
name = "ProductId"
type = "S"
}
}
output "table_name" {
value = aws_dynamodb_table.products.name
}
terraform init
terraform apply -auto-approve
2. AWS CLI でベクトルインデックスを追加する
以降のコマンドは同じシェルセッションで上から順にコピペで実行できます。
環境変数の設定
export REGION=ap-northeast-1
export TABLE_NAME=vector-search-demo-products
export INDEX_NAME=DescriptionIndex
export MODEL_ID=amazon.titan-embed-text-v2:0
export DIMENSIONS=256
ベクトルインデックスの作成
aws dynamodb update-table \
--region "$REGION" \
--table-name "$TABLE_NAME" \
--vector-index-updates \
"[
{
\"Create\": {
\"IndexName\": \"$INDEX_NAME\",
\"VectorAttribute\": {\"AttributeName\": \"DescriptionVector\"},
\"Projection\": {\"ProjectionType\": \"ALL\"},
\"Dimensions\": $DIMENSIONS,
\"DistanceFunction\": \"COSINE\"
}
}
]"
IndexStatus が ACTIVE になるまで待ちます。aws dynamodb wait table-exists はテーブル自体のステータスしか見ないため使えません。
IndexStatusのポーリング
while true; do
STATUS=$(aws dynamodb describe-table --region "$REGION" --table-name "$TABLE_NAME" \
--query 'Table.VectorIndexes[0].IndexStatus' --output text)
echo "IndexStatus=$STATUS"
[ "$STATUS" = "ACTIVE" ] && break
sleep 10
done
空のテーブルに新規作成する場合でも、実際に手元で試したところ ACTIVE になるまで約 8 分半かかりました。想定より待たされるので、その間に次の埋め込み生成コードを準備しておきます。
3. 埋め込みを作ってデータを投入する
3 カテゴリ(コーヒー用品・オフィス用品・ランニング用品)×3 商品、計 9 件だけの小さいカタログで検証します。テキストは英語のままにしています(Titan モデルの挙動をチュートリアル通りに再現するため)。
商品データ投入と埋め込み生成
# データファイル作成
mkdir -p emb
# 商品データ作成
cat > products.tsv <<'EOF'
p01 Insulated Travel Mug A vacuum insulated stainless steel mug that keeps hot drinks warm for up to twelve hours.
p02 Stovetop Espresso Maker A compact aluminum pot that brews strong espresso style coffee directly on a burner.
p03 Manual Burr Coffee Grinder A hand cranked grinder with adjustable ceramic burrs for consistent coffee grounds.
p11 Ergonomic Mesh Office Chair An adjustable desk chair with breathable mesh back and lumbar support.
p12 Sit Stand Desk Converter A height adjustable platform that raises a monitor and keyboard for standing work.
p13 Monitor Arm Mount A clamp mounted articulating arm that lifts a display off the desk surface.
p06 Lightweight Running Shoe A breathable mesh road shoe with cushioned foam midsole for daily training runs.
p08 Moisture Wicking Running Socks Ankle height socks knitted from synthetic yarn that pulls sweat away from the skin.
p10 Hydration Waist Belt An elastic belt that holds two small water flasks during long runs.
EOF
# 埋め込み生成とDynamoDB投入
while IFS=$'\t' read -r id title description; do
aws bedrock-runtime invoke-model \
--region "$REGION" \
--model-id "$MODEL_ID" \
--body "$(jq -n --arg t "$description" --argjson d "$DIMENSIONS" '{inputText:$t,dimensions:$d,normalize:true}')" \
--cli-binary-format raw-in-base64-out \
--content-type application/json \
--accept application/json \
"emb/$id.json" >/dev/null
item=$(jq -c --arg id "$id" --arg title "$title" \
'{ProductId:{S:$id},Title:{S:$title},DescriptionVector:{L:[.embedding[]|{"N":(.|tostring)}]}}' \
"emb/$id.json")
aws dynamodb put-item --region "$REGION" --table-name "$TABLE_NAME" --item "$item"
echo "put-item done: $id ($title)"
done < products.tsv
# DynamoDBに投入した件数を確認
aws dynamodb scan --region "$REGION" --table-name "$TABLE_NAME" --select COUNT --query 'Count'
# => 9
ベクトル属性は List 型(L)で、各要素が Number 型(N)というシンプルな構造です。
4. SearchVectors で検索する
クエリの埋め込み化と検索実行
# 検索クエリを埋め込みベクトルに変換し
QUERY_TEXT="How can I make my desk more comfortable to work at"
aws bedrock-runtime invoke-model \
--region "$REGION" \
--model-id "$MODEL_ID" \
--body "$(jq -n --arg t "$QUERY_TEXT" --argjson d "$DIMENSIONS" '{inputText:$t,dimensions:$d,normalize:true}')" \
--cli-binary-format raw-in-base64-out \
--content-type application/json \
--accept application/json \
query.json >/dev/null
jq '[.embedding[]|{"N":(.|tostring)}]' query.json > query-vector.json
# DynamoDB SearchVectors API で検索する
aws dynamodb search-vectors \
--region "$REGION" \
--table-name "$TABLE_NAME" \
--index-name "$INDEX_NAME" \
--search-vector file://query-vector.json \
--top-k 3 \
--projection-expression "ProductId, Title"
「デスク周りを快適にしたい」という英文クエリに対して、実際に返ってきた結果です。
| 順位 | ProductId | Title | Score(COSINE, 低いほど類似) |
|---|---|---|---|
| 1 | p11 | Ergonomic Mesh Office Chair | 0.607 |
| 2 | p12 | Sit Stand Desk Converter | 0.713 |
| 3 | p06 | Lightweight Running Shoe | 0.792 |
コンソールからも同じ検索ができます。テーブルのアイテム一覧で検索モードを「検索」に切り替えて実行します。
注意点として、コンソールの検索ベクトル欄は CLI の--search-vector({"N": "..."}が並ぶ DynamoDB AttributeValue形式)ではなく、[-0.234, 0.891, ...]のようなプレーンな数値配列を求めます。{"N": "..."}形式のまま貼り付けると「検索ベクトルは有効な数値の配列でなければなりません」というエラーになるので、以下のようにembeddingをそのまま抜き出してから使ってください。
jq '.embedding' query.json
5. 後片付け
ベクトルインデックスとテーブルの削除
# ベクトルインデックスを削除してからテーブルを削除する
aws dynamodb update-table \
--region "$REGION" \
--table-name "$TABLE_NAME" \
--vector-index-updates "[{\"Delete\": {\"IndexName\": \"$INDEX_NAME\"}}]"
# IndexStatus が null になるまで待つ
while true; do
COUNT=$(aws dynamodb describe-table --region "$REGION" --table-name "$TABLE_NAME" \
--query 'length(Table.VectorIndexes || `[]`)' --output text)
echo "残インデックス数: $COUNT"
[ "$COUNT" = "0" ] && break
sleep 10
done
# テーブルを削除する
terraform destroy -auto-approve



