Skip to main content
A relation in a context recipe shows the engine other documents beside the one it judges, and Brussle judges the answer again when what the relation shows changes. Judge a document with its related documents walks through one relation, and relations compares the kinds.

Define a relation

A payment reads the open invoices in its block (the invoices with the same customer and currency). One relation shows the 10 newest as text, and a second counts all of them from the last 180 days and adds their amounts:
Each judgment has up to 4 relations, and each relation has fields, aggregate or both. A relation reads the answers or outcomes of at most 4 judgments, and a document and every document related to it are in the same namespace.

The four joins

  • The documents that point at the judged one: {"theirs": "attributes.<name>", "mine": "id"}. A document belongs to the judged document whose id is in its theirs attribute, and the relation takes last_n, window or both.
  • The document the judged one points at: {"theirs": "id", "mine": "attributes.<name>"}. See read the document the judged one points at.
  • The documents that share a key: {"theirs": "attributes.<key>", "mine": "attributes.<key>"}, the same attribute on both sides. This blocking relation reads the candidates for matching, and it needs window unless it sets relative.
  • The documents a matching judgment linked: one side id, the other answers.<j>.value of a judgment that chooses among a blocking relation’s candidates, with when naming one of its thresholds. See read the matches from the invoice.
Brussle refuses two different attributes on the two sides with invalid_request.

Which documents a relation reads

  • Newest created first, unless order changes it. Brussle orders by created_at, so last_n: 8 is the 8 most recently created. It applies window first, then the order, then last_n.
  • At most the newest 1,000. A relation reads at most the newest 1,000 matching documents, whatever its last_n, window and order. Brussle applies a match on answers or outcomes to each of those 1,000 before last_n keeps the newest that qualify.

How window counts

The window counts back from the later of two times (when Brussle received the judged document’s newest write, and the newest creation among its related documents) and never from the clock, so an edit to an old related document never moves it. You can set a document’s created_at but not the time of a write, so after an import a window counts back from the import. Use last_n to read imported history. On a blocking relation, the window counts back from the newest document in the block. A relation that reads the document the judged one points at takes no window.

Count, sum and show the latest

aggregate computes numbers over the documents the relation selected, in the shape {"count": true, "count_where": {...}, "sum": [paths], "avg": [paths], "min": [paths], "max": [paths], "latest": [paths]}, with at most 8 paths in all.
  • count is the number of documents selected. labelled, on a relation that reads an outcome, counts those that have one.
  • count_where counts the documents whose answers or newest outcome meet a condition. See count the matches of a matcher.
  • sum, avg, min and max skip values that are not numbers. With nothing to work on, sum is 0, and avg, min and max are null.
  • latest is the value in the most recently created document that has one, whatever the order. It can name an image (images.photo, shown as latest(images.photo)), and an engine that reads images counts it like any other image.
  • latest, min and max can name created_at or updated_at, which gives the time of the newest, oldest or most recent document as RFC 3339 text. The feature age(<relation>.latest(created_at)) turns that time into days, measured from the judged document’s own write.
  • "render": false keeps the aggregates (all but latest) out of what the engine reads. Features still read them, so Brussle records a change that moves only them with no engine call.
To show a few documents and count many, use two relations with the same match. Brussle calculates aggregates before it cuts anything, and never cuts them.

What the engine sees

Each relation becomes one related.<name> entry in the compiled context, after the fields entries, in the order of the relations’ names:
  • records holds the selected documents in the relation’s order, keyed by path.
  • One key per aggregate, such as count and sum(state.amount).
  • capped: true appears when the relation may be missing a matching document, and the evaluation then shows relations_capped: true. A relation without last_n is capped when it matched more than 1,000 documents, since the numbers then cover the newest 1,000. A relation that selects by an answer or outcome is capped when it found fewer than last_n among the newest 1,000 and more matched. A blocking relation is capped when it found more candidates in its block than its last_n, so the right one may be unread.
  • range_full: true appears on a relation with an order when more documents matched than the 1,000 it sorts.
  • missing: true appears on a relation that reads the document the judged one points at, when that document does not exist. See a missing referenced document.

What is cut first

Brussle removes content in this order when the context is over max_tokens, until it fits, and records context_truncated: true:
  1. Whole images, on an engine that reads them: those of the relations, then of previous, then of fields, the last one first. Each becomes {"digest": "sha256:...", "omitted": "over_the_limit"}, and if the text alone is over, every image goes.
  2. The records of the last relation by name. Brussle keeps them whole, in the relation’s order, while they fit, so its oldest records go first (or, with an order, those that sort last). Brussle cuts the first record that does not fit instead of removing it, by shortening its longest text value, and the records after it keep at most half of the space left. One long record (a meeting transcript, say) therefore does not push out all the others. An image in a record is never cut.
  3. The records of the relation before it, and so on.
  4. Any previous entries.
  5. The fields, from the end of the last field.
Raise max_tokens if the engine must read long records whole. Brussle bills each answer by the size class of the context it sent rather than by max_tokens, so only the answers that read long records cost more.

Read the document the judged one points at

A relation with join: {"theirs": "id", "mine": "attributes.<name>"} reads the one document whose id is in the judged document’s attribute, its referenced document (an order line reads its product, a message its conversation).
  • It shows as any relation, with at most one record, and takes no window.
  • With a last_n, up to 254, it reads the documents that a named list of ids names.
  • It can show the referenced document’s answers.
  • It is empty when the attribute is missing, is not a document id, or names a document outside match.
  • A change to what it shows judges again the documents that point at it, inside the judgment’s re-judge scope, which freshness.fanout sets. See fan-out and judge a document with the document it points at for the cost and the controls.

Pick documents with order

A relation keeps its newest documents by default, while order keeps the documents that come first by a field you choose (the invoices due soonest, or the oldest open item):
  • by is created_at (the default), updated_at, a state.<path> or an attributes.<name>. desc is true (largest first) unless you set it.
  • Values sort by type first: numbers, then strings, then true and false, then missing values and everything else. desc reverses the order inside each type, never the order of the types, so a missing value always comes last.
  • Inside a type, numbers compare by value, whole numbers exactly up to 64 bits. Strings compare by Unicode code point, so an ISO date such as "2026-10-02" sorts as a date, and false comes before true.
  • Ties go to the most recently created document, then the lower id.
  • records show in that order, and over max_tokens the records that sort last go first. latest is still the newest document’s value.
  • Where it works. order works on a relation that reads the documents that point at the judged one, and on a named list, where it replaces the list order. Brussle refuses it on a blocking relation, whose candidates are always the newest, and on a relation that reads one referenced document.
An order sorts only inside the newest 1,000. With 1,001 open invoices, the oldest is outside the newest 1,000 before the order runs, so an order by due date cannot pick it however early it is due. range_full: true tells you this happened. Use a window or a narrower match to keep the documents that matter inside the newest 1,000. The create response warns you if a judged document already has more than 1,000 matching documents. To sort, a relation with an order reads up to 1,000 documents for each judged document, where one without reads only its last_n. The price of an answer does not change, since Brussle bills judging by the context and not by what it reads. A judged document with many related documents does take a larger share of a pass of judging, so a catch-up over many of them takes longer. A relation whose match names an answer or an outcome reads the same 1,000 documents. The create response warns with the factor (a relation with last_n: 5 reads up to about 200 times as much). An edit never moves a document into or out of the selection when the order is by created_at. With an order by another field it can. A write that changes state.due can move that invoice in or out, and Brussle judges again if what the engine sees changed. The create response warns about this, and about an order by a long text field, which sorts up to 1,000 long values for each judgment.

Show a number as a band

An entry in a relation’s fields can show a number as its band in place of the number, so a score of 0.42 shows as medium:
  • bands are 1 to 9 cut points, each larger than the one before. labels has exactly one more entry, each up to 64 bytes.
  • A number shows as one label, at the position given by the number of cut points at or below it, so 0.29 is low, 0.3 is medium, and 0.7 or more is high.
  • A value that is not a number shows unchanged, and a missing value is left out.
  • Aggregates use the number before banding. On a judgment with features, that p is the corrected one once a fit applies (see which number readers use).
The engine sees the word, so a move inside a band does not change the context. The answer stays, Brussle bills nothing, and a referenced document fans out to nothing. Use bands for a number that changes often but matters only past a few thresholds.