Danny Buonocore
Danny Buonocore

Reputation: 3777

Get max from unwound arrays

I have a collection of documents where I want to find the maximum values of each of the ratios of every possible pair of fields in the data object. For example:

Documents:

[
    { data: { a: 1, b: 5, c: 2 } },
    { data: { a: 4, b: 1, c: 1 } },
    { data: { a: 2, b: 4, c: 3 } }
]

Desired output:

{
    a: { a: 1, b: 4, c: 4   },
    b: { a: 5, b: 1, c: 2.5 },
    c: { a: 2, b: 1, c: 1   }
}

So the output a.b is the largest of the a:b ratios 1/5, 4/1, and 2/4.

So I figure I first use $objectToArray to convert data, then $unwind on the result, but I'm having a hard time figuring out how to group everything together. The number of documents I have won't be too large, but the number of keys in data can be in the low thousands, so I'm not sure how well Mongo will be able to handle doing a bunch of $lookup's and comparing the values like that.

Upvotes: 2

Views: 47

Answers (1)

mickl
mickl

Reputation: 49985

You can try following aggregation:

db.col.aggregate([
    {
        $addFields: { data: { $objectToArray: "$data" } }
    },
    {
        $project: {
            pairs: {
                $map: {
                    input: { $range: [ 0, { $multiply: [ { $size: "$data" }, { $size: "$data" } ] } ] },
                    as: "index",
                    in: {
                        $let: {
                            vars: {
                                leftIndex: { $floor: { $divide: [ "$$index", { $size: "$data" } ] } },
                                rightIndex: { $mod: [ "$$index", { $size: "$data" } ] }
                            },
                            in: {
                                l: { $arrayElemAt: [ "$data", "$$leftIndex" ] },
                                r: { $arrayElemAt: [ "$data", "$$rightIndex" ] }
                            }
                        }
                    }
                }
            }
        }
    },
    { $unwind: "$pairs" },
    {
        $group: {
            _id: { l: "$pairs.l.k", r: "$pairs.r.k" },
            value: { $max: { $divide: [ "$pairs.l.v", "$pairs.r.v" ]  } }
        }
    },
    {
        $sort: {
            "_id.l": 1, "_id.r": 1
        }
    },
    {
        $group: {
            _id: "$_id.l",
            values: { $push: { k: "$_id.r", v: "$value"  } }
        }
    },
    {
        $addFields: { values: { $arrayToObject: "$values" } }
    },
    {
        $project: {
            root: [ { k: "$_id", v: "$values" } ]
        }
    },
    {
        $sort: { "root.k": 1 }
    },
    {
        $replaceRoot: {
            newRoot: {
                $arrayToObject: "$root"
            }
        }
    }
])

Basically you need $objectToArray and $arrayToObject to transform between arrays and objects. Basically the point is that for each object you need to generate nxn pairs (3x3=9 in this case). You can perform such iteration using $range operator. Then using $mod and $divide with $floor you can get index pairs like (0,0)...(2,2). Then you just need $group with $max to get max values for each pair type (like a with b and so on). To get final shape you also need $replaceRoot.

Outputs:

{ "a" : { "a" : 1, "b" : 4, "c" : 4 } }
{ "b" : { "a" : 5, "b" : 1, "c" : 2.5 } }
{ "c" : { "a" : 2, "b" : 1, "c" : 1 } }

Upvotes: 1

Related Questions