7.6.1 Gating an expensive judgment — what actually narrows it
An ai.* primitive is not evaluated per row the way an ordinary Cypher function is, so the
position of the call in a WHERE decides nothing. Writing the cheap test first and the judgment last
does not make the judgment lazy, and AND does not short-circuit it — nothing in openCypher promises
an evaluation order for boolean operands anyway.
What does bound the cost is this: every deterministic condition the engine can attach to the node is applied before the judge runs, wherever it appears in the query. A condition it cannot attach is still honoured — the rows that come back are exactly right — but it is applied after judging, so the model has already read everything fetched. The result looks identical. Only the bill differs, and nothing in the answer says so.
So a gate has to be written in a shape the engine can attach:
| Condition | Bounds the judgment? |
|---|---|
r.amount >= 20000000 |
yes |
toFloat(r.amount) >= 20000000 |
yes — wrapping is fine |
size(trim(coalesce(r.description,''))) <= 60 |
yes — see below: a length is screened by the engine, not by the source |
r.description CONTAINS 'lease' |
yes |
r.description IS NOT NULL |
no |
toLower(r.description) = toLower(r.title) |
no — compares two properties, not a value |
| a condition on a different node | no |
r.amount >= $threshold in a view |
yes — a view’s declared params become literals before the query is read |
r.amount >= $threshold with caller-bound params |
no — the value is not known when the query is read |
A wrapper that CHANGES the compared quantity gates too — but locally. toFloat(r.amount) >= 5
still compares the amount, so it can be handed to the source. size(r.description) <= 60 compares a
LENGTH the source has never heard of, so it is never handed to one; the engine evaluates it itself on
every fetched record before the judge is asked, and the judge sees only the rows it keeps. This holds
for a chain of size / length / char_length / toString over coalesce / trim / toLower /
toUpper / toFloat / toInteger. A record the engine cannot decide — a missing value whose
coalesce default it does not evaluate — is kept for the judge and settled by the query afterwards,
so the screen only ever admits rows, never drops one the query would keep. A wrapper outside that
set (substring, reverse, …) is not screened: the condition is honoured in full, after judging.
That last pair is the one that surprises people. The same text bounds the cost inside a view and does not bound it when the parameter is bound by the caller at execution time. If a screen carries an expensive judgment, define it as a view with declared parameters (§8) and the gates work as written.
Write the gate for cost, not as a finding. A gate decides which rows are worth asking about; it
is not evidence about them. “Short description” is a good gate for a disclosure screen and a bad
finding: a lease naming a street address is brief and completely checkable, while a bare product noun
is brief and tells a reader nothing. Only the judgment can tell those apart — which is exactly why
the length test belongs in the WHERE and the verdict belongs to ai.score.
One criterion, one judgment. A filter and a projection sharing the same criterion string share a single judgment, so this reads each row once:
WHERE size(trim(coalesce(r.description,''))) <= 60 AND ai.score(r, 'a reader could identify what this bought') < 0.4RETURN r.cnId, ai.score(r, 'a reader could identify what this bought') AS disclosureTwo different spellings of the same idea are two judgments, at twice the cost, and they may disagree about the same row.

