5

In a MongoDB/NodeJS project, I've ordered elements of my collection like a family tree. Within a particular route, I'm trying to get the parent of an element, but also parent's siblings, with a given depth as shown in the image below :

Collection tree, items wanted through the request

In the collection, for each item, I store, among other data :

  • parentId
  • grandParentId
  • isRoot (boolean)

I've tried to do something with GraphLookup, basing my request on linking parentId to grandParentId, like this :

db.arguments.aggregate([
    {$match: { _id: mongoose.Types.ObjectId(id) }},
    ,
    {$graphLookup: {
        from: 'arguments',
        startWith: '$grandParentId',
        connectFromField: 'grandParentId',
        connectToField: 'parentId',
        maxDepth: Number(parentsDepth),
        as: 'parentsHierarchy',
        depthField: 'depth',
        restrictSearchWithMatch: { isDeleted: false }
    }}
])

It works pretty well, but the problem is that it cannot retrieve root element, which have no parentId. I've thought about doing two separate views containing each one a GraphLookup (one based on parentId/grandParentId, the other on id/parentId), then merging both views while deleting duplicates, but it looks weird to perform two potentially big requests in order to get just the root element.

I would like to find a reliable solution as I plan to allow an item to have multiple parents.

x00
  • 13,643
  • 3
  • 16
  • 40
Goupil
  • 51
  • 5

1 Answers1

2

You can change it to be in multiple steps:

  1. Find the array of direct ascendents (parent, grand-parent, etc.)
  2. Find immediate descendents of each grand-parent and "higher" (thus giving you parent siblings, grand-parent siblings etc.)
  3. Merge the two arrays into a single set (thus ensuring uniqueness)
db.arguments.aggregate([
    {$match: { _id: mongoose.Types.ObjectId(id) }},
    {$graphLookup: {
        from: 'arguments',
        startWith: '$parentId',
        connectFromField: 'parentId',
        connectToField: '_id',
        maxDepth: Number(parentsDepth),
        as: 'parentsHierarchy',
        depthField: 'depth',
        restrictSearchWithMatch: { isDeleted: false }
    }},
    {$unwind: "$parentsHierarchy"},
    {$lookup: {
        from: 'arguments',
        let: { id: '$parentsHierarchy._id', depth: '$parentsHierarchy.depth' },
        pipeline: [
        {$match:{$expr:{
            $and: [{
                $eq: ['$$id', '$parentId']
            },{
                $gte: ["$$depth", 2]
            }]
        }}},
        {$addFields:{
            depth: {$sum: ["$$depth", -1]}
        }}],
        as: 'children'
    }},
    {$group:{
        _id: "$_id",
        parentsHierarchy: {$addToSet: "$parentsHierarchy"},
        children: {$push: "$children"}
        // The rest of your root fields will have to be added here (someField: {$first: "$someField"})
    }},
    {$addFields:{
        hierarchy: {$setUnion: ["$children", "$parentsHierarchy"]}
    }}
])

See How to push multiple columns' value within group regarding the $setUnion.

Original Answer:

If you are only interested in the parent and the parent's siblings, you can use a $lookup stage instead of $graphLookup, since you don't need the recursion the graph gives you.

Your $lookup could be done like this:

db.test.aggregate([
    {$lookup: {
        from: 'arguments',
        let: { parentId: '$parentId', grandParentId: '$grandParentId' },
        pipeline: [{$match:{$expr:{
            $or: [{
                $eq: ['$_id', '$$parentId']
            },{
                $eq: [{$ifNull: ['$parentId', 'xyz']}, '$$grandParentId']
            }]
        }}}],
        as: 'parentsAndTheirSiblings'
    }}
])

This way your root element should still be found by the first part of the $match in the pipeline.

Note that I am using $ifNull in the second part to filter out "root" elements, since the $parentId and $$grandparentId will be null or undefined when looking for an element at depth 1. If the intended behaviour is that all root elements should be found for any depth 1 element (if root elements are considered siblings), then you can get rid of it and simply compare $parentId and $$grandparentId straight up.

Docs for lookup: https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/

abondoa
  • 1,613
  • 13
  • 23
  • Thanks ! It is working but actually I would like to be able to chose a depth level, which means I could need recursion. Sometimes I would need parents + parents siblings but also grandparent grandparents siblings – Goupil Apr 14 '20 at 10:07
  • I see. Then I think you are forced to have either two separate queries or having two `$graphLookup` stages (one for parentId/grandParentId and one for id/parentId). Alternatively, you might be able to model your data differently to accomodate the root node. Maybe having the root node point to itself as parent and having its children point to the root node as grandParent will solve it, but I don't know if that will cause any other side-effects. – abondoa Apr 15 '20 at 11:01
  • Have an idea that might work for you, please see the updated answer. If you have some sample data available, I might be able to give a more complete answer – abondoa Apr 15 '20 at 11:37
  • That's great, thanks a lot I'm gonna try all of this – Goupil Apr 15 '20 at 12:49