@@ -5,40 +5,57 @@ import { DEFAULT_SAMPLE_SIZE, query } from "@/utils";
55import { 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 */
2439export 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