Modeling Hierarchies, Networks, and Feedback Loops
Not all graphs are the same shape. A file system is a tree. A social network is many-to-many. A supply chain with feedback has cycles. Each shape has different query patterns, different failure modes, and different production constraints.
This tutorial covers all three so you can recognize which shape your domain needs and choose the right query approach from the start.
Shape 1: Hierarchies (trees)
Example domain: Organizational chart — COMPANY → DIVISION → DEPARTMENT → TEAM → EMPLOYEE
Ingesting the tree
- Python
- TypeScript
- shell
from rushdb import RushDB
db = RushDB("RUSHDB_API_KEY", base_url="https://api.rushdb.com/api/v1")
company = db.records.create("COMPANY", {"name": "Acme Corp"})
division = db.records.create("DIVISION", {"name": "Engineering"})
dept = db.records.create("DEPARTMENT", {"name": "Platform", "budget": 2000000})
team_infra = db.records.create("TEAM", {"name": "Infra", "size": 6})
lena = db.records.create("EMPLOYEE", {"name": "Lena Müller", "role": "Lead SRE", "level": "L5"})
marco = db.records.create("EMPLOYEE", {"name": "Marco Rossi", "role": "Engineer", "level": "L4"})
db.records.attach(company.id, division.id, {"type": "HAS_DIVISION"})
db.records.attach(division.id, dept.id, {"type": "HAS_DEPARTMENT"})
db.records.attach(dept.id, team_infra.id, {"type": "HAS_TEAM"})
db.records.attach(team_infra.id, lena.id, {"type": "MEMBER_OF", "direction": "in"})
db.records.attach(team_infra.id, marco.id, {"type": "MEMBER_OF", "direction": "in"})
import RushDB from '@rushdb/javascript-sdk'
const db = new RushDB('RUSHDB_API_KEY')
const company = await db.records.create({ label: 'COMPANY', data: { name: 'Acme Corp' } })
const division = await db.records.create({ label: 'DIVISION', data: { name: 'Engineering' } })
const dept = await db.records.create({ label: 'DEPARTMENT', data: { name: 'Platform', budget: 2000000 } })
const teamInfra = await db.records.create({ label: 'TEAM', data: { name: 'Infra', size: 6 } })
const lena = await db.records.create({ label: 'EMPLOYEE', data: { name: 'Lena Müller', role: 'Lead SRE', level: 'L5' } })
const marco = await db.records.create({ label: 'EMPLOYEE', data: { name: 'Marco Rossi', role: 'Engineer', level: 'L4' } })
await db.records.attach({ source: company, target: division, options: { type: 'HAS_DIVISION' } })
await db.records.attach({ source: division, target: dept, options: { type: 'HAS_DEPARTMENT' } })
await db.records.attach({ source: dept, target: teamInfra, options: { type: 'HAS_TEAM' } })
await db.records.attach({ source: teamInfra, target: lena, options: { type: 'MEMBER_OF', direction: 'in' } })
await db.records.attach({ source: teamInfra, target: marco, options: { type: 'MEMBER_OF', direction: 'in' } })
BASE="https://api.rushdb.com/api/v1"
TOKEN="RUSHDB_API_KEY"
H='Content-Type: application/json'
COMPANY_ID=$(curl -s -X POST "$BASE/records" -H "$H" -H "Authorization: Bearer $TOKEN" \
-d '{"label":"COMPANY","data":{"name":"Acme Corp"}}' | jq -r '.data.__id')
DIV_ID=$(curl -s -X POST "$BASE/records" -H "$H" -H "Authorization: Bearer $TOKEN" \
-d '{"label":"DIVISION","data":{"name":"Engineering"}}' | jq -r '.data.__id')
curl -s -X POST "$BASE/records/$COMPANY_ID/relations" -H "$H" -H "Authorization: Bearer $TOKEN" \
-d "{\"targets\":[\"$DIV_ID\"],\"options\":{\"type\":\"HAS_DIVISION\"}}"
Querying the tree: all employees with their full org path
- Python
- TypeScript
- shell
headcount = db.records.find({
"labels": ["EMPLOYEE"],
"where": {
"TEAM": {
"$alias": "$team",
"$relation": {"type": "MEMBER_OF", "direction": "out"},
"DEPARTMENT": {
"$alias": "$dept",
"DIVISION": {
"$alias": "$div",
"COMPANY": {"name": "Acme Corp"}
}
}
}
},
"select": {
"employeeName": "$record.name",
"role": "$record.role",
"teamName": "$team.name",
"deptName": "$dept.name"
},
"orderBy": {"employeeName": "asc"}
})
const headcount = await db.records.find({
labels: ['EMPLOYEE'],
where: {
TEAM: {
$alias: '$team',
$relation: { type: 'MEMBER_OF', direction: 'out' },
DEPARTMENT: {
$alias: '$dept',
DIVISION: {
$alias: '$div',
COMPANY: { name: 'Acme Corp' }
}
}
}
},
select: {
employeeName: '$record.name',
role: '$record.role',
teamName: '$team.name',
deptName: '$dept.name',
divisionName: '$div.name'
},
orderBy: { employeeName: 'asc' }
})
curl -s -X POST "$BASE/records/search" \
-H "$H" -H "Authorization: Bearer $TOKEN" \
-d '{
"labels": ["EMPLOYEE"],
"where": {
"TEAM": {
"$alias": "$team",
"$relation": {"type": "MEMBER_OF", "direction": "out"},
"DEPARTMENT": {"DIVISION": {"COMPANY": {"name": "Acme Corp"}}}
}
},
"select": {"employeeName": "$record.name", "teamName": "$team.name"}
}'
Tree query: headcount per department
- Python
- TypeScript
- shell
dept_headcount = db.records.find({
"labels": ["DEPARTMENT"],
"where": {
"TEAM": {
"EMPLOYEE": {"$alias": "$emp", "$relation": {"type": "MEMBER_OF", "direction": "out"}}
},
"DIVISION": {"COMPANY": {"name": "Acme Corp"}}
},
"select": {
"deptName": "$record.name",
"headcount": {"$count": "$emp"}
},
"groupBy": ["deptName", "headcount"],
"orderBy": {"headcount": "desc"}
})
const deptHeadcount = await db.records.find({
labels: ['DEPARTMENT'],
where: {
TEAM: {
EMPLOYEE: { $alias: '$emp', $relation: { type: 'MEMBER_OF', direction: 'out' } }
},
DIVISION: { COMPANY: { name: 'Acme Corp' } }
},
select: {
deptName: '$record.name',
headcount: { $count: '$emp' }
},
groupBy: ['deptName', 'headcount'],
orderBy: { headcount: 'desc' }
})
curl -s -X POST "$BASE/records/search" \
-H "$H" -H "Authorization: Bearer $TOKEN" \
-d '{
"labels": ["DEPARTMENT"],
"where": {
"TEAM": {"EMPLOYEE": {"$alias": "$emp", "$relation": {"type": "MEMBER_OF", "direction": "out"}}},
"DIVISION": {"COMPANY": {"name": "Acme Corp"}}
},
"select": {
"deptName": "$record.name",
"headcount": {"$count": "$emp"}
},
"groupBy": ["deptName", "headcount"],
"orderBy": {"headcount": "desc"}
}'
Shape 2: Many-to-many networks
Example domain: Research graph — AUTHOR ↔ PAPER ↔ TOPIC, PAPER → PAPER (citations)
Ingesting the network
- Python
- TypeScript
- shell
lena_author = db.records.create("AUTHOR", {"name": "Lena Müller", "hIndex": 14})
marco_author = db.records.create("AUTHOR", {"name": "Marco Rossi", "hIndex": 9})
topic_gnn = db.records.create("TOPIC", {"name": "Graph Neural Networks"})
paper1 = db.records.create("PAPER", {"title": "GNN Scaling Strategies", "year": 2024, "citations": 87})
paper2 = db.records.create("PAPER", {"title": "Attention is All You Need", "year": 2017, "citations": 90000})
db.records.attach(lena_author.id, paper1.id, {"type": "CO_AUTHORED"})
db.records.attach(marco_author.id, paper1.id, {"type": "CO_AUTHORED"})
db.records.attach(paper1.id, topic_gnn.id, {"type": "COVERS"})
db.records.attach(paper1.id, paper2.id, {"type": "CITES"})
const lenaAuthor = await db.records.create({ label: 'AUTHOR', data: { name: 'Lena Müller', hIndex: 14 } })
const marcoAuthor = await db.records.create({ label: 'AUTHOR', data: { name: 'Marco Rossi', hIndex: 9 } })
const topicGNN = await db.records.create({ label: 'TOPIC', data: { name: 'Graph Neural Networks' } })
const topicDistrib = await db.records.create({ label: 'TOPIC', data: { name: 'Distributed Systems' } })
const paper1 = await db.records.create({ label: 'PAPER', data: { title: 'GNN Scaling Strategies', year: 2024, citations: 87 } })
const paper2 = await db.records.create({ label: 'PAPER', data: { title: 'Attention is All You Need', year: 2017, citations: 90000 } })
await Promise.all([
db.records.attach({ source: lenaAuthor, target: paper1, options: { type: 'CO_AUTHORED' } }),
db.records.attach({ source: marcoAuthor, target: paper1, options: { type: 'CO_AUTHORED' } }),
db.records.attach({ source: paper1, target: topicGNN, options: { type: 'COVERS' } }),
db.records.attach({ source: paper1, target: topicDistrib, options: { type: 'COVERS' } }),
db.records.attach({ source: paper1, target: paper2, options: { type: 'CITES' } }),
])
LENA_ID=$(curl -s -X POST "$BASE/records" -H "$H" -H "Authorization: Bearer $TOKEN" \
-d '{"label":"AUTHOR","data":{"name":"Lena Müller","hIndex":14}}' | jq -r '.data.__id')
PAPER_ID=$(curl -s -X POST "$BASE/records" -H "$H" -H "Authorization: Bearer $TOKEN" \
-d '{"label":"PAPER","data":{"title":"GNN Scaling Strategies","year":2024}}' | jq -r '.data.__id')
curl -s -X POST "$BASE/records/$LENA_ID/relations" -H "$H" -H "Authorization: Bearer $TOKEN" \
-d "{\"targets\":[\"$PAPER_ID\"],\"options\":{\"type\":\"CO_AUTHORED\"}}"
Network query: co-authors on a topic
- Python
- TypeScript
- shell
co_authors = db.records.find({
"labels": ["AUTHOR"],
"where": {
"PAPER": {
"$alias": "$paper",
"$relation": {"type": "CO_AUTHORED", "direction": "out"},
"TOPIC": {"name": "Graph Neural Networks"}
}
},
"select": {
"authorName": "$record.name",
"hIndex": "$record.hIndex",
"paperCount": {"$count": "$paper"}
},
"groupBy": ["authorName", "hIndex", "paperCount"],
"orderBy": {"paperCount": "desc"}
})
const coAuthors = await db.records.find({
labels: ['AUTHOR'],
where: {
PAPER: {
$alias: '$paper',
$relation: { type: 'CO_AUTHORED', direction: 'out' },
TOPIC: { name: 'Graph Neural Networks' }
}
},
select: {
authorName: '$record.name',
hIndex: '$record.hIndex',
paperCount: { $count: '$paper' }
},
groupBy: ['authorName', 'hIndex', 'paperCount'],
orderBy: { paperCount: 'desc' }
})
curl -s -X POST "$BASE/records/search" \
-H "$H" -H "Authorization: Bearer $TOKEN" \
-d '{
"labels": ["AUTHOR"],
"where": {
"PAPER": {
"$alias": "$paper",
"$relation": {"type": "CO_AUTHORED", "direction": "out"},
"TOPIC": {"name": "Graph Neural Networks"}
}
},
"select": {
"authorName": "$record.name",
"paperCount": {"$count": "$paper"}
},
"groupBy": ["authorName", "paperCount"],
"orderBy": {"paperCount": "desc"}
}'
Shape 3: Cyclic systems (dependency graphs)
Example domain: Package dependency graph — PACKAGE depends on other PACKAGEs through transitive chains.
SearchQuery does not expose arbitrary-depth recursive traversal. Instead, scope multi-hop traversal explicitly to the depth your product requires. For blast-radius analysis (which packages are affected if crypto-utils has a CVE?), traverse up to a known safe depth.
Ingesting the dependency graph
- Python
- TypeScript
- shell
app_core = db.records.create("PACKAGE", {"name": "app-core", "version": "2.1.0"})
auth_lib = db.records.create("PACKAGE", {"name": "auth-lib", "version": "1.4.2"})
data_client = db.records.create("PACKAGE", {"name": "data-client", "version": "3.0.1"})
crypto_utils = db.records.create("PACKAGE", {"name": "crypto-utils", "version": "0.9.8"})
db.records.attach(app_core.id, auth_lib.id, {"type": "DEPENDS_ON"})
db.records.attach(app_core.id, data_client.id, {"type": "DEPENDS_ON"})
db.records.attach(auth_lib.id, crypto_utils.id, {"type": "DEPENDS_ON"})
db.records.attach(data_client.id, crypto_utils.id, {"type": "DEPENDS_ON"})
const appCore = await db.records.create({ label: 'PACKAGE', data: { name: 'app-core', version: '2.1.0' } })
const authLib = await db.records.create({ label: 'PACKAGE', data: { name: 'auth-lib', version: '1.4.2' } })
const dataClient = await db.records.create({ label: 'PACKAGE', data: { name: 'data-client', version: '3.0.1' } })
const cryptoUtils = await db.records.create({ label: 'PACKAGE', data: { name: 'crypto-utils', version: '0.9.8' } })
await Promise.all([
db.records.attach({ source: appCore, target: authLib, options: { type: 'DEPENDS_ON' } }),
db.records.attach({ source: appCore, target: dataClient, options: { type: 'DEPENDS_ON' } }),
db.records.attach({ source: authLib, target: cryptoUtils, options: { type: 'DEPENDS_ON' } }),
db.records.attach({ source: dataClient, target: cryptoUtils, options: { type: 'DEPENDS_ON' } }),
])
CORE_ID=$(curl -s -X POST "$BASE/records" -H "$H" -H "Authorization: Bearer $TOKEN" \
-d '{"label":"PACKAGE","data":{"name":"app-core","version":"2.1.0"}}' | jq -r '.data.__id')
AUTH_ID=$(curl -s -X POST "$BASE/records" -H "$H" -H "Authorization: Bearer $TOKEN" \
-d '{"label":"PACKAGE","data":{"name":"auth-lib","version":"1.4.2"}}' | jq -r '.data.__id')
CRYPTO_ID=$(curl -s -X POST "$BASE/records" -H "$H" -H "Authorization: Bearer $TOKEN" \
-d '{"label":"PACKAGE","data":{"name":"crypto-utils","version":"0.9.8"}}' | jq -r '.data.__id')
curl -s -X POST "$BASE/records/$CORE_ID/relations" -H "$H" -H "Authorization: Bearer $TOKEN" \
-d "{\"targets\":[\"$AUTH_ID\"],\"options\":{\"type\":\"DEPENDS_ON\"}}"
curl -s -X POST "$BASE/records/$AUTH_ID/relations" -H "$H" -H "Authorization: Bearer $TOKEN" \
-d "{\"targets\":[\"$CRYPTO_ID\"],\"options\":{\"type\":\"DEPENDS_ON\"}}"
Cyclic query: two-hop blast radius for a vulnerable package
Find all packages that depend on crypto-utils directly (hop 1) or through one intermediate package (hop 2):
- Python
- TypeScript
- shell
# Direct dependents of crypto-utils
direct = db.records.find({
"labels": ["PACKAGE"],
"where": {
"PACKAGE": {
"$alias": "$dep",
"$relation": {"type": "DEPENDS_ON", "direction": "out"},
"name": "crypto-utils"
}
},
"select": {
"packageName": "$record.name",
"version": "$record.version"
}
})
# Two-hop: packages that depend on a package that depends on crypto-utils
indirect = db.records.find({
"labels": ["PACKAGE"],
"where": {
"PACKAGE": {
"$alias": "$mid",
"$relation": {"type": "DEPENDS_ON", "direction": "out"},
"PACKAGE": {
"$relation": {"type": "DEPENDS_ON", "direction": "out"},
"name": "crypto-utils"
}
}
},
"select": {
"packageName": "$record.name",
"via": "$mid.name"
}
})
// Hop 1: direct dependents
const direct = await db.records.find({
labels: ['PACKAGE'],
where: {
PACKAGE: {
$alias: '$dep',
$relation: { type: 'DEPENDS_ON', direction: 'out' },
name: 'crypto-utils'
}
},
select: {
packageName: '$record.name',
version: '$record.version',
hop: { $count: '$dep' }
},
groupBy: ['packageName', 'version', 'hop']
})
// Hop 2: packages whose dependencies depend on crypto-utils
const indirect = await db.records.find({
labels: ['PACKAGE'],
where: {
PACKAGE: {
$alias: '$mid',
$relation: { type: 'DEPENDS_ON', direction: 'out' },
PACKAGE: {
$relation: { type: 'DEPENDS_ON', direction: 'out' },
name: 'crypto-utils'
}
}
},
select: {
packageName: '$record.name',
version: '$record.version',
via: '$mid.name'
}
})
# Direct dependents
curl -s -X POST "$BASE/records/search" \
-H "$H" -H "Authorization: Bearer $TOKEN" \
-d '{
"labels": ["PACKAGE"],
"where": {
"PACKAGE": {
"$relation": {"type": "DEPENDS_ON", "direction": "out"},
"name": "crypto-utils"
}
},
"select": {"packageName": "$record.name", "version": "$record.version"}
}'
Comparison of the three shapes
| Shape | Key property | Query pattern | Ambush |
|---|---|---|---|
| Tree | Single parent per node | Top-down with full-path select | Deep trees require explicit hop count |
| Many-to-many | Nodes can appear in multiple relationships | Aggregation by relationship type | Fan-out can be large without limit |
| Cyclic | Loops are possible | Explicit depth bounds | Unbounded traversal is not supported |
Production caveat
Each shape has a fan-out risk. In trees, deep hierarchies multiply candidates at every hop. In networks, highly-connected hubs (an author with 200 papers) inflate traversal cost. In cyclic graphs, even a two-hop traversal can cover thousands of paths in large dependency graphs.
Apply limit conservatively and filter early on high-selectivity properties (e.g. name, status, version). Measure response times before and after adding hops to your query.
Next steps
- Choosing Relationship Types That Age Well — when generic vs. typed edges matter
- Temporal Graphs: Modeling State and Event Time Together — adding time dimension to any of these shapes
- SearchQuery Deep Dive —
$relation,$alias, andcollectpatterns