Skip to main content
A judgment usually reads one document. It can also read the documents that point at the one it judges: an account with its tickets and invoices, a user with their posts, a conversation with its messages. The answer stays current as any of those documents change, and each evaluation records exactly which ones it read. Without it, you copy the related records into the judged document yourself and rewrite it whenever one of them changes. With it, Brussle tracks which judged documents each write touches, waits for bursts of writes to settle, and judges each one once per burst. This guide covers documents that point at the judged one. A judgment can also read the one document the judged document points at, such as the product an order line names; see judge a document with the document it points at. Relations compares the two.

Start with the simplest context

Before you add related documents, decide what the question depends on. Try these in order:
  1. A few recent records, as raw text. Use this when the answer is in what was said: an angry reply, a mention of a competitor, a question nobody answered. Keep last_n small, around 5 to 10, and show only the fields that carry meaning.
  2. Aggregates. Use these when the answer depends on how much or how often: how many tickets, the total of overdue invoices, the status of the latest one. An aggregate costs a few tokens where raw text costs hundreds.
  3. Both, as two relations on the same documents. Show the last few as text and count the last 90 days.
Nothing else is needed for most questions. As you choose:
  • Which works depends on the question. Text, counts or both can do best, so try both against your outcomes.
  • A few recent records usually carry most of the signal. Newest first with a small last_n is the right default.
  • Adding counts beside text rarely hurts. A composite judgment can take a relation’s aggregates as features beside a judgment’s answer.
Running a judgment on each related record only to feed the parent multiplies the cost, because every child judgment is billed; measure it against your outcomes before relying on it (see measure, improve and tune). When you already run a judgment on the related records for its own sake, a relation can read its answers: see roll up answers from related documents.

Define one

Related documents point at the judged one through an ordinary attribute. In this example the judged documents are accounts, and each ticket and invoice carries attributes.account_id. The judgment is an ordinary judgment with related in its context recipe, usually with applies_to so it judges only accounts:
The same shape fits a user and their posts (match on posts, theirs: "attributes.user_id") or a conversation and its messages (theirs: "attributes.conversation_id").
  • applies_to limits which documents the judgment judges, answers and bills. Other documents have no answer for it: answers leaves it out, and filters treat it as missing.
  • match picks which documents a relation reads, and join says how they point at the judged one: a document belongs to the judged document whose id equals its theirs attribute.
  • last_n, window or both bound each relation, so a context cannot grow without limit. A relation reads at most the newest 1,000 documents either way.
  • fields are the paths shown from each document, and aggregate adds numbers over the same documents. Each relation needs one or both.
The context recipe guide describes every key, how the context is rendered, and the limits.
The first version that joins on a new attribute builds a reference index for it, as a reference_index job, and the create response lists it in job_ids, one job per new attribute. Each job’s attribute names the attribute it indexes. A version that joins on an attribute that already has an index, but over documents the index does not cover yet (another match, or the same attribute used the other way round), gets a job for it too: the index is rebuilt to cover them, and still counts as one. A version that joins the same way as an existing one gets no job. The judgment answers once the job is done. Until then its answers are unavailable, except that under on_change a document written since the judgment was created reads pending: its evaluation waits for the index. A namespace has at most 8 reference indexes, one per attribute its relations join on, in either direction, blocking keys included. Reuse an attribute when you can: a create that would need a ninth is refused, and the error names the eight you have.

Newest created first, and what an edit costs

A relation orders its documents by created_at, newest first. window keeps those created within the window, and last_n then keeps the newest n. So “the last 8 tickets” means the 8 most recently created. The window counts back from the later of the judged document’s own newest write and its newest related document’s creation, not from the clock. Editing a related document moves neither the list nor the window. created_at is when a document was first written to Brussle, unless the write that created it gave its own created_at. When you import existing data, send each record’s original creation time as created_at, so relations read your history in the order it happened, and a window counts it from when it happened. A write to a related document always marks the judged document it points at as touched, but it only costs an evaluation when it changes what the engine would see:
  • A new related document changes the list, so the judged document is judged again, once its writes settle.
  • An edit to one of the last 8, in a field the relation shows, changes the context, so it is judged again.
  • An edit to an older one, or to a field no relation shows, leaves the compiled context exactly as it was. The answer is kept, and nothing is billed.
The tradeoff is that activity on an old related document goes unseen. If that matters, record it as a new document, such as an event, rather than an edit to the old one. Aggregates count the same selection, so they follow the same rule. To show a few documents but count many, use two relations on the same match: one with a small last_n and fields, one with a window and aggregate.

Debounce and its ceiling

A judged document with busy related documents, such as an account whose tickets keep arriving, would be judged on every write without a debounce. debounce_ms makes Brussle wait until its writes have been quiet that long, so a burst of 100 tickets in five minutes costs one evaluation. A debounce alone never ends for a judged document that gets a new related document every few minutes. The ceiling, max_wait_ms, judges it anyway once that long has passed since its oldest unjudged write. With a 10-minute debounce and a 1-hour ceiling, a ticket every minute for 3 hours costs 3 evaluations, not 180 and not 0.
  • max_wait_ms defaults to 12 × debounce_ms for a judgment with related documents and a debounce_ms above 0. With no debounce, and for a judgment of a single document, it defaults to no ceiling.
  • It must be at least debounce_ms, and null turns it off.
  • Like the debounce, it is a setting: change it with a PATCH and no new version. GET returns the value in effect.
Answers therefore lag their related documents by the debounce, typically minutes, and by at most the ceiling. That is the price of judging an account once per burst of tickets. A question about a single event, such as whether one payment is fraudulent, should stay a judgment of that document.

The replay estimate

The cost of a judgment with related documents depends on how often those documents change, not on how many judged documents you have. So before one runs on_change, Brussle replays your namespace’s last 30 days of writes through its relations, debounce and ceiling, and tells you what it would have cost. Two requests return this estimate and create nothing until you send confirm: true: creating a version with related on a judgment that runs on_change, and switching such a judgment to on_change.
  • entities is how many documents the judgment applies to now.
  • judgments_per_month is how many evaluations the replay counted, and judgment_units_per_month the judgments they would bill, each counted by its size class, from the compiled context and question of up to 1,000 of your judged documents. Here each is standard, so the two are the same.
  • cost_usd_per_month prices those judgments at the tiers your organization would be in, counting what it has already used this billing period.
  • bulk_pool_share is the share of the background judging rate available to the judgment that those evaluations would use. Above 1, the judgment cannot keep up with your writes: raise the debounce or the ceiling, or narrow the relations.
  • replayed_days is below 30 in a younger namespace, and the monthly figures are scaled up from it. A document counts as created at its created_at, so records imported with their original creation time are left out when they are older than the days replayed. Imported without it, they count as created during the import, and the replay describes the import rather than your ongoing writes.
“At least.” The replay cannot see every edit to a related document, only its creation and its latest write. For documents written once, such as events, messages and invoices, it is exact. For often-edited documents the figures are a lower bound: lower_bound is true, and the dashboard shows them as “at least”. The replay also does not count the evaluations that unchanged contexts save, which only lower the bill. The namespace budget still caps real spend. A confirmed request whose cost_usd_per_month is more than the namespace’s budget is refused with budget_exceeded, and the dashboard says so before you confirm.

What each answer and evaluation shows

An answer of a judgment with related documents carries a watermark: the position in the namespace’s log that its context was read at. Every write at or below it, to the judged document or to any document that points at it, is reflected in the answer.
  • A write to a related document makes the judged document it points at pending at the next read. It is fresh again once an answer lands with a watermark at or above that write.
  • revision still names the judged document’s own revision.
  • wait_for and wait_ms wait for the written or read document’s own answer. Writing a ticket with wait_for: ["churn_risk"] returns at once, because churn_risk does not apply to tickets.
Each evaluation also lists related_documents: every related document its context read, rendered or aggregated, with the relation and the revision it was read at. It comes back with include=history,context and from GET /namespaces/{ns}/evaluations/{id}. That list is what makes an audit exact after the documents have changed, and what an outcome with a horizon joins to: “this account churned” labels the evaluation whose 60-day prediction window it falls in, and that evaluation names the tickets it read. The dashboard’s evaluation page shows them grouped by relation.

When the context is too long

A judgment’s compiled context, the judged document’s fields plus its related documents, is capped at the recipe’s max_tokens or the engine’s limit, whichever is lower. For Jev current that is 32,000 tokens for the context plus the longest question; see limits. It rarely comes to that, because each relation is bounded by its last_n or window, and reads at most the newest 1,000 documents. When a context is still over the cap, it is cut in this order:
  1. The oldest records of the last relation, then those of the relation before it. Relations are cut in the order of their names.
  2. Then any previous entries, the last first.
  3. Then the judged document’s own fields, from the end of the last field.
  4. Aggregates are never cut. They are computed over the full selection before anything is cut, so a count or a sum still covers every record in the window even when only the newest few fit as text.
Recent records carry most of the signal, so they are the last to go. The evaluation records context_truncated: true and context_tokens, and its context shows the exact text the engine saw. You are billed for the context after cutting. If a judgment is often truncated, lower last_n, show fewer or shorter fields, or move volume into aggregates. The full rules are in context recipes.

Keep it affordable

A few habits keep the bill small, and most of them also make the answers better:
  • Keep last_n small. The last 5 to 10 records usually suffice and keep the judgment standard; a year of history can make it large or extra-large, which count as 4 and 16.
  • Prefer aggregates when volume is the signal. A count is a few tokens.
  • Show only the fields that matter. Leave long bodies out of a relation unless the question depends on them.
  • Share recipes. Judgments with the same context recipe, such as churn_risk and expansion on the same relations, are answered together. Each is still billed, but together they are lighter on rate limits, so bulk_pool_share stays low.
  • Pick the debounce for how fresh the answer must be, not shorter. A 10-minute debounce with a 1-hour ceiling judges a busy account at most a few times an hour.
  • Read the replay estimate. It is your own traffic, so it is the best guide to what a change will cost.

The document each judged document points at

A relation can also run the other way, and read the one document the judged document points at through its own attribute, such as an order line reading its product. A change to that document re-judges the documents that point at it, which has its own costs and controls. See judge a document with the document it points at.

More kinds of relation

A relation can also show another judgment’s answers for the documents it reads (roll-ups), or read the documents that share a key with the judged one and choose among them (matching). Relations lists every kind.

Not available yet

  • Oldest-first ordering, such as the opening messages of a thread.
  • Named collections. Use applies_to on an attribute such as kind.
  • Judgments that read the answers of a judgment with related documents: a relation reads only plain judgments.