The $graphLookup stage recursively walks relationships in a collection—returning every connected document in one array. Think org charts, nested comments, product categories, or bill-of-materials trees without writing application-level loops.
01
Recursive join
Beyond one level.
02
startWith
Where to begin.
03
connect fields
Link parent ↔ child.
04
maxDepth
Limit hop count.
05
depthField
Track tree level.
06
vs $lookup
When to use each.
Fundamentals
Definition and Usage
$graphLookup is an aggregation stage that performs a recursive search on a collection. Starting from a value you specify (startWith), MongoDB finds documents where connectToField matches that value, then uses each match’s connectFromField to find the next level—and repeats until the graph is exhausted or maxDepth is reached.
💡
Beginner tip
Picture an org chart: each employee has reportsTo pointing to their manager’s _id. Set startWith: "$_id", connectFromField: "_id", and connectToField: "reportsTo" to fetch every subordinate—direct reports, their reports, and so on.
Use $graphLookup when relationships span multiple levels and you want them in a single aggregation query. For a flat one-to-many join (orders → line items), $lookup is simpler and faster.
Foundation
📝 Syntax
$graphLookup accepts a document describing the graph traversal:
Each employee (except Alice, the VP) has reportsTo pointing to their manager’s _id. This parent-pointer pattern is the most common setup for $graphLookup.
Example 2 — All reports under Alice (full recursion)
MongoDB starts with Alice’s _id (1), finds employees where reportsTo === 1 (Bob, Carol), then uses each match’s _id (via connectFromField) to find Dave, Eve, Frank, and finally Grace—until no new matches appear.
📈 Practical Patterns
Depth limits, level tracking, and filtered traversals.
Example 3 — Direct reports only (maxDepth: 0)
mongosh
db.employees.aggregate([
{ $match: { name: "Bob Smith" } },
{
$graphLookup: {
from: "employees",
startWith: "$_id",
connectFromField: "_id",
connectToField: "reportsTo",
as: "directReports",
maxDepth: 0
}
},
{
$project: {
name: 1,
directReports: {
$map: {
input: "$directReports",
as: "r",
in: "$$r.name"
}
},
_id: 0
}
}
])
// maxDepth: 0 → only Dave and Eve (report directly to Bob)
// Grace (reports to Dave) is excluded
📤 maxDepth: 0:
directReports: [ "Dave Kim", "Eve Patel" ]
One hop from Bob only
How It Works
maxDepth is zero-based: 0 means one connection hop from startWith. Use maxDepth: 1 for direct reports plus their reports (two hops).
Each doc in allReports gets a "level" field
level 0 = direct reports
level 1 = reports of reports
…
How It Works
depthField adds a numeric depth to every document in the result array—handy for indented UI trees or grouping by org tier without post-processing in your app.
Example 5 — Active employees only (restrictSearchWithMatch)
activeReportNames: 5 names (Grace skipped)
restrictSearchWithMatch applies during each hop
How It Works
restrictSearchWithMatch filters candidates in the from collection at every recursion step—like an inline $match scoped to the graph search. Swap field names for comment threads (connectFromField: "_id", connectToField: "parentId") or category trees.
Compare
📋 $graphLookup vs related join stages
Stage
Effect
Best for
$graphLookup
Recursive multi-hop search on a graph
Org charts, threads, deep hierarchies
$lookup
Single-level join (like SQL LEFT JOIN)
Orders → customer, post → author
$match + app loop
Manual recursion in application code
Very deep trees where DB recursion is too costly
Materialized path
Store full path string on each doc
Read-heavy trees; prefix queries on path field
$unionWith
Combine collections in one pipeline
Not a substitute—different use case
🧠 How $graphLookup Works
1
Evaluate startWith
MongoDB reads startWith from each input document—that value becomes the first search key.
Seed
2
Find connected docs
Search from where connectToField equals the current key. Apply restrictSearchWithMatch if set.
Match
3
Recurse
Read connectFromField from each match and repeat until no new docs or maxDepth is hit.
Loop
=
🕸
Array on input doc
All discovered documents land in the as array—optionally tagged with depthField.
Important
📝 Notes
$graphLookup can search the same collection (self-referential) or a different one via from.
Index connectToField in the from collection—each hop is an equality lookup on that field.
Without maxDepth, recursion continues until the graph ends—risky on large or cyclic data (cycles are handled; duplicates are not re-added).
depthField values start at 0 for documents directly connected to startWith.
Results are a flat array, not nested—use depthField or post-process to build a tree UI.
$graphLookup brings recursive graph traversal into aggregation: set startWith, wire connectFromField and connectToField, name your output with as, and optionally cap depth or filter matches.
Reach for $lookup when one join level is enough. Use $graphLookup when the relationship chain goes deeper. Next: $group for bucket aggregation.
Index connectToField (and often connectFromField) in the from collection
Use $match before $graphLookup to reduce input documents
Set maxDepth when the UI only needs N levels of hierarchy
Use depthField for level-aware sorting or indentation
Apply restrictSearchWithMatch to exclude inactive or archived nodes
❌ Don’t
Use $graphLookup for simple one-level joins—use $lookup instead
Run unlimited recursion on very large graphs without testing performance
Swap connectFromField and connectToField—direction matters
Expect nested tree JSON—the output is a flat array unless you reshape it
Forget that maxDepth: 0 still returns directly connected documents
Summary
Key Takeaways
Knowledge Unlocked
Five things to remember about $graphLookup
Use these points whenever you traverse hierarchies in aggregation.
5
Core concepts
🕸01
Recursive
Many hops.
Basics
🔗02
connect fields
Define edges.
Syntax
📈03
maxDepth
Cap levels.
Control
📚04
depthField
Level labels.
Output
🔍05
vs $lookup
One vs many.
Compare
❓ Frequently Asked Questions
$graphLookup recursively searches a collection and returns all connected documents as an array. It walks a graph or tree—following links from one document to the next—ideal for org charts, comment threads, bill-of-materials, and category trees.
$lookup joins one level: each input document matches related documents once. $graphLookup keeps searching: it uses values from matched documents to find more matches, recursively, until no new connections exist or maxDepth is reached.
connectToField is the field in the from collection that must equal the current search value (e.g. reportsTo). connectFromField is read from each match and becomes the next search value (e.g. _id). MongoDB repeats: find docs where connectToField equals the previous connectFromField value.
maxDepth limits how many recursive hops $graphLookup performs. maxDepth: 1 returns direct connections only; maxDepth: 2 includes grandchildren. Omit maxDepth for unlimited depth (use carefully on large graphs).
Yes. The from option names the collection to traverse—often the same collection (self-referential org chart) but it can be any collection where connectFromField and connectToField define the relationship.
Index connectToField in the from collection, use maxDepth when you only need N levels, add restrictSearchWithMatch to narrow candidates, and $match early to reduce input documents before $graphLookup runs.
Did you know?
$graphLookup was introduced in MongoDB 3.4. The depthField option adds a zero-based level to each matched document—level 0 is the first hop from startWith. For very wide trees, consider storing a materialized path or using application-side batching. See the official $graphLookup docs.