Skip to content

Commit f569cbb

Browse files
committed
Sample each edge type with its own limit, 10 types per request
1 parent cffbc5d commit f569cbb

4 files changed

Lines changed: 292 additions & 183 deletions

File tree

‎packages/graph-explorer/src/connector/gremlin/fetchEdgeConnections/edgeConnectionsTemplate.test.ts‎

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,26 @@ import { normalizeWithNoSpace as normalize } from "@/utils/testing";
44
import edgeConnectionsTemplate from "./edgeConnectionsTemplate";
55

66
describe("Gremlin > edgeConnectionsTemplate", () => {
7-
it("should scan a batch of edge types and group the endpoint labels by edge type", () => {
7+
it("should give each sampled edge type its own limited branch", () => {
88
const template = edgeConnectionsTemplate({
99
types: [createEdgeType("route"), createEdgeType("contains")],
1010
});
1111

12+
// A single limit after hasLabel() would be shared, and a dominant type would
13+
// fill it before the rest were read at all.
1214
expect(normalize(template)).toBe(
1315
normalize(`
14-
g.E().hasLabel('route', 'contains')
16+
g.V().limit(1).union(
17+
V().outE('route').limit(10000),
18+
V().outE('contains').limit(10000)
19+
)
1520
.group()
1621
.by(label())
1722
.by(
18-
limit(10000)
19-
.project('sourceType', 'targetType')
20-
.by(outV().label())
21-
.by(inV().label())
22-
.dedup()
23-
.fold()
23+
project('s', 't')
24+
.by(outV().label().fold())
25+
.by(inV().label().fold())
26+
.groupCount()
2427
)
2528
`),
2629
);
@@ -31,6 +34,6 @@ describe("Gremlin > edgeConnectionsTemplate", () => {
3134
types: [createEdgeType("edge'with'quotes")],
3235
});
3336

34-
expect(template).toContain("hasLabel('edge\\'with\\'quotes')");
37+
expect(template).toContain("V().outE('edge\\'with\\'quotes')");
3538
});
3639
});

‎packages/graph-explorer/src/connector/gremlin/fetchEdgeConnections/edgeConnectionsTemplate.ts‎

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,40 +5,57 @@ import { DEFAULT_SAMPLE_SIZE, query } from "@/utils";
55
import { fragment } from "../fragments";
66

77
/**
8-
* Returns a Gremlin query that discovers distinct edge connection patterns for a
9-
* batch of edge types in a single request.
8+
* Keys of the projected endpoint labels. The template writes them and the
9+
* response parser reads them, so both derive from this object.
10+
*/
11+
export const projectionKeys = {
12+
sourceType: "s",
13+
targetType: "t",
14+
} as const;
15+
16+
/**
17+
* Returns a Gremlin query that counts the endpoint label combinations of up to
18+
* `DEFAULT_SAMPLE_SIZE` edges of each given type, grouped by edge type.
19+
*
20+
* One limit after `hasLabel(A, B, ...)` would be shared, so a dominant type fills
21+
* it and the rest come back empty. Each type gets its own `union()` branch
22+
* instead, and on Neptune each branch is an index lookup by edge label.
23+
* Mid-traversal `V()` rather than `E()`, which needs TinkerPop 3.7, and anchored
24+
* on `V().limit(1)` because anchoring on `inject()` is not native on Neptune.
25+
*
26+
* Grouped by edge type before counting because Neptune's DFE engine cannot count
27+
* one `project()` key across several full branches: two took 54s and five timed
28+
* out, where grouping first handled ten in 9s.
1029
*
11-
* `g.E().hasLabel(...)` is a native, index-backed edge scan on Neptune; the
12-
* `group().by(label())` reduction buckets the edges by edge type, and each
13-
* bucket samples up to `DEFAULT_SAMPLE_SIZE` edges and projects the distinct
14-
* (source label, target label) pairs. The caller regroups by the returned edge
15-
* label.
30+
* Callers send 10 types per request, so one request reads at most 100,000
31+
* edges. On a db.t3.medium, 100 types in one request took 116s and left the
32+
* instance refusing even a single-type sample for two minutes afterwards.
1633
*
17-
* The `limit` sits inside the `group()` value traversal, which TinkerPop runs
18-
* per group, so each edge type is sampled independently — no shared cap that
19-
* starves rarer types. It bounds the per-type label-resolution and dedup work,
20-
* not the initial edge scan: `group()` still enumerates every edge of the
21-
* batched types to bucket them. A per-type scan cap is not expressible in one
22-
* native TinkerPop 3.6.2 request.
34+
* The endpoint labels are folded because engines disagree on what `label()`
35+
* emits for a multi-label vertex. Neptune 1.4 emits one `::` composite, but
36+
* 1.3.5 emits each label separately, and a bare `by(outV().label())` keeps only
37+
* the first, silently dropping the vertex's other types.
2338
*/
2439
export default function edgeConnectionsTemplate({
2540
types,
2641
}: {
2742
types: EdgeType[];
2843
}) {
29-
const labels = types.map(fragment.identifier);
44+
const limit = fragment.number(DEFAULT_SAMPLE_SIZE);
45+
const branches = types.map(
46+
type => `V().outE(${fragment.identifier(type)}).limit(${limit})`,
47+
);
48+
const keys = Object.values(projectionKeys).map(fragment.identifier);
3049

3150
return query`
32-
g.E().hasLabel(${labels.join(", ")})
51+
g.V().limit(1).union(${branches.join(", ")})
3352
.group()
3453
.by(label())
3554
.by(
36-
limit(${DEFAULT_SAMPLE_SIZE})
37-
.project('sourceType', 'targetType')
38-
.by(outV().label())
39-
.by(inV().label())
40-
.dedup()
41-
.fold()
55+
project(${keys.join(", ")})
56+
.by(outV().label().fold())
57+
.by(inV().label().fold())
58+
.groupCount()
4259
)
4360
`;
4461
}

0 commit comments

Comments
 (0)