Skip to content

add Neo4j heuristics calculator - #1647

Open
andyfelder16 wants to merge 6 commits into
masterfrom
neo4j-heuristics-core
Open

add Neo4j heuristics calculator#1647
andyfelder16 wants to merge 6 commits into
masterfrom
neo4j-heuristics-core

Conversation

@andyfelder16

@andyfelder16 andyfelder16 commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

First of a few PRs splitting #1641, per review feedback.

  • Computes H(Q,G): how close a graph is to satisfying a parsed Cypher MATCH query, as a Truthness <ofTrue, ofFalse>
  • Neo4jHeuristicsCalculator, Neo4jStructuralMatcher (matched_elements), Neo4jConditionEvaluator (condition + operand evaluation), Neo4jMapping
  • Neo4jNode/Neo4jEdge/Neo4jGraph: in-memory graph model
  • Works in Truthness end-to-end, mirroring the SQL heuristics calculator
  • 11 tests

@andyfelder16
andyfelder16 requested a review from jgaleotti July 21, 2026 14:16
@andyfelder16
andyfelder16 requested a review from jgaleotti July 25, 2026 15:00
@jgaleotti

Copy link
Copy Markdown
Collaborator

@andyfelder16 is this ready for re-review? Please do not forget to ask for a re-review whenever ready.

@andyfelder16
andyfelder16 requested a review from jgaleotti July 31, 2026 22:36
@jgaleotti
jgaleotti requested a review from arcuri82 August 3, 2026 14:08
*/
private Truthness equalityTruthness(Object a, Object b) {
if (a == null || b == null) {
return null;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in line of princple, i don't like passing null like this when computing heuristics score. in DistanceHelper, we have things like H_REACHED_BUT_NULL which is lower than H_NOT_NULL. i think we should "score a gradient" in these cases, and not rely on null

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it makes sense, you could even introduce a new value that is lower than H_REACHED_BUT_NULL (but still > 0), eg H_INVALID, and have a special new boolean field in Truthness called like invalid. if a truthness object is invalid, its heuristics is H_INVALID. any operation on invalid (eg AND or OR) would result in an invalid result. this would avoid passing around null. @jgaleotti what do you think?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

introducing this new H_INVALID could be done in a new PR before this one is merged

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on not returning null, and scoring a low value instead would already help the gradient.

The part I'm less sure about is having invalid propagate through AND/OR. Small example:

MATCH (p:Person) WHERE p.age = 25 AND p.nickname = "bob"

against a node (:Person {age: 27}), which has no nickname property. The conditions score:

condition ρ (ofTrue)
p:Person 1.0 label matches
p.age = 25 0.333 1 - normalizeValue(|27-25|)
p.nickname = "bob" ? property is absent, nothing to measure

Depending on what we do with that third one:

  • today, skipped: avgOfTrue = (1 + 0.333) / 2 = 0.667, and mutating age to 25 moves it to 1.0
  • scored low and kept in the average: avgOfTrue = (1 + 0.333 + 0.05) / 3 = 0.461, and the same mutation moves it to 0.683
  • invalid absorbing the AND: the whole conjunction is H_INVALID, and mutating age changes nothing

So a low ofTrue that still takes part in the aggregation gives us the "no nulls" property without losing the gradient, while an absorbing invalid loses it exactly on the queries where some predicate can't be valuated.

Happy to go either way, just let me know which one you prefer and I'll implement it

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andyfelder16 indeed, we should not lose the gradient! ;)
however, I need some clarifications here. Assume same query but with an OR:

MATCH (p:Person) WHERE p.age = 25 OR p.nickname = "bob"

now, consider the node (:Person {age: 25})... what would be the result here? null? we need to handle such case properly (and have at least 1 test to verify it)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not null, it gives <1.0, 0.1> and distance 0, which matches Cypher semantics: true OR null is true, and Neo4j does return that node. The unvaluatable branch is dropped and the OR is satisfied by the age one

I also ran the neighbouring cases to see where the current skipping breaks down:

query node distance today Neo4j returns
p.age = 25 OR p.nickname = "bob" {age: 25} 0.0 the node
p.age = 25 AND p.nickname = "bob" {age: 25} 0.0 nothing
p.nickname = "bob" {age: 25} 0.0 nothing

The last two are false positives. In Cypher true AND null is null, so neither query matches anything, but we report them as fully satisfied. That is the worst case for the search

My take is that this settles it in favour of your suggestion. Scoring a low value instead of skipping fixes both cases: taking 0.05 as that value, the AND goes from 0.0 to about 0.107, while the OR stays at 0.0 because the aggregation still picks up the satisfied branch

I'll add tests for the three cases above

@andyfelder16
andyfelder16 requested a review from arcuri82 August 4, 2026 13:59
}

private Truthness computeHeuristicPattern(MatchPattern pattern, Neo4jGraph graph, List<Neo4jMapping> mappings) {
Truthness nodes = computeHeuristicMatchNodes(pattern.nodeCount(), graph.nodeCount());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename to hNodes and hEdges. Otherwise it might be confused with the actual nodes and edges


private Truthness edgesForMapping(List<PatternEdge> patternEdges, Neo4jGraph graph,
Neo4jMapping mapping) {
Truthness[] perEdge = new Truthness[patternEdges.size()];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename to truthnessPerEdge


private Truthness computeHeuristicPattern(MatchPattern pattern, Neo4jGraph graph, List<Neo4jMapping> mappings) {
Truthness nodes = computeHeuristicMatchNodes(pattern.nodeCount(), graph.nodeCount());
Truthness edges = computeHeuristicMatchEdges(pattern.getEdges(), graph, mappings);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename to hEdges or similar

}

/** Existence-only edge check: TRUE if some graph relationship matches the edge's endpoints. */
private Truthness edgeMatch(PatternEdge edge, Neo4jGraph graph, Neo4jMapping mapping) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

edge -> patternEdge ?

private Truthness edgeMatch(PatternEdge edge, Neo4jGraph graph, Neo4jMapping mapping) {
Neo4jNode source = mapping.getNode(edge.getSourceVariable());
Neo4jNode target = mapping.getNode(edge.getTargetVariable());
if (source == null || target == null) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can source or taget be null?

if (source == null || target == null) {
return FALSE_TRUTHNESS;
}
for (Neo4jEdge rel : graph.getEdges()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rel is an actual edge

if (conditions.isEmpty()) {
return TRUE_TRUTHNESS;
}
List<Truthness> truths = new ArrayList<>();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename truths to listOfTruthness

}
List<Truthness> truths = new ArrayList<>();
for (CypherCondition c : conditions) {
truths.add(evaluator.evaluateCondition(c, mapping));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

store evaluator.evaluateCondition to a local variable before adding to the list

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants