Skip to content

Commit e32e74e

Browse files
authored
feat: Support S3 Vectors (#3330)
* feat: Support S3 Vectors Bring Amazon S3 Vectors — AWS's native, cost-optimised vector store for similarity search, RAG, and AI agents — to AWS SDK for pandas. With this change, indexing a DataFrame and running an approximate- nearest-neighbour query is a one-liner; on-the-fly embedding via Amazon Bedrock is built in. Implementation lives in a private subpackage `awswrangler/s3/_vectors/` and is re-exported flat on `wr.s3.*`, matching the s3_tables convention. Public surface (14 functions on `wr.s3`): - Buckets: create_vector_bucket, delete_vector_bucket, list_vector_buckets, get_vector_bucket - Indexes: create_vector_index, delete_vector_index, list_vector_indexes, get_vector_index - Data: put_vectors, put_vectors_from_df, get_vectors, delete_vectors, list_vectors, query_vectors Highlights: - End-to-end RAG: pass `text_column` + `bedrock_model_id` to put_vectors_from_df and awswrangler embeds each row via Bedrock (Titan / Cohere) and writes the resulting vectors plus all other columns as filterable metadata. query_vectors mirrors this with `query_text` / `query_vector`. - MongoDB-style metadata filters ($eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists, $and, $or) evaluated server-side during search. - Automatic chunking to AWS API limits (500/put, 100/get, 500/delete) and parallel-segment list_vectors (up to 16 segments). - Float32 coercion + non-finite rejection; NaN / pd.NA / None metadata cells dropped per row. * test: add mocked unit tests and live integration tests for S3 Vectors - tests/unit/test_s3_vectors_mocked.py — 46 tests using unittest.mock, no AWS required. Covers chunking, target resolution, float32 coercion, NaN/pd.NA metadata drop, parallel-segment list, query top-k bounds, Bedrock Titan/Cohere request and response shapes, and aliasing identity. - tests/unit/test_s3_vectors.py — 10 live integration tests using new vector_bucket (session-scope) and vector_index (function-scope) fixtures in tests/conftest.py. Fixtures self-bootstrap via create_vector_bucket / create_vector_index; no CDK stack required. * docs: api reference and tutorial for S3 Vectors - docs/source/api.rst: new "Amazon S3 Vectors" section after "Amazon S3 Tables", listing all 14 public functions. - tutorials/043 - Amazon S3 Vectors.ipynb: end-to-end walkthrough covering bucket/index lifecycle, discovery, Bedrock-embedded writes, semantic queries with metadata filters, per-key CRUD, bulk export, and cleanup. - README.md: tutorial 043 entry. * fix: type-parametrize np.ndarray for mypy on Python 3.10 * fix: use hyphen in vector index fixture name (underscore is invalid) * fix: avoid zero-norm vectors in live integration tests (cosine rejects them) * feat: add `chunked` parameter to list_vectors Memory-friendly streaming for indexes too large to materialise in one DataFrame. Mirrors the `chunked: bool | int` convention used by `s3.read_parquet` and `athena.read_sql_query`: - chunked=False (default) — unchanged; returns a DataFrame and keeps the parallel-segment fan-out (up to 16 segments). - chunked=True — yields one DataFrame per underlying API page. - chunked=INTEGER — yields DataFrames of exactly N rows (final frame may be shorter). Chunked streaming is single-segment and sequential by design; `use_threads` is ignored in that mode, since lazy iteration across parallel segments would require buffering and defeat the memory win. Internally, `_list_segment` is now a thin materialiser around a new `_iter_list_pages` generator, so the per-segment pagination logic (including `max_items` enforcement across pages) is shared by both the eager and streaming paths. Tests: 4 new cases cover per-page yield, exact-size chunking, laziness (no API call before the first `next()`), and `max_items` truncation of the chunked stream.
1 parent 7ab4b55 commit e32e74e

16 files changed

Lines changed: 2814 additions & 4 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ Read our [docs](https://aws-sdk-pandas.readthedocs.io/en/3.16.1/scale.html) or h
153153
- [039 - Athena Iceberg](https://github.com/aws/aws-sdk-pandas/blob/main/tutorials/039%20-%20Athena%20Iceberg.ipynb)
154154
- [040 - EMR Serverless](https://github.com/aws/aws-sdk-pandas/blob/main/tutorials/040%20-%20EMR%20Serverless.ipynb)
155155
- [041 - Apache Spark on Amazon Athena](https://github.com/aws/aws-sdk-pandas/blob/main/tutorials/041%20-%20Apache%20Spark%20on%20Amazon%20Athena.ipynb)
156+
- [043 - Amazon S3 Vectors](https://github.com/aws/aws-sdk-pandas/blob/main/tutorials/043%20-%20Amazon%20S3%20Vectors.ipynb)
156157
- [**API Reference**](https://aws-sdk-pandas.readthedocs.io/en/3.16.1/api.html)
157158
- [Amazon S3](https://aws-sdk-pandas.readthedocs.io/en/3.16.1/api.html#amazon-s3)
158159
- [AWS Glue Catalog](https://aws-sdk-pandas.readthedocs.io/en/3.16.1/api.html#aws-glue-catalog)

awswrangler/_utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767

6868
ServiceName = Literal[
6969
"athena",
70+
"bedrock-runtime",
7071
"cleanrooms",
7172
"dynamodb",
7273
"ec2",
@@ -85,6 +86,7 @@
8586
"secretsmanager",
8687
"sts",
8788
"s3tables",
89+
"s3vectors",
8890
"timestream-query",
8991
"timestream-write",
9092
]

awswrangler/s3/__init__.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,22 @@
2121
)
2222
from awswrangler.s3._select import select_query
2323
from awswrangler.s3._upload import upload
24+
from awswrangler.s3._vectors import (
25+
create_vector_bucket,
26+
create_vector_index,
27+
delete_vector_bucket,
28+
delete_vector_index,
29+
delete_vectors,
30+
get_vector_bucket,
31+
get_vector_index,
32+
get_vectors,
33+
list_vector_buckets,
34+
list_vector_indexes,
35+
list_vectors,
36+
put_vectors,
37+
put_vectors_from_df,
38+
query_vectors,
39+
)
2440
from awswrangler.s3._wait import wait_objects_exist, wait_objects_not_exist
2541
from awswrangler.s3._write_deltalake import to_deltalake, to_deltalake_streaming
2642
from awswrangler.s3._write_excel import to_excel
@@ -71,4 +87,18 @@
7187
"delete_table",
7288
"from_iceberg",
7389
"to_iceberg",
90+
"create_vector_bucket",
91+
"delete_vector_bucket",
92+
"list_vector_buckets",
93+
"get_vector_bucket",
94+
"create_vector_index",
95+
"delete_vector_index",
96+
"list_vector_indexes",
97+
"get_vector_index",
98+
"put_vectors",
99+
"put_vectors_from_df",
100+
"get_vectors",
101+
"delete_vectors",
102+
"list_vectors",
103+
"query_vectors",
74104
]
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""Amazon S3 Vectors (PRIVATE subpackage).
2+
3+
Public functions are re-exported through ``awswrangler.s3``. This module is private —
4+
its layout may change without notice.
5+
"""
6+
7+
from awswrangler.s3._vectors._mgmt import (
8+
create_vector_bucket,
9+
create_vector_index,
10+
delete_vector_bucket,
11+
delete_vector_index,
12+
get_vector_bucket,
13+
get_vector_index,
14+
list_vector_buckets,
15+
list_vector_indexes,
16+
)
17+
from awswrangler.s3._vectors._read import (
18+
get_vectors,
19+
list_vectors,
20+
query_vectors,
21+
)
22+
from awswrangler.s3._vectors._write import (
23+
delete_vectors,
24+
put_vectors,
25+
put_vectors_from_df,
26+
)
27+
28+
__all__ = [
29+
"create_vector_bucket",
30+
"delete_vector_bucket",
31+
"list_vector_buckets",
32+
"get_vector_bucket",
33+
"create_vector_index",
34+
"delete_vector_index",
35+
"list_vector_indexes",
36+
"get_vector_index",
37+
"put_vectors",
38+
"put_vectors_from_df",
39+
"get_vectors",
40+
"delete_vectors",
41+
"list_vectors",
42+
"query_vectors",
43+
]
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""Amazon S3 Vectors - Bedrock embedding helper (PRIVATE)."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
import logging
7+
from typing import TYPE_CHECKING, Any, Callable
8+
9+
import boto3
10+
11+
from awswrangler import _utils, exceptions
12+
from awswrangler._executor import _get_executor
13+
14+
if TYPE_CHECKING:
15+
from botocore.client import BaseClient
16+
17+
_logger: logging.Logger = logging.getLogger(__name__)
18+
19+
20+
def embed_texts(
21+
texts: list[str],
22+
model_id: str,
23+
model_kwargs: dict[str, Any] | None = None,
24+
use_threads: bool | int = True,
25+
boto3_session: boto3.Session | None = None,
26+
) -> list[list[float]]:
27+
"""Embed a list of strings via Amazon Bedrock, optionally in parallel.
28+
29+
Supported model id prefixes: ``amazon.titan-embed-text-*``, ``cohere.embed-*``.
30+
"""
31+
if not texts:
32+
return []
33+
34+
extra = dict(model_kwargs or {})
35+
build_body: Callable[[str], dict[str, Any]]
36+
parse_response: Callable[[dict[str, Any]], list[float]]
37+
38+
if model_id.startswith("amazon.titan-embed-text"):
39+
40+
def build_body(text: str) -> dict[str, Any]:
41+
return {"inputText": text, **extra}
42+
43+
def parse_response(payload: dict[str, Any]) -> list[float]:
44+
return list(payload["embedding"])
45+
46+
elif model_id.startswith("cohere.embed"):
47+
extra.setdefault("input_type", "search_document")
48+
49+
def build_body(text: str) -> dict[str, Any]:
50+
return {"texts": [text], **extra}
51+
52+
def parse_response(payload: dict[str, Any]) -> list[float]:
53+
return list(payload["embeddings"][0])
54+
55+
else:
56+
raise exceptions.InvalidArgument(
57+
f"Unsupported Bedrock embedding model_id '{model_id}'. "
58+
"Pre-compute embeddings and pass them via `vector_column` instead. "
59+
"Supported model id prefixes: 'amazon.titan-embed-text', 'cohere.embed'."
60+
)
61+
62+
def embed_one(client: "BaseClient", text: str) -> list[float]:
63+
response = client.invoke_model( # type: ignore[attr-defined]
64+
modelId=model_id,
65+
accept="application/json",
66+
contentType="application/json",
67+
body=json.dumps(build_body(text)).encode("utf-8"),
68+
)
69+
return parse_response(json.loads(response["body"].read()))
70+
71+
client = _utils.client(service_name="bedrock-runtime", session=boto3_session)
72+
executor = _get_executor(use_threads=use_threads)
73+
return list(executor.map(embed_one, client, texts))

0 commit comments

Comments
 (0)