前回の記事では、IBM i (AS/400)のマスターデータに対してPostgreSQL + pgvectorを使ったセマンティック検索を実装しました。今回は、サーバーレスでインフラ管理の負担を削減できるというAmazon S3 Vectorsが登場したので、ベクトルストアを置き換えてみました。
記事のソースコードはGitHubリポジトリで公開しています。
前回のおさらい
前回構築したシステムでは、以下の構成でセマンティック検索を実現していました。
- IBM i (DB2): マスターデータの保持(Single Source of Truth)
- AWS Bedrock Cohere: テキストのベクトル化
- PostgreSQL + pgvector: ベクトルデータの保存・類似度検索
この構成で「意味による検索」は実現できましたが、実際に構築してみると気になる点がいくつかありました。
pgvectorの運用課題
1. インフラ管理の負担
PostgreSQLのインスタンスを維持するには、以下の運用作業が必要です。
- インスタンスの起動・停止管理: 開発環境では不要な時間帯に停止したいが、手動管理は面倒
- バージョンアップ対応: PostgreSQLおよびpgvector拡張のアップデート
- スケーリング: データ量増加に伴うインスタンスサイズの見直し
2. コスト構造
RDS(またはEC2上のPostgreSQL)はインスタンス時間課金です。たとえ検索リクエストがゼロの時間帯でも、インスタンスが起動していればコストが発生します。一方S3 Vectorsはストレージ + API呼び出しの従量課金で、インスタンスの維持費がかかりません。ただし、クエリ頻度が高い場合はAPI課金が積み上がるため、一概にどちらが安いとは言えません。コスト構造が異なるので、利用パターンに応じた比較が必要です。
3. ベクトル保存のためだけにPostgreSQLを運用するコスト
前回の構成でPostgreSQLに保存していたのは、商品コードとベクトルだけです。RDBMSの機能はほぼ不要でした。ちょうどそのタイミングで登場したAmazon S3 Vectorsは、まさにこのユースケースに合致するサービスでした。
Amazon S3 Vectorsとは
Amazon S3 Vectorsは、2025年12月にGAとなったS3のネイティブベクトルストレージ機能です。S3の上にベクトル専用のバケットタイプが追加され、ベクトルの保存・類似度検索をサーバーレスで実行できます。
主な特徴
| 特徴 | 内容 |
|---|---|
| サーバーレス | インスタンスの管理不要。自動的にスケール |
| 従量課金 | ストレージ + PUT + クエリの3要素で課金 |
| 強い整合性 | 書き込み直後から検索可能 |
| スケール | 1インデックスあたり最大20億ベクトル |
| 対応距離関数 | コサイン類似度、ユークリッド距離 |
S3 Vectorsの構成要素
Vector Bucket(ベクトル専用バケット)
└── Vector Index(インデックス)
├── 次元数: 1024(作成時に固定)
├── 距離関数: cosine
└── Vectors(個々のベクトル)
├── Key: 商品コード
├── Data: float32の1024次元ベクトル
└── Metadata: オプションのメタデータ
システム構成の変化
Before: PostgreSQL + pgvector
After: Amazon S3 Vectors
アーキテクチャ図の変更は最小限です。PostgreSQLがS3 Vectorsに置き換わっただけで、データフローの基本構造は変わりません。これは前回の設計で商品コードとベクトルのみをPostgreSQLに保存していたことが功を奏しています。
データフロー
1. データ同期フロー(初回・定期実行)
2. セマンティック検索フロー(ユーザー検索時)
前回のフローと比較すると、PostgreSQLへのSQL操作がS3 VectorsのAPI呼び出しに変わっただけです。
事前準備:Vector BucketとVector Indexの作成
S3 Vectorsを利用するには、事前にVector BucketとVector Indexを作成しておく必要があります。AWSマネジメントコンソールから作成できます。
1. Vector Bucketの作成
S3コンソールから「ベクトルバケットを作成」を選択し、バケット名を指定します。
2. Vector Indexの作成
作成したVector Bucket内で「ベクトルインデックスを作成」を選択し、以下を設定します。
| 設定項目 | 値 | 説明 |
|---|---|---|
| インデックス名 | 任意 | 環境変数 S3V_INDEX_NAME で指定する名前 |
| 次元数 | 1024 | Cohere embed-multilingual-v3 の出力次元数に合わせる |
| 距離関数 | cosine | コサイン類似度による検索 |
| データ型 | float32 | ベクトルデータの型 |
作成後、環境変数にバケット名とインデックス名を設定します。
export S3V_BUCKET_NAME=your-vector-bucket
export S3V_INDEX_NAME=your-vector-index
Note: サンプルコードの
InitSchema関数でもプログラム的に作成を試みますが、既に存在する場合はスキップされます。本番環境ではコンソールやIaCで事前に作成しておくことを推奨します。
実装のポイント
1. pgvectorからの変更箇所
移行に必要なコード変更は、主にdatabase.goの置き換えだけです。
| 変更箇所 | pgvector(変更前) | S3 Vectors(変更後) |
|---|---|---|
| 依存ライブラリ |
pgvector-go, lib/pq
|
aws-sdk-go-v2/service/s3vectors |
| 初期化 | PostgreSQL接続 + CREATE EXTENSION vector
|
CreateVectorBucket + CreateIndex
|
| ベクトル保存 | INSERT ... ON CONFLICT DO UPDATE |
PutVectors API |
| 類似度検索 | SELECT ... ORDER BY name_vector <=> $1 |
QueryVectors API |
| インデックス | HNSW(手動作成) | 自動管理(設定不要) |
2. S3 Vectorsクライアントの実装
PostgreSQL接続を管理していたdatabase.goを、S3 Vectorsクライアントに置き換えます。
type VectorStore struct {
client *s3vectors.Client
bucketName string
indexName string
}
func NewVectorStore(ctx context.Context, region, bucketName, indexName string) (*VectorStore, error) {
cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region))
if err != nil {
return nil, fmt.Errorf("AWS設定の読み込みに失敗: %w", err)
}
client := s3vectors.NewFromConfig(cfg)
return &VectorStore{
client: client,
bucketName: bucketName,
indexName: indexName,
}, nil
}
3. スキーマ初期化:CREATE TABLE → CreateVectorBucket + CreateIndex
pgvector(変更前):
func (d *Database) InitSchema(ctx context.Context) error {
_, err := d.db.ExecContext(ctx, `
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE IF NOT EXISTS product_vectors (
id SERIAL PRIMARY KEY,
code VARCHAR(50) NOT NULL UNIQUE,
name_vector vector(1024) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_product_vectors_hnsw
ON product_vectors USING hnsw (name_vector vector_cosine_ops);
`)
return err
}
S3 Vectors(変更後):
func (v *VectorStore) InitSchema(ctx context.Context) error {
// Vector Bucketの作成
_, err := v.client.CreateVectorBucket(ctx, &s3vectors.CreateVectorBucketInput{
VectorBucketName: aws.String(v.bucketName),
})
if err != nil {
// 既に存在する場合はスキップ
var conflict *types.ConflictException
if !errors.As(err, &conflict) {
return fmt.Errorf("Vector Bucketの作成に失敗: %w", err)
}
}
// Vector Indexの作成
_, err = v.client.CreateIndex(ctx, &s3vectors.CreateIndexInput{
VectorBucketName: aws.String(v.bucketName),
IndexName: aws.String(v.indexName),
Dimension: aws.Int32(1024),
DistanceMetric: types.DistanceMetricCosine,
DataType: types.DataTypeFloat32,
})
if err != nil {
// 既に存在する場合はスキップ
var conflict *types.ConflictException
if !errors.As(err, &conflict) {
return fmt.Errorf("Vector Indexの作成に失敗: %w", err)
}
}
return nil
}
HNSWインデックスの作成が不要になりました。S3 Vectorsではインデックスの管理が自動化されているため、次元数・距離関数・データ型を指定するだけです。
4. ベクトル保存:INSERT → PutVectors
pgvector(変更前):
func (d *Database) UpsertProductVectors(ctx context.Context, codes []string, vectors [][]float32) error {
tx, _ := d.db.BeginTx(ctx, nil)
defer tx.Rollback()
stmt, _ := tx.PrepareContext(ctx, `
INSERT INTO product_vectors (code, name_vector)
VALUES ($1, $2)
ON CONFLICT (code) DO UPDATE SET name_vector = EXCLUDED.name_vector
`)
for i, code := range codes {
stmt.ExecContext(ctx, code, pgvector.NewVector(vectors[i]))
}
return tx.Commit()
}
S3 Vectors(変更後):
func (v *VectorStore) UpsertProductVectors(ctx context.Context, codes []string, vectors [][]float32) error {
// PutVectorsは1回のリクエストで最大500件
batchSize := 500
for i := 0; i < len(codes); i += batchSize {
end := i + batchSize
if end > len(codes) {
end = len(codes)
}
putInputs := make([]types.PutInputVector, end-i)
for j := 0; j < end-i; j++ {
putInputs[j] = types.PutInputVector{
Key: aws.String(codes[i+j]),
Data: &types.VectorDataMemberFloat32{Value: vectors[i+j]},
}
}
_, err := v.client.PutVectors(ctx, &s3vectors.PutVectorsInput{
VectorBucketName: aws.String(v.bucketName),
IndexName: aws.String(v.indexName),
Vectors: putInputs,
})
if err != nil {
return fmt.Errorf("ベクトルの保存に失敗: %w", err)
}
}
return nil
}
トランザクション管理やprepared statementが不要になり、コードがシンプルになりました。同じKeyでPutVectorsを呼べば自動的に上書きされるため、ON CONFLICTのようなUPSERTロジックも不要です。
5. 類似度検索:SQL → QueryVectors
pgvector(変更前):
func (d *Database) SemanticSearch(ctx context.Context, queryVector []float32, limit int) ([]CodeWithScore, error) {
rows, _ := d.db.QueryContext(ctx, `
SELECT code, 1 - (name_vector <=> $1) as similarity
FROM product_vectors
ORDER BY name_vector <=> $1
LIMIT $2
`, pgvector.NewVector(queryVector), limit)
var results []CodeWithScore
for rows.Next() {
var cs CodeWithScore
rows.Scan(&cs.Code, &cs.Score)
results = append(results, cs)
}
return results, nil
}
S3 Vectors(変更後):
func (v *VectorStore) SemanticSearch(ctx context.Context, queryVector []float32, limit int) ([]CodeWithScore, error) {
output, err := v.client.QueryVectors(ctx, &s3vectors.QueryVectorsInput{
VectorBucketName: aws.String(v.bucketName),
IndexName: aws.String(v.indexName),
QueryVector: &types.VectorDataMemberFloat32{Value: queryVector},
TopK: aws.Int32(int32(limit)),
ReturnDistance: aws.Bool(true),
})
if err != nil {
return nil, fmt.Errorf("ベクトル検索に失敗: %w", err)
}
results := make([]CodeWithScore, len(output.Vectors))
for i, v := range output.Vectors {
// コサイン距離(1-cosine_similarity)からコサイン類似度を復元
similarity := 1.0 - float64(aws.ToFloat32(v.Distance))
results[i] = CodeWithScore{
Code: aws.ToString(v.Key),
Score: similarity,
}
}
return results, nil
}
SQL文を組み立てる必要がなくなり、TopKパラメータで直感的に取得件数を指定できます。
pgvector vs S3 Vectors 比較
機能比較
| 項目 | pgvector | S3 Vectors |
|---|---|---|
| インフラ管理 | RDSインスタンスの運用が必要 | サーバーレス(管理不要) |
| 初期セットアップ | PostgreSQLインストール、拡張有効化、テーブル作成、インデックス作成 | バケット作成、インデックス作成(2 API呼び出し) |
| 検索レイテンシ | ~10ms以下(HNSW) | 100ms〜1s(ウォーム時〜コールド時) |
| スケール上限 | インスタンスのメモリ・ディスクに依存 | 20億ベクトル/インデックス |
| コスト構造 | インスタンス時間課金(固定費型) | 従量課金(使った分だけ) |
| SQL対応 | フルSQL(JOIN、集約、トランザクション) | 専用API のみ |
| インデックス管理 | 手動(HNSW/IVFFlat選択・チューニング) | 自動(設定不要) |
| 距離関数 | コサイン、ユークリッド、内積 等 | コサイン、ユークリッドのみ |
コスト比較(参考)
1,000件の商品マスター(1024次元)を保存する場合の月額目安:
| 項目 | pgvector (RDS db.t3.micro) | S3 Vectors |
|---|---|---|
| インスタンス/ストレージ | ~$15〜20/月(固定) | ~$0.01/月 |
| 1日100回検索 | $0 | ~$0.01/月 |
| 1日10,000回検索 | $0 | ~$0.75/月 |
pgvectorはクエリ数に関係なくインスタンス費用が一定です。S3 Vectorsはクエリ数に比例してAPI課金が増えますが、1,000件規模であれば高頻度でもインスタンス費用を下回ります。ただし、データ量やクエリ頻度がさらに大きくなる場合は逆転する可能性があるため、利用パターンに応じた見積もりが必要です。
どちらを選ぶべきか
S3 Vectorsが向いているケース:
- ベクトルの保存・検索だけが目的(SQLのJOINやトランザクションが不要)
- インフラ管理の負担を最小限にしたい
- データ量が将来的に大きくなる可能性がある
pgvectorが向いているケース:
- 低レイテンシ(10ms以下)が求められるリアルタイムアプリケーション
- ベクトル検索とリレーショナルデータを組み合わせた複雑なクエリが必要
- 既にPostgreSQLを運用しており、追加コストが小さい
今回のIBM iセマンティック検索では、PostgreSQLに保存していたのが商品コードとベクトルだけだったため、S3 Vectorsへの移行が自然な選択でした。
移行時の注意点
1. レイテンシの変化
pgvectorのHNSWインデックスでは10ms以下の検索が可能でしたが、S3 Vectorsではウォーム時でも約100ms程度かかります。ユーザー向けの検索UIでは、AWS Bedrock Cohereによるベクトル化(数百ms)も含めた全体のレスポンスタイムを考慮すると、S3 Vectors側のレイテンシ増加は体感上の差を生みにくいと考えられます。
2. 近似検索であること
S3 VectorsのQueryVectorsは**近似最近傍検索(ANN)**を使用しており、平均リコール率は90%以上とされています。pgvectorのHNSWインデックスも近似検索ですが、パラメータのチューニングで精度を調整できます。S3 Vectorsではこの調整はできないため、精度が重要なユースケースでは注意が必要です。
3. 距離関数の制約
S3 Vectorsが対応する距離関数はコサインとユークリッドのみです。pgvectorでは内積(Inner Product)やL1距離なども利用できますが、今回のユースケースではコサイン類似度を使用しているため、問題ありません。
動作確認
S3 Vectorsに移行後も、前回と同様にセマンティック検索が動作することを確認できました。
検索クエリ「テスト」に対して、キーワード検索(左)では文字列の部分一致で4件がヒットし、セマンティック検索(右)では意味的に関連する「電子トルクテスター」などの商品が返されています。pgvector使用時と同等の検索品質が維持されていることが確認できます。
まとめ
PostgreSQL + pgvectorからAmazon S3 Vectorsへの移行により、以下の改善が得られました。
- インフラ管理の削減: PostgreSQLインスタンスの運用が不要に
- コスト最適化: 固定費型から従量課金型へ。小規模ユースケースでは大幅なコスト削減
- コードの簡素化: トランザクション管理やインデックス作成が不要に
一方で、レイテンシの増加(~10ms → ~100ms)や近似検索の精度調整ができない点はトレードオフです。
前回の記事で「PostgreSQLには商品コードとベクトルのみを保存する」という設計方針を採用していたことが、今回のスムーズな移行につながりました。ベクトルストアの役割を明確に分離しておくことで、技術の進化に合わせた柔軟な構成変更が可能になります。
参考リンク
- Amazon S3 Vectors 公式ドキュメント
- Amazon S3 Vectors GA 発表ブログ
- 前回の記事: IBM i(AS/400)にあるマスターデータを「意味」で検索する
- 本プロジェクトのGitHubリポジトリ
