0
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 S3 Vectorsのメタデータフィルタでセマンティック検索に絞り込みを加える

0
Last updated at Posted at 2026-06-03

前回の記事では、IBM i (AS/400)のマスターデータに対するセマンティック検索を PostgreSQL + pgvector → Amazon S3 Vectors へ移行しました(その前段の1作目で pgvector による基本構成を構築しています)。サーバーレスでベクトル検索を運用できるようになりましたが、実際の業務では「全件を意味で検索する」のではなく、カテゴリなどで絞り込んだうえでセマンティック検索を行いたいという要望があります。

そこで今回は、S3 Vectors のメタデータフィルタを使って、セマンティック検索にカテゴリによる絞り込み機能を追加します。

本記事のソースコードはGitHubリポジトリで公開しています。

なぜ絞り込みが必要か

セマンティック検索は「意味」で関連性を取りに行く性質上、カテゴリをまたいで意味的に近いものを拾ってきてしまいます。たとえば「ベルト」で検索したときに、工具・自動車部品カテゴリの「タイミングベルト」「ファンベルト」だけでなく、衣料品カテゴリの「紳士用革ベルト」まで意味的な近さで上位に混ざる、というイメージです。

実運用では「工具カテゴリの中で意味検索したい」「ある部門の取扱品の中で意味検索したい」といった、先に絞り込んでから意味検索する ユースケースが多くあります。S3 Vectors のメタデータフィルタはまさにこの絞り込みを実現する機能です。

なお、S3 Vectors のメタデータフィルタは完全一致や範囲指定はできても、文字列の部分一致(LIKE)や曖昧検索はサポートしていません。「カテゴリ=工具」のような 構造化された属性での絞り込み専用 と捉えるのが正確です。

システム構成

ポイントは次の2点です。

  • 同期時にPutVectors でベクトルに category メタデータを付与 しておく
  • 検索時にUIで選択されたカテゴリをQueryVectorsFilter パラメータに渡す

S3 Vectors のメタデータとは

S3 Vectors はベクトル1件ごとに、JSONライクな構造のメタデータを付与できます。PutVectors API で Metadata フィールドにマップを渡すだけです。

types.PutInputVector{
    Key:      aws.String("SH00123"),
    Data:     &types.VectorDataMemberFloat32{Value: vector},
    Metadata: document.NewLazyDocument(map[string]any{
        "category": "T001", // 商品マスタのカテゴリコード (SHSHCG)
    }),
}

メタデータは、デフォルトで全キーがフィルタ可能です。インデックス作成時に MetadataConfiguration.NonFilterableMetadataKeys を指定すれば、特定キーを「フィルタ不可・保存のみ」にできます(フィルタ用インデックスを張らないことでコストやストレージを節約)。

Note: メタデータの構造(フィルタ可能/不可)はインデックス作成時に決まる項目なので、本番運用では「あとから増やすメタデータ」も見越して、最初に NonFilterableMetadataKeys を設計しておくと安全です。今回はカテゴリ1キーだけのシンプル構成なので、デフォルト(全キーフィルタ可能)でいきます。

実装:ベクトル保存時にメタデータを付与する

s3vectors_client.goUpsertProductVectors を拡張して、メタデータを受け取れるようにします。

// UpsertProductVectors は複数の商品コードとベクトルを一括保存する(500件ずつバッチ処理)
// metadatas は各ベクトルに対応するメタデータ。nilまたは長さ0の場合はメタデータなしで保存する。
func (v *VectorStore) UpsertProductVectors(
    ctx context.Context,
    codes []string,
    vectors [][]float32,
    metadatas []map[string]any,
) error {
    // (バリデーション省略)

    batchSize := 500
    for i := 0; i < len(codes); i += batchSize {
        end := min(i+batchSize, len(codes))

        putInputs := make([]types.PutInputVector, end-i)
        for j := i; j < end; j++ {
            input := types.PutInputVector{
                Key:  aws.String(codes[j]),
                Data: &types.VectorDataMemberFloat32{Value: vectors[j]},
            }
            if metadatas != nil && len(metadatas[j]) > 0 {
                input.Metadata = document.NewLazyDocument(metadatas[j])
            }
            putInputs[j-i] = input
        }

        _, 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("failed to put vectors (batch %d-%d): %w", i, end, err)
        }
    }

    return nil
}

document.NewLazyDocument は AWS SDK の汎用ドキュメント型を作るヘルパーで、map[string]any をそのまま渡せます。

同期処理(sync.go)側では、商品データから Category を取り出してメタデータマップに詰めます。

metadatas := make([]map[string]any, len(batch))
for j, p := range batch {
    texts[j] = p.Name + " " + p.Category + " " + p.Description
    codes[j] = p.Code
    meta := map[string]any{}
    if p.Category != "" {
        meta["category"] = p.Category
    }
    metadatas[j] = meta
}

embeddings, _ := s.cohereClient.GenerateEmbeddings(ctx, texts, "search_document")
s.db.UpsertProductVectors(ctx, codes, embeddings, metadatas)

実装:検索時にメタデータでフィルタする

クエリ側は QueryVectorsInput.Filter にメタデータ条件を渡します。条件は MongoDB ライクな構文で、$eq, $in, $and, $or, $gt などが使えます。今回はカテゴリの完全一致なので、シンプルに {"category": "T001"} (工具のカテゴリコード) です。

// SemanticSearch はベクトル類似度検索を実行する
// filter にメタデータ絞り込み条件を渡せる(空またはnilでフィルタなし)
func (v *VectorStore) SemanticSearch(
    ctx context.Context,
    queryVector []float32,
    filter map[string]any,
    limit int,
) ([]CodeWithScore, error) {
    input := &s3vectors.QueryVectorsInput{
        VectorBucketName: aws.String(v.bucketName),
        IndexName:        aws.String(v.indexName),
        QueryVector:      &types.VectorDataMemberFloat32{Value: queryVector},
        TopK:             aws.Int32(int32(limit)),
        ReturnDistance:   true,
    }
    if len(filter) > 0 {
        input.Filter = document.NewLazyDocument(filter)
    }

    output, err := v.client.QueryVectors(ctx, input)
    if err != nil {
        return nil, fmt.Errorf("failed to query vectors: %w", err)
    }

    // (結果整形は省略)
}

ハンドラ側では、リクエストの category パラメータを filter に変換して渡します。

var filter map[string]any
if category != "" {
    filter = map[string]any{"category": category}
}
codeResults, err := h.db.SemanticSearch(ctx, queryVector, filter, limit)

これで、セマンティック検索の結果から 指定カテゴリ以外の商品が一切返ってこなくなります

フロントエンド:カテゴリプルダウンを追加

検索バーの隣にカテゴリ選択プルダウンを置き、アプリ起動時にAS/400からカテゴリ一覧を取得して候補を埋めます。選択されたカテゴリは検索リクエストの category パラメータとしてサーバーに渡され、上で実装した Filter に反映されます。

S3 Vectors の挙動メモ:メタデータフィルタは in-tandem 方式

S3 Vectors はメタデータフィルタを ベクトル検索と同時に評価する(in-tandem 方式) という仕組みを採用しています。

S3 Vectors performs vector search and filter evaluation in tandem. (...) In contrast to applying the metadata filter after the vector search, this filtering approach is more likely to find matching results.
AWS S3 User Guide: Metadata filtering

Pinecone など一部サービスで知られる「ANN で TopK を取ってから後段フィルタで絞った結果、TopK 未満しか返らない」式のハマりは、S3 Vectors では構造的に発生しにくい設計になっています。フィルタ条件にマッチするベクトルが十分に存在する限り、指定した TopK は満たされる挙動です。

ビフォーアフター

同じクエリでも、メタデータフィルタの導入前後で結果がどう変わるかを比較します。

クエリ「トルク」のセマンティック検索結果:

  • フィルタなし … トルクと意味的に近い別カテゴリ商品(例: 工具とは別カテゴリにある「トルクテスター」)が混在

サンプル画面_フィルタなし.png

  • カテゴリ「工具」で絞り込み … 工具カテゴリ内で関連性の高い順に整列。業務上見たい結果だけが並ぶ

サンプル画面_カテゴリ絞り込み.png

意味検索の「ふわっと拾える」良さを保ちつつ、業務上のスコープから外れたものをきちんと除外できるようになりました。

まとめ

今回の改善で、IBM i セマンティック検索デモは以下のように進化しました。

観点 これまで 今回の改善
絞り込み なし。全商品が候補 S3 Vectors の Filter で カテゴリ絞り込み
メタデータ運用 商品コード + ベクトルのみ category メタデータも一緒に保存
UI 検索バーのみ カテゴリプルダウン追加

S3 Vectors のメタデータフィルタは構造化された属性での絞り込み専用ですが、業務システムにおける「先にスコープを切ってから意味検索する」というユースケースに非常によくフィットします。インデックスの再作成は不要(メタデータ無しの既存ベクトルも、同じKeyで再 PutVectors すれば上書きでメタデータが付与される)なので、既存システムへの導入もスムーズです。

参考リンク


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