S3 Vectors metadata pre-filtering landed on September 30, and it fixes the quiet failure mode of every filtered vector search: you ask for the top 10 matches inside one customer’s documents and get back three, or zero, even though dozens exist. The new ENHANCED index mode resolves your metadata filter first and only then runs similarity search over the survivors. AWS claims up to 5x higher recall on selective filters. Here is why the old behavior lost results, what changed, and how to prove the improvement on your own data instead of taking a blog post’s word for it.
Why selective filters kill recall in ANN search
Approximate nearest neighbor (ANN) indexes are fast because they skip most of the corpus. Graph-based structures like HNSW walk from an entry point toward the query vector, keeping a bounded candidate list. That shortcut is the whole point, and it is also the problem.
There are three ways to combine a filter with ANN search:
- Post-filtering: find the top K nearest vectors globally, then drop the ones that fail the filter. If your filter matches 0.005% of the index, almost everything you found gets thrown away.
- In-traversal filtering: evaluate the filter while walking the graph. Better, but the search still explores a neighborhood shaped by the whole corpus. Matching vectors scattered across distant regions never get visited before the candidate budget runs out.
- Pre-filtering: compute the set of vectors that pass the filter, then search only that set. Recall on the filtered set stays high because nothing outside it competes for candidate slots.
The launch post uses a support knowledge base with 8 million tickets where one customer owns 400. That filter selects 0.005% of the index. Post-filtering or in-traversal filtering at that selectivity is a coin flip at best.
S3 Vectors metadata pre-filtering: CLASSIC vs ENHANCED
CLASSIC is the original behavior: S3 Vectors evaluates the filter in tandem with vector search. For broad filters (“language is English”) it works fine. For narrow ones (“tenant is t-10428”) it under-returns, and nothing in the response tells you it did. You just get fewer hits than topK or worse hits than exist.
ENHANCED flips the order: filter resolution first, similarity search second. The rules on which mode you get:
- Indexes in vector buckets created on or after September 30, 2026 default to
ENHANCED. - Indexes in older buckets stay
CLASSICuntil you switch them or change the bucket default. - Switching does not require re-ingesting vectors.
- No extra charge. You pay normal S3 Vectors pricing for storage, PUTs and queries.
Check what you have before you assume anything:
aws s3vectors list-indexes --vector-bucket-name support-kb
aws s3vectors get-index \
--vector-bucket-name support-kb \
--index-name tickets
The filter grammar you get to work with
Every metadata field is filterable by default unless you declared it non-filterable at index creation. Filterable types are string, number, boolean and list. The metadata filtering docs list the operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists, $and, $or, plus the new $startsWith prefix match, which only works with ENHANCED. A typical scoped query looks like this:
aws s3vectors query-vectors \
--vector-bucket-name support-kb \
--index-name tickets \
--query-vector file://q.json \
--top-k 20 \
--return-metadata \
--filter '{"$and": [
{"tenant_id": {"$eq": "t-10428"}},
{"status": {"$in": ["open", "escalated"]}},
{"created_at": {"$gte": 1756684800}}
]}'
Two limits matter, both documented on the limits page:
- 2 KB of filterable metadata per vector (40 KB total including non-filterable keys, max 50 keys).
- 100 filter constraints per query on
ENHANCEDindexes, where each evaluated value counts. An$inwith 60 IDs burns 60 of your 100.
Designing a schema that respects those budgets is its own topic. Here we care about whether the engine returns what it should.

Measure filtered recall yourself
“Up to 5x” is a ceiling, not a promise. Your gain depends on filter selectivity and how your embeddings cluster. The honest way to know is to compute ground truth: for a given filter, brute-force the exact nearest neighbors over every matching vector, then compare against what query_vectors returns. For a few hundred or a few thousand matching vectors, exact search on a laptop takes milliseconds.
import boto3, numpy as np
s3v = boto3.client("s3vectors", region_name="us-east-1")
BUCKET, INDEX, K = "support-kb", "tickets", 20
def filtered_ground_truth(tenant, qvec):
keys, vecs, token = [], [], None
while True:
kw = dict(vectorBucketName=BUCKET, indexName=INDEX,
returnData=True, returnMetadata=True)
if token:
kw["nextToken"] = token
page = s3v.list_vectors(**kw)
for v in page["vectors"]:
if v["metadata"].get("tenant_id") == tenant:
keys.append(v["key"])
vecs.append(v["data"]["float32"])
token = page.get("nextToken")
if not token:
break
m = np.array(vecs); q = np.array(qvec)
sims = m @ q / (np.linalg.norm(m, axis=1) * np.linalg.norm(q))
return {keys[i] for i in np.argsort(-sims)[:K]}
def recall_at_k(tenant, qvec):
truth = filtered_ground_truth(tenant, qvec)
got = s3v.query_vectors(
vectorBucketName=BUCKET, indexName=INDEX,
queryVector={"float32": qvec}, topK=K,
filter={"tenant_id": tenant})["vectors"]
return len(truth & {v["key"] for v in got}) / min(K, len(truth))
Full scans with list_vectors get expensive on big indexes, so in practice you cache the scan once (it supports up to 16 parallel segments) and reuse it across many queries. Then build a test set that actually stresses the system:
- Bucket filters by selectivity: broad (>10% of vectors), medium (0.1–10%), narrow (<0.1%). Report recall per bucket, not a blended average that hides the tail.
- Use real queries from logs, not random vectors. Random queries land in empty space and flatter everything.
- Track short results: count queries where fewer than
min(K, matches)came back. That is the “missing document” bug your users already complain about. - Record latency alongside recall. Pre-filtering changes the work profile, so check p50 and p99 against your SLO. The CloudWatch metrics help here.
Migrating an existing index from CLASSIC
If your buckets predate September 30, your indexes are still running the old behavior. The fix is one call per index, with no re-ingestion:
# Baseline: run the recall harness against the CLASSIC index first
python recall_eval.py --index tickets --out classic.json
aws s3vectors update-index-mode \
--vector-bucket-name support-kb \
--index-name tickets \
--index-mode ENHANCED
# Make future indexes in this bucket ENHANCED too
aws s3vectors put-vector-bucket-default-index-mode \
--vector-bucket-name support-kb \
--default-index-mode ENHANCED
python recall_eval.py --index tickets --out enhanced.json
Run the harness before and after, diff per selectivity bucket, and keep the report. A few things to check during the switch:
- Downstream assumptions. If your app padded thin results with an unfiltered fallback query, that hack may now return duplicates or leak out-of-scope content. Rip it out.
- Constraint counts. The 100-constraint cap applies to
ENHANCED. Generated filters with giant$inlists that ran before may now get rejected. Grep your query builders. - Bedrock Knowledge Bases. If S3 Vectors sits behind a Bedrock Knowledge Base, the mode lives on the index, so retrieval filters benefit once the index flips. Re-run your retrieval evals anyway.
When pre-filtering is not the whole answer
Pre-filtering fixes recall inside the filter. It does not fix bad chunking, weak embeddings or metadata you never wrote. If tenant IDs are missing on 3% of your vectors, ENHANCED will faithfully exclude them every time. And for workloads that need very high QPS, hybrid keyword search or heavy aggregations, exporting to OpenSearch is still the documented path. For large, cost-sensitive RAG corpora with tenant- or matter-scoped retrieval, though, this is the upgrade S3 Vectors needed: correct results by default, for the same price, with a one-line migration.
At Enkompass (enkompass.net), this is our home turf. We ship production Bedrock RAG and agent workloads on S3 Vectors and Knowledge Bases, and we build recall harnesses like this one before any index migration touches customer traffic. If you’d rather have seasoned cloud people handle it instead of learning the hard way at 2 AM, let’s talk. Reach out through our contact page or call us directly at +1-828-436-5667 — we’ll help you turn this from a someday-project into a done-deal.
