S3 Vectors の新機能「メタデータプレフィルタリング」を試してみた
はじめに
2026年9月30日、Amazon S3 Vectors がメタデータのプレフィルタリングに対応しました。
この記事では、Classmethod ブログのタイトルと要約を組み合わせたベクトルを S3 Vectors に投入し、メタデータフィルターを使った QueryVectors を試しました。
あわせて、ENHANCED モードで一致件数が topK を下回るときの動作を確認し、従来の CLASSIC モードとの違いを整理しました。
データ準備・環境構成
ベクトルは過去に生成した 60,785件 × 1024次元のものを流用しました。
バケットとインデックスは東京リージョン(ap-northeast-1)に、インデックスモードを指定せずに作成しました。
aws s3vectors create-vector-bucket \
--vector-bucket-name <バケット名>
aws s3vectors create-index \
--vector-bucket-name <バケット名> \
--index-name articles \
--data-type float32 \
--dimension 1024 \
--distance-metric cosine
aws s3vectors get-index \
--vector-bucket-name <バケット名> \
--index-name articles
get-index の結果です(ARN 等は省略)。デフォルトで indexMode は ENHANCED です。
{
"index": {
"vectorBucketName": "<バケット名>",
"indexName": "articles",
"creationTime": "2026-10-02T01:27:42+09:00",
"dataType": "float32",
"dimension": 1024,
"distanceMetric": "cosine",
"indexMode": "ENHANCED"
}
}
投入は Python スクリプトから AWS CLI の put-vectors を 500件ずつ呼び出す形で行い、60,785件すべて成功しました。メタデータは language、published_year、slug、title の4つです。published_year は後で $gte を使うため数値で入れています。
# meta がある場合のみ metadata を付与
if meta:
metadata = {
"language": meta.get("language", ""),
"published_year": extract_year(meta.get("published_at", "")), # "2024-01-15" → 2024
"slug": meta.get("slug", ""),
"title": meta.get("title", ""),
}
else:
metadata = {}
item = {
"key": article_id,
"data": {"float32": vec},
}
if metadata:
item["metadata"] = metadata
ENHANCED モードでのメタデータフィルター
AWS のドキュメントによると、ENHANCED インデックスではフィルターを先に評価し、条件に一致したベクトルだけを対象に類似検索を行います。
クエリベクトルには、インデックス内の Cognito 関連記事(2024年)のベクトルを使いました。クエリに使った記事自身もインデックスに含まれるため、フィルターなしでは1位にその記事が distance 0.0000 で返ります。topK はすべて 10 です。
フィルターは --filter に JSON で渡します。次のコマンドの --filter だけを差し替えて、各パターンを実行しました。
aws s3vectors query-vectors \
--vector-bucket-name <バケット名> \
--index-name articles \
--query-vector '{"float32": [<クエリベクトル>]}' \
--top-k 10 \
--filter '{"published_year": {"$eq": 2026}}' \
--return-distance \
--return-metadata
| フィルター | 返却件数 | 返った記事の年 | distance の範囲 |
|---|---|---|---|
| なし | 10 | 2019, 2023, 2024, 2025, 2026 | 0.0000〜0.1388 |
{"published_year": {"$eq": 2026}} |
10 | 2026 のみ | 0.1287〜0.1843 |
{"published_year": {"$eq": 2024}} |
10 | 2024 のみ | 0.0000〜0.1610 |
{"published_year": {"$gte": 2024}} |
10 | 2024, 2025, 2026 | 0.0000〜0.1523 |
条件に一致する件数は、最も少ない published_year == 2026 でも 4,478件(60,785件中)あり、どのパターンも topK=10 を十分に上回ります。published_year == 2026 の1位(distance 0.1287)は、フィルターなしでは6位だった2026年の記事です。
$startsWith 演算子の動作確認
$startsWith は文字列の前方一致演算子で、ENHANCED インデックスで使えます(CLASSIC インデックスでの扱いは次の節で触れます)。英語の slug、日本語の title、年との $and の3パターンを試しました。
{"slug": {"$startsWith": "enable-"}}
{"title": {"$startsWith": "[アップデート]"}}
{
"$and": [
{"published_year": {"$gte": 2025}},
{"title": {"$startsWith": "[アップデート]"}}
]
}
| フィルター | 返却件数 | distance の範囲 |
|---|---|---|
slug が enable- で始まる |
10 | 0.0000〜0.2630 |
title が [アップデート] で始まる |
10 | 0.0000〜0.1735 |
2025年以降、かつ title が [アップデート] で始まる |
10 | 0.1287〜0.1952 |
どのパターンも、返った10件すべてが条件を満たしていました。title の結果には、[アップデート] Amazon Cognito … のように閉じ括弧の後に空白があるものと、[アップデート]AWS SSOで… のように空白がないものが混在していました。語の区切りではなく、文字列の先頭をそのまま比較しています。$and では、title だけの条件では上位にあった2024年以前の記事は外れ、2025年と2026年の記事だけが返りました。
ENHANCED と CLASSIC の違い
今回のバケット(2026年10月2日作成)で update-index-mode に CLASSIC を指定すると、次のエラーが返りました。
ValidationException: indexMode cannot be set to CLASSIC because the vector bucket does not support CLASSIC mode
2026年9月30日以降に作成したバケットは ENHANCED 固定です。それより前に作成したバケットは CLASSIC がデフォルトで、update-index-mode で ENHANCED へ変更でき、CLASSIC へ戻すこともできます。
CLASSIC の動作は手元で試せないため、違いはドキュメントと発表ブログをもとに整理します。
| 項目 | ENHANCED | CLASSIC |
|---|---|---|
| フィルターの評価 | フィルターを先に評価し、一致したベクトルだけを検索する | 候補ベクトルを探索しながら、同時にフィルター条件を判定する |
| 返却件数 | 一致件数と topK の小さい方 | 一致件数が少ないと topK 件に満たないことがある |
$startsWith |
使える | クエリに queryMode=ENHANCED を付けた場合だけ使える |
一致件数が topK を下回るときの動作
一致件数が topK を下回る場合の ENHANCED の動作を実データで確かめました。インデックス全体で3件しか存在しない slug prefix を使い、topK を3より大きく設定してクエリしました。
# インデックス全体で 'langfuse-' で始まる slug は 3件のみ
aws s3vectors query-vectors \
--vector-bucket-name <バケット名> \
--index-name articles \
--query-vector '{"float32": [<クエリベクトル>]}' \
--top-k 10 \
--filter '{"slug": {"$startsWith": "langfuse-"}}' \
--return-distance \
--return-metadata
| フィルター | 全体の一致件数 | topK 指定 | 返却件数 |
|---|---|---|---|
slug $startsWith 'langfuse-' |
3 | 10 | 3 |
slug $startsWith 'langfuse-' |
3 | 50 | 3 |
slug $startsWith 'asr-v3' |
3 | 10 | 3 |
published_year == 2024 |
6,628 | 10 | 10 |
topK に 10 や 50 を指定しても、一致が3件なら3件だけ返ります。ENHANCED ではフィルターを先に評価するため、返却件数は「インデックス全体の一致件数」か「topK」の小さい方に一致します。
CLASSIC の場合、まずベクトル的に近い topK 件を取り出してからフィルターを確認します。今回のケースでは、インデックス全体に langfuse- で始まる slug の記事が3件あっても、最初のベクトル検索で取り出した topK 件にその3件が含まれていなければ、結果には返りません。
まとめ
CLASSIC モードは候補ベクトルを探索しながら同時にフィルターを判定する仕組みのため、絞り込みが強い条件では topK 件に満たないことがあり、topK を多めに取ってアプリ側で絞り直す対処が必要でした。ENHANCED モードでは先にフィルターを評価するため、条件を通過したベクトルを類似度順で取り出せます。
CLASSIC インデックスで意図した検索結果が得られず、S3 Vectors を諦めたワークロードがあれば、ENHANCED で試してみてください。
なお、2026年9月30日以降に作成するバケットは ENHANCED のみです。新規導入だけでなく、スキーマ変更やリージョン移行などでバケットを作り直す場合も同様です。CLASSIC の動作を前提にした実装は見直しが必要となる可能性がある点はご留意ください。




