DEV Community

Cover image for Closer look on DynamoDB vector search vs S3 Vectors
Muhammed Ashraf
Muhammed Ashraf

Posted on

Closer look on DynamoDB vector search vs S3 Vectors

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
}
Enter fullscreen mode Exit fullscreen mode

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:
    1. Calls Amazon Bedrock (amazon.titan-embed-text-v2:0) to create 1024-dimensional normalized vector embeddings.
    2. Ingests items into DynamoDB (VectorBenchmarkDynamo) using dynamodb.put_item.
    3. Ingests vector objects into S3 Vectors (s3-vector-benchmark-index) using s3vectors.put_vectors.
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()
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode

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:
    1. Generates the query embedding once.
    2. Performs 2 warm-up iterations to prime AWS SDK connection pools and internal caches.
    3. Executes 10 benchmark iterations to measure response latencies.
    4. 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)
Enter fullscreen mode Exit fullscreen mode

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 native VectorIndexes configured 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 Items along with calculated SimilarityScore attributes.

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
------------------------------------------------------------------------
Enter fullscreen mode Exit fullscreen mode

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
------------------------------------------------------------------------
Enter fullscreen mode Exit fullscreen mode

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}$).

📚 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, and DevOps.
  • 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 ms p50) than writing individual database items.
    • Flat TopK Latency: Retrieval latency remains rock-solid at ~240 ms whether 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)