0022 — pgvector in services/intelligence
One service is allowed the vector extension and HNSW indexes, because embeddings can always be rebuilt from source and decide nothing on their own.
Decided. The intelligence service may install the Postgres vector extension in its own database
and build HNSW indexes on embeddings. No other service may install an extension.
Some background. An embedding is a list of numbers that stands for a piece of text. Two pieces of
text that mean similar things get similar numbers. Searching by embedding finds text that means what
you asked for, even when it uses none of your words. The vector extension is what lets Postgres
store those numbers and search them quickly.
The platform rule is "no database extensions" (Architecture). This decision carves out one service. It does not cancel the rule.
This rests on something we have not checked. We do not know whether our managed Postgres offers the
vector extension. It is in the local Docker image and in flow-context-data's compose file, which
tells us nothing about production. If the managed service does not have it, this decision changes
and the alternatives below come back. Check before the first migration ships.
Alternatives
A separate vector database — Pinecone, Qdrant, Weaviate. These are built for this job, and rebuilding an index in one of them cannot slow down the main database.
Rejected because it is a second database to run, with its own uptime, its own credentials, and no shared transaction with the rows it indexes. Every write would have to succeed in two places, and we would have to write the code that repairs it when one of them fails.
Compare vectors in Go. Load the candidates into the service and score them there.
Rejected because it works up to a few thousand vectors and then stops. That is far below the size this service is built for.
Keyword search only, no embeddings. The cheapest option, and it genuinely does half the job (see 0024).
Rejected because keyword search cannot find a decision that used none of the words you typed. Finding those is most of why this service exists.
Why
- The rule is about correctness, not about extensions. We banned extensions so that none of them
can calculate a balance, generate an id, or enforce a rule. That would put business logic somewhere
our tests cannot reach.
vectordoes none of that. It gives us an index and a way to measure how far apart two lists of numbers are. - Nothing stored here is a source of truth. Embeddings are calculated from text that lives in another service. They never decide who may see something and never order a ledger. We can delete all of them and rebuild from source. That is the same argument 0002 makes for why a derived read shape is not CQRS.
- If it breaks, it breaks one database. No other service gets the extension, and intelligence going down does not take the UI with it.
Consequences
- Intelligence's first migration runs
create extension if not exists vector, and its local Postgres image has to include the extension. No other service's database changes. - Every stored embedding records which model produced it, and every query filters to the model currently in use. Vectors from two different models cannot be compared — the distance between them means nothing. Leaving this filter out does not cause an error. It quietly returns wrong results.
vector(N)has a fixed width, so changing the number of dimensions means building a new table and switching over, not altering the old one. That and a model change can both happen while the service is running, because the index can always be rebuilt.- This applies to
services/intelligenceonly. A second service that wants an extension needs its own decision, not a pointer to this one.
0021 — Datadog log search in the DevOps console
The operations console reads retained logs from Datadog through a devops-service proxy, because the Application key cannot reach a browser and every query must stay inside one environment.
0023 — One sqlc package per domain, and no joins between domains
A service with several domains generates one sqlc package for each, which turns a cross-domain join into a compile error instead of a review comment.