Advanced: ALONG, FOLD, BEST BY¶
ALONG (Path-Carried Values)¶
ALONG carries state through recursive expansion.
CREATE RULE shortest AS
MATCH (a)-[e:EDGE]->(b)
ALONG dist = prev.dist + e.weight
YIELD KEY a, KEY b, dist
Use prev.<field> to reference prior recursive step values.
FOLD (Aggregation)¶
FOLD aggregates rule outputs after derivation.
FOLD in a recursive rule¶
A recursive FOLD rolls up per KEY, one level at a time: a self-reference
binds one row per KEY of the target carrying that KEY's folded value, so a
parent folds its children's values and each child has already folded its own.
CREATE RULE build AS
MATCH (p:Part) WHERE p IS NOT assembly
YIELD KEY p, 0.5 AS b
CREATE RULE build AS
MATCH (p:Part)-[:CONTAINS]->(c:Part)
WHERE c IS build
FOLD b = MPROD(b)
YIELD KEY p, b
For TOP → MID → {L1, L2} with both leaves at 0.5, MID is 0.25 and TOP
folds that single value, so TOP is 0.25 too.
ALONG is the per-path alternative, and it wins on its own clause: a clause
carrying ALONG reads the pre-fold rows, because prev.<field> accumulates
along one path and a per-KEY aggregate is not defined per path. The choice is
made per clause, so a sibling clause of the same rule that folds an inherited
value still gets the folded value. See
Rule semantics.
Standard Aggregators¶
| Operator | Description | Example |
|---|---|---|
COUNT(*) |
Row count | FOLD n = COUNT(*) |
COUNT(expr) |
Non-null count | FOLD n = COUNT(b.value) |
SUM(expr) |
Sum | FOLD total = SUM(b.amount) |
AVG(expr) |
Average | FOLD avg = AVG(b.score) |
MIN(expr) |
Minimum | FOLD low = MIN(b.price) |
MAX(expr) |
Maximum | FOLD high = MAX(b.price) |
COLLECT(expr) |
Collect into list | FOLD paths = COLLECT(b.name) |
SUM and AVG are non-monotone — they can decrease between iterations, which violates fixpoint requirements, so the compiler rejects them inside a recursive stratum. The rest (COUNT(*), COUNT(expr), MIN, MAX, COLLECT) declare a monotone join and are accepted in recursion.
COUNT and COLLECT are monotone but unbounded — the accumulator has no top element. That only costs iterations for COUNT, whose accumulator the fixpoint loop tracks: a recursive fold over it can iterate until max_iterations rather than converging. COLLECT is assembled after the fixpoint, so it does not itself keep the loop iterating.
Monotonic Aggregators (Safe in Recursion)¶
Monotonic variants guarantee the aggregate only ever moves forward in its own lattice order between iterations — upward for MSUM/MMAX/MCOUNT/MNOR, downward for MMIN/MPROD — enabling safe use inside recursive strata:
| Operator | Formula | Identity | Use When |
|---|---|---|---|
MSUM(expr) |
Running sum | 0 |
Non-negative additive accumulation |
MMAX(expr) |
Running maximum | −∞ |
Worst-case / dominant value |
MMIN(expr) |
Running minimum | +∞ |
Best-case / bottleneck |
MCOUNT(expr) |
Running count | 0 |
Monotonically growing count |
MNOR(expr) |
1 − ∏(1 − pᵢ) |
0.0 |
Independent OR-causes (probabilities) |
MPROD(expr) |
∏ pᵢ |
1.0 |
Independent AND-conditions (probabilities) |
These six spellings are Locy's declared lattice folds. Writing one is an explicit assertion that the fold is a lattice join, and it is what the BEST BY guard keys on. MSUM and MCOUNT are unbounded, so the same max_iterations caveat applies to them.
Monotonic Probabilistic Folds¶
For probability domains, use MNOR (noisy-OR) and MPROD (product):
CREATE RULE failure_risk AS
MATCH (c:Component)-[:HAS_SIGNAL]->(s:QualitySignal)
FOLD risk = MNOR(1.0 - s.pass_rate)
YIELD KEY c, risk
See Probabilistic Logic for full documentation.
Multiple FOLD Clauses¶
A rule can have multiple FOLD clauses to compute several aggregates simultaneously:
CREATE RULE exposure AS
MATCH (a:Account)-[t:TRANSFER*]->(b:Account)
WHERE b IS suspicious
FOLD total = MSUM(t.amount),
path_count = MCOUNT()
YIELD KEY a, total, path_count
Post-FOLD WHERE (HAVING)¶
A WHERE clause after FOLD filters aggregated groups — equivalent to SQL's HAVING. Only rows where the condition holds are kept.
CREATE RULE frequent_payer AS
MATCH (p:Person)-[r:PAID]->(i:Invoice)
FOLD n = COUNT(*)
WHERE n >= 3
YIELD KEY p, n
This yields only people who made 3 or more payments. The WHERE n >= 3 runs after FOLD computes the count.
Multiple Conditions¶
Combine conditions with AND:
CREATE RULE big_spenders AS
MATCH (p:Person)-[r:PAID]->(i:Invoice)
FOLD n = COUNT(*), total = SUM(r.amount)
WHERE n >= 2 AND total >= 1000
YIELD KEY p, n, total
Available Columns¶
Post-FOLD WHERE can reference:
- FOLD output columns (n, total in the examples above)
- KEY columns (the grouped-by entities)
It cannot reference pre-aggregation columns that were consumed by FOLD.
Relationship to QUERY WHERE¶
Post-FOLD WHERE filters during rule evaluation. QUERY ... WHERE filters after:
// Filter inside the rule (during FOLD):
CREATE RULE counts AS
MATCH (e:Ev) FOLD n = COUNT(*)
WHERE n >= 3
YIELD KEY e.action, n
// Filter outside (at query time):
QUERY counts WHERE n >= 5 RETURN *
Both are valid. Use post-FOLD WHERE when the filter is intrinsic to the rule's semantics. Use QUERY WHERE for ad-hoc filtering at call sites.
BEST BY (Witness Selection)¶
BEST BY picks the best candidate row by ordering expression.
CREATE RULE cheapest AS
MATCH (a)-[e:EDGE]->(b)
ALONG cost = prev.cost + e.weight
BEST BY cost ASC
YIELD KEY a, KEY b, cost
BEST BY and FOLD¶
BEST BY picks one witness row; a declared lattice fold aggregates across all of them. Combining the two in one rule is rejected at compile time with BestByWithMonotonicFold.
The guard is syntactic over the six declared lattice folds (MSUM, MMAX, MMIN, MCOUNT, MNOR, MPROD) and applies to every rule, recursive or not. It is deliberately decoupled from the recursion monotonicity check, so BEST BY combined with MAX, MIN, COUNT or COLLECT is legal even though those aggregates are monotone.
To get both, compute the aggregate in its own rule and reference it with IS from the BEST BY rule.
Using similar_to in ALONG and BEST BY¶
The similar_to() expression function works in ALONG accumulators and BEST BY selectors, enabling semantic similarity scoring along recursive paths.
Semantic Relevance Along Paths¶
CREATE RULE semantic_path AS
MATCH (a:Document)-[:LINKS_TO]->(b:Document)
ALONG relevance = prev.relevance * similar_to(b.embedding, $query)
YIELD KEY a, KEY b, relevance
Best Semantically Similar Path¶
CREATE RULE best_match AS
MATCH (a:Topic)-[:RELATED]->(b:Topic)
ALONG score = prev.score + similar_to(b.embedding, $query)
BEST BY score DESC
YIELD KEY a, KEY b, score
Hybrid Scoring in Rules¶
CREATE RULE hybrid_relevant AS
MATCH (q:Query)-[:SEARCHES]->(d:Document)
WHERE similar_to([d.embedding, d.content], q.text,
{method: 'weighted', weights: [0.7, 0.3]}) > 0.5
YIELD KEY q, KEY d
See the Vector Search guide for full similar_to documentation.
Practical Guidance¶
- Use
ALONGfor accumulators (distance, risk, confidence, similarity). - Use
FOLDwhen you need grouped summaries. - Use post-FOLD
WHEREto discard groups that don't meet a threshold (e.g.,WHERE count >= 3). - Use
BEST BYwhen you need one witness path, not all candidates.