The Dynoxide hexagonal logo centred on a near-black background

Running DynamoDB vector search locally

Dynoxide has had DynamoDB vector search since 1.0.0 landed on the 25th of August: vector indexes, SearchVectors, and the write paths keeping them current. It's on the native binary, the browser build and the MCP server, so an agent can create an index and search it without any of that reaching AWS.

I announced it at the time and then went on holiday instead of writing it up. This is the write-up, plus 1.1.0, which landed on the 3rd.

The index says ACTIVE about fifteen minutes early

Before building any of it I spent two days pointing things at real DynamoDB and writing down what came back, because I wanted vector search in the conformance suite and the docs didn't agree with themselves about when an index is ready.

Add a vector index to a table that already has data in it and it has to backfill. Fine, expected. So you poll DescribeTable and wait for IndexStatus to reach ACTIVE, which is what you'd do for a GSI and what AWS's docs told you to do at the time.

It reaches ACTIVE, you search it, and it refuses.

On a table with twenty-five items in it, that carried on for about a quarter of an hour.

Dynoxide does the same thing, deliberately. The index goes ACTIVE, searches still get refused, and a while later it starts answering. A readiness check built on polling for ACTIVE therefore breaks on your laptop rather than in production. Put the search in a retry loop and treat the refusal as "not yet".

The full lifecycle, for reference:

Phase TableStatus IndexStatus Backfilling SearchVectors Delete of the index
Allocating UPDATING CREATING false refused refused
Backfilling ACTIVE CREATING true refused accepted
Active, not searchable ACTIVE ACTIVE absent refused accepted
Searchable ACTIVE ACTIVE absent served accepted

The base table catches people out too. It's back to ACTIVE while the index is still CREATING, so a table waiter looks like the right gate about three phases too early.

And Backfilling doesn't go false when it finishes, it disappears. Which means "wait until IndexStatus is ACTIVE and Backfilling is false" never comes true, on AWS or here. That was AWS's own documented advice until they rewrote it.

Timings are the one thing not to copy across. Dynoxide's phases run in tens of seconds so you can watch them happen; AWS's are minutes and vary with the table. Don't write sleep(30) and assume you're ready.

Where it won't ever match

Dynoxide compares your query against every vector in the index, so you get the genuinely nearest results back. AWS builds an approximate index and skips most of the comparisons, which is how it stays quick on a huge table and why it will occasionally miss something that was really there. So don't pin an exact list of results in a test that also runs against AWS - assert what you actually care about, like "the item I put in comes back". It also means a local table holding millions of vectors isn't what this is for.

Ties are the other one. Dynoxide breaks them by primary key, AWS promises nothing, and three identical calls came back in three different orders. Sort them yourself if it matters.

My favourite is VectorSearchRequestBytes, where AWS can't reproduce its own number. Five identical searches over an index nobody had touched: 14214, 13903, 14214, 14214, 14518. Dynoxide reports something stable instead, which is handy for comparing one local search against another and useless for asserting against AWS.

Three months watching one error message

1.1.0 is a single behaviour change, and it's the end of something I started writing about in June.

Back then my conformance suite went red and the thing that failed was AWS. It had quietly reworded a pile of its validation errors, with nothing in any changelog. I checked four regions: two had the new wording, two didn't. I said I reckoned that was a rollout caught mid-flight rather than a permanent regional split, and that I had no way to prove it.

The suite sweeps 33 regions now. As of the 2nd September, BatchGetItem with an empty RequestItems map answers the new wording in 32 of them, and me-central-1 is the last holdout. So it was a rollout, and it took about three months.

The part I didn't see coming is that it moves per operation as well as per region. BatchWriteItem sits right beside it, takes the same empty map, hits the same validation, and is still on the old wording nearly everywhere - one region, eu-north-1, had crossed as of the 22nd August. Dynoxide follows eu-west-2, so one of the pair changed in 1.1.0 and its sibling didn't. They answer differently now, with a test pinning each, because that's what the real thing does.

The boring bits

1.0.0 carried a lot besides vector search. Consumed capacity is sized the way DynamoDB sizes it now, a stack of PartiQL fixes landed, and several requests that used to be accepted aren't any more. If you've got tests asserting on exact answers, read the changelog before you upgrade.

One worth calling out on its own: StorageBackend is sealed. Vector indexes alone added seven methods to that trait, and left open, every one of those would have been a major.

Everything moved to 1.0.0 together and the browser engine dropped its -preview suffix. That number isn't a claim that conformance is finished - the live standings are the honest answer to that.

Get it

  • npm: npm install --save-dev dynoxide to pin it in a project, or npx dynoxide to run the latest without installing
  • Homebrew: brew install nubo-db/tap/dynoxide
  • Cargo: cargo install dynoxide-rs (crates.io, docs.rs)
  • Docker: ghcr.io/nubo-db/dynoxide, mirrored best-effort to Docker Hub (nubodb/dynoxide) and ECR Public
  • GitHub: source and pre-built binaries, plus the nubo-db/dynoxide/action GitHub Action for CI