Skip to content

Commit 1d73c9d

Browse files
committed
perf: more optimizations on using GAV from in Gremlin and SQL
1 parent 504b843 commit 1d73c9d

6 files changed

Lines changed: 279 additions & 12 deletions

File tree

engine/src/main/java/com/arcadedb/function/sql/graph/SQLFunctionAstar.java

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import com.arcadedb.database.RID;
2525
import com.arcadedb.database.Record;
2626
import com.arcadedb.graph.Edge;
27+
import com.arcadedb.graph.GraphTraversalProvider;
28+
import com.arcadedb.graph.GraphTraversalProviderRegistry;
2729
import com.arcadedb.graph.Vertex;
2830
import com.arcadedb.query.sql.executor.CommandContext;
2931
import com.arcadedb.query.sql.executor.MultiValue;
@@ -164,24 +166,24 @@ private LinkedList<RID> internalExecute(final CommandContext ctx, final Database
164166
}
165167

166168
closedSet.add(current);
167-
for (final Edge neighborEdge : getNeighborEdges(current)) {
168169

169-
final Vertex neighbor = getNeighbor(current, neighborEdge, graph);
170+
// Try CSR + edge property columns first for O(1) neighbor + weight access
171+
final Map<Vertex, Double> neighborWeights = getNeighborWeightsCSR(current, ctx);
172+
for (final Map.Entry<Vertex, Double> entry : neighborWeights.entrySet()) {
173+
final Vertex neighbor = entry.getKey();
170174
// Ignore the neighbor which is already evaluated.
171-
if (closedSet.contains(neighbor)) {
175+
if (closedSet.contains(neighbor))
172176
continue;
173-
}
174177
// The distance from start to a neighbor
175-
final double tentative_gScore = gScore.get(current) + getDistance(neighborEdge);
178+
final double tentative_gScore = gScore.get(current) + entry.getValue();
176179
final boolean contains = open.contains(neighbor);
177180

178181
if (!contains || tentative_gScore < gScore.get(neighbor)) {
179182
gScore.put(neighbor, tentative_gScore);
180183
fScore.put(neighbor, tentative_gScore + getHeuristicCost(neighbor, current, goal, ctx));
181184

182-
if (contains) {
185+
if (contains)
183186
open.remove(neighbor);
184-
}
185187
open.offer(neighbor);
186188
cameFrom.put(neighbor, current);
187189
}
@@ -218,14 +220,60 @@ protected Set<Edge> getNeighborEdges(final Vertex node) {
218220
final Set<Edge> neighbors = new HashSet<Edge>();
219221
if (node != null) {
220222
for (final Edge v : node.getEdges(paramDirection, paramEdgeTypeNames)) {
221-
final Edge ov = v;
222-
if (ov != null)
223-
neighbors.add(ov);
223+
if (v != null)
224+
neighbors.add(v);
224225
}
225226
}
226227
return neighbors;
227228
}
228229

230+
/**
231+
* Returns neighbor vertices with their edge weights using CSR + edge property columns when available.
232+
* Falls back to OLTP edge traversal when GAV doesn't have edge properties or doesn't cover the node.
233+
*/
234+
protected Map<Vertex, Double> getNeighborWeightsCSR(final Vertex node, final CommandContext ctx) {
235+
final Map<Vertex, Double> result = new HashMap<>();
236+
if (node == null)
237+
return result;
238+
239+
final GraphTraversalProvider provider = GraphTraversalProviderRegistry.findProvider(
240+
ctx.getDatabase(), paramEdgeTypeNames);
241+
if (provider != null && provider.hasEdgeProperties()) {
242+
final int nodeId = provider.getNodeId(node.getIdentity());
243+
if (nodeId >= 0) {
244+
final int[] neighborIds = paramEdgeTypeNames != null && paramEdgeTypeNames.length > 0
245+
? provider.getNeighborIds(nodeId, paramDirection, paramEdgeTypeNames)
246+
: provider.getNeighborIds(nodeId, paramDirection);
247+
final String edgeType = paramEdgeTypeNames != null && paramEdgeTypeNames.length > 0 ? paramEdgeTypeNames[0] : null;
248+
for (int i = 0; i < neighborIds.length; i++) {
249+
final RID neighborRid = provider.getRID(neighborIds[i]);
250+
if (neighborRid != null) {
251+
double weight = MIN;
252+
if (edgeType != null) {
253+
final Object wObj = provider.getEdgeProperty(nodeId, i, paramDirection, edgeType, paramWeightFieldName);
254+
if (wObj instanceof Number num)
255+
weight = num.doubleValue();
256+
}
257+
try {
258+
result.put(neighborRid.asVertex(), weight);
259+
} catch (final Exception e) {
260+
// deleted vertex — skip
261+
}
262+
}
263+
}
264+
return result;
265+
}
266+
}
267+
268+
// OLTP fallback
269+
for (final Edge edge : node.getEdges(paramDirection, paramEdgeTypeNames)) {
270+
final Vertex neighbor = getNeighbor(node, edge, ctx.getDatabase());
271+
if (neighbor != null)
272+
result.put(neighbor, getDistance(edge));
273+
}
274+
return result;
275+
}
276+
229277
private void bindAdditionalParams(final Object additionalParams, final SQLFunctionAstar context) {
230278
if (additionalParams == null) {
231279
return;

engine/src/main/java/com/arcadedb/function/sql/graph/SQLFunctionBellmanFord.java

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import com.arcadedb.database.Record;
2424
import com.arcadedb.function.sql.math.SQLFunctionMathAbstract;
2525
import com.arcadedb.graph.Edge;
26+
import com.arcadedb.graph.GraphTraversalProvider;
27+
import com.arcadedb.graph.GraphTraversalProviderRegistry;
2628
import com.arcadedb.graph.Vertex;
2729
import com.arcadedb.query.sql.executor.CommandContext;
2830
import com.arcadedb.schema.DocumentType;
@@ -95,13 +97,49 @@ public Object execute(final Object self, final Identifiable currentRecord, final
9597
Arrays.fill(prev, -1);
9698
dist[startIdx] = 0.0;
9799

98-
// Collect all edges based on direction
100+
// Collect all edges based on direction — use CSR + edge properties when available
99101
final List<int[]> edgeList = new ArrayList<>();
100102
final List<Double> edgeWeights = new ArrayList<>();
103+
final Vertex.DIRECTION dir = parseDirection(direction);
104+
105+
final GraphTraversalProvider provider = GraphTraversalProviderRegistry.findProvider(db);
106+
final boolean useCSR = provider != null && provider.hasEdgeProperties();
101107

102108
for (int i = 0; i < n; i++) {
103109
final Vertex v = vertices.get(i);
104-
final Vertex.DIRECTION dir = parseDirection(direction);
110+
111+
if (useCSR) {
112+
final int nodeId = provider.getNodeId(v.getIdentity());
113+
if (nodeId >= 0) {
114+
final int[] neighborIds = provider.getNeighborIds(nodeId, dir);
115+
for (int ni = 0; ni < neighborIds.length; ni++) {
116+
final var neighborRid = provider.getRID(neighborIds[ni]);
117+
if (neighborRid == null)
118+
continue;
119+
// Find vertex index — look up by RID in the map
120+
final Vertex neighborVertex;
121+
try {
122+
neighborVertex = neighborRid.asVertex();
123+
} catch (final Exception e) {
124+
continue;
125+
}
126+
final Integer j = vertexIndex.get(neighborVertex);
127+
if (j == null)
128+
continue;
129+
double w = 1.0;
130+
if (weightProperty != null && !weightProperty.isEmpty()) {
131+
final Object wObj = provider.getEdgeProperty(nodeId, ni, dir, null, weightProperty);
132+
if (wObj instanceof Number num)
133+
w = num.doubleValue();
134+
}
135+
edgeList.add(new int[] { i, j });
136+
edgeWeights.add(w);
137+
}
138+
continue;
139+
}
140+
}
141+
142+
// OLTP fallback
105143
for (final Edge edge : v.getEdges(dir)) {
106144
final Vertex neighbor;
107145
if (dir == Vertex.DIRECTION.OUT)

engine/src/main/java/com/arcadedb/graph/CSRVertexIterable.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
/**
2727
* Lazy iterable that converts CSR dense node IDs to Vertex objects on demand.
2828
* Used by SQL graph functions (out/in/both) when a GAV provider is available.
29+
* <p>
30+
* Provides an O(1) {@link #size()} method that returns the neighbor count directly
31+
* from the CSR array length, avoiding the need to iterate all elements.
32+
*
33+
* @author Luca Garulli (l.garulli@arcadedata.com)
2934
*/
3035
public class CSRVertexIterable implements Iterable<Vertex> {
3136
private final GraphTraversalProvider provider;
@@ -36,6 +41,14 @@ public CSRVertexIterable(final GraphTraversalProvider provider, final int[] neig
3641
this.neighborIds = neighborIds;
3742
}
3843

44+
/**
45+
* Returns the neighbor count in O(1) directly from the CSR array length.
46+
* This avoids materializing all vertices just to count them.
47+
*/
48+
public int size() {
49+
return neighborIds.length;
50+
}
51+
3952
@Override
4053
public Iterator<Vertex> iterator() {
4154
return new Iterator<>() {

engine/src/main/java/com/arcadedb/query/sql/method/collection/SQLMethodSize.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import com.arcadedb.database.Identifiable;
2222
import com.arcadedb.database.Record;
23+
import com.arcadedb.graph.CSRVertexIterable;
2324
import com.arcadedb.query.sql.executor.CommandContext;
2425
import com.arcadedb.query.sql.executor.MultiValue;
2526
import com.arcadedb.query.sql.executor.Result;
@@ -44,6 +45,7 @@ public Object execute(final Object value, final Identifiable currentRecord, fina
4445
final int size;
4546
if (value != null) {
4647
switch (value) {
48+
case CSRVertexIterable csr -> size = csr.size(); // O(1) via CSR array length
4749
case Result result -> size = result.getRecord().isPresent() ? result.getRecord().get().size() : -1;
4850
case Identifiable rid -> {
4951
final Record record = rid.getRecord(true);
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-FileCopyrightText: 2021-present Arcade Data Ltd (info@arcadedata.com)
17+
* SPDX-License-Identifier: Apache-2.0
18+
*/
19+
package com.arcadedb.gremlin;
20+
21+
import com.arcadedb.database.RID;
22+
import com.arcadedb.graph.GraphTraversalProvider;
23+
import com.arcadedb.graph.Vertex;
24+
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
25+
import org.apache.tinkerpop.gremlin.process.traversal.Traverser;
26+
import org.apache.tinkerpop.gremlin.process.traversal.step.filter.FilterStep;
27+
import org.apache.tinkerpop.gremlin.structure.Direction;
28+
import org.apache.tinkerpop.gremlin.structure.util.StringFactory;
29+
30+
import java.util.function.Predicate;
31+
32+
/**
33+
* GAV-accelerated filter step that replaces the pattern
34+
* {@code where(outE('X').count().is(predicate))} with O(1) degree lookup via
35+
* {@link GraphTraversalProvider#countEdges}.
36+
* <p>
37+
* Instead of materializing all edges to count them, this step calls
38+
* {@code provider.countEdges(nodeId, direction, edgeLabels)} which is a simple
39+
* offset subtraction on the CSR arrays.
40+
*
41+
* @author Luca Garulli (l.garulli@arcadedata.com)
42+
*/
43+
class ArcadeEdgeCountFilterStep extends FilterStep<org.apache.tinkerpop.gremlin.structure.Vertex> {
44+
private final GraphTraversalProvider provider;
45+
private final Vertex.DIRECTION direction;
46+
private final String[] edgeLabels;
47+
private final Predicate<Long> predicate;
48+
49+
ArcadeEdgeCountFilterStep(final Traversal.Admin<?, ?> traversal, final GraphTraversalProvider provider,
50+
final Direction direction, final String[] edgeLabels, final Predicate<Long> predicate) {
51+
super(traversal);
52+
this.provider = provider;
53+
this.direction = ArcadeGraph.mapDirection(direction);
54+
this.edgeLabels = edgeLabels;
55+
this.predicate = predicate;
56+
}
57+
58+
@Override
59+
protected boolean filter(final Traverser.Admin<org.apache.tinkerpop.gremlin.structure.Vertex> traverser) {
60+
final org.apache.tinkerpop.gremlin.structure.Vertex gremlinVertex = traverser.get();
61+
62+
final RID rid;
63+
if (gremlinVertex instanceof ArcadeVertex arcadeVertex)
64+
rid = arcadeVertex.getBaseElement().getIdentity();
65+
else
66+
rid = null;
67+
68+
if (rid != null) {
69+
final int nodeId = provider.getNodeId(rid);
70+
if (nodeId >= 0) {
71+
final long count = edgeLabels.length == 0
72+
? provider.countEdges(nodeId, direction)
73+
: provider.countEdges(nodeId, direction, edgeLabels);
74+
return predicate.test(count);
75+
}
76+
}
77+
78+
// OLTP fallback: iterate edges and count
79+
long count = 0;
80+
final var edgeIter = gremlinVertex.edges(
81+
switch (direction) {
82+
case OUT -> Direction.OUT;
83+
case IN -> Direction.IN;
84+
case BOTH -> Direction.BOTH;
85+
}, edgeLabels);
86+
while (edgeIter.hasNext()) {
87+
edgeIter.next();
88+
count++;
89+
}
90+
return predicate.test(count);
91+
}
92+
93+
@Override
94+
public String toString() {
95+
return StringFactory.stepString(this, direction, String.join(",", edgeLabels), predicate, "GAV");
96+
}
97+
}

0 commit comments

Comments
 (0)