diff --git a/algo-common/src/main/java/org/neo4j/gds/applications/algorithms/machinery/AlgorithmLabel.java b/algo-common/src/main/java/org/neo4j/gds/applications/algorithms/machinery/AlgorithmLabel.java index 74ab2640396..d5da2fb3439 100644 --- a/algo-common/src/main/java/org/neo4j/gds/applications/algorithms/machinery/AlgorithmLabel.java +++ b/algo-common/src/main/java/org/neo4j/gds/applications/algorithms/machinery/AlgorithmLabel.java @@ -68,6 +68,7 @@ public enum AlgorithmLabel implements Label { MCMF("MinCostMaxFlow"), Modularity("Modularity"), ModularityOptimization("ModularityOptimization"), + NodeCount("NodeCount"), NodeSimilarity("Node Similarity"), Node2Vec("Node2Vec"), PageRank("PageRank"), diff --git a/algo-params/miscellaneous-params/src/main/java/org/neo4j/gds/nodecount/NodeCountParameters.java b/algo-params/miscellaneous-params/src/main/java/org/neo4j/gds/nodecount/NodeCountParameters.java new file mode 100644 index 00000000000..2c0c270e088 --- /dev/null +++ b/algo-params/miscellaneous-params/src/main/java/org/neo4j/gds/nodecount/NodeCountParameters.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.gds.nodecount; + +import org.neo4j.gds.AlgorithmParameters; +import org.neo4j.gds.core.concurrency.Concurrency; + +/** + * The immutable, user-input-free parameters the algorithm actually runs on. + * Node counting needs nothing but the concurrency that the surrounding machinery expects; a real algorithm + * would carry its tuning knobs here, derived from the configuration via {@code toParameters()}. + */ +public record NodeCountParameters(Concurrency concurrency) implements AlgorithmParameters { +} diff --git a/algo/src/main/java/org/neo4j/gds/MiscellaneousAlgorithmsTasks.java b/algo/src/main/java/org/neo4j/gds/MiscellaneousAlgorithmsTasks.java index 91ec06868d6..cde92217d1d 100644 --- a/algo/src/main/java/org/neo4j/gds/MiscellaneousAlgorithmsTasks.java +++ b/algo/src/main/java/org/neo4j/gds/MiscellaneousAlgorithmsTasks.java @@ -24,6 +24,7 @@ import org.neo4j.gds.core.utils.progress.tasks.Task; import org.neo4j.gds.indexInverse.InverseRelationshipsTask; import org.neo4j.gds.indexinverse.InverseRelationshipsParameters; +import org.neo4j.gds.nodecount.NodeCountTask; import org.neo4j.gds.scaleproperties.ScalePropertiesParameters; import org.neo4j.gds.scaleproperties.ScalePropertiesTask; import org.neo4j.gds.undirected.ToUndirectedTask; @@ -31,6 +32,10 @@ public final class MiscellaneousAlgorithmsTasks { private MiscellaneousAlgorithmsTasks() {} + public static Task nodeCount(Graph graph){ + return NodeCountTask.create(graph); + } + public static Task scaleProperties(Graph graph, ScalePropertiesParameters parameters){ return ScalePropertiesTask.create(graph, parameters); } diff --git a/algo/src/main/java/org/neo4j/gds/nodecount/NodeCount.java b/algo/src/main/java/org/neo4j/gds/nodecount/NodeCount.java new file mode 100644 index 00000000000..1d6a54505d5 --- /dev/null +++ b/algo/src/main/java/org/neo4j/gds/nodecount/NodeCount.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.gds.nodecount; + +import org.neo4j.gds.Algorithm; +import org.neo4j.gds.api.Graph; +import org.neo4j.gds.core.utils.progress.tasks.ProgressTracker; +import org.neo4j.gds.termination.TerminationFlag; + +/** + * A deliberately trivial example algorithm: it counts the number of nodes in the graph and returns it. + *

+ * Its only purpose is to act as a template that shows the full set of modules and wiring required to add a + * new algorithm to GDS, without the distraction of any real algorithmic complexity. To implement a real + * algorithm, replace the body of {@link #compute()} with the actual computation; everything around it + * (config, parameters, facades, procedures, progress tracking) follows the same shape demonstrated here. + */ +public class NodeCount extends Algorithm { + + private final Graph graph; + + public NodeCount(Graph graph, ProgressTracker progressTracker, TerminationFlag terminationFlag) { + super(progressTracker); + this.graph = graph; + this.terminationFlag = terminationFlag; + } + + @Override + public NodeCountResult compute() { + progressTracker.beginSubTask(); + + terminationFlag.assertRunning(); + var nodeCount = graph.nodeCount(); + progressTracker.onProgress(nodeCount); + + progressTracker.endSubTask(); + + return new NodeCountResult(nodeCount); + } +} diff --git a/algo/src/main/java/org/neo4j/gds/nodecount/NodeCountMemoryEstimateDefinition.java b/algo/src/main/java/org/neo4j/gds/nodecount/NodeCountMemoryEstimateDefinition.java new file mode 100644 index 00000000000..84ccbcd9096 --- /dev/null +++ b/algo/src/main/java/org/neo4j/gds/nodecount/NodeCountMemoryEstimateDefinition.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.gds.nodecount; + +import org.neo4j.gds.mem.MemoryEstimateDefinition; +import org.neo4j.gds.mem.MemoryEstimation; +import org.neo4j.gds.mem.MemoryEstimations; + +public class NodeCountMemoryEstimateDefinition implements MemoryEstimateDefinition { + + @Override + public MemoryEstimation memoryEstimation() { + // Counting nodes only requires holding on to a single long, independent of the graph size. + return MemoryEstimations.builder(NodeCount.class) + .fixed("node count", Long.BYTES) + .build(); + } +} diff --git a/algo/src/main/java/org/neo4j/gds/nodecount/NodeCountResult.java b/algo/src/main/java/org/neo4j/gds/nodecount/NodeCountResult.java new file mode 100644 index 00000000000..fb6f5734a41 --- /dev/null +++ b/algo/src/main/java/org/neo4j/gds/nodecount/NodeCountResult.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.gds.nodecount; + +public record NodeCountResult(long nodeCount) { +} diff --git a/algo/src/main/java/org/neo4j/gds/nodecount/NodeCountTask.java b/algo/src/main/java/org/neo4j/gds/nodecount/NodeCountTask.java new file mode 100644 index 00000000000..dc1183cf2cd --- /dev/null +++ b/algo/src/main/java/org/neo4j/gds/nodecount/NodeCountTask.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.gds.nodecount; + +import org.neo4j.gds.api.Graph; +import org.neo4j.gds.applications.algorithms.machinery.AlgorithmLabel; +import org.neo4j.gds.core.utils.progress.tasks.Task; +import org.neo4j.gds.core.utils.progress.tasks.Tasks; + +public final class NodeCountTask { + private NodeCountTask() {} + + public static Task create(Graph graph) { + return Tasks.leaf(AlgorithmLabel.NodeCount.asString(), graph.nodeCount()); + } +} diff --git a/algo/src/test/java/org/neo4j/gds/nodecount/NodeCountTest.java b/algo/src/test/java/org/neo4j/gds/nodecount/NodeCountTest.java new file mode 100644 index 00000000000..70a3ab8849e --- /dev/null +++ b/algo/src/test/java/org/neo4j/gds/nodecount/NodeCountTest.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.gds.nodecount; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.neo4j.gds.MiscellaneousAlgorithmsTasks; +import org.neo4j.gds.TestGraph; +import org.neo4j.gds.TestProgressTrackerHelper; +import org.neo4j.gds.core.concurrency.Concurrency; +import org.neo4j.gds.core.utils.progress.tasks.ProgressTracker; +import org.neo4j.gds.extension.GdlExtension; +import org.neo4j.gds.extension.GdlGraph; +import org.neo4j.gds.extension.Inject; +import org.neo4j.gds.termination.TerminationFlag; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.neo4j.gds.assertj.Extractors.removingThreadId; +import static org.neo4j.gds.compat.TestLog.INFO; + +@GdlExtension +class NodeCountTest { + + @GdlGraph + static String GDL = + "(a)-->(b)" + + ",(b)-->(c)" + + ",(c)-->(a)"; + + @Inject + private TestGraph graph; + + @Test + void countsNodes() { + var algorithm = new NodeCount(graph, ProgressTracker.NULL_TRACKER, TerminationFlag.RUNNING_TRUE); + + var result = algorithm.compute(); + + assertEquals(graph.nodeCount(), result.nodeCount()); + assertEquals(3L, result.nodeCount()); + } + + @Test + void progressLogging() { + var progressTrackerWithLog = TestProgressTrackerHelper.create( + MiscellaneousAlgorithmsTasks.nodeCount(graph), + new Concurrency(1) + ); + + var progressTracker = progressTrackerWithLog.progressTracker(); + var log = progressTrackerWithLog.log(); + + new NodeCount(graph, progressTracker, TerminationFlag.RUNNING_TRUE).compute(); + + Assertions.assertThat(log.getMessages(INFO)) + // avoid asserting on the thread id + .extracting(removingThreadId()) + .contains( + "NodeCount :: Start", + "NodeCount :: Finished" + ); + } +} diff --git a/applications/algorithms/miscellaneous-algorithms/src/main/java/org/neo4j/gds/applications/algorithms/miscellaneous/MiscellaneousAlgorithms.java b/applications/algorithms/miscellaneous-algorithms/src/main/java/org/neo4j/gds/applications/algorithms/miscellaneous/MiscellaneousAlgorithms.java index 3f774bce83a..4da9a8e87f5 100644 --- a/applications/algorithms/miscellaneous-algorithms/src/main/java/org/neo4j/gds/applications/algorithms/miscellaneous/MiscellaneousAlgorithms.java +++ b/applications/algorithms/miscellaneous-algorithms/src/main/java/org/neo4j/gds/applications/algorithms/miscellaneous/MiscellaneousAlgorithms.java @@ -28,6 +28,8 @@ import org.neo4j.gds.core.utils.progress.tasks.ProgressTracker; import org.neo4j.gds.indexInverse.InverseRelationships; import org.neo4j.gds.indexinverse.InverseRelationshipsParameters; +import org.neo4j.gds.nodecount.NodeCount; +import org.neo4j.gds.nodecount.NodeCountResult; import org.neo4j.gds.scaleproperties.ScaleProperties; import org.neo4j.gds.scaleproperties.ScalePropertiesParameters; import org.neo4j.gds.scaleproperties.ScalePropertiesResult; @@ -46,6 +48,12 @@ public MiscellaneousAlgorithms( TerminationFlag terminationFlag) { this.terminationFlag = terminationFlag; } + public NodeCountResult nodeCount(Graph graph, ProgressTracker progressTracker) { + var algorithm = new NodeCount(graph, progressTracker, terminationFlag); + + return algorithm.compute(); + } + public SingleTypeRelationships collapsePath(GraphStore graphStore, CollapsePathParameters parameters) { var algorithm = CollapsePath.create( diff --git a/applications/algorithms/miscellaneous-algorithms/src/main/java/org/neo4j/gds/applications/algorithms/miscellaneous/MiscellaneousAlgorithmsBusinessFacade.java b/applications/algorithms/miscellaneous-algorithms/src/main/java/org/neo4j/gds/applications/algorithms/miscellaneous/MiscellaneousAlgorithmsBusinessFacade.java index ca19d68afd2..0950c2ddcc0 100644 --- a/applications/algorithms/miscellaneous-algorithms/src/main/java/org/neo4j/gds/applications/algorithms/miscellaneous/MiscellaneousAlgorithmsBusinessFacade.java +++ b/applications/algorithms/miscellaneous-algorithms/src/main/java/org/neo4j/gds/applications/algorithms/miscellaneous/MiscellaneousAlgorithmsBusinessFacade.java @@ -29,6 +29,8 @@ import org.neo4j.gds.core.loading.SingleTypeRelationships; import org.neo4j.gds.indexInverse.InverseRelationshipsConfig; import org.neo4j.gds.indexInverse.InverseRelationshipsParamsTransformer; +import org.neo4j.gds.nodecount.NodeCountBaseConfig; +import org.neo4j.gds.nodecount.NodeCountResult; import org.neo4j.gds.scaleproperties.ScalePropertiesBaseConfig; import org.neo4j.gds.scaleproperties.ScalePropertiesResult; import org.neo4j.gds.undirected.ToUndirectedConfig; @@ -48,6 +50,18 @@ public MiscellaneousAlgorithmsBusinessFacade(MiscellaneousAlgorithms miscellaneo this.miscellaneousAlgorithms = miscellaneousAlgorithms; } + NodeCountResult nodeCount(Graph graph, NodeCountBaseConfig configuration) { + var params = configuration.toParameters(); + var task = MiscellaneousAlgorithmsTasks.nodeCount(graph); + var progressTracker = progressTrackerCreator.createProgressTracker(task, configuration); + + return algorithmMachinery.getResult( + () -> miscellaneousAlgorithms.nodeCount(graph, progressTracker), + progressTracker, + params.concurrency() + ); + } + public SingleTypeRelationships collapsePath(GraphStore graphStore, CollapsePathConfig configuration) { var params = CollapsePathParamsTransformer.create(configuration,graphStore); return miscellaneousAlgorithms.collapsePath(graphStore, params); diff --git a/applications/algorithms/miscellaneous-algorithms/src/main/java/org/neo4j/gds/applications/algorithms/miscellaneous/MiscellaneousApplicationsEstimationModeBusinessFacade.java b/applications/algorithms/miscellaneous-algorithms/src/main/java/org/neo4j/gds/applications/algorithms/miscellaneous/MiscellaneousApplicationsEstimationModeBusinessFacade.java index 01f28135f84..9feab11c9fa 100644 --- a/applications/algorithms/miscellaneous-algorithms/src/main/java/org/neo4j/gds/applications/algorithms/miscellaneous/MiscellaneousApplicationsEstimationModeBusinessFacade.java +++ b/applications/algorithms/miscellaneous-algorithms/src/main/java/org/neo4j/gds/applications/algorithms/miscellaneous/MiscellaneousApplicationsEstimationModeBusinessFacade.java @@ -25,6 +25,8 @@ import org.neo4j.gds.exceptions.MemoryEstimationNotImplementedException; import org.neo4j.gds.indexInverse.InverseRelationshipsMemoryEstimateDefinition; import org.neo4j.gds.mem.MemoryEstimation; +import org.neo4j.gds.nodecount.NodeCountBaseConfig; +import org.neo4j.gds.nodecount.NodeCountMemoryEstimateDefinition; import org.neo4j.gds.scaleproperties.ScalePropertiesBaseConfig; import org.neo4j.gds.scaleproperties.ScalePropertiesMemoryEstimateDefinition; import org.neo4j.gds.undirected.ToUndirectedConfig; @@ -42,6 +44,21 @@ public MemoryEstimation collapsePath() { throw new MemoryEstimationNotImplementedException(); } + public MemoryEstimation nodeCount() { + return new NodeCountMemoryEstimateDefinition().memoryEstimation(); + } + + public MemoryEstimateResult nodeCount( + NodeCountBaseConfig configuration, + Object graphNameOrConfiguration + ) { + return algorithmEstimationTemplate.estimate( + configuration, + graphNameOrConfiguration, + nodeCount() + ); + } + public MemoryEstimation indexInverse(AlgoBaseConfig configuration) { return new InverseRelationshipsMemoryEstimateDefinition(configuration.relationshipTypes()).memoryEstimation(); } diff --git a/applications/algorithms/miscellaneous-algorithms/src/main/java/org/neo4j/gds/applications/algorithms/miscellaneous/MiscellaneousApplicationsStreamModeBusinessFacade.java b/applications/algorithms/miscellaneous-algorithms/src/main/java/org/neo4j/gds/applications/algorithms/miscellaneous/MiscellaneousApplicationsStreamModeBusinessFacade.java index b6f726db3db..1332ca2e393 100644 --- a/applications/algorithms/miscellaneous-algorithms/src/main/java/org/neo4j/gds/applications/algorithms/miscellaneous/MiscellaneousApplicationsStreamModeBusinessFacade.java +++ b/applications/algorithms/miscellaneous-algorithms/src/main/java/org/neo4j/gds/applications/algorithms/miscellaneous/MiscellaneousApplicationsStreamModeBusinessFacade.java @@ -22,11 +22,14 @@ import org.neo4j.gds.api.GraphName; import org.neo4j.gds.applications.algorithms.machinery.AlgorithmProcessingTemplateConvenience; import org.neo4j.gds.applications.algorithms.machinery.StreamResultBuilder; +import org.neo4j.gds.nodecount.NodeCountResult; +import org.neo4j.gds.nodecount.NodeCountStreamConfig; import org.neo4j.gds.scaleproperties.ScalePropertiesResult; import org.neo4j.gds.scaleproperties.ScalePropertiesStreamConfig; import java.util.stream.Stream; +import static org.neo4j.gds.applications.algorithms.machinery.AlgorithmLabel.NodeCount; import static org.neo4j.gds.applications.algorithms.machinery.AlgorithmLabel.ScaleProperties; public class MiscellaneousApplicationsStreamModeBusinessFacade { @@ -44,6 +47,21 @@ public class MiscellaneousApplicationsStreamModeBusinessFacade { this.algorithmProcessingTemplateConvenience = algorithmProcessingTemplateConvenience; } + public Stream nodeCount( + GraphName graphName, + NodeCountStreamConfig configuration, + StreamResultBuilder resultBuilder + ) { + return algorithmProcessingTemplateConvenience.processRegularAlgorithmInStreamMode( + graphName, + configuration, + NodeCount, + estimationFacade::nodeCount, + (graph, __) -> miscellaneousAlgorithms.nodeCount(graph, configuration), + resultBuilder + ); + } + public Stream scaleProperties( GraphName graphName, ScalePropertiesStreamConfig configuration, diff --git a/open-packaging/src/test/java/org/neo4j/gds/OpenGdsProcedureSmokeTest.java b/open-packaging/src/test/java/org/neo4j/gds/OpenGdsProcedureSmokeTest.java index d451283bab5..539e1f32947 100644 --- a/open-packaging/src/test/java/org/neo4j/gds/OpenGdsProcedureSmokeTest.java +++ b/open-packaging/src/test/java/org/neo4j/gds/OpenGdsProcedureSmokeTest.java @@ -150,6 +150,9 @@ class OpenGdsProcedureSmokeTest extends BaseProcTest { "gds.hits.write", "gds.hits.write.estimate", + "gds.nodeCount.stream", + "gds.nodeCount.stream.estimate", + "gds.nodeSimilarity.filtered.mutate", "gds.nodeSimilarity.filtered.mutate.estimate", "gds.nodeSimilarity.filtered.stats", @@ -643,7 +646,7 @@ void countShouldMatch() { ); // If you find yourself updating this count, please also update the count in SmokeTest.kt - int expectedCount = 482; + int expectedCount = 484; assertEquals( expectedCount, returnedRows, diff --git a/procedures/algorithms-facade/src/main/java/org/neo4j/gds/procedures/algorithms/miscellaneous/LocalMiscellaneousProcedureFacade.java b/procedures/algorithms-facade/src/main/java/org/neo4j/gds/procedures/algorithms/miscellaneous/LocalMiscellaneousProcedureFacade.java index 7fe04b1e408..a68681fd696 100644 --- a/procedures/algorithms-facade/src/main/java/org/neo4j/gds/procedures/algorithms/miscellaneous/LocalMiscellaneousProcedureFacade.java +++ b/procedures/algorithms-facade/src/main/java/org/neo4j/gds/procedures/algorithms/miscellaneous/LocalMiscellaneousProcedureFacade.java @@ -34,6 +34,7 @@ import org.neo4j.gds.procedures.algorithms.miscellaneous.stubs.LocalToUndirectedMutateStub; import org.neo4j.gds.procedures.algorithms.miscellaneous.stubs.MiscellaneousStubs; import org.neo4j.gds.procedures.algorithms.stubs.GenericStub; +import org.neo4j.gds.nodecount.NodeCountStreamConfig; import org.neo4j.gds.scaleproperties.ScalePropertiesMutateConfig; import org.neo4j.gds.scaleproperties.ScalePropertiesStatsConfig; import org.neo4j.gds.scaleproperties.ScalePropertiesStreamConfig; @@ -129,6 +130,33 @@ public MiscellaneousStubs miscellaneousStubs() { return stubs; } + @Override + public Stream nodeCountStream( + String graphName, + Map configuration + ) { + var resultBuilder = new NodeCountResultBuilderForStreamMode(); + + return streamModeBusinessFacade.nodeCount( + GraphName.parse(graphName), + configurationParser.parseConfiguration(configuration, NodeCountStreamConfig::of), + resultBuilder + ); + } + + @Override + public Stream nodeCountStreamEstimate( + Object graphNameOrConfiguration, + Map algorithmConfiguration + ) { + var result = estimationModeBusinessFacade.nodeCount( + configurationParser.parseConfiguration(algorithmConfiguration, NodeCountStreamConfig::of), + graphNameOrConfiguration + ); + + return Stream.of(result); + } + @Override public Stream collapsePathMutate(String graphName, Map configuration) { return stubs.collapsePath().execute(graphName,configuration); diff --git a/procedures/algorithms-facade/src/main/java/org/neo4j/gds/procedures/algorithms/miscellaneous/NodeCountResultBuilderForStreamMode.java b/procedures/algorithms-facade/src/main/java/org/neo4j/gds/procedures/algorithms/miscellaneous/NodeCountResultBuilderForStreamMode.java new file mode 100644 index 00000000000..9ed383715e5 --- /dev/null +++ b/procedures/algorithms-facade/src/main/java/org/neo4j/gds/procedures/algorithms/miscellaneous/NodeCountResultBuilderForStreamMode.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.gds.procedures.algorithms.miscellaneous; + +import org.neo4j.gds.api.Graph; +import org.neo4j.gds.api.GraphStore; +import org.neo4j.gds.applications.algorithms.machinery.StreamResultBuilder; +import org.neo4j.gds.nodecount.NodeCountResult; + +import java.util.Optional; +import java.util.stream.Stream; + +/** + * Turns the algorithm result into the rows the {@code gds.nodeCount.stream} procedure yields. + * The count is a single scalar, so we emit exactly one row (or none on an empty graph result). + */ +class NodeCountResultBuilderForStreamMode implements StreamResultBuilder { + + @Override + public Stream build( + Graph graph, + GraphStore graphStore, + Optional result + ) { + if (result.isEmpty()) return Stream.empty(); + + return Stream.of(new NodeCountStreamResult(result.get().nodeCount())); + } +} diff --git a/procedures/facade-api/configs/miscellaneous-configs/src/main/java/org/neo4j/gds/nodecount/NodeCountBaseConfig.java b/procedures/facade-api/configs/miscellaneous-configs/src/main/java/org/neo4j/gds/nodecount/NodeCountBaseConfig.java new file mode 100644 index 00000000000..1b5f8883ab0 --- /dev/null +++ b/procedures/facade-api/configs/miscellaneous-configs/src/main/java/org/neo4j/gds/nodecount/NodeCountBaseConfig.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.gds.nodecount; + +import org.neo4j.gds.annotation.Configuration; +import org.neo4j.gds.config.AlgoBaseConfig; + +/** + * The configuration shared by every mode of the algorithm. + * Node counting exposes no algorithm-specific settings, so it simply inherits the common options + * (concurrency, node/relationship filters, ...) from {@link AlgoBaseConfig} and maps them onto the + * algorithm {@link NodeCountParameters parameters}. A real algorithm would declare its own settings here. + */ +public interface NodeCountBaseConfig extends AlgoBaseConfig { + + @Configuration.Ignore + default NodeCountParameters toParameters() { + return new NodeCountParameters(concurrency()); + } +} diff --git a/procedures/facade-api/configs/miscellaneous-configs/src/main/java/org/neo4j/gds/nodecount/NodeCountStreamConfig.java b/procedures/facade-api/configs/miscellaneous-configs/src/main/java/org/neo4j/gds/nodecount/NodeCountStreamConfig.java new file mode 100644 index 00000000000..173e5750399 --- /dev/null +++ b/procedures/facade-api/configs/miscellaneous-configs/src/main/java/org/neo4j/gds/nodecount/NodeCountStreamConfig.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.gds.nodecount; + +import org.neo4j.gds.annotation.Configuration; +import org.neo4j.gds.core.CypherMapWrapper; + +@Configuration +public interface NodeCountStreamConfig extends NodeCountBaseConfig { + + static NodeCountStreamConfig of(CypherMapWrapper userInput) { + return new NodeCountStreamConfigImpl(userInput); + } +} diff --git a/procedures/facade-api/miscellaneous-facade-api/src/main/java/org/neo4j/gds/procedures/algorithms/miscellaneous/MiscellaneousProcedureFacade.java b/procedures/facade-api/miscellaneous-facade-api/src/main/java/org/neo4j/gds/procedures/algorithms/miscellaneous/MiscellaneousProcedureFacade.java index a812ae721fd..f232e2c6fce 100644 --- a/procedures/facade-api/miscellaneous-facade-api/src/main/java/org/neo4j/gds/procedures/algorithms/miscellaneous/MiscellaneousProcedureFacade.java +++ b/procedures/facade-api/miscellaneous-facade-api/src/main/java/org/neo4j/gds/procedures/algorithms/miscellaneous/MiscellaneousProcedureFacade.java @@ -29,6 +29,16 @@ public interface MiscellaneousProcedureFacade { MiscellaneousStubs miscellaneousStubs(); + Stream nodeCountStream( + String graphName, + Map configuration + ); + + Stream nodeCountStreamEstimate( + Object graphNameOrConfiguration, + Map algorithmConfiguration + ); + Stream collapsePathMutate( String graphName, Map configuration diff --git a/procedures/facade-api/miscellaneous-facade-api/src/main/java/org/neo4j/gds/procedures/algorithms/miscellaneous/NodeCountStreamResult.java b/procedures/facade-api/miscellaneous-facade-api/src/main/java/org/neo4j/gds/procedures/algorithms/miscellaneous/NodeCountStreamResult.java new file mode 100644 index 00000000000..5e1698880d5 --- /dev/null +++ b/procedures/facade-api/miscellaneous-facade-api/src/main/java/org/neo4j/gds/procedures/algorithms/miscellaneous/NodeCountStreamResult.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.gds.procedures.algorithms.miscellaneous; + +/** + * One row of the {@code gds.nodeCount.stream} procedure. Because node counting yields a single scalar, + * the stream produces exactly one row holding the count. + */ +public record NodeCountStreamResult(long nodeCount) { +} diff --git a/procedures/misc/src/main/java/org/neo4j/gds/nodecount/Constants.java b/procedures/misc/src/main/java/org/neo4j/gds/nodecount/Constants.java new file mode 100644 index 00000000000..abb32851b21 --- /dev/null +++ b/procedures/misc/src/main/java/org/neo4j/gds/nodecount/Constants.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.gds.nodecount; + +final class Constants { + + private Constants() {} + + static final String NODE_COUNT_DESCRIPTION = "Counts the number of nodes in the graph."; + +} diff --git a/procedures/misc/src/main/java/org/neo4j/gds/nodecount/NodeCountStreamProc.java b/procedures/misc/src/main/java/org/neo4j/gds/nodecount/NodeCountStreamProc.java new file mode 100644 index 00000000000..56e50e8737f --- /dev/null +++ b/procedures/misc/src/main/java/org/neo4j/gds/nodecount/NodeCountStreamProc.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.gds.nodecount; + +import org.neo4j.gds.applications.algorithms.machinery.MemoryEstimateResult; +import org.neo4j.gds.procedures.GraphDataScienceProcedures; +import org.neo4j.gds.procedures.algorithms.miscellaneous.NodeCountStreamResult; +import org.neo4j.procedure.Context; +import org.neo4j.procedure.Description; +import org.neo4j.procedure.Name; +import org.neo4j.procedure.Procedure; + +import java.util.Map; +import java.util.stream.Stream; + +import static org.neo4j.gds.nodecount.Constants.NODE_COUNT_DESCRIPTION; +import static org.neo4j.gds.procedures.ProcedureConstants.MEMORY_ESTIMATION_DESCRIPTION; +import static org.neo4j.procedure.Mode.READ; + +/** + * The thin Neo4j procedure layer: it only injects the {@link GraphDataScienceProcedures} context object and + * delegates to the procedure facade. Neo4j discovers this class by scanning the packaged plugin jar for + * {@link Procedure}-annotated methods, so no explicit registration is needed. + */ +public class NodeCountStreamProc { + @Context + public GraphDataScienceProcedures facade; + + @Procedure(value = "gds.nodeCount.stream", mode = READ) + @Description(NODE_COUNT_DESCRIPTION) + public Stream stream( + @Name(value = "graphName") String graphName, + @Name(value = "configuration", defaultValue = "{}") Map configuration + ) { + return facade.algorithms().miscellaneous().nodeCountStream(graphName, configuration); + } + + @Procedure(value = "gds.nodeCount.stream.estimate", mode = READ) + @Description(MEMORY_ESTIMATION_DESCRIPTION) + public Stream estimate( + @Name(value = "graphNameOrConfiguration") Object graphName, + @Name(value = "algoConfiguration") Map configuration + ) { + return facade.algorithms().miscellaneous().nodeCountStreamEstimate(graphName, configuration); + } +} diff --git a/procedures/misc/src/test/java/org/neo4j/gds/nodecount/NodeCountStreamProcTest.java b/procedures/misc/src/test/java/org/neo4j/gds/nodecount/NodeCountStreamProcTest.java new file mode 100644 index 00000000000..60baad34058 --- /dev/null +++ b/procedures/misc/src/test/java/org/neo4j/gds/nodecount/NodeCountStreamProcTest.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.gds.nodecount; + +import org.intellij.lang.annotations.Language; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.neo4j.gds.BaseProcTest; +import org.neo4j.gds.GdsCypher; +import org.neo4j.gds.catalog.GraphProjectProc; +import org.neo4j.gds.extension.Neo4jGraph; + +import java.util.List; +import java.util.Map; + +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.isA; + +class NodeCountStreamProcTest extends BaseProcTest { + + @Neo4jGraph + @Language("Cypher") + private static final String DB_CYPHER = + "CREATE" + + " (n0:A)" + + ",(n1:A)" + + ",(n2:A)"; + + @BeforeEach + void setUp() throws Exception { + registerProcedures( + GraphProjectProc.class, + NodeCountStreamProc.class + ); + + runQuery("CALL gds.graph.project('g', 'A', '*')"); + } + + @Test + void stream() { + var query = GdsCypher + .call("g") + .algo("gds.nodeCount") + .streamMode() + .yields(); + + assertCypherResult(query, List.of(Map.of("nodeCount", 3L))); + } + + @Test + void estimate() { + var query = GdsCypher + .call("g") + .algo("gds.nodeCount") + .streamEstimation() + .yields(); + + assertCypherResult(query, List.of(Map.of( + "mapView", isA(Map.class), + "treeView", isA(String.class), + "bytesMax", greaterThanOrEqualTo(0L), + "heapPercentageMin", greaterThanOrEqualTo(0.0), + "nodeCount", 3L, + "relationshipCount", 0L, + "requiredMemory", isA(String.class), + "bytesMin", greaterThanOrEqualTo(0L), + "heapPercentageMax", greaterThanOrEqualTo(0.0) + )) + ); + } +}