add Neo4j heuristics calculator - #1647
Conversation
|
@andyfelder16 is this ready for re-review? Please do not forget to ask for a re-review whenever ready. |
| */ | ||
| private Truthness equalityTruthness(Object a, Object b) { | ||
| if (a == null || b == null) { | ||
| return null; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
introducing this new H_INVALID could be done in a new PR before this one is merged
There was a problem hiding this comment.
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 to1.0 - scored low and kept in the average:
avgOfTrue = (1 + 0.333 + 0.05) / 3 = 0.461, and the same mutation moves it to0.683 invalidabsorbing the AND: the whole conjunction isH_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
There was a problem hiding this comment.
@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)
There was a problem hiding this comment.
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
| } | ||
|
|
||
| private Truthness computeHeuristicPattern(MatchPattern pattern, Neo4jGraph graph, List<Neo4jMapping> mappings) { | ||
| Truthness nodes = computeHeuristicMatchNodes(pattern.nodeCount(), graph.nodeCount()); |
There was a problem hiding this comment.
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()]; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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) { |
| 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) { |
There was a problem hiding this comment.
can source or taget be null?
| if (source == null || target == null) { | ||
| return FALSE_TRUTHNESS; | ||
| } | ||
| for (Neo4jEdge rel : graph.getEdges()) { |
| if (conditions.isEmpty()) { | ||
| return TRUE_TRUTHNESS; | ||
| } | ||
| List<Truthness> truths = new ArrayList<>(); |
There was a problem hiding this comment.
rename truths to listOfTruthness
| } | ||
| List<Truthness> truths = new ArrayList<>(); | ||
| for (CypherCondition c : conditions) { | ||
| truths.add(evaluator.evaluateCondition(c, mapping)); |
There was a problem hiding this comment.
store evaluator.evaluateCondition to a local variable before adding to the list
First of a few PRs splitting #1641, per review feedback.
H(Q,G): how close a graph is to satisfying a parsed Cypher MATCH query, as a Truthness<ofTrue, ofFalse>matched_elements), Neo4jConditionEvaluator (condition + operand evaluation), Neo4jMapping