> ## Documentation Index
> Fetch the complete documentation index at: https://docs.brussle.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Tune a matcher

> Find why a payment's pick missed its invoice, reach the oldest open invoices in a large block, and size the key against what a matcher costs and the limits it runs under.

When a [matcher](/guides/entity-matching) picks the wrong invoice, or none, this page shows how to find out why: what the engine read, and where your outcomes say the true invoice was. The fix is usually the key, and the same key sets what the matcher costs.

The examples use the `settles` judgment from [match a payment to its invoice](/guides/entity-matching).

## Inspect an unexpected answer

`fresh` says the answer agrees with each document it read, at their current revisions. It does not say that the engine read the invoice you expected. When a pick surprises you, open its evaluation with `GET /namespaces/{ns}/evaluations/{id}`:

<CodeGroup>
  ```python Python theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  answer = ns.get("pay_881", answers=["settles"])["answers"]["settles"]
  evaluation = ns.evaluation(answer["evaluation_id"])
  for read in evaluation.get("related_documents", []):
      print(read["relation"], read["document_id"], read["revision"])
  print("cut:", evaluation["context_truncated"], "capped:", evaluation["relations_capped"])
  ```

  ```ts TypeScript theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  const answer = (await ns.get("pay_881", { answers: ["settles"] })).answers?.settles;
  if (!answer?.evaluation_id) throw new Error("pay_881 has no evaluation yet");
  const evaluation = await ns.evaluation(answer.evaluation_id);
  for (const read of evaluation.related_documents ?? []) console.log(read.relation, read.document_id, read.revision);
  console.log("cut:", evaluation.context_truncated, "capped:", evaluation.relations_capped);
  ```
</CodeGroup>

* **`related_documents`** lists each invoice the engine read, with the `relation` and the `revision`. An invoice not in it was not a candidate then.
* **`context_truncated`** is `true` when the context passed `max_tokens` and Brussle cut part of it. Brussle drops a relation's records before the payment's own fields, so a cut can remove candidates. See [what is cut first](/guides/context-recipes#related-documents).
* **`relations_capped`** is `true` when a relation stopped before it considered every matching document, so a candidate can be missing.

| What you see | What to check |
| - | - |
| The invoice is not in `related_documents`, and it was `open`. | The `match`: the invoice's `attributes.kind` and `attributes.status`. The `join`: its `attributes.block` must equal the payment's. The `window`: created within 180 days of the newest invoice in the block. `last_n`: no more than 10 newer open invoices. For a large block, see [the oldest open invoice](#the-oldest-open-invoice). |
| The invoice is there, at a `revision` older than the one you wrote. | The evaluation is older than the write. Read the payment again: it is `pending`, or has a new `evaluation_id`. |
| `context_truncated` is `true`. | Raise `max_tokens`, or shorten the labels. `stats.truncation_rate` on the judgment shows how often Brussle cuts. |
| `relations_capped` is `true`. | Make the key finer. |

To see the exact text the engine read, get the payment with `include=history,context` (`include_history` and `include_context` in Python, `includeHistory` and `includeContext` in TypeScript), or open the payment in the dashboard. Inspect one evaluation before you connect the queue to your workflow. See [a fresh answer can have incomplete inputs](/concepts/freshness#a-fresh-answer-can-have-incomplete-inputs).

## Count the misses by cause

One evaluation shows one payment. Across all your [outcomes](/train/outcomes), the [calibration report](/train/report)'s `match_misses` counts the outcomes that named an invoice the evaluation did not read, by cause:

| Cause | Meaning |
| - | - |
| `candidate_newer_than_evaluation` | The invoice was created after the evaluation. |
| `beyond_last_n` | The invoice was in the block, but the evaluation did not read it: `last_n` cut it, it was outside `window` or `match`, or with `relative` it was on the other side of the payment. |
| `not_in_block` | The invoice had a different key: the key misses a condition of a match. |
| `evaluation_failed` | The evaluation failed. |

## The oldest open invoice

The relation reads the newest candidates first, so in a large block the oldest open invoices go unread. In receivables, the invoice a payment settles is often the oldest.

Make the key finer first: add the currency, the amount band or the month the invoice is due, so that each block holds only the invoices a payment could settle.

When you import history, or Brussle judges a payment long after it arrived, the newest invoices in the block can be ones issued after the payment. Add `"relative": "before"` and remove `window`. Each payment then reads the `last_n` invoices of its block created immediately before it, by `created_at`, whatever came later, and a new invoice judges again only the payments whose `last_n` it is in. See [records in sequence](/guides/related-documents#records-in-sequence).

If `match_misses.beyond_last_n` grows, the key is too coarse, or the payments do not read the invoices they settle.

## Freshness

A write to a candidate makes each payment in its block `pending` from the moment Brussle acknowledges it. Once the block has been quiet for `fanout.debounce_ms` (10 minutes by default), or `fanout.max_wait_ms` (1 hour) has passed, Brussle reads the block once and compares it with what it showed before:

* **Nothing the payments read changed,** such as an edit to a field the relation does not show: the payments are `fresh` again, with no engine call.
* **Something changed:** a new open invoice, a settled one, or one replaced by a different invoice with the same fields. Brussle judges again each payment in scope whose own candidates changed. A payment whose candidates did not change keeps its answer.

## Cost and limits

**Size class.** A matcher's question includes each candidate with its label fields, so it counts toward the [size class](/pricing#judgments) with the context. Ten candidates with three short labels each come to about a thousand tokens: a standard judgment, which counts as 1. The same payment as pair documents is ten yes/no judgments. Long labels or more candidates can make the question large, which counts as 4, so label with the fields that tell candidates apart, not every field you have.

**A month of re-judges** is the block changes in a month multiplied by the payments in scope in each block. The create response's [replay estimate](/guides/related-documents#the-replay-estimate) counts the writes to the payments and each invoice created in the last 30 days, because a new invoice changes the candidates of every payment in its block. It cannot see invoices edited or deleted in that time, so it says `"excludes": ["candidate_edits", "candidate_deletes"]` and shows its figures as "at least". In receivables a settled invoice is an edit, so each invoice changes its block at least twice: when you create it and when it is settled. Expect about twice the estimate.

**The limits** sit in `freshness.fanout`, and a `PATCH` changes them:

* **`block_cap`** is the most payments in one block, 1,000 by default. Brussle refuses a create when your data already has a larger block, naming the key value in `details.key`, so you find a coarse key before its first fan-out. A block that grows past it later waits: its payments read `stale` (`limit_reached`), and the events feed records `judgment.limit_reached` with `limit` `block_cap` and the key value in `key`. A higher cap starts it again.
* **`rolling_limit`** is the most re-judges the block changes can cause in any 30 days, 300,000 by default. Past it, the rest wait as `stale` (`limit_reached`) until the window has room. When the window is short, the busiest blocks wait first, so a few busy counterparties cannot make every other counterparty's payments stale. See [the rolling limit](/guides/fan-out#the-rolling-limit).

Each blocking key uses one of the namespace's [reference indexes](/limits).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.