Below is the sample data on MongoDB version 3.5.7
db.testcol.find();
{
"data" : [
[
{
"movie" : "starwars",
"showday" : "monday"
},
{
"movie" : "batman",
"showday" : "thursday"
},
{
"movie" : "sleepless",
"showday" : "tuesday"
}
],
[
{
"actor" : "angelina",
"location" : "new york"
},
{
"actor" : "jamie",
"location" : "california"
},
{
"actor" : "mcavoy",
"location" : "arizona"
}
]
]
}
This is the working javascript implementation.
function cartesianProduct(arr)
{
return arr.reduce(function(a,b){
return a.map(function(x){
return b.map(function(y){
return x.concat(y);
})
}).reduce(function(a,b){ return a.concat(b) },[])
}, [[]])
}
The closest MongoDB aggregation code for above js implementation which outputs empty array.
db.testcol.aggregate({
$project: {
cp: {
$reduce: {
input: "$data",
initialValue: [],
in: {
$let: {
vars: {
currentr: "$$this",
currenta: "$$value"
},
in:{
$reduce: {
input: {
$map: {
input: "$$currenta",
as: "a",
in: {
$map: {
input: "$$currentr",
as: "r",
in: {
$mergeObjects: ["$$a", "$$r"]
}
}
}
}
},
initialValue: [],
in: {
$concatArrays: ["$$value", "$$this"]
}
}
}
}
}
}
}
}
});
Output:
{ "_id" : ObjectId("592cce1cc23b82abed7f7b97"), "cp" : [ ] }
I have narrowed down the reason being specification for reduce function in js vs MongoDB.
If no initialValue is provided, then accumulator will be equal to the first value in the array, and currentValue will be equal to the second.
The MongoDB behavior is not mentioned in the docs (may be because it is not a special condition), but it looks like the $$value is not set and $$this will be first value in the array.
Can you explain if this is indeed the case ?
Okay now to accommodate the difference in specification, I have set the initial value to the first element in the array and added a conditional expression to check accumulator value ($$value) is same as current value ($$this) and return.
Can you advise if this is right way to go about it ?
Something like
db.testcol.aggregate({
$project: {
cp: {
$reduce: {
input: "$data",
initialValue: {
$arrayElemAt: ["$data", 0] // Set the initial value to the first element of the arrays.
},
in: {
$let: {
vars: {
currentr: "$$this", // Current processing element
currenta: "$$value" // Current accumulated value
},
in: {
$cond: [{ // Conditional expression to return the accumulated value as initial value for first element
$eq: ["$$currentr", "$$currenta"]
},
"$$currenta",
{ // From second element onwards prepare the cartesian product
$reduce: {
input: {
$map: {
input: "$$currenta",
as: "a",
in: {
$map: {
input: "$$currentr",
as: "r",
in: {
$mergeObjects: ["$$a", "$$r"] // Merge accumulated value with the current processing element
}
}
}
}
},
initialValue: [],
in: {
$concatArrays: ["$$value", "$$this"] // Reduce the merged values which will be used as accumulator for next element
}
}
}]
}
}
}
}
}
}
});
Other implementation is to use $setUnions instead of $cond & $concatArrays.
db.testcol.aggregate({
$project: {
cp: {
$reduce: {
input: "$data",
initialValue: {
$arrayElemAt: ["$data", 0]
},
in: {
$let: {
vars: {
currentr: "$$this",
currenta: "$$value"
},
in:{
$reduce: {
input: {
$map: {
input: "$$currenta",
as: "a",
in: {
$map: {
input: "$$currentr",
as: "r",
in: {
$mergeObjects: ["$$a", "$$r"]
}
}
}
}
},
initialValue: [],
in: {
$setUnion: ["$$value", "$$this"]
}
}
}
}
}
}
}
}
});
Appreciate the reply. Let me know if you have need more details.
More details can be found here
Thanks,
Shiv