Skip to content

Commit ed29b8e

Browse files
committed
Clean up apoc.algo.*
1 parent 714112a commit ed29b8e

5 files changed

Lines changed: 372 additions & 62 deletions

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
package apoc.algo;
20+
21+
import apoc.path.RelationshipTypeAndDirections;
22+
import org.apache.commons.lang3.tuple.Pair;
23+
import org.neo4j.graphalgo.EstimateEvaluator;
24+
import org.neo4j.graphdb.Direction;
25+
import org.neo4j.graphdb.Node;
26+
import org.neo4j.graphdb.PathExpander;
27+
import org.neo4j.graphdb.PathExpanderBuilder;
28+
import org.neo4j.graphdb.RelationshipType;
29+
import org.neo4j.values.storable.PointValue;
30+
31+
public class CorePathFindingUtils {
32+
public static class GeoEstimateEvaluatorPointCustom implements EstimateEvaluator<Double> {
33+
// -- from org.neo4j.graphalgo.impl.util.GeoEstimateEvaluator
34+
private static final double EARTH_RADIUS = 6371 * 1000; // Meters
35+
private Node cachedGoal;
36+
private final String pointPropertyKey;
37+
private double[] cachedGoalCoordinates;
38+
39+
public GeoEstimateEvaluatorPointCustom(String pointPropertyKey) {
40+
this.pointPropertyKey = pointPropertyKey;
41+
}
42+
43+
@Override
44+
public Double getCost(Node node, Node goal) {
45+
double[] nodeCoordinates = getCoordinates(node);
46+
if (cachedGoal == null || !cachedGoal.equals(goal)) {
47+
cachedGoalCoordinates = getCoordinates(goal);
48+
cachedGoal = goal;
49+
}
50+
return distance(nodeCoordinates[0], nodeCoordinates[1], cachedGoalCoordinates[0], cachedGoalCoordinates[1]);
51+
}
52+
53+
private static double distance(double latitude1, double longitude1, double latitude2, double longitude2) {
54+
latitude1 = Math.toRadians(latitude1);
55+
longitude1 = Math.toRadians(longitude1);
56+
latitude2 = Math.toRadians(latitude2);
57+
longitude2 = Math.toRadians(longitude2);
58+
double cLa1 = Math.cos(latitude1);
59+
double xA = EARTH_RADIUS * cLa1 * Math.cos(longitude1);
60+
double yA = EARTH_RADIUS * cLa1 * Math.sin(longitude1);
61+
double zA = EARTH_RADIUS * Math.sin(latitude1);
62+
double cLa2 = Math.cos(latitude2);
63+
double xB = EARTH_RADIUS * cLa2 * Math.cos(longitude2);
64+
double yB = EARTH_RADIUS * cLa2 * Math.sin(longitude2);
65+
double zB = EARTH_RADIUS * Math.sin(latitude2);
66+
return Math.sqrt((xA - xB) * (xA - xB) + (yA - yB) * (yA - yB) + (zA - zB) * (zA - zB));
67+
}
68+
// -- end from org.neo4j.graphalgo.impl.util.GeoEstimateEvaluator
69+
70+
private double[] getCoordinates(Node node) {
71+
return ((PointValue) node.getProperty(pointPropertyKey)).coordinate();
72+
}
73+
}
74+
75+
public static PathExpander<Double> buildPathExpander(String relationshipsAndDirections) {
76+
PathExpanderBuilder builder = PathExpanderBuilder.empty();
77+
for (Pair<RelationshipType, Direction> pair : RelationshipTypeAndDirections.parse(relationshipsAndDirections)) {
78+
if (pair.getLeft() == null) {
79+
if (pair.getRight() == null) {
80+
builder = PathExpanderBuilder.allTypesAndDirections();
81+
} else {
82+
builder = PathExpanderBuilder.allTypes(pair.getRight());
83+
}
84+
} else {
85+
if (pair.getRight() == null) {
86+
builder = builder.add(pair.getLeft());
87+
} else {
88+
builder = builder.add(pair.getLeft(), pair.getRight());
89+
}
90+
}
91+
}
92+
return builder.build();
93+
}
94+
}

core/src/main/java/apoc/algo/Cover.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@ public record AlgoCoverRelationshipResult(
5050
@Description("Returns all `RELATIONSHIP` values connecting the given set of `NODE` values.")
5151
public Stream<AlgoCoverRelationshipResult> coverCypher5(
5252
@Name(value = "nodes", description = "The nodes to look for connected relationships on.") Object nodes) {
53-
Set<Node> nodeSet = Util.nodeStream((InternalTransaction) tx, nodes).collect(Collectors.toSet());
54-
return coverNodes(nodeSet).map(AlgoCoverRelationshipResult::new);
53+
return coverStream(nodes);
5554
}
5655

5756
@Deprecated
@@ -60,6 +59,10 @@ public Stream<AlgoCoverRelationshipResult> coverCypher5(
6059
@Description("Returns all `RELATIONSHIP` values connecting the given set of `NODE` values.")
6160
public Stream<AlgoCoverRelationshipResult> cover(
6261
@Name(value = "nodes", description = "The nodes to look for connected relationships on.") Object nodes) {
62+
return coverStream(nodes);
63+
}
64+
65+
private Stream<AlgoCoverRelationshipResult> coverStream(Object nodes) {
6366
Set<Node> nodeSet = Util.nodeStream((InternalTransaction) tx, nodes).collect(Collectors.toSet());
6467
return coverNodes(nodeSet).map(AlgoCoverRelationshipResult::new);
6568
}

core/src/main/java/apoc/algo/PathFinding.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
*/
1919
package apoc.algo;
2020

21-
import static apoc.algo.PathFindingUtils.buildPathExpander;
21+
import static apoc.algo.CorePathFindingUtils.buildPathExpander;
2222

2323
import apoc.result.PathResult;
2424
import apoc.result.WeightedPathResult;
@@ -27,8 +27,16 @@
2727
import java.util.Map;
2828
import java.util.stream.Stream;
2929
import java.util.stream.StreamSupport;
30-
import org.neo4j.graphalgo.*;
31-
import org.neo4j.graphdb.*;
30+
import org.neo4j.graphalgo.BasicEvaluationContext;
31+
import org.neo4j.graphalgo.CommonEvaluators;
32+
import org.neo4j.graphalgo.EstimateEvaluator;
33+
import org.neo4j.graphalgo.GraphAlgoFactory;
34+
import org.neo4j.graphalgo.PathFinder;
35+
import org.neo4j.graphalgo.WeightedPath;
36+
import org.neo4j.graphdb.GraphDatabaseService;
37+
import org.neo4j.graphdb.Node;
38+
import org.neo4j.graphdb.Path;
39+
import org.neo4j.graphdb.Transaction;
3240
import org.neo4j.procedure.Context;
3341
import org.neo4j.procedure.Description;
3442
import org.neo4j.procedure.Name;
@@ -94,7 +102,7 @@ public Stream<WeightedPathResult> aStarConfig(
94102
String pointPropertyName = (String) config.get("pointPropName");
95103
final EstimateEvaluator<Double> estimateEvaluator;
96104
if (pointPropertyName != null) {
97-
estimateEvaluator = new PathFindingUtils.GeoEstimateEvaluatorPointCustom(pointPropertyName);
105+
estimateEvaluator = new CorePathFindingUtils.GeoEstimateEvaluatorPointCustom(pointPropertyName);
98106
} else {
99107
String latPropertyName = config.getOrDefault("y", "latitude").toString();
100108
String lonPropertyName = config.getOrDefault("x", "longitude").toString();

core/src/test/java/apoc/algo/CoverTest.java

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@
2424
import com.neo4j.test.extension.ImpermanentEnterpriseDbmsExtension;
2525
import java.util.List;
2626
import org.junit.jupiter.api.BeforeAll;
27+
import org.junit.jupiter.api.BeforeEach;
2728
import org.junit.jupiter.api.Test;
2829
import org.neo4j.graphdb.GraphDatabaseService;
2930
import org.neo4j.test.extension.Inject;
3031

31-
@ImpermanentEnterpriseDbmsExtension()
32+
@ImpermanentEnterpriseDbmsExtension
3233
class CoverTest {
3334

3435
@Inject
@@ -39,6 +40,11 @@ void beforeAll() {
3940
TestUtil.registerProcedure(db, Cover.class);
4041
}
4142

43+
@BeforeEach
44+
void setUp() {
45+
db.executeTransactionally("MATCH (n) DETACH DELETE n");
46+
}
47+
4248
@Test
4349
void testCover() {
4450
db.executeTransactionally("CREATE (a)-[:X]->(b)-[:X]->(c)-[:X]->(d)");
@@ -58,4 +64,102 @@ RETURN count(*) AS c
5864
(r) -> assertEquals(3L, r.get("c")));
5965
}
6066
}
67+
68+
@Test
69+
void testCoverEmpty() {
70+
TestUtil.testCall(
71+
db, "CALL apoc.algo.cover([]) YIELD rel RETURN count(*) AS c", r -> assertEquals(0L, r.get("c")));
72+
}
73+
74+
@Test
75+
void testCoverSingleNode() {
76+
db.executeTransactionally("CREATE (:Single)");
77+
TestUtil.testCall(
78+
db,
79+
"""
80+
MATCH (n:Single)
81+
WITH collect(n) AS nodes
82+
CALL apoc.algo.cover(nodes)
83+
YIELD rel
84+
RETURN count(*) AS c
85+
""",
86+
r -> assertEquals(0L, r.get("c")));
87+
}
88+
89+
@Test
90+
void testCoverDisconnectedNodes() {
91+
db.executeTransactionally("CREATE (:Disco), (:Disco)");
92+
TestUtil.testCall(
93+
db,
94+
"""
95+
MATCH (n:Disco)
96+
WITH collect(n) AS nodes
97+
CALL apoc.algo.cover(nodes)
98+
YIELD rel
99+
RETURN count(*) AS c
100+
""",
101+
r -> assertEquals(0L, r.get("c")));
102+
}
103+
104+
@Test
105+
void testCoverSelfLoop() {
106+
db.executeTransactionally("CREATE (a:SelfLoop)-[:X]->(a)");
107+
TestUtil.testCall(
108+
db,
109+
"""
110+
MATCH (n:SelfLoop)
111+
WITH collect(n) AS nodes
112+
CALL apoc.algo.cover(nodes)
113+
YIELD rel RETURN count(*) AS c
114+
""",
115+
r -> assertEquals(1L, r.get("c")));
116+
}
117+
118+
@Test
119+
void testCoverMultipleRelTypes() {
120+
db.executeTransactionally("CREATE (a:Multi)-[:X]->(b:Multi)-[:Y]->(c:Multi)");
121+
TestUtil.testCall(
122+
db,
123+
"""
124+
MATCH (n:Multi)
125+
WITH collect(n) AS nodes
126+
CALL apoc.algo.cover(nodes)
127+
YIELD rel
128+
RETURN count(*) AS c
129+
""",
130+
r -> assertEquals(2L, r.get("c")));
131+
}
132+
133+
@Test
134+
void testCoverIncomingRelationship() {
135+
// relationship points a<-b; cover finds it via b's outgoing rels when both nodes are in the set
136+
db.executeTransactionally("CREATE (a:Incoming)<-[:X]-(b:Incoming)");
137+
TestUtil.testCall(
138+
db,
139+
"""
140+
MATCH (n:Incoming)
141+
WITH collect(n) AS nodes
142+
CALL apoc.algo.cover(nodes)
143+
YIELD rel
144+
RETURN count(*) AS c
145+
""",
146+
r -> assertEquals(1L, r.get("c")));
147+
}
148+
149+
@Test
150+
void testCoverSubset() {
151+
// chain of 3; passing only the first two nodes should return only the relationship between them
152+
db.executeTransactionally("CREATE (:Sub {id: 1})-[:X]->(:Sub {id: 2})-[:X]->(:Sub {id: 3})");
153+
TestUtil.testCall(
154+
db,
155+
"""
156+
MATCH (n:Sub)
157+
WHERE n.id IN [1, 2]
158+
WITH collect(n) AS nodes
159+
CALL apoc.algo.cover(nodes)
160+
YIELD rel
161+
RETURN count(*) AS c
162+
""",
163+
r -> assertEquals(1L, r.get("c")));
164+
}
61165
}

0 commit comments

Comments
 (0)