Essay

The sentence that killed the database


Published
2026-08-24
Reading time
5 min
Tags
retrieval, infrastructure

I designed this site's retrieval feature around Postgres with pgvector, then deleted the database before it existed. Here is the sentence that killed it.

01

The plan, which was ordinary

The site has an ask box. You type a question about one of the projects and it answers from the case studies, the content layer, and twelve repository audits written from the code. Every answer cites the passage it came from.

The obvious way to build that is the way everyone builds it. Chunk the corpus, embed each chunk, put the vectors in a database that can do nearest-neighbour search, and query it at request time. I had the schema written. Tables for the chunks, pgvector for the embeddings, an index, two edge functions, row-level security on everything.

What stopped it was not a technical objection. It was a bill. A new Postgres project on the organisation I would have used costs ten dollars a month, and paying ten dollars a month for something makes you ask what it is for.

02

The sentence that killed it

The corpus is 310 chunks. Serialised with its embeddings it is under three megabytes, and without them it is about two hundred kilobytes of text.

A vector database would add a network hop, an API key, a cold start and a monthly bill, in order to replace a loop over an array.

That is the whole argument. Once it was written down there was nothing to discuss, because the loop over the array finishes in about a twentieth of a millisecond. I measured it: two hundred iterations of the real retrieval path over five real questions, warmed, and the ninety-fifth percentile is 0.05 milliseconds. A network round trip to a database in the same region is roughly a thousand times that, and the database was only ever going to return the same rows.

Scale changes this answer and I know where. Past a few thousand chunks a linear scan starts to cost something a user can feel, and past that an approximate index earns its keep. I am not there. Nobody building a personal site is there.

03

What it actually is now

A build step reads the content layer and the audit documents, splits them on headings and blank lines, embeds each chunk once, and writes one JSON file. The file ships with the server bundle and is imported like any other module.

At request time, scoring is BM25 over stemmed terms, plus cosine similarity against the query embedding, fused with reciprocal rank fusion. BM25 does most of the work, because the questions people ask about a project use the project's own vocabulary, which is exactly where lexical scoring is strongest. The vectors earn their place on the questions that do not.

There is one operational rule that comes with this shape: the file must never reach the browser. Two megabytes of float32 is not something to make a visitor download, and the only thing keeping it server-side is that nothing in a component's import graph touches the retrieval module. That is easy to break by accident, so it is asserted rather than assumed, and the assertion was proved by planting a violation and watching the build fail.

04

The general case

I do not think this generalises to a rule about vector databases. Vector databases are good, and at real scale the argument runs the other way round.

What generalises is the sentence. Before adding infrastructure, write down what it adds and what it replaces, in one sentence, next to each other. A network hop, a key, a cold start and a bill, to replace a loop over an array. If the sentence is embarrassing to read, the infrastructure was decided by habit rather than by need.

The same test killed a second thing on this site. The contact form was going to write rows to a table. The point of a contact form on a personal site is that the message reaches an inbox, and a row in a table nobody reads is not that. It sends mail, with one retry, and falls back to a mailto link when nothing is configured.

All writing