Your application data lives in DynamoDB. Orders, products, customers all of it. Now you want users to search it by meaning rather than exact keywords: "waterproof jacket for hiking" should find the right product even if those exact words never appear in the description.
Until recently this meant adding a second database. You would copy your data into a dedicated vector store, embed it there, and run a pipeline to keep the two in sync. That brought a few costs:
- Another system to run, secure, and pay for
- Sync lag and drift. Update a product description in DynamoDB and the embedding in the vector store is now stale. Search keeps returning confident, well-ranked, wrong results, and nothing errors
- An extra round trip. Vector stores return IDs and similarity scores, so every query needs a second lookup against DynamoDB to fetch the actual records
The solution
DynamoDB now supports vector search natively. Embeddings are stored as an attribute on the same item as your operational data, and you query them with a SearchVectors call against a vector index on that attribute. AWS made this generally available on 5 August 2026, quoting single-digit millisecond latency at over 99% recall.
One table. No pipeline. No drift.
This post walks through building it: a retail transactions table, embeddings from Amazon Bedrock, and an AI agent on Lambda that queries it in natural language. Full code is at github.com/vinod827/dynamodb-vector-agent.
The table is just a table
There is no special vector table. It is a normal DynamoDB table holding order lines with all the usual attributes like product name, description, price, brand, category, country of origin, shipment details. The embedding is one more column on the same item.
Vectors use the existing List type, so there is no new data type and no migration. For an existing table, you add embeddings to your items with UpdateItem and create the index afterwards.
The index definition is short (infra/setup_table.py in the repo), but three choices in it matter.
The partition key. This example uses marketplace. Every search must specify exactly one value for it. That sounds restrictive, but it is how the index scales, a search in the US never touches UK data.
Inline filters are equality-only. No ranges, no BETWEEN. To filter on price, store a bucketed string like priceBand: "50-150" alongside the raw number. That is a modeling decision to make before loading anything.
Cosine, for text. DynamoDB also supports Euclidean, for when vector magnitude is meaningful, and Dot product, for cases like recommendations where direction and magnitude both matter. The rule of thumb is to match whatever your embedding model was trained with.
One thing to watch: TableStatus reaches ACTIVE while the vector index is still building. Poll DescribeTable until the index reports ACTIVE too.
Embed once per product, not once per row
Transactional data repeats. In this dataset, 10,000 order lines reference only 800 distinct products.
Embedding row by row means 10,000 Bedrock calls producing 800 different vectors. Caching by product ID makes it 800 calls twelve times fewer here, and more than a thousandfold over a million rows against the same catalogue.
It is just a dictionary, but it is easy to miss while writing the loop.
The search understands the question
The embedding model here is Titan Text Embeddings V2. Worth being clear that this is not a chat model, it takes text and returns 1024 numbers, deterministically, and nothing else.
Searching for "wireless headphones for commuting" in the US marketplace:
Rows 2 and 3 are the interesting part. The Soundcore keyboard description literally contains the words "built for daily commuting". The Logitech headphones say "travel and field work" and never mention commuting at all. Semantic search ranked the headphones higher anyway, because it identified that the question was fundamentally about headphones.
A keyword search would have ranked them the other way round.
Every hit also came back with brand, price, country of origin and order status attached which solves the extra round trip from the problem statement. The match and the record arrive together.
(The sample data is generated from a handful of description templates, so scores cluster more tightly than real product copy would. The ranking behavior is real; the absolute numbers are not meaningful.)
The agent
The agent is small: one Lambda, one tool (agent/handler.py). What makes it an agent rather than a search box is that the model decides what to search for and how to phrase it — users rarely phrase questions the way a product description is written.
Asked "show me winter gear people bought in the UK":
Here are some examples of winter gear purchased in the UK:
Vango Ripstop Nylon Hiking Pack — Outdoor, £60.90. A ripstop nylon hiking pack built for cold conditions, ultralight and packable.
Craghoppers Windproof Accessory — Apparel, £99.49–£111.26. Windproof accessory for cold-weather running, with a breathable mesh design.
Neither description contains the word "winter". The embedding matched "cold conditions" and "cold-weather running" to the idea, and the agent read the rows and wrote them up.
Where it falls short
The handler also logs what the agent searched for:
--- searches the agent ran ---
{'marketplace': 'UK', 'query': 'winter gear'} -> 5 hits
It passed the user's words straight through. The system prompt asks it to rewrite the question into product-description language — something like "insulated thermal layers waterproof outerwear" — which would have retrieved a wider set. It skipped that step.
This run used Amazon Nova 2 Lite. Dropping instructions that require an extra reasoning step is common in smaller models, and the failure is quiet: the answer looks fine, and only the trace shows the interesting part did not happen.
Two takeaways. Log what your agent searched for, not just what it answered. And because the Converse API is model agnostic for tool use, switching models is one environment variable and no code change:
export AGENT_MODEL_ID=us.anthropic.claude-sonnet-4-6
Things that will trip you up
The SDK. You need botocore >= 1.43.64. Lambda's bundled version lags, so bundle your own.
Bedrock model access. The Model access console page is retired and models auto-enable on first invocation, but an account can sit at NOT_AUTHORIZED until the model is invoked once from the playground. Anthropic models additionally require a one time use case form.
A missing partition key fails silently. If an item lacks the vector index partition key attribute, the write to the table succeeds and the item is left out of the index. No error, it simply never appears in search results.
Summary
The problem was maintaining two copies of the same data in two systems, and paying for it in cost, complexity, drift and an extra round trip on every query.
The solution removes one of the copies. The embedding is a column, the search is an index, and both live in the same table under the same permissions and the same pay-per-request billing as everything else.
If your data already lives in DynamoDB, semantic search is now a column and an index away.
Code: github.com/vinod827/dynamodb-vector-agent


Top comments (0)