$lookup is MongoDB 1,815 's left outer join. For every input document it searches another collection and appends the matches as an array field — always an array, even when one document matches, and empty when none does.
{ $lookup: { from: 'customers', localField: 'customerId', foreignField: '_id', as: 'customer' } } run over order o-1003 appends the one matching customer. For a many-to-one join, follow it with { $set: { customer: { $first: '$customer' } } } to flatten that one-element array into a subdocument.

localField/foreignField tests equality only. Add a pipeline and the join runs a full aggregation against the foreign collection, which is how you fetch the two most recent delivered orders rather than all of them.
db.customers.aggregate([
{ $match: { _id: 'c1' } },
{ $lookup: { from: 'orders', localField: '_id', foreignField: 'customerId', as: 'recent',
pipeline: [{ $match: { status: 'delivered' } }, { $sort: { placedAt: -1 } },
{ $limit: 2 }, { $project: { _id: 1, shipping: 1 } }] } },
{ $project: { name: 1, recent: 1 } }
])[
{
_id: 'c1',
name: 'Amara Okafor',
recent: [ { _id: 'o-1009', shipping: 0 }, { _id: 'o-1003', shipping: 0 } ]
}
]For joins that are not equality, drop localField and foreignField and use let: let: { sku: '$items.sku' } with { $match: { $expr: { $eq: ['$_id', '$$sku'] } } } in the sub-pipeline. Inside that pipeline a bare $field means the foreign document, which is why the input values must arrive as $$ variables. $graphLookup follows a reference recursively; $unionWith concatenates another collection's pipeline output onto the current stream.