🔍

【Go + AWS】 S3 VectorsとBedrockでベクトル検索を実装する

に公開

はじめに

AWSに新サービスのS3 Vectorsが登場しました。
本記事では、S3 VectorsとBedrockを活用し、ベクトル検索をGoで実装します。
OpenSearch Service Serverlessなどと比較し、大幅にコストを抑えることが可能です。

利用サービスの概要

S3 Vectors

S3 Vectorsは、ベクトル検索に特化したAWSの新しいベクトルストレージです。
これまでベクトル検索を行うには専用のデータベースを立てる必要がありましたが、S3 Vectorsを使用することでS3ライクな操作感でベクトルを保存・検索できます。

S3 Vectorsは以下の特徴を持っています。

  • 圧倒的なコスト削減
    • これまでのベクトル検索システムと比べて、運用コストを最大90%も削減できます。
  • 桁違いのスケール
    • 最大数十億のベクトルが保存でき、1秒未満で検索結果が返ってきます。
  • 運用の手軽さ
    • 面倒なインフラ管理は不要です。APIを叩くだけでベクトルの保存・検索ができます。

また、S3 Vectorsを構成する機能についても押さえておきます。

  • ベクトルバケット
    • ベクトルを保存・検索するために専用で構築されたバケットのこと
  • ベクトルインデックス
    • ベクトルバケット内のベクトルデータを管理する機能のこと
    • ベクトルインデックス内のベクトルデータに対して検索を実行する
    • ひとつのベクトルバケット内に最大10,000個のベクトルインデックスを作成可能

Bedrock

生成AIの基盤モデルを提供するAWSのマネージドサービスです。
インフラ管理が不要で、APIを叩くだけで高品質なエンベディングが生成できます。

事前準備

前提

今回使用する言語とパッケージのバージョンは以下です。

  • Go v1.25.0
  • github.com/aws/aws-sdk-go-v2 v1.38.3
  • github.com/aws/aws-sdk-go-v2/config v1.31.3
  • github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.39.0
  • github.com/aws/aws-sdk-go-v2/service/s3vectors v1.4.2

必ずしも揃える必要はありませんが、パッケージのバージョンの差異によっては動作に違いが出る可能性があります。

必要なパッケージをインストール

まずはGoのプロジェクトを作成して、必要なパッケージをインストールします。

go get github.com/aws/aws-sdk-go-v2/config
go get github.com/aws/aws-sdk-go-v2/service/s3vectors
go get github.com/aws/aws-sdk-go-v2/service/bedrockruntime
go get github.com/aws/aws-sdk-go-v2/aws

AWS認証の設定

ローカル環境の場合はAWS CLIを使って認証情報を設定します。
認証情報を設定するためには、S3 VectorsとBedrockの権限を付与したIAMユーザーでAWSコンソールにログインし、アクセスキーを発行する必要があります。
アクセスキーを発行した後、以下のコマンドで聞かれるアクセスキーIDなどを入力することで、AWSの認証が完了します。

aws configure

ベクトル検索を実装する

今回は2025年秋アニメの情報を元に、キーワードを使ってベクトル検索する形で実装します。
エンベディングするアニメの情報はアニメハックから少数ピックアップしました。

S3 Vectorsのベクトルバケット、ベクトルインデックスの作成

まずはS3 Vectorsのベクトルバケットとベクトルインデックスを作成します。

./aws/s3.go
package aws

import (
	"context"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/s3vectors"
	"github.com/aws/aws-sdk-go-v2/service/s3vectors/types"
)

type S3VectorClient struct {
	client *s3vectors.Client
}

// NewS3VectorClient S3 Vectorのクライアントを初期化する
func NewS3VectorClient(ctx context.Context, region string) (*S3VectorClient, error) {
	cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region))
	if err != nil {
		return nil, err
	}
	client := s3vectors.NewFromConfig(cfg)
	return &S3VectorClient{client: client}, nil
}

// CreateVectorBucket ベクトルバケットを作成する
func (s *S3VectorClient) CreateVectorBucket(ctx context.Context, bucketName string) error {
	_, err := s.client.CreateVectorBucket(ctx, &s3vectors.CreateVectorBucketInput{
		VectorBucketName: aws.String(bucketName),
	})
	return err
}

// CreateVectorIndex ベクトルインデックスを作成する
func (s *S3VectorClient) CreateVectorIndex(ctx context.Context, bucketName, indexName string, dimension int32) error {
	_, err := s.client.CreateIndex(ctx, &s3vectors.CreateIndexInput{
		VectorBucketName: aws.String(bucketName),
		IndexName:        aws.String(indexName),
		DataType:         types.DataTypeFloat32,
		Dimension:        aws.Int32(dimension),
		DistanceMetric:   types.DistanceMetricCosine,
	})
	return err
}

Bedrockで検索対象のエンベディングを生成する

今回はAmazon Titan Text Embeddings V2モデルでエンベディングを生成します。

Amazon Titan Text Embeddings V2モデルの利用料金は1,000トークン(入力)あたりで$0.000029(約0.004円)です。
最新の料金情報は公式のAmazon Bedrockの料金を参照してください。

./aws/bedrock.go
package aws

import (
	"context"
	"encoding/json"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/bedrockruntime"
)

type BedrockClient struct {
	client *bedrockruntime.Client
}

type EmbeddingRequest struct {
	// InputText エンベディング対象のテキスト
	InputText string `json:"inputText"`
	// Dimensions 次元数
	Dimensions int32 `json:"dimensions"`
	// Normalize ベクトルの長さ(ノルム)を1に正規化するか
	Normalize bool `json:"normalize"`
}

type EmbeddingResponse struct {
	Embedding           []float32 `json:"embedding"`
	InputTextTokenCount int       `json:"inputTextTokenCount"`
}

// NewBedrockClient Bedrockのクライアントを初期化する
func NewBedrockClient(ctx context.Context, region string) (*BedrockClient, error) {
	cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region))
	if err != nil {
		return nil, err
	}
	client := bedrockruntime.NewFromConfig(cfg)
	return &BedrockClient{client: client}, nil
}

// EmbedText テキストをエンベディングする
func (b *BedrockClient) EmbedText(modelID, inputText string, dimensions int32, normalize bool) ([]float32, error) {
	request := EmbeddingRequest{
		InputText:  inputText,
		Dimensions: dimensions,
		Normalize:  normalize,
	}
	requestBytes, err := json.Marshal(request)
	if err != nil {
		return nil, err
	}

	output, err := b.client.InvokeModel(context.Background(), &bedrockruntime.InvokeModelInput{
		Body:        requestBytes,
		ModelId:     aws.String(modelID),
		ContentType: aws.String("application/json"),
	})
	if err != nil {
		return nil, err
	}

	var resp EmbeddingResponse
	if err := json.Unmarshal(output.Body, &resp); err != nil {
		return nil, err
	}
	return resp.Embedding, nil
}

ベクトルバケットにベクトルを格納する

Bedrockで生成したベクトルをS3 Vectorsに格納します。
./aws/s3.goは「S3 Vectorsのベクトルバケット、ベクトルインデックスの作成」と同じファイルですが、このセクションではベクトルを格納する処理のみを記載しています。

./aws/s3.go
package aws

import (
	"context"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/s3vectors"
	"github.com/aws/aws-sdk-go-v2/service/s3vectors/document"
	"github.com/aws/aws-sdk-go-v2/service/s3vectors/types"
)

type S3VectorClient struct {
	client *s3vectors.Client
}

type VectorData struct {
	Key      string
	Data     []float32
	Metadata map[string]any
}

// PutVectors ベクトルを設置する
func (s *S3VectorClient) PutVectors(ctx context.Context, bucketName, indexName string, vectors []VectorData) error {
	inputVectors := make([]types.PutInputVector, len(vectors))

	for i, v := range vectors {
		inputVectors[i] = types.PutInputVector{
			Key:      aws.String(v.Key),
			Data:     &types.VectorDataMemberFloat32{Value: v.Data},
			Metadata: document.NewLazyDocument(v.Metadata),
		}
	}

	_, err := s.client.PutVectors(ctx, &s3vectors.PutVectorsInput{
		VectorBucketName: aws.String(bucketName),
		IndexName:        aws.String(indexName),
		Vectors:          inputVectors,
	})
	return err
}

今回は扱いませんが、ベクトルデータにはキーと値のペアで構成されるメタデータを設定することができ、検索時にメタデータを使ってベクトルをフィルタリングすることが可能です。
文字列、数値、ブール値、リスト型のメタデータがサポートされています。

キーワードを使って検索する

アニメ情報のベクトルがS3 Vectorsに格納できたので、エンベディングしたキーワードを使って検索処理を実装します。
./aws/s3.goは前セクションと同じファイルですが、このセクションではキーワードで検索する処理のみを記載しています。

./aws/s3.go
package aws

import (
	"context"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/s3vectors"
	"github.com/aws/aws-sdk-go-v2/service/s3vectors/types"
)

type S3VectorClient struct {
	client *s3vectors.Client
}

// QueryVectors 検索ベクトルを元に検索する
func (s *S3VectorClient) QueryVectors(ctx context.Context, bucketName, indexName string, queryVector []float32, count int32) ([]types.QueryOutputVector, error) {
	queryOutput, err := s.client.QueryVectors(ctx, &s3vectors.QueryVectorsInput{
		VectorBucketName: aws.String(bucketName),
		IndexName:        aws.String(indexName),
		QueryVector:      &types.VectorDataMemberFloat32{Value: queryVector},
		TopK:             aws.Int32(count),
		ReturnDistance:   true,  // 距離を返却する
		ReturnMetadata:   false, // メタデータは返却しない
	})
	if err != nil {
		return nil, err
	}
	return queryOutput.Vectors, nil
}

全体の実装

ベクトル検索を実装するためのパーツが全て揃ったため、全体の実装をmain.goに記載します。
処理の中に含まれていますが、ベクトルバケットやベクトルインデックスを毎回作成する必要はありません。

./main.go
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/wasuwa/vector/aws"
)

// 費用を抑えるために大量のデータは扱わない
var animations = map[string]string{
	"anime_1": "ワンパンマン(第3期) | 趣味でヒーローを始めた男、サイタマ。3年間の特訓により無敵のパワーを手に入れ、あらゆる敵を一撃(ワンパン)で倒すヒーローである。ひょんなことから弟子となったジェノスと共にヒーロー協会で正式なヒーロー活動を開始する。突如現れた怪人協会に幹部の子供を人質に取られてしまったヒーロー協会。S級ヒーロー達が集まり、人質奪還のために怪人協会のアジトへの突入作戦が練られる。一方、人間怪人ガロウは怪人協会のアジトで目を覚ます。",
	"anime_2": "SPY×FAMILY Season 3 | 人はみな 誰にも見せぬ自分を 持っている――世界各国が水面下で熾烈な情報戦を繰り広げていた時代。東国オスタニアと西国ウェスタリスは、十数年間にわたる冷戦状態にあった。西国の情報局対東課〈WISE(ワイズ)〉所属である凄腕スパイの〈黄昏(たそがれ)〉は、東西平和を脅かす危険人物、東国の国家統一党総裁ドノバン・デズモンドの動向を探るため、ある極秘任務を課せられる。その名も、オペレーション〈梟(ストリクス)〉。内容は、“一週間以内に家族を作り、デズモンドの息子が通う名門校の懇親会に潜入せよ”。〈黄昏〉は、精神科医ロイド・フォージャーに扮し、家族を作ることに。だが、彼が出会った娘・アーニャは心を読むことができる超能力者、妻・ヨルは殺し屋だった!3人の利害が一致したことで、お互いの正体を隠しながら共に暮らすこととなる。ハプニング連続の仮初めの家族に、世界の平和は託された――。",
	"anime_3": "とんでもスキルで異世界放浪メシ2 | ある日突然異世界へと召喚された普通のサラリーマン、向田 剛志(ムコーダ)。異世界の住人となった彼の固有スキルは 『ネットスーパー』という一見しょぼいものだった…。落胆するムコーダだったが、 実はこのスキルで取り寄せた現代の食品は異世界だととんでもない効果を発揮して……!?",
	"anime_4": "ガチアクタ | 『ガチアクタ』は犯罪者の子孫たちが暮らすスラム街に生まれた孤児の少年・ルドを主人公にしたバトルアクション。スラム街で生きる人間たちは、境界線の向こうの人々から“族民”とさげすまれ差別を受けながら生活している。そんな世界でルドは、育ての親であるレグトとともに、“ゴミ場荒らし”と呼ばれながらも常人離れした身体能力を武器に生計を立てていた。だがある日、身に覚えのない罪を着せられ、誰もが恐れる「奈落」へと落とされてしまう。",
	"anime_5": "忍者と極道 | 陰に忍んで悪しき闇を討たんとする、“忍者”。孤立した者の居場所となり悪事を重ねる、“極道”。江戸時代、明暦の大火の裏で刻まれた忍者と極道の因縁が、現代に再び燃え盛ろうとしていた――。トラウマにより笑顔を失った忍者・多仲忍者と表向きはエリート会社員ながら裏社会を取り仕切る極道・輝村極道。女児向けアニメの話題で意気投合した二人は、互いの正体を知らぬまま友情を深めていくのだが……。忍者と極道、両者の戦争が激化する中、その“運命”が交錯する。決めようか、忍者と極道、何方が生存るか死滅るか!!!",
}

const (
	// s3Region S3 Vectorsのリージョン(S3 Vectorsがプレビュー版なので東京リージョンは利用不可)
	s3Region = "us-east-1"
	// bedrockRegion Bedrockのリージョン(Amazon Titan Text Embeddings V2モデルは東京リージョンで利用可能)
	bedrockRegion = "ap-northeast-1"
	// bedrockModelID Bedrockで使用するモデルID
	bedrockModelID = "amazon.titan-embed-text-v2:0"
	// bucketName ベクトルバケット名(3 ~ 63文字まで)
	bucketName = "anime-vectors-bucket"
	// indexName ベクトルインデックス名(3 ~ 63文字まで、「a~z、0~9、-、.」が有効な文字)
	indexName = "anime-vectors-index"
	// dimension ベクトルの次元数(1024、512、256)
	dimension = 512
	// queryKeyword 検索キーワード
	queryKeyword = "ヒーローアニメ"
)

func main() {
	ctx := context.Background()

	s3Client, err := aws.NewS3VectorClient(ctx, s3Region)
	if err != nil {
		log.Fatalln(err)
	}

	bedrockClient, err := aws.NewBedrockClient(ctx, bedrockRegion)
	if err != nil {
		log.Fatalln(err)
	}

	// ベクトルバケットを作成
	if err := s3Client.CreateVectorBucket(ctx, bucketName); err != nil {
		log.Fatalln(err)
	}

	// ベクトルインデックスを作成
	if err := s3Client.CreateVectorIndex(ctx, bucketName, indexName, dimension); err != nil {
		log.Fatalln(err)
	}

	// 検索キーワードをエンベディングする
	embeddingQuery, err := bedrockClient.EmbedText(bedrockModelID, queryKeyword, dimension, true)
	if err != nil {
		log.Fatalln(err)
	}

	// アニメ情報をエンベディングしてS3 Vectorsに格納する
	if err := initAnimationVector(ctx, s3Client, bedrockClient); err != nil {
		log.Fatalln(err)
	}

	// アニメ情報のベクトルを元に、検索キーワードのベクトルで検索する
	results, err := s3Client.QueryVectors(ctx, bucketName, indexName, embeddingQuery, 1)
	if err != nil {
		log.Fatalln(err)
	}

	// 検索結果を出力する
	for _, result := range results {
		fmt.Println("Key:", *result.Key)
		// コサイン距離が0に近いほど類似している
		fmt.Println("Distance:", *result.Distance)
	}
}

// initAnimationVector アニメ情報のベクトルを初期化する
func initAnimationVector(ctx context.Context, s3Client *aws.S3VectorClient, bedrockClient *aws.BedrockClient) error {
	// 非同期で良い場合はバッチ推論でエンベディングした方が半額になるためコスト面で優れている
	// 今回はシンプルに書きたいためバッチ推論は使わない
	for key, inputText := range animations {
		embedding, err := bedrockClient.EmbedText(bedrockModelID, inputText, dimension, true)
		if err != nil {
			return err
		}

		if err := s3Client.PutVectors(ctx, bucketName, indexName, []aws.VectorData{
			{Key: key, Data: embedding, Metadata: map[string]any{}},
		}); err != nil {
			return err
		}
	}
	return nil
}

main.goが作成できたらgo run main.goを実行し、ターミナルで検索結果を確認しましょう。

Key: anime_1
Distance: 0.72144437

Key: anime_1はワンパンマン(第3期)のアニメ情報を指します。
検索キーワードはヒーローアニメなので、正しくベクトル検索ができていそうです。

ベクトルバケット、ベクトルインデックス、ベクトルデータを削除する

最後にサンプルで作成したベクトルバケット、ベクトルインデックス、ベクトルデータを削除しましょう。
2025年8月30日現在、これらはAWSコンソールから削除することができないため、AWS CLIやAWS SDKを使う必要があります。
今回はAWS CLIで削除するため、ターミナルで以下のコマンドを実行してください。

  1. ベクトルデータの削除
aws s3vectors delete-vectors --vector-bucket-name "anime-vectors-bucket" --index-name "anime-vectors-index" --region "us-east-1" --keys "anime_1" "anime_2" "anime_3" "anime_4" "anime_5"
  1. ベクトルインデックスの削除
aws s3vectors delete-index --vector-bucket-name "anime-vectors-bucket" --index-name "anime-vectors-index" --region "us-east-1"
  1. ベクトルバケットの削除
aws s3vectors delete-vector-bucket --vector-bucket-name "anime-vectors-bucket" --region "us-east-1"

ベクトルデータが残っているとベクトルバケットが削除できないため注意が必要です。

まとめ

S3 VectorsとBedrockを組み合わせることで、面倒なインフラ管理なしで安価なベクトル検索機能が簡単に実装することが可能です。
費用を抑えてスモールスタートしたい場合には良い選択肢だと個人的に考えています。
S3 Vectorsは今のところプレビュー版なので、正式版がリリースされるまで気長に待ちましょう。

Discussion