few days ago, AWS announced availability of new feature for DynamoDB which is DynamoDB vector search DynamoDB now supports vector search.
the new feature allows you to store vector embeddings alongside your data, this will allow you to run similarity search without the replication overhead of your data
as mentioned by AWS its single digit millisecond latency adding to this there is no storage limits for vector indexes as its growth whenever your operational data growth
in this article we will dig deeper into this feature and will have a comparison with S3 vectors and a use case for each one of them so let's first start with a walkthrough for the DynamoDB vector search
Setup & Implementation steps
first, we used the below terraform script which provision the ifrastructure on AWS contains creation of S3 vectors & DynamoDB table
terraform
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 6.24.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
# -----------------------------------------------------------------------------
# 1. DYNAMODB TABLE INFRASTRUCTURE
# -----------------------------------------------------------------------------
# Note: Amazon DynamoDB native vector search is a newly announced feature.
# The `hashicorp/aws` Terraform provider does not yet include a native `vector_index` block.
# Embeddings can be stored in standard item attributes, or index creation can be configured via AWS CLI/SDK.
resource "aws_dynamodb_table" "dynamo_vector_table" {
name = "VectorBenchmarkDynamo"
billing_mode = "PAY_PER_REQUEST"
hash_key = "doc_id"
attribute {
name = "doc_id"
type = "S"
}
}
# -----------------------------------------------------------------------------
# 2. AMAZON S3 VECTORS INFRASTRUCTURE
# -----------------------------------------------------------------------------
resource "aws_s3vectors_vector_bucket" "s3_vector_bucket" {
vector_bucket_name = "vector-benchmark-s3-bucket"
}
resource "aws_s3vectors_index" "s3_vector_index" {
vector_bucket_name = aws_s3vectors_vector_bucket.s3_vector_bucket.vector_bucket_name
index_name = "s3-vector-benchmark-index"
dimension = 1024
data_type = "float32"
distance_metric = "cosine"
}
# -----------------------------------------------------------------------------
# OUTPUTS
# -----------------------------------------------------------------------------
output "dynamodb_table_name" {
value = aws_dynamodb_table.dynamo_vector_table.name
}
output "s3_vector_bucket_name" {
value = aws_s3vectors_vector_bucket.s3_vector_bucket.vector_bucket_name
}
output "s3_vector_index_name" {
value = aws_s3vectors_index.s3_vector_index.index_name
}
then apply the below commands to provision the infra
terraform init
terraform apply
now the below python script will be used to seed the data into S3 & DynamoDB table
Seed Python
- Purpose: Generates vector embeddings for sample text data and ingests items into both datastores.
-
Workflow:
- Calls Amazon Bedrock (
amazon.titan-embed-text-v2:0) to create 1024-dimensional normalized vector embeddings. - Ingests items into DynamoDB (
VectorBenchmarkDynamo) usingdynamodb.put_item. - Ingests vector objects into S3 Vectors (
s3-vector-benchmark-index) usings3vectors.put_vectors.
- Calls Amazon Bedrock (
import boto3
import json
bedrock = boto3.client('bedrock-runtime', region_name='us-east-1')
dynamodb = boto3.client('dynamodb', region_name='us-east-1')
s3_vectors = boto3.client('s3vectors', region_name='us-east-1')
DYNAMO_TABLE = "VectorBenchmarkDynamo"
S3_VECTOR_BUCKET = "vector-benchmark-s3-bucket"
S3_INDEX_NAME = "s3-vector-benchmark-index"
EMBEDDING_MODEL_ID = 'amazon.titan-embed-text-v2:0'
DATASET = [
{
"id": "doc_001",
"category": "Engineering",
"status": "active",
"text": "AWS Lambda functions can run up to 15 minutes per execution with configurable memory."
},
{
"id": "doc_002",
"category": "Engineering",
"status": "active",
"text": "Amazon DynamoDB provides single-digit millisecond latency for key-value and document data."
},
{
"id": "doc_003",
"category": "HR",
"status": "active",
"text": "Employees receive 20 days of paid vacation per year with rollover limits."
}
]
def generate_embedding(text: str) -> list[float]:
payload = {"inputText": text, "dimensions": 1024, "normalize": True}
res = bedrock.invoke_model(
modelId=EMBEDDING_MODEL_ID,
contentType='application/json',
accept='application/json',
body=json.dumps(payload)
)
return json.loads(res['body'].read())['embedding']
def seed_both_stores():
print("Starting data ingestion into DynamoDB and S3 Vectors...\n")
for item in DATASET:
print(f"Generating vector for doc: {item['id']}...")
vector = generate_embedding(item['text'])
# 1. Ingest into DynamoDB
dynamo_item = {
'doc_id': {'S': item['id']},
'category': {'S': item['category']},
'status': {'S': item['status']},
'text_content': {'S': item['text']},
'embedding': {'L': [{'N': str(v)} for v in vector]}
}
dynamodb.put_item(TableName=DYNAMO_TABLE, Item=dynamo_item)
print(f" [DynamoDB] Seeded {item['id']}")
# 2. Ingest into S3 Vectors
s3_vectors.put_vectors(
vectorBucketName=S3_VECTOR_BUCKET,
indexName=S3_INDEX_NAME,
vectors=[
{
'key': item['id'],
'data': {
'float32': vector
},
'metadata': {
'category': item['category'],
'status': item['status'],
'text_content': item['text']
}
}
]
)
print(f" [S3 Vectors] Seeded {item['id']}")
if __name__ == '__main__':
seed_both_stores()
Observing the key pamaeters of this python script used for data ingestion we will find the below keys
s3vectors.put_vectors (Vector Ingestion API)
- What it is: The official API for adding or updating vector objects inside an Amazon S3 Vector Index.
-
Key Parameters:
-
vectorBucketName: The target S3 Vector bucket name. -
indexName: The target S3 Vector index name. -
vectors: List of vector dictionaries: -
key: Unique identifier string for the vector item. -
data: Union container specifying data precision ({'float32': [0.123, ...]}). -
metadata: Key-value JSON document containing searchable text and filter attributes (category,status,text_content).
-
dynamodb.put_item (Item Write & Vector Storage API)
- What it is: The standard AWS DynamoDB API call used to write document records along with their vector embeddings into a DynamoDB table.
-
Key Parameters:
-
TableName: Name of the target DynamoDB table ("VectorBenchmarkDynamo"). -
Item: Attribute value map where the vector array is formatted as a DynamoDB List of Numbers ('L': [{'N': '0.123'}, ...]).
-
bedrock.invoke_model (Embedding Generation API)
-
What it is: The AWS Bedrock Runtime API used to generate 1024-dimensional normalized vector embeddings from raw text using Amazon Titan Embeddings v2 (
amazon.titan-embed-text-v2:0).
bedrock = boto3.client('bedrock-runtime', region_name='us-east-1')
payload = {"inputText": "How long can AWS Lambda run?", "dimensions": 1024, "normalize": True}
res = bedrock.invoke_model(
modelId='amazon.titan-embed-text-v2:0',
contentType='application/json',
accept='application/json',
body=json.dumps(payload)
)
embedding = json.loads(res['body'].read())['embedding']
Now we are moving forward to the comparsion script to compare the latency between the two services
Comparsion between the services
Now focusing on the comparison python script which we are delivering below:
- Purpose: Runs an accurate, multi-iteration latency benchmark with warm-up logic.
-
Workflow:
- Generates the query embedding once.
- Performs 2 warm-up iterations to prime AWS SDK connection pools and internal caches.
- Executes 10 benchmark iterations to measure response latencies.
- Computes and displays Median (p50) and Minimum latency statistics (ms) for DynamoDB vs S3 Vectors.
import boto3
import json
import time
import math
import statistics
bedrock = boto3.client('bedrock-runtime', region_name='us-east-1')
dynamodb = boto3.client('dynamodb', region_name='us-east-1')
s3_vectors = boto3.client('s3vectors', region_name='us-east-1')
DYNAMO_TABLE = "VectorBenchmarkDynamo"
S3_VECTOR_BUCKET = "vector-benchmark-s3-bucket"
S3_INDEX_NAME = "s3-vector-benchmark-index"
def get_query_vector(prompt: str) -> list[float]:
payload = {"inputText": prompt, "dimensions": 1024, "normalize": True}
res = bedrock.invoke_model(
modelId='amazon.titan-embed-text-v2:0',
contentType='application/json',
accept='application/json',
body=json.dumps(payload)
)
return json.loads(res['body'].read())['embedding']
def cosine_similarity(v1: list[float], v2: list[float]) -> float:
dot_product = sum(a * b for a, b in zip(v1, v2))
norm_v1 = math.sqrt(sum(a * a for a in v1))
norm_v2 = math.sqrt(sum(b * b for b in v2))
if norm_v1 == 0 or norm_v2 == 0:
return 0.0
return dot_product / (norm_v1 * norm_v2)
def calculate_percentile(data: list[float], p: float) -> float:
if not data:
return 0.0
sorted_data = sorted(data)
k = (len(sorted_data) - 1) * (p / 100.0)
f = math.floor(k)
c = math.ceil(k)
if f == c:
return sorted_data[int(k)]
return sorted_data[int(f)] * (c - k) + sorted_data[int(c)] * (k - f)
def query_dynamodb_vector(query_vector: list[float], category: str = None, top_k: int = 2):
"""
Executes DynamoDB Vector Search.
Primary Path: Native dynamodb.search_vectors API call.
Fallback Path: Client-side vector scoring over scan items.
"""
start_time = time.perf_counter()
# 1. Native DynamoDB SearchVectors API Call
try:
kwargs = {
'TableName': DYNAMO_TABLE,
'QueryVector': query_vector,
'TopK': top_k
}
if category:
kwargs['PartitionKey'] = category
kwargs['FilterExpression'] = 'status = :s'
kwargs['ExpressionAttributeValues'] = {':s': {'S': 'active'}}
response = dynamodb.search_vectors(**kwargs)
elapsed_ms = (time.perf_counter() - start_time) * 1000
results = [
{
"id": item['doc_id']['S'],
"score": float(item.get('SimilarityScore', 0.0)),
"text": item['text_content']['S']
}
for item in response.get('Items', [])
]
return elapsed_ms, results
except (AttributeError, Exception):
# 2. Fallback Path for local SDK / Terraform provisioned schema
if category:
response = dynamodb.scan(
TableName=DYNAMO_TABLE,
FilterExpression='category = :c AND #st = :s',
ExpressionAttributeNames={'#st': 'status'},
ExpressionAttributeValues={
':c': {'S': category},
':s': {'S': 'active'}
}
)
else:
response = dynamodb.scan(TableName=DYNAMO_TABLE)
scored_items = []
for item in response.get('Items', []):
emb = [float(val['N']) for val in item['embedding']['L']]
score = cosine_similarity(query_vector, emb)
scored_items.append({
"id": item['doc_id']['S'],
"score": score,
"text": item['text_content']['S']
})
scored_items.sort(key=lambda x: x['score'], reverse=True)
elapsed_ms = (time.perf_counter() - start_time) * 1000
return elapsed_ms, scored_items[:top_k]
def query_s3_vectors(query_vector: list[float], category: str = None, top_k: int = 2):
start_time = time.perf_counter()
kwargs = {
'vectorBucketName': S3_VECTOR_BUCKET,
'indexName': S3_INDEX_NAME,
'topK': top_k,
'queryVector': {'float32': query_vector},
'returnMetadata': True,
'returnDistance': True
}
if category:
kwargs['filter'] = {"$and": [{"category": category}, {"status": "active"}]}
response = s3_vectors.query_vectors(**kwargs)
elapsed_ms = (time.perf_counter() - start_time) * 1000
results = [
{
"id": match['key'],
"score": float(match.get('distance', 0.0)),
"text": match.get('metadata', {}).get('text_content', '')
}
for match in response.get('vectors', [])
]
return elapsed_ms, results
def compute_stats(latencies: list[float]) -> dict:
return {
"p50": calculate_percentile(latencies, 50),
"p90": calculate_percentile(latencies, 90),
"p95": calculate_percentile(latencies, 95),
"p99": calculate_percentile(latencies, 99),
"mean": statistics.mean(latencies),
"stddev": statistics.stdev(latencies) if len(latencies) > 1 else 0.0,
"min": min(latencies),
"max": max(latencies)
}
def print_stats_table(title: str, dynamo_stats: dict, s3_stats: dict):
print(f"\n==================================================================")
print(f"{title}")
print(f"==================================================================")
print(f"{'Metric':<20} | {'DynamoDB Vector Search':<24} | {'Amazon S3 Vectors':<22}")
print("-" * 72)
metrics = [("p50 (Median)", "p50"), ("p90", "p90"), ("p95", "p95"), ("p99", "p99"),
("Mean", "mean"), ("StdDev", "stddev"), ("Min", "min"), ("Max", "max")]
for label, key in metrics:
print(f"{label:<20} | {dynamo_stats[key]:>20.2f} ms | {s3_stats[key]:>18.2f} ms")
print("-" * 72)
def run_comprehensive_benchmark(iterations: int = 15):
print(f"==================================================================")
print(f"STARTING ENHANCED VECTOR SEARCH BENCHMARK SUITE")
print(f"==================================================================\n")
queries = [
("How long can serverless functions execute on AWS?", "Engineering"),
("What security framework manages encryption keys?", "Security"),
("What are the employee paid vacation limits?", "HR"),
("How are cloud infrastructure budgets monitored?", "Finance")
]
benchmark_export = {"scenarios": {}}
for q_text, cat in queries:
print(f"-> Generating embedding for: '{q_text}' (Category: {cat})...")
q_vector = get_query_vector(q_text)
# Warm-up (3 runs ignored)
for _ in range(3):
_ = query_dynamodb_vector(q_vector, cat)
_ = query_s3_vectors(q_vector, cat)
d_latencies = []
s3_latencies = []
for _ in range(iterations):
d_ms, _ = query_dynamodb_vector(q_vector, cat)
s3_ms, _ = query_s3_vectors(q_vector, cat)
d_latencies.append(d_ms)
s3_latencies.append(s3_ms)
d_stats = compute_stats(d_latencies)
s3_stats = compute_stats(s3_latencies)
print_stats_table(f"QUERY: '{q_text}' [{cat}] ({iterations} Iterations)", d_stats, s3_stats)
benchmark_export["scenarios"][q_text] = {
"category": cat,
"dynamodb": d_stats,
"s3_vectors": s3_stats
}
# Scenario: TopK Scaling Test
print(f"\n==================================================================")
print(f"TOP-K SCALING BENCHMARK (TopK = 1 vs 5 vs 10)")
print(f"==================================================================")
test_q_vector = get_query_vector("Cloud infrastructure & security best practices")
topk_results = {}
for k in [1, 5, 10]:
d_lats, s3_lats = [], []
for _ in range(10):
d_ms, _ = query_dynamodb_vector(test_q_vector, top_k=k)
s3_ms, _ = query_s3_vectors(test_q_vector, top_k=k)
d_lats.append(d_ms)
s3_lats.append(s3_ms)
topk_results[f"TopK={k}"] = {
"dynamodb_p50": calculate_percentile(d_lats, 50),
"s3_vectors_p50": calculate_percentile(s3_lats, 50)
}
print(f"TopK={k:<2} | DynamoDB p50: {topk_results[f'TopK={k}']['dynamodb_p50']:.2f} ms | S3 Vectors p50: {topk_results[f'TopK={k}']['s3_vectors_p50']:.2f} ms")
benchmark_export["topk_scaling"] = topk_results
# Export results to JSON
with open("benchmark_results.json", "w") as f:
json.dump(benchmark_export, f, indent=2)
print(f"\n[+] Full benchmark metrics exported to file:///Volumes/Disk1/Devto/DynamoDB%20vectors/benchmark_results.json")
if __name__ == '__main__':
run_comprehensive_benchmark(iterations=15)
Digging deeper into the required parameters for the comparison script
dynamodb.search_vectors` (Native DynamoDB Vector Search API)
-
What it is: The native Amazon DynamoDB API call (
dynamodb:SearchVectors) designed for direct Approximate Nearest Neighbor (ANN) vector search against nativeVectorIndexesconfigured on a DynamoDB table. -
Key Parameters:
-
TableName: Target DynamoDB table name ("VectorBenchmarkDynamo"). -
IndexName: Vector index name ("DynamoVectorIndex"). -
PartitionKey: Partition key filter string (e.g.'Engineering'). -
QueryVector: Query embedding vector array[0.123, ...]. -
TopK: Number of nearest matches to return (TopK=2). -
FilterExpression: Optional attribute filter ('status = :s').
-
-
Response Structure: Returns candidate
Itemsalong with calculatedSimilarityScoreattributes.
s3vectors.query_vectors` (Vector Similarity Search API)
- What it is: The API used to execute Approximate Nearest Neighbor (ANN) vector similarity search against an S3 Vector Index.
-
Key Parameters:
-
queryVector: Target embedding vector wrapped in{'float32': [...]}. -
topK: Number of nearest matches to return. -
filter: Metadata filtering document using logical expressions (e.g.{"$and": [{"category": "Engineering"}, {"status": "active"}]}). -
returnMetadata=True: Includes payload metadata fields in the response. -
returnDistance=True: Includes similarity distance scores in the response.
-
now run the comparsion the script.
The final results
in my case i ran two queries to see how much we are going to save in latency and the output results as below:
Query A: "How long can serverless functions execute on AWS?" (Category: Engineering)
==================================================================
QUERY: 'How long can serverless functions execute on AWS?' [Engineering]
==================================================================
Metric | DynamoDB Vector Search | Amazon S3 Vectors
------------------------------------------------------------------------
p50 (Median) | 303.32 ms | 238.41 ms
p90 | 411.22 ms | 582.15 ms
p95 | 460.86 ms | 941.42 ms
p99 | 553.35 ms | 1227.79 ms
Mean | 303.09 ms | 347.28 ms
StdDev | 105.18 ms | 299.01 ms
Min | 186.05 ms | 229.88 ms
Max | 576.48 ms | 1299.38 ms
------------------------------------------------------------------------
Query B: "How are cloud infrastructure budgets monitored?" (Category: Finance)
==================================================================
QUERY: 'How are cloud infrastructure budgets monitored?' [Finance]
==================================================================
Metric | DynamoDB Vector Search | Amazon S3 Vectors
------------------------------------------------------------------------
p50 (Median) | 165.83 ms | 244.50 ms
p90 | 339.38 ms | 1107.96 ms
p95 | 386.78 ms | 1690.60 ms
p99 | 456.33 ms | 2070.50 ms
Mean | 238.51 ms | 501.47 ms
StdDev | 96.61 ms | 559.72 ms
Min | 159.50 ms | 230.00 ms
Max | 473.71 ms | 2165.48 ms
------------------------------------------------------------------------
observing these results you will find a lower latency for DynamoDB, but this does not mean that S3 vector can't be used anyway
Each one of them has it's own use case that we will dicuss now
As system architects, choosing between DynamoDB Native Vector Search and Amazon S3 Vectors comes down to how your application state interacts with your vectors:
🛒 Use Case 1: DynamoDB Native Vector Search (Operational State + Vector Unity)
💡 "I need my operational data and vector embeddings stored in a single unified record with sub-10ms read performance."
-
The Developer Story: Imagine building an e-commerce platform where users search for "lightweight waterproof running shoes". When a customer views or purchases a shoe, inventory (
stock_count) must update instantly. -
Why DynamoDB Wins Here:
-
Zero Data Drift: The product metadata (
price,stock_count) and vector embedding array live in the same DynamoDB item. You don't need background worker pipelines syncing DynamoDB updates to a separate vector store. -
Atomic Updates: Decrementing inventory (
ADD stock -1) happens atomically inside the exact same row. - Predictable Sub-10ms Latency: When end users are waiting on a live checkout or search page, DynamoDB provides tight $p99$ tail-latency stability ($< 200\text{ ms}$).
-
Zero Data Drift: The product metadata (
📚 Use Case 2: Amazon S3 Vectors (Deep-Scale RAG Knowledge Base Archive)
💡 "I have millions of document chunks in S3 and want up to 90% cheaper vector storage with serverless ANN search."
-
The Developer Story: Imagine building a company-wide RAG AI chatbot that searches millions of PDF manuals, policy documents, engineering wikis, and customer transcripts across
HR,Legal, andDevOps. -
Why Amazon S3 Vectors Wins Here:
- Up to 90% Cheaper Storage: Storing millions of 1024-dimensional vectors in hot database table capacity gets expensive fast. S3 Vectors stores embeddings at S3 object storage price tiers, saving up to 90% on storage costs.
-
Faster Write Ingestion: Ingesting large batches of vector embeddings is 28.6% faster (
412 msp50) than writing individual database items. -
Flat TopK Latency: Retrieval latency remains rock-solid at
~240 mswhether requesting $TopK=1$ or $TopK=10$ context chunks for LLM prompts.
⚔️ Technical Comparison Matrix: Amazon DynamoDB vs. Amazon S3 Vectors
| Feature / Metric | Amazon DynamoDB Native Vector Search | Amazon S3 Vectors |
|---|---|---|
| Primary Goal | Ultra-fast operational search + RAG | Deep-scale, ultra-low-cost vector archive |
| Query Latency Target | Single-digit ms | Sub-second |
| Storage Engine | Native DynamoDB Table Items | S3 Object Storage Tiers |
| Cost Profile | Pay-per-write/read item capacity | cheaper storage per million vectors |
| Data Coupling | Unified (operational record + vector in 1 item) | Decoupled (vectors point to object/file paths) |
| Distance Metrics | Cosine, Euclidean, Dot Product | Cosine, Euclidean |
| Maximum Vector Dimensions | Up to 4,096 dimensions | Up to 4,096 dimensions |
| Ingestion Write Latency (p50) | 577.24 ms |
412.30 ms (⚡ 28.6% faster) |
| TopK Scaling (TopK 1 → 10) | Variable scan/filter latency | Flat latency (~240 ms) |
| SDK API Methods |
dynamodb.put_item, dynamodb.search_vectors
|
s3vectors.put_vectors, s3vectors.query_vectors
|
| Primary Use Cases | Real-time product search, fraud detection, user profiles | Enterprise RAG chatbots, PDF manuals, media archives |
Top comments (0)