Intermediate16 min read

DynamoDB Vector Search

Before August 5, 2026, similarity search over DynamoDB data meant a copy of the table in OpenSearch or a separate vector database, kept in sync. For many workloads that pipeline is no longer needed.

Yes, natively since August 5, 2026. You store embeddings as a plain List of Number values, add a vector index, and run approximate nearest neighbor (ANN) queries with the SearchVectors API.

It works on on-demand tables only, up to 4,096 dimensions, and bills per byte written, searched, and stored.

  • A third index family: vector indexes sit beside and LSIs, with their own read API (SearchVectors) and ANN only.
  • It is fast, measured: on a live 1024-dim index in us-east-1 (August 2026 probe), SearchVectors kept pace with GetItem from the same client (p50 39 ms vs 44 ms, 30 calls each).
  • Metering is per byte: $0.52/GB written to the index, $0.002/GB a search examines and returns, $0.25/GB-month stored (us-east-1 list price). An indexed embedding bills table writes at 4 bytes per dimension (measured); unindexed, ~1.9× that.
  • S3 Vectors is still the bulk store: at least ~8× cheaper at rest and far cheaper to batch-load. DynamoDB wins on millisecond reads, streaming writes, and vectors living next to the item they describe.

How a vector index works

There is no new attribute type. An embedding is an ordinary list of numbers on the item, {"L": [{"N": "0.0132"}, {"N": "-0.0475"}, …]} on the wire, written with the same PutItem and UpdateItem you already use.

The index is a separate structure. DynamoDB replicates the vector into it asynchronously at 32-bit float precision, along with any attributes you project or filter on. Search results are eventually consistent, like a read.

The lag is small in practice. In the one measured run, the first search sent after PutItem returned already found the vector, 136 ms after the write was sent (both round trips included; a separate single PutItem in the same run took 99 ms).

Small is not zero, so don't make a search confirm a write. When a user must see what they just saved, read it back with GetItem and ConsistentRead: true and merge it into the results yourself, or retry the search until the key shows up.

async replication, f32PutItem / UpdateItemBase tableembedding as List of NumbersVector indexvectors + filter attributesSearchVectorsTopK + equality filtersTop-K items with scores

Where it sits next to the index types you know:

Vector indexGSILSI
Max per table5 (default quota)20 (default quota)5
Read APISearchVectorsQuery, ScanQuery, Scan
PartiQLNoYesYes
Capacity modeOn-demand onlyBothBoth
ConsistencyEventualEventualStrong available
Add after table creationYesYesNo

Each index fixes its dimension count and distance function at creation (the traps cover replacing one). COSINE and EUCLIDEAN score lower-is-more-similar; DOT_PRODUCT scores higher-is-more-similar and can go negative.

The index holds f32 copies. Higher-precision values are accepted and rounded on the way in, so measure recall against f32, not your float64 originals.

Create one and search it

Say you run semantic search over support tickets, so an agent can find "users who hit this before" without matching keywords. Each ticket item carries an embedding of its subject and body, from any model you like.

Titan Text Embeddings V2, for one, costs $0.02 per million input tokens on Bedrock (us-east-1, on-demand).

The table must be on-demand. Switch a provisioned one with aws dynamodb update-table --table-name SupportTickets --billing-mode PAY_PER_REQUEST; AWS allows four such switches per rolling 24 hours, each taking minutes.

If the move worries you on cost, set an on-demand maximum throughput as a ceiling; on-demand vs provisioned covers the trade.

Then add the index. The HASH element scopes every search to one service value; INLINE_FILTER attributes (up to 18) allow equality filters at search time:

aws dynamodb update-table \
  --table-name SupportTickets \
  --attribute-definitions AttributeName=service,AttributeType=S \
                          AttributeName=severity,AttributeType=S \
  --vector-index-updates '[{"Create": {
    "IndexName": "TicketEmbeddings",
    "VectorAttribute": {"AttributeName": "embedding"},
    "SearchSchema": [
      {"AttributeName": "service", "SearchSchemaElementType": "HASH"},
      {"AttributeName": "severity", "SearchSchemaElementType": "INLINE_FILTER"}
    ],
    "Projection": {"ProjectionType": "KEYS_ONLY"},
    "Dimensions": 1024,
    "DistanceFunction": "COSINE"
  }}]'

Each HASH value is capped at 10 MBps of index writes and 1 GBps of search data examined (default quotas, raised through AWS Support). Throughput scales across values, so key the index on an attribute with many of them.

AWS's own example divides by the item size: at about 4 KB per item (a 1024-dim vector is 4,161 B on its own), that is roughly 2,500 writes a second per value, and fewer for larger items. A busier value needs a finer HASH (service#region, say), searched once per value and merged by Score. The search schema is fixed once built, so a new HASH means building a replacement index alongside the old one.

Adding an index to an existing table backfills it, and SearchVectors returns ValidationException until that ends, with no partial results.

AWS warns the backfill "can take a substantial amount of time even when the base table holds very few items", so plan for minutes.

Even after DescribeTable says ACTIVE, the search endpoint can keep rejecting for a short while. There is no waiter; probe with a real search in a retry loop.

Creating the index inside CreateTable, with a new empty table, skips the backfill. In the August 2026 probe (one table, DescribeTable polled every 5 s) it reported ACTIVE about 26 s after the call, and the first search succeeded.

Searching takes the query embedding as a bare JSON array of {"N": …} values. Do not wrap it in a DynamoDB L. The stored attribute uses the list type, the request parameter does not, and mixing them up is an easy first mistake:

aws dynamodb search-vectors \
  --table-name SupportTickets \
  --index-name TicketEmbeddings \
  --search-vector file://query-embedding.json \
  --top-k 5 \
  --search-condition-expression "service = :svc AND severity = :sev" \
  --expression-attribute-values '{":svc": {"S": "billing"}, ":sev": {"S": "high"}}'

You get back up to TopK items sorted most-similar-first, each with a Score, plus ConsumedCapacity when you ask for it. TopK caps at 100, there is no pagination, and the response caps at 16 MB.

Need more than 100 neighbors? Use S3 Vectors (TopK up to 10,000, paginated), or split the search by HASH or filter value and merge by Score, knowing each split still returns at most 100.

The embedding itself is excluded from results unless you project it and request it. Leave it out: returning vectors inflates both the response and the search bill.

SearchConditionExpression accepts equality (=) only; AWS lists <>, <, <=, >, >= and IN as not yet available. When the index defines a HASH attribute, every search must pin exactly one value for it. Until that changes:

  • IN, or several HASH values: run one search per value and merge the results by Score. Same index, same distance function, so the scores compare.
  • A range (dates, prices): write a coarse bucket attribute such as month, declare it as an INLINE_FILTER, and filter on the bucket with =. The search schema is fixed, so a live index needs a replacement built alongside, and a range spanning several buckets takes one search per bucket, merged by Score.
  • Anything else: over-fetch (TopK up to 100) and filter client-side. Only projected attributes come back, so project the ones you filter on.

SearchVectors needs its own IAM action, dynamodb:SearchVectors, on the index ARN. AWS notes that existing policies granting DynamoDB read access don't include it.

It also uses its own hostnames. With an account ID in your credentials, the AWS SDK for JavaScript v3 sends searches to {account-id}.search-ddb.{region}.amazonaws.com, otherwise to search-dynamodb.{region}.amazonaws.com.

Permit both, plus the dual-stack search-dynamodb.{region}.api.aws, wherever a VPC endpoint, proxy or egress allowlist covers only dynamodb.{region}. Otherwise writes succeed and search fails with a connection error that never says why.

Don't pass --endpoint-url either: a single override can't serve both hostnames.

DynamoDB Local has no vector support. It answers the update-table above with ValidationException: Nothing to update and SearchVectors with UnknownOperationException, so test against a small on-demand table in a dev account.

What a vector index bills

Two new per-byte meters (vector writes, vector search) plus ordinary storage for the index copy, with a 1 KB minimum per write and per search request, on top of normal table charges (us-east-1 list price from the AWS pricing API, rechecked 2026-10-09):

MeterStandardStandard-IA
Vector writes$0.52/GB$0.65/GB
Vector data a search examines/returns$0.002/GB$0.0025/GB
Storage (table and index)$0.25/GB-mo$0.10/GB-mo

AWS's tutorial warns that an embedding's base-table copy, stored as decimal digits in a List, "can be considerably larger" than the index's f32 copy. Live write-unit measurements say otherwise once it is indexed.

Unindexed, an embedding bills by its decimal digits: ~1.9× the f32 size at 9 significant digits (fewer digits, smaller). Point a vector index at that same attribute and its base-table write billing drops to exactly 4 bytes per dimension:

DimensionsUnindexed List attribute (billed)Same attribute, vector-indexed (billed)
2561,914 B1,024 B
7685,760 B3,072 B
1,0247,653 B4,096 B
1,53611,501 B6,144 B
3,07222,957 B12,288 B

Measured by binary-searching the write-unit boundary with a padding attribute, a fresh item key per write. The probe's full 1024-dim ticket item billed 5 write units; without an index on embedding, 8. Storage metering was not measured.

The vector-write meter tracked the f32 size closely in the same runs. VectorWriteRequestBytes came back as 4 bytes per dimension plus 11 B of key overhead on a bare index, and plus 65 B in the probe's index, whose search schema added two short string attributes (service and severity). Expect a different figure with other filter attributes.

Search billing is the meter you can't compute in advance. VectorSearchRequestBytes counts the vector data a search examines plus the data it returns, so it rises with TopK and with what you project.

AWS's guidance is to measure it with ReturnConsumedCapacity on a representative dataset rather than estimate it from dimension count.

Model the table-side costs in the pricing calculator; the vector meters stack on top of the write units it already computes.

DynamoDB vector search vs S3 Vectors

AWS sells two serverless vector stores, built for opposite access patterns.

DynamoDB vector searchS3 Vectors
GAAug 2026Dec 2025
Latency classSingle-digit ms (AWS claim)~100 ms frequent, sub-1 s infrequent (AWS claim)
Scale ceilingNo stated vector cap; 600 GB table cap for index creation (soft)2B vectors per index
Max dimensions4,0964,096
Distance functionsCosine, Euclidean, dot productCosine, Euclidean
Index writesAsync from table (eventually consistent)Strongly consistent
FilteringEquality only, ≤18 attributes + 1 partition keyRich metadata filters, 2 KB filterable cap per vector
TopK100, no pagination10,000, paginated
Storage$0.25/GB-mo, twice (table + index)$0.06/GB-mo, once
Writes$0.52/GB, 1 KB min/request$0.20/GB, 128 KB min/PUT
Queries$0.002/GB examined and returned$2.50/M requests + whole-index processed-bytes charge

The write minimums decide the streaming case. Writing one 1024-dim vector at a time, per million writes (list rates, with the measured 5 write units + 4,161 vector-write bytes per item):

Write patternDynamoDBS3 Vectors
Single-vector writes~$5.14/M~$24.41/M
Batched (500 per PutVectors)n/a (writes are per item)~$0.78/M

S3's 128 KB minimum per PUT makes it the expensive option for one-at-a-time writes.

Monthly storage, and S3 Vectors' query charge at 1M queries a month, for a 1024-dim corpus at us-east-1 list price. S3 Vectors' query charge follows its published formula: whole index size times a tiered rate.

DynamoDB storage here assumes the table copy is also stored at f32, which was not measured. At the unindexed decimal size it runs about 1.4× higher.

CorpusDynamoDB storageS3 Vectors storageS3 Vectors queries
1M vectors~$1.95~$0.23~$11
10M vectors~$19.50~$2.35~$80
100M vectors~$195~$23.50~$217

DynamoDB's query charge follows the bytes each search meters. AWS says that figure grows only logarithmically as you add items ("doubling the number of items in an index adds a small, constant amount"), and a HASH key shrinks it further.

Per 1M searches a month, at us-east-1 list price:

Bytes metered per searchCost per 1M searches
~22 KB (measured: TopK 10, 50-vector partition, keys-only index)~$0.04
400 KB (assumption)~$0.76
4 MB (assumption)~$7.81
40 MB (assumption)~$78

The same search over a 1-vector partition still metered 21.4 KB, far above the 1 KB minimum, and AWS's tutorial reports 31,449 bytes for its own 50-item example.

The measured row comes from a tiny partition, so meter your own searches on a representative one before trusting any row.

When to use which

  • Vectors describe live items you already keep in DynamoDB (tickets, knowledge-base articles, user profiles, agent memory): use the vector index. The vector is written with the item, so there is no sync pipeline to drift.
  • Millions of embeddings, queried occasionally (RAG over documents, archives, nightly jobs): use S3 Vectors. Batch-load cheaply, pay $0.06/GB at rest, tolerate a few hundred ms.
  • High QPS with hybrid ranking (text + vector relevance, facets, aggregations): use OpenSearch. A Classic serverless collection bills at least 2 OCUs (~$350/month at the $0.24/OCU-hour us-east-1 list price); NextGen has no minimum and scales to zero.
  • Vectors joined to relational data: Aurora PostgreSQL with pgvector, which scales to zero. A list-price estimate puts a small RAG workload under ~$50/month.

To feed OpenSearch from the table, use the DynamoDB zero-ETL integration with Amazon OpenSearch Service (OpenSearch Ingestion). It needs point-in-time recovery and Streams with new and old images enabled.

It loads a snapshot through an S3 export, then follows the stream, without using your table's read or write capacity. An interruption longer than 24 hours can drop updates and force a fresh index build.

For most DynamoDB shops the answer is both. Keep hot, filterable vectors on the table where writes are atomic with the item, and archive the long tail to S3 Vectors, whose strongly consistent batched writes make it a clean sink.

Vector search in DynoTable

DynoTable creates the index and binds an embedding model to it. In the Table indexes pane, Create index offers a vector index beside a GSI and asks which model feeds it and which attributes its text comes from.

The index's shape (dimensions, distance function, search schema) is fixed by DynamoDB. The model and source attributes can be edited later from the index row.

An item-editor save re-embeds the item when its source text changed, if AI is on, licensed, and configured. Otherwise the save goes through with a notice that the vector describes the old text.

Bulk-edit commits, PartiQL writes, and the Direct Run don't re-embed.

Search by typing a sentence, by pasting a raw vector, or with Find similar on a row; results carry a Score column. The full walkthrough is in the vector search documentation.

The traps

  • Silent de-indexing: an item missing the index's HASH attribute writes fine and never enters the vector index. Reproduced live: the PutItem succeeded with no error, and 15 seconds later the vector was in no partition's results.

    Require the attribute in your write path; a ConditionExpression can't, since it checks the stored item. Find stragglers with a Scan filtered on attribute_exists(embedding) AND attribute_not_exists(service), then rewrite them.

  • Wrong-dimension writes are rejected: after a model switch, every write fails with a ValidationException that names the index and the vector attribute and gives the expected and actual dimensions (1024 against 512).

    To switch models, write the new vectors to a second attribute and build a second vector index on it. Backfill, compare relevance, move your reads, then delete the old index.

  • Stale embeddings: DynamoDB never recomputes vectors. Edit a ticket's text without rewriting embedding and searches silently match the old content, and nothing records which items drifted.

    Regenerate the vector in the same write that changes the text, and fan out through to a regeneration consumer for writers you don't control.

  • TopK has no relevance cutoff: when at least K vectors match the partition and filters, you get K back, however poor. Ask for 10 with three good matches and you still get 10; fewer come back only when fewer vectors match.

    Pick a Score threshold from your own data and drop results past it. The score direction flips between distance functions.

  • The 1 KB minimums: low-dimension vectors do not meter proportionally cheaper, on writes or searches. Below about 250 dimensions (1 KB at f32), a smaller model saves nothing on the vector meters.

  • The index's shape is fixed: dimensions, distance function, search schema, and an INCLUDE projection's attribute set can't change. Deleting and recreating in place leaves search failing for the whole backfill.

    Build the replacement beside the old index instead (AWS's own example puts a second index with a different distance function on the same attribute). Switch reads once it serves, then delete the old one.

  • Indexes build one at a time: a second create or delete on the same table fails with LimitExceededException while one runs, and a replacement needs a free slot under the five-index default quota.

    Index storage bills for the index's whole lifetime, queried or not, so delete the ones you stop using.

Try it against your own tables

Size the embedding before you commit to it. Item size limits still apply, and an indexed 3072-dim embedding adds 12 KB to every write of the item, plus about as much again on the vector-write meter whenever the vector itself is written.

The item size calculator gives an embedding's unindexed size, and the search lag behaves like a GSI's.

Then try DynoTable to create the index, bind a model to it, and re-embed items as you save them in the item editor. Bulk edits, PartiQL writes, and writes from your own code still need the regeneration path above.

By Dmitriy · Updated