Locy Logic Programming¶
Locy is a Datalog-with-Cypher-syntax logic programming language built into IndentiaDB. It allows you to define recursive inference rules over the LPG projection of your data and derive new facts using bottom-up fixpoint evaluation.
Use Locy when: - You need recursive reachability (transitive closure, multi-hop reachability) - You want to derive new facts from existing graph patterns - You need aggregated inference (count followers-of-followers, sum costs along a path) - You want stratified negation (derive facts about what is not connected)
Endpoint¶
Request Format¶
| Field | Type | Default | Description |
|---|---|---|---|
program |
string | Required | Locy source program |
max_iterations |
integer | 1000 | Maximum fixpoint iterations per recursive stratum |
timeout_ms |
integer | 30000 | Overall evaluation timeout in milliseconds |
Response Format¶
{
"derived": {
"reachable": [
{ "n": "http://example.org/alice", "m": "http://example.org/carol" },
{ "n": "http://example.org/alice", "m": "http://example.org/dave" }
],
"reachable$query": [
{ "n": "http://example.org/alice", "m": "http://example.org/carol" }
]
},
"warnings": [],
"total_facts": 5,
"timed_out": false
}
The derived object contains one key per rule name. When a QUERY command is present, its filtered results are stored under "<rule_name>$query".
Language Reference¶
CREATE RULE¶
Defines an inference rule. Multiple clauses with the same name produce a union (OR semantics).
CREATE RULE <name> AS
MATCH <pattern>
[WHERE <condition> [AND <condition> ...]]
[FOLD <binding> = <aggregate_fn>(<expr>) OVER <pattern>]
YIELD KEY <expr> [, <expr> ...]
Example — direct edge rule:
Example — base + recursive rule (two clauses, same name):
CREATE RULE reachable AS
MATCH (n:Person)-[:KNOWS]->(m:Person)
YIELD KEY n, m
CREATE RULE reachable AS
MATCH (n:Person)-[:KNOWS]->(mid:Person)
WHERE mid IS reachable TO m
YIELD KEY n, m
The second clause uses IS reachable TO m — a join against derived facts from the previous stratum/iteration, binding the variable m to the matched target column.
MATCH Pattern¶
Locy MATCH patterns follow OpenCypher syntax:
MATCH (n:Label) -- single node
MATCH (n:Label)-[:TYPE]->(m:Label) -- single hop
MATCH (a)-[:T1]->(b)-[:T2]->(c) -- chained hops
Property filters inside patterns ({name: "Alice"}) are passed to the WHERE clause. Node and edge variables bound in MATCH are available in WHERE and YIELD.
WHERE Conditions¶
Standard expression filters and IS-references:
WHERE n.age > 18
WHERE n.name = "Alice"
WHERE n IS connected -- filter: n must appear in 'connected'
WHERE NOT n IS isolated -- negation
WHERE mid IS reachable TO m -- join: bind m from 'reachable' where subject=mid
IS reference semantics:
| Syntax | Meaning |
|---|---|
x IS rule |
Keep rows where x appears in the first column of rule's facts |
NOT x IS rule |
Keep rows where x does NOT appear in rule's facts (stratified negation) |
x IS rule TO y |
Join: bind y to the matching target column in rule's facts |
YIELD KEY¶
Specifies the output columns of a rule. KEY marks the columns that form the identity of a derived fact (used for duplicate elimination and IS-reference matching).
FOLD Aggregations¶
Aggregates values over a sub-pattern before yielding:
CREATE RULE friend_count AS
MATCH (n:Person)
FOLD count = COUNT(m) OVER MATCH (n)-[:KNOWS]->(m)
YIELD KEY n, count
| Function | Description |
|---|---|
COUNT(x) |
Number of matched rows |
SUM(x) |
Numeric sum of x |
AVG(x) |
Numeric average of x |
MIN(x) |
Minimum value of x |
MAX(x) |
Maximum value of x |
COLLECT(x) |
Array of all non-null x values |
QUERY Command¶
Filters derived facts from a named rule and stores results under "<rule>$query":
QUERY reachable -- all facts from 'reachable'
QUERY reachable WHERE m.department = "Engineering"
QUERY friend_count WHERE count > 5
Examples¶
Transitive Reachability¶
Find all persons reachable from any person through KNOWS edges:
CREATE RULE reachable AS
MATCH (n:Person)-[:KNOWS]->(m:Person)
YIELD KEY n, m
CREATE RULE reachable AS
MATCH (n:Person)-[:KNOWS]->(mid:Person)
WHERE mid IS reachable TO m
YIELD KEY n, m
QUERY reachable
curl -X POST http://localhost:7001/locy/query \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"program": "CREATE RULE reachable AS MATCH (n:Person)-[:KNOWS]->(m:Person) YIELD KEY n, m\nCREATE RULE reachable AS MATCH (n:Person)-[:KNOWS]->(mid:Person) WHERE mid IS reachable TO m YIELD KEY n, m\nQUERY reachable",
"timeout_ms": 10000
}'
Stratified Negation¶
Find nodes that have no outgoing edges (isolated nodes):
CREATE RULE has_outgoing AS
MATCH (n)-[:EDGE]->(m)
YIELD KEY n
CREATE RULE isolated AS
MATCH (n)
WHERE NOT n IS has_outgoing
YIELD KEY n
QUERY isolated
Counting Followers¶
Count the number of direct followers per person:
CREATE RULE follower_count AS
MATCH (n:Person)
FOLD count = COUNT(f) OVER MATCH (f:Person)-[:FOLLOWS]->(n)
YIELD KEY n, count
QUERY follower_count WHERE count > 100
Multi-Hop Influence Score¶
Derive a custom "influence" score by joining direct and transitive reaches:
CREATE RULE direct_reach AS
MATCH (n:Person)-[:KNOWS]->(m:Person)
YIELD KEY n, m
CREATE RULE transitive_reach AS
MATCH (n:Person)-[:KNOWS]->(mid:Person)
WHERE mid IS transitive_reach TO m
YIELD KEY n, m
CREATE RULE influence AS
MATCH (n:Person)
FOLD direct = COUNT(d) OVER (n IS direct_reach TO d)
FOLD transitive = COUNT(t) OVER (n IS transitive_reach TO t)
YIELD KEY n, direct, transitive
QUERY influence
Cycle Detection via Rules¶
Detect nodes that are part of a cycle:
CREATE RULE reachable AS
MATCH (n)-[:EDGE]->(m)
YIELD KEY n, m
CREATE RULE reachable AS
MATCH (n)-[:EDGE]->(mid)
WHERE mid IS reachable TO m
YIELD KEY n, m
CREATE RULE in_cycle AS
MATCH (n)
WHERE n IS reachable TO n
YIELD KEY n
QUERY in_cycle
Evaluation Model¶
Locy uses bottom-up fixpoint evaluation with topological stratification:
- Rules are compiled into strata based on their dependency graph.
- Non-recursive strata are evaluated in a single pass.
- Recursive strata iterate until no new facts are produced (fixpoint) or
max_iterationsis reached. - IS-references within a stratum resolve against the current derived-fact set (earlier iterations).
- Negation (
NOT IS) is only allowed between strata (stratified negation): a rule may not negate a fact derived in the same stratum.
Guaranteed termination for programs with only monotone rules (no negation) on finite graphs — the Herbrand base is finite.
ACL Behaviour¶
- Read permission is required. Returns
401without a valid token;403if the actor lacks read permission. - The full LPG projection is loaded for evaluation. Result facts are not filtered at the row level — the program sees all nodes and edges the LPG projection contains.
- If your actor has restricted graph access (graph-level ACL), only the visible portion of the LPG is loaded, so derived facts will only reflect accessible nodes and edges.
Supported vs. Planned Features¶
| Feature | Status |
|---|---|
CREATE RULE … MATCH … WHERE … YIELD KEY |
✅ |
| Multi-clause rules (union semantics) | ✅ |
| Recursive rules with fixpoint iteration | ✅ |
IS reference filter (x IS rule) |
✅ |
IS reference join (x IS rule TO y) |
✅ |
Stratified negation (NOT x IS rule) |
✅ |
| FOLD aggregations (COUNT/SUM/AVG/MIN/MAX/COLLECT) | ✅ |
| QUERY command with WHERE filter | ✅ |
| Multi-hop MATCH patterns | ✅ |
| ASSUME blocks (abductive reasoning) | Planned |
| ABDUCE queries | Planned |
| ALONG (path-carried values) | Planned |
| BEST BY optimisation | Planned |
| Probabilistic rules (MNOR/MPROD) | Planned |
| EXPLAIN RULE provenance trees | Planned |