Skip to main content

Choosing Relationship Types That Age Well

Every time you attach two records in RushDB, you pick a relationship type. This choice feels minor in the moment and becomes significant three months later — when another developer reads your graph, or when a query needs to traverse it in a direction you didn't anticipate.

This tutorial covers the tradeoffs clearly so you can make the right call up front.


Two strategies

Strategy A: Generic nesting

You let RushDB's automatic nesting create edges by importing JSON with deeply nested objects. The resulting relationship type is a generated name derived from the parent label.

db.records.import_json(
label="ORDER",
data=[{
"total": 149.00,
"PRODUCT": [{"name": "Lens Cap 58mm", "price": 12.99}]
}]
)

This creates ORDERPRODUCT records connected by an edge RushDB names after the child label. The relationship type exists but carries no semantic meaning beyond the structural parent-child edge.

Strategy B: Explicit typed relationships

You create records independently and attach them with a named relationship type:

order = db.records.create("ORDER", {"total": 149.00})
product = db.records.create("PRODUCT", {"name": "Lens Cap 58mm", "price": 12.99})

db.records.attach(
source=order,
target=product,
options={"type": "CONTAINS", "direction": "out"}
)

The edge now carries meaning: an ORDER contains a PRODUCT.


When to use each strategy

Use explicit typed relationships when:

  • You need to filter traversal by edge type ($relation: { type: 'AUTHORED' })
  • Two records can be connected in multiple ways (a user can both AUTHORED and REVIEWED a document)
  • The direction of the relationship matters for different query perspectives
  • You want the relationship to be self-documenting for other developers or agents reading the ontology

Use generic nesting when:

  • The structure is purely hierarchical and the relationship type adds no meaning
  • You are ingesting denormalized data (CSV rows, API responses) and the parent-child is implicit
  • Speed of ingest matters more than traversal precision

The readability problem with generic edges

Consider a graph where USER records are linked to DOCUMENT records. With generic edges:

result = db.records.find({
"labels": ["DOCUMENT"],
"where": {
"USER": {"name": "Lena Müller"}
}
})

This query returns documents connected to Lena — but connected how? Did she write them? Read them? Approve them? The graph cannot answer that question.

With typed relationships, the intent is explicit:

# Documents authored
authored = db.records.find({
"labels": ["DOCUMENT"],
"where": {
"USER": {
"$alias": "$author",
"$relation": {"type": "AUTHORED", "direction": "in"},
"name": "Lena Müller"
}
}
})

# Documents reviewed
reviewed = db.records.find({
"labels": ["DOCUMENT"],
"where": {
"USER": {
"$alias": "$reviewer",
"$relation": {"type": "REVIEWED", "direction": "in"},
"name": "Lena Müller"
}
}
})

The analytics problem with generic edges

Generic edges make dimensional aggregations ambiguous. With typed edges you can count distinct relationship types as separate metrics:

user_stats = db.records.find({
"labels": ["USER"],
"where": {
"DOCUMENT": {
"$alias": "$authored",
"$relation": {"type": "AUTHORED", "direction": "out"}
}
},
"select": {
"userName": "$record.name",
"documentsAuthored": {"$count": "$authored"}
},
"groupBy": ["userName", "documentsAuthored"],
"orderBy": {"documentsAuthored": "desc"},
"limit": 20
})

Migrating from generic to typed edges

If you imported data with generic nesting and now need typed relationships:

  1. Query the existing records using the generic traversal to find both endpoint IDs.
  2. Attach new typed relationships between them.
  3. Optionally detach the generic edges if your queries no longer need them.
pairs = db.records.find({
"labels": ["DOCUMENT"],
"where": {"USER": {"$alias": "$user"}},
"select": {"documentId": "$record.__id", "userId": "$user.__id"}
})

for pair in pairs.data:
db.records.attach(
pair["userId"],
pair["documentId"],
{"type": "AUTHORED", "direction": "out"}
)

Naming conventions that age well

Relationship types that age well share a few properties:

  • Verb-first: AUTHORED, CONTAINS, DEPENDS_ON, ASSIGNED_TO — not USER_DOCUMENT or LINK
  • Direction-aware: the verb should read correctly in the out direction: USER --AUTHORED--> DOCUMENT
  • Domain-specific: prefer business verbs (PURCHASED, APPROVED) over generic ones (HAS, RELATED_TO)
  • Uppercase: consistent with Neo4j conventions and easier to scan in query code

Avoid:

  • HAS — does not indicate what the relationship means
  • LINKED — directionally ambiguous
  • REL_USER_DOCUMENT — table-join style naming

Production caveat

You cannot rename or change a relationship type after records are attached using it. If you need to change a relationship type at scale, you must query the existing edges, attach new ones, and detach the old ones as a bulk operation. Plan your types before ingestion, not after.


Next steps