Improving Inference (v2, Beta)
This guide is a practical companion to Inference (v2): how to take a working prediction and make it better. It uses one running example — predicting which employee should process an invoice (the processor field) — and walks through the levers that most improve quality on v2 collections: schema links, supporting fields, fuzzy $numeric matching, scoping the candidates, correcting for correlated features with config.ai, and priors for cold-start cases.
The example schema (v2 collection tables):
{
"companies": {
"type": "collection",
"columns": {
"id": { "type": "Int" },
"name": { "type": "Text", "analyzer": "english" },
"size": { "type": "Int" },
"employeeCount": { "type": "Int" }
}
},
"invoices": {
"type": "collection",
"columns": {
"id": { "type": "Int" },
"date": { "type": "String" },
"company": { "type": "Int", "link": "companies.id" },
"sender": { "type": "Text", "analyzer": "english" },
"product": { "type": "Text", "analyzer": "english" },
"description": { "type": "Text", "analyzer": "english" },
"glCode": { "type": "Int", "link": "glCodes.id" },
"processor": { "type": "Int", "link": "employees.id" }
}
},
"employees": {
"type": "collection",
"columns": {
"id": { "type": "Int" },
"name": { "type": "Text", "analyzer": "english" },
"role": { "type": "String" },
"department": { "type": "String" },
"company": { "type": "Int", "link": "companies.id" },
"tags": { "type": "Text", "analyzer": "english" }
}
}
}
Start with a baseline
The simplest processor prediction uses the free-text fields of the invoice:
{
"from": "invoices",
"where": {
"product": "AWS cloud hosting",
"description": "Monthly cloud infrastructure invoice"
},
"predict": "processor"
}
This already works, but it only knows invoices by their text. The sections below add structure so Aito can reason about who the processors are and which of them is plausible.
Link related tables
A "link" column turns a foreign key into a relationship Aito can reason across. Here processor links to employees, so Aito can use an employee's role, department, and name as evidence — not just the raw processor id.
"processor": { "type": "Int", "link": "employees.id" }
Links unlock three things:
- Predicting the link —
"predict": "processor"returns employee ids. - Referencing linked fields in
wherewith dot notation — e.g."processor.department": "IT". - Priors from the linked table — even a processor who has never appeared in the invoice history can be predicted from their employee attributes (see Lean on priors).
Link a column for every meaningful relationship: company → companies, glCode → glCodes, processor → employees. Two practical limits: the linked types must match exactly (Int link → Int id), and links resolve one hop only — processor.department works, processor.company.industry does not. (v2 also supports member links — a Set/Array column whose items are links — for many-to-many relationships; see CollectionDb.)
Add supporting data and metadata
Aito does no manual feature engineering — every column becomes a potential feature, and it automatically selects the ones that predict the target. So the most direct way to improve a prediction is to give it more to work with:
- More columns on
invoices—sender,product,description,glCode,date. A glCode or sender strongly correlated with a processor becomes a powerful signal. - Richer linked tables —
role,department,tagsonemployeeslet Aito learn "infrastructure invoices go to the IT department".
When the predicted column is a link, basedOn chooses which fields of the linked table feed the prior estimate. Start broad, then narrow to the fields that actually carry signal — unrelated fields add noise:
{
"from": "invoices",
"where": {
"product": "AWS cloud hosting",
"description": "Monthly cloud infrastructure invoice",
"glCode": 4200
},
"predict": "processor",
"basedOn": ["role", "department"]
}
If predictions look noisy, reduce the fields in basedOn rather than adding more.
Fuzzy numeric matching with $numeric
By default a number in a where clause is matched exactly — "company.size": 500 is the feature "size equals exactly 500", which almost never recurs and so carries little signal. The $numeric operator makes the match inexact: "somewhere near 500", with a region whose width Aito derives from the spread and density of the data.
{
"from": "invoices",
"where": {
"description": "Monthly cloud infrastructure invoice",
"company.size": { "$numeric": 500 }
},
"predict": "processor"
}
Reach for $numeric on any continuous quantity — a company's size or employeeCount, an invoice amount, line counts. In v2 a numeric or Decimal field also offers a binned number prior by default, so sparse numeric evidence is smoothed by its neighbourhood rather than memorized. Use exact matching only for numbers that are really categorical (status codes, a fixed glCode).
Scope the candidates with where constraints
Most where propositions are soft: Aito turns each into a feature, keeps the subset that best predicts the target, and ignores the rest. So "product" and "description" describe the situation and nudge the prediction; Aito decides how much to trust them.
But a where constraint on the prediction target's own linked attributes does more than nudge — it scopes the candidates. Because you are predicting processor (a link to employees), constraining processor.company or processor.department restricts the prediction to the employees that match:
{
"from": "invoices",
"where": {
"product": "AWS cloud hosting",
"description": "Monthly cloud infrastructure invoice",
"processor.company": 42,
"processor.department": "IT"
},
"predict": "processor",
"basedOn": ["role", "department"]
}
Here the prediction is scoped to processors in the IT department of company 42 — no special filter operator is needed, just a plain statement about the target. It both improves accuracy (no implausible candidates from other companies) and speeds the query up (a smaller option space).
A good rule of thumb:
- Constrain the predicted field's own attributes (
processor.company,processor.department) to restrict which employees are considered. - Add descriptive signals (
product,description,glCode, amounts) to tell Aito about the invoice; it will weigh these automatically.
Correct for correlated features with config.ai
This is the lever v2 adds. When several where features are correlated — sender, product, and description on an invoice all point at the same vendor, or role and department move together — counting each as independent evidence over-weights the overlap and produces over-confident predictions: a $p near 1.0 that the accuracy doesn't earn.
v2 defaults to config.ai: "group" (group formation): the engine detects features that move together and folds them into one group that contributes a single, redundancy-adjusted vote. You usually don't need to set anything — the honest-confidence behaviour is on out of the box. The knob is there when you want to change it:
{
"from": "invoices",
"where": {
"sender": "Amazon Web Services",
"product": "AWS cloud hosting",
"description": "Monthly cloud infrastructure invoice"
},
"predict": "processor",
"config": { "ai": "high" }
}
config.ai | Use it when |
|---|---|
v2 (alias high) (default) | The everyday choice — And + Group re-expression; correlated evidence is common and this keeps confidence honest, while still peeling strong feature combinations into joint terms. Most robust against correlated overconfidence. |
group | Group re-expression only — collapse correlated evidence without the extra And feature-combination step. |
and (alias v1) | The classic v1 combination behaviour, without group formation. |
fast (alias flat) | The features are genuinely independent and you want the cheapest path (plain naive Bayes, no re-expression). |
See the effect in $why: with group active, correlated features appear as a single $group factor with one calibrated lift instead of several near-duplicate votes. Full table in Inference (v2) — Tuning.
Lean on priors for cold-start cases
Aito always combines the direct evidence above with hierarchical priors drawn from the linked tables — which is what makes it work even when a processor has little or no invoice history. For a brand-new employee, the prior is built from their employees attributes (role, department, name, tags) and from word-matches between the invoice text and those attributes — an invoice mentioning "infrastructure" lifts employees in the IT department; an invoice addressed to "Jane" lifts employees named Jane.
In v2 every field offers a prior uniformly: a link field contributes its linked-table attributes, a numeric field a binned neighbourhood, a text field its semantic tokens. You shape the link prior with basedOn (which linked fields to use) and, when you need fields from the invoice table inside a nested proposition, with $context. As historical data accumulates, direct evidence increasingly dominates and the prior matters less — so priors help most exactly when you have the least data.
Putting it together
A tuned processor prediction combines all of the above — links for reasoning about employees, supporting fields for signal, $numeric for the company size, a target constraint to scope the candidate set, basedOn to shape the prior, and the default group re-expression keeping confidence honest across the correlated text fields:
{
"from": "invoices",
"where": {
"product": "AWS cloud hosting",
"description": "Monthly cloud infrastructure invoice",
"glCode": 4200,
"company.size": { "$numeric": 500 },
"processor.company": 42
},
"predict": "processor",
"basedOn": ["role", "department", "name"]
}
Inspecting and iterating
Don't tune blind — measure each change:
- Add
"select": ["$p", "$why"]to a predict to see the probability and which features drove it ($why). If a field you expected to matter isn't in$why, it isn't carrying signal — or it's being ignored as noise. With the group default, watch for$groupfactors: they tell you which features Aito treated as one theme. $whyalso shows asupport-tempering(auto)factor: Aito tempers each prediction's confidence by the effective statistical support of its evidence (on by default). A value well below 1.0 means the prediction rests on thin or overlapping evidence; ≈ 1.0 means the evidence is well supported.- Use
_evaluateto measureaccuracy,meanRank, andgeomMeanPbefore and after each change — link a table, addbasedOnfields, switch a number to$numeric, scope the candidates, or changeconfig.ai— and keep the changes that move the metrics.geomMeanPis where the group default earns its keep: same accuracy, better-calibrated confidence. - If all candidate scores are similar, the
whereclause isn't distinguishing them: add more context, or scope the candidates with a target constraint.
See also Inference (v2) for the underlying model and Schema Design (v2) for getting links and types right in the first place.