Skip to content

Commit b6cfab5

Browse files
committed
perf: using GAV from basic gremlin and sql for simple use cases
1 parent c04d12f commit b6cfab5

4 files changed

Lines changed: 385 additions & 6 deletions

File tree

engine/src/main/java/com/arcadedb/query/sql/executor/MatchEdgeTraverser.java

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

2121
import com.arcadedb.database.Document;
2222
import com.arcadedb.database.Identifiable;
23+
import com.arcadedb.graph.GraphTraversalProvider;
24+
import com.arcadedb.graph.GraphTraversalProviderRegistry;
25+
import com.arcadedb.graph.Vertex;
2326
import com.arcadedb.query.sql.parser.MatchPathItem;
2427
import com.arcadedb.query.sql.parser.Rid;
2528
import com.arcadedb.query.sql.parser.WhereClause;
@@ -110,6 +113,41 @@ protected void init(final CommandContext context) {
110113
if (startingElem instanceof Result result) {
111114
startingElem = result.getElement().orElse(null);
112115
}
116+
117+
// Expand-into optimization: when both endpoints are already bound, use isConnectedTo()
118+
// for O(log degree) binary search instead of traversing all neighbors
119+
final String endPointAlias = getEndpointAlias();
120+
final Object prevValue = sourceRecord.getProperty(endPointAlias);
121+
if (prevValue != null && startingElem instanceof Vertex sourceVertex) {
122+
Identifiable targetElem = null;
123+
if (prevValue instanceof Identifiable identifiable)
124+
targetElem = identifiable;
125+
else if (prevValue instanceof Result r)
126+
targetElem = r.getElement().orElse(null);
127+
128+
if (targetElem != null) {
129+
final GraphTraversalProvider provider = GraphTraversalProviderRegistry.findProvider(context.getDatabase());
130+
if (provider != null) {
131+
final int srcId = provider.getNodeId(sourceVertex.getIdentity());
132+
final int tgtId = provider.getNodeId(targetElem.getIdentity());
133+
if (srcId >= 0 && tgtId >= 0) {
134+
// Determine direction from the edge traversal
135+
final Vertex.DIRECTION direction = (edge != null && !edge.out) ? Vertex.DIRECTION.IN : Vertex.DIRECTION.OUT;
136+
if (provider.isConnectedTo(srcId, tgtId, direction)) {
137+
// Connected: return the target as the single result
138+
final Document doc = (Document) targetElem.getRecord();
139+
downstream = List.of(new ResultInternal(doc)).iterator();
140+
return;
141+
} else {
142+
// Not connected: empty result
143+
downstream = Collections.emptyIterator();
144+
return;
145+
}
146+
}
147+
}
148+
}
149+
}
150+
113151
downstream = executeTraversal(context, this.item, startingElem, 0, null).iterator();
114152
}
115153
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
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.query.sql.executor;
20+
21+
import com.arcadedb.TestHelper;
22+
import com.arcadedb.graph.MutableVertex;
23+
import com.arcadedb.graph.olap.GraphAnalyticalView;
24+
import org.junit.jupiter.api.Test;
25+
26+
import java.util.ArrayList;
27+
import java.util.List;
28+
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
31+
/**
32+
* Tests that SQL MATCH statements use GAV expand-into optimization (isConnectedTo)
33+
* when both endpoints are already bound.
34+
*/
35+
class MatchGAVExpandIntoTest extends TestHelper {
36+
37+
@Test
38+
void matchExpandIntoWithGAV() {
39+
database.getSchema().createVertexType("Person");
40+
database.getSchema().createEdgeType("KNOWS");
41+
42+
// Build graph: A -> B -> C, A -> C
43+
database.begin();
44+
final MutableVertex a = database.newVertex("Person").set("name", "Alice").save();
45+
final MutableVertex b = database.newVertex("Person").set("name", "Bob").save();
46+
final MutableVertex c = database.newVertex("Person").set("name", "Charlie").save();
47+
a.newEdge("KNOWS", b);
48+
b.newEdge("KNOWS", c);
49+
a.newEdge("KNOWS", c);
50+
database.commit();
51+
52+
// Build GAV
53+
final GraphAnalyticalView gav = new GraphAnalyticalView(database);
54+
gav.build(new String[] { "Person" }, new String[] { "KNOWS" });
55+
try {
56+
// Two-pattern MATCH where both endpoints get bound — the second pattern
57+
// should benefit from expand-into (isConnectedTo)
58+
database.begin();
59+
final ResultSet rs = database.query("sql",
60+
"MATCH {type: Person, as: a, where: (name = 'Alice')} -KNOWS-> {as: b}," +
61+
" {as: a} -KNOWS-> {as: c, where: (name = 'Charlie')} " +
62+
"RETURN a.name as aName, b.name as bName, c.name as cName");
63+
64+
final List<String> bNames = new ArrayList<>();
65+
while (rs.hasNext()) {
66+
final Result row = rs.next();
67+
assertThat(row.<String>getProperty("aName")).isEqualTo("Alice");
68+
assertThat(row.<String>getProperty("cName")).isEqualTo("Charlie");
69+
bNames.add(row.getProperty("bName"));
70+
}
71+
// Alice knows Bob and Charlie, so b should be Bob and Charlie
72+
assertThat(bNames).containsExactlyInAnyOrder("Bob", "Charlie");
73+
database.commit();
74+
} finally {
75+
gav.drop();
76+
}
77+
}
78+
79+
@Test
80+
void matchExpandIntoNotConnected() {
81+
database.getSchema().createVertexType("Person");
82+
database.getSchema().createEdgeType("KNOWS");
83+
84+
// Build graph: A -> B, C is isolated
85+
database.begin();
86+
final MutableVertex a = database.newVertex("Person").set("name", "Alice").save();
87+
final MutableVertex b = database.newVertex("Person").set("name", "Bob").save();
88+
final MutableVertex c = database.newVertex("Person").set("name", "Charlie").save();
89+
a.newEdge("KNOWS", b);
90+
database.commit();
91+
92+
final GraphAnalyticalView gav = new GraphAnalyticalView(database);
93+
gav.build(new String[] { "Person" }, new String[] { "KNOWS" });
94+
try {
95+
// MATCH where Alice -KNOWS-> Charlie should return nothing (not connected)
96+
database.begin();
97+
final ResultSet rs = database.query("sql",
98+
"MATCH {type: Person, as: a, where: (name = 'Alice')} -KNOWS-> {as: b, where: (name = 'Charlie')} " +
99+
"RETURN a.name as aName, b.name as bName");
100+
assertThat(rs.hasNext()).isFalse();
101+
database.commit();
102+
} finally {
103+
gav.drop();
104+
}
105+
}
106+
107+
@Test
108+
void matchSameResultsWithAndWithoutGAV() {
109+
database.getSchema().createVertexType("Person");
110+
database.getSchema().createEdgeType("KNOWS");
111+
112+
// Build graph: A -> B -> C
113+
database.begin();
114+
final MutableVertex a = database.newVertex("Person").set("name", "Alice").save();
115+
final MutableVertex b = database.newVertex("Person").set("name", "Bob").save();
116+
final MutableVertex c = database.newVertex("Person").set("name", "Charlie").save();
117+
a.newEdge("KNOWS", b);
118+
b.newEdge("KNOWS", c);
119+
database.commit();
120+
121+
final String query = "MATCH {type: Person, as: a, where: (name = 'Alice')} -KNOWS-> {as: b} -KNOWS-> {as: c} " +
122+
"RETURN a.name as aName, b.name as bName, c.name as cName";
123+
124+
// Query without GAV
125+
database.begin();
126+
final ResultSet rsWithout = database.query("sql", query);
127+
final List<String> withoutResults = new ArrayList<>();
128+
while (rsWithout.hasNext()) {
129+
final Result row = rsWithout.next();
130+
withoutResults.add(row.getProperty("aName") + "->" + row.getProperty("bName") + "->" + row.getProperty("cName"));
131+
}
132+
database.commit();
133+
134+
// Build GAV and query again
135+
final GraphAnalyticalView gav = new GraphAnalyticalView(database);
136+
gav.build(new String[] { "Person" }, new String[] { "KNOWS" });
137+
try {
138+
database.begin();
139+
final ResultSet rsWith = database.query("sql", query);
140+
final List<String> withResults = new ArrayList<>();
141+
while (rsWith.hasNext()) {
142+
final Result row = rsWith.next();
143+
withResults.add(row.getProperty("aName") + "->" + row.getProperty("bName") + "->" + row.getProperty("cName"));
144+
}
145+
database.commit();
146+
147+
assertThat(withResults).containsExactlyInAnyOrderElementsOf(withoutResults);
148+
assertThat(withResults).containsExactly("Alice->Bob->Charlie");
149+
} finally {
150+
gav.drop();
151+
}
152+
}
153+
}

gremlin/src/main/java/com/arcadedb/gremlin/ArcadeVertex.java

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,13 @@
1818
*/
1919
package com.arcadedb.gremlin;
2020

21+
import com.arcadedb.database.Database;
2122
import com.arcadedb.database.EmbeddedDocument;
2223
import com.arcadedb.database.MutableEmbeddedDocument;
24+
import com.arcadedb.database.RID;
25+
import com.arcadedb.exception.RecordNotFoundException;
26+
import com.arcadedb.graph.GraphTraversalProvider;
27+
import com.arcadedb.graph.GraphTraversalProviderRegistry;
2328
import com.arcadedb.graph.MutableEdge;
2429
import com.arcadedb.graph.MutableVertex;
2530
import com.arcadedb.schema.DocumentType;
@@ -197,19 +202,46 @@ public Iterator<Edge> edges(final Direction direction, final String... edgeLabel
197202

198203
@Override
199204
public Iterator<Vertex> vertices(final Direction direction, final String... edgeLabels) {
200-
final List<Vertex> result = new ArrayList<>();
205+
// CSR fast path: use GAV provider when available for O(1) neighbor lookup
206+
final GraphTraversalProvider provider = graph.getDatabase() instanceof Database db
207+
? (edgeLabels.length == 0
208+
? GraphTraversalProviderRegistry.findProvider(db)
209+
: GraphTraversalProviderRegistry.findProvider(db, edgeLabels))
210+
: null;
211+
if (provider != null) {
212+
final int nodeId = provider.getNodeId(baseElement.getIdentity());
213+
if (nodeId >= 0) {
214+
final com.arcadedb.graph.Vertex.DIRECTION dir = ArcadeGraph.mapDirection(direction);
215+
final int[] neighborIds = edgeLabels.length == 0
216+
? provider.getNeighborIds(nodeId, dir)
217+
: provider.getNeighborIds(nodeId, dir, edgeLabels);
218+
final List<Vertex> result = new ArrayList<>(neighborIds.length);
219+
for (final int neighborId : neighborIds) {
220+
final RID rid = provider.getRID(neighborId);
221+
if (rid != null) {
222+
try {
223+
final com.arcadedb.graph.Vertex v = rid.asVertex();
224+
result.add(new ArcadeVertex(this.graph, v));
225+
} catch (final RecordNotFoundException e) {
226+
// vertex deleted since CSR was built — skip
227+
}
228+
}
229+
}
230+
return result.iterator();
231+
}
232+
}
201233

234+
// OLTP fallback
235+
final List<Vertex> result = new ArrayList<>();
202236
if (edgeLabels.length == 0) {
203-
for (final com.arcadedb.graph.Vertex vertex : this.baseElement.getVertices(ArcadeGraph.mapDirection(direction))) {
204-
if (graph.getDatabase().existsRecord(vertex.getIdentity())) // FILTER OUT DELETED VERTICES
237+
for (final com.arcadedb.graph.Vertex vertex : this.baseElement.getVertices(ArcadeGraph.mapDirection(direction)))
238+
if (graph.getDatabase().existsRecord(vertex.getIdentity()))
205239
result.add(new ArcadeVertex(this.graph, vertex));
206-
}
207240
} else {
208241
for (final com.arcadedb.graph.Vertex vertex : this.baseElement.getVertices(ArcadeGraph.mapDirection(direction), edgeLabels))
209-
if (graph.getDatabase().existsRecord(vertex.getIdentity())) // FILTER OUT DELETED VERTICES
242+
if (graph.getDatabase().existsRecord(vertex.getIdentity()))
210243
result.add(new ArcadeVertex(this.graph, vertex));
211244
}
212-
213245
return result.iterator();
214246
}
215247

0 commit comments

Comments
 (0)