Group 2 objects into 1 object and merge the keys - mongodb

I am trying to group 2 objects in my MongoDB documents into a single object with the keys merged. What I have is:
{
"_id": ObjectId("..."),
"object_a": { "keyA": 1, "keyB": "valueB" }
"object_b": { "keyC": 2 }
}
And what I try to get is the following:
{
"_id": ObjectId("..."),
"object_a": { "keyA": 1, "keyB": "valueB", "keyC": 2 }
}
I can assert that there is no key conflict
I don't know the key names in advance
I tried with the $addFields operator but this will nest object_b inside object_a, not merge the keys.
{
"$addFields":
{
"object_a": "$object_b"
}
}
In other words, I am looking for a $setUnion but for objects.

You can use $mergeObjects in 3.6.
{"$addFields":{"object_a": {"$mergeObjects": ["$object_a", "$object_b"]}}}
For 3.4 you can use $arrayToObject and $objectToArray to merge keys.
{"$addFields":{"object_a": { "$arrayToObject": {"$setUnion": [{"$objectToArray": "$object_a"},{"$objectToArray": "$object_b"}]}}}}

Here is an improved version for Mongo 3.4.4+ that handles null values. It is based on the solution provided by #Veeram
function $mergeObjects(...items) {
return {
$arrayToObject: {
$setUnion: items.map(_ => (
{$filter: {input: {$objectToArray: _}, cond: {$ne: ["$$this.v", null]}}}
))
}
}
}

Related

Is there a way to give order field to the result of MongoDB aggregation?

Is there any way to give order or rankings to MongoDB aggregation results?
My result is:
{
"score":100
"name": "John"
},
{
"score":80
"name": "Jane"
},
{
"score":60
"name": "Lee"
}
My wanted result is:
{
"score":100
"name": "John",
"rank": 1
},
{
"score":80
"name": "Jane"
"rank": 2
},
{
"score":60
"name": "Lee"
"rank": 3
}
I know there is a operator called $includeArrayIndex but this only works with $unwind operator.
Is there any way to give rank without using $unwind?
Using $unwind requires grouping on my collection, and I'm afraid grouping pipeline would be too huge to process.
The other way is to use $map and add rank in document using its index, and don't use $unwind stage because it would be single field array you can directly access using its key name as mention in last line of code,
$group by null and make array of documents in root array,
$map to iterate loop of root array, get the index of current object from root array using $indexOfArray and increment that returned index number using $add because index start from 0, and that is how we are creating rank field, merge object with current element object and rank field using $mergeObjects
let result = await db.collection.aggregate([
{
$group: {
_id: null,
root: {
$push: "$$ROOT"
}
}
},
{
$project: {
_id: 0,
root: {
$map: {
input: "$root",
in: {
$mergeObjects: [
"$$this",
{
rank: { $add: [{ $indexOfArray: ["$root", "$$this"] }, 1] }
}
]
}
}
}
}
}
]);
// you can access result using root key
let finalResult = result[0]['root'];
Playground

how to project fields using another field's value in mongo db?

I have a mongo document like this:
{"_id": {"$oid":"xx"} ,"start": "a", "elements": {"a":"large object", "b": "large object"}
My expected query result is to project only the start element, in this case, it is {"elements.a:"large object"}. But with the value of "start" unknow before the query, I don't know how to write the query.
2 undesirable alternatives:
One way I could figure is to query start once with _id, and project for start to get "a", and another for elements.a。(
Another way is query all, and get the start element in code. But I don't want to query all at once for the document may be very large)
You can make use of $objectToArray, $arrayToObject and $filter operators.
The below query will be helpful:
db.collection.aggregate([
{
$project: {
elements: {
$arrayToObject: {
$filter: {
input: {
$objectToArray: "$elements"
},
as: "e",
cond: {
$eq: [
"$$e.k",
"$start"
]
}
}
}
}
}
}
])
Output:
[
{
"_id": 1,
"elements": {
"a": "large object"
}
}
]
MongoPlayGroundLink
I hope, this is what you want.

How to automatically add all fields except one when doing aggregation?

I have a mongodb collection which contains objects which have multiple properties (possibly a lot). One of this is an array of another object type, and this type has a boolean property StateChanged.
I want to make a query returning all the records from this collection, and filter the array to only get documents with StateChanged = true.
Here is what I already did:
db.getCollection('Cycles').aggregate([
{
$project: {
_id: 0,
// Here I could add Field1: 1, Field2: 1,...
'Subcycles' : {
$filter : {
input: '$Subcycles',
as : 'sub',
cond: { $eq: ['$$sub.StateChanged',true]}
}
}
}
}
])
However this only brings me the "Subcycles" collection.
What I want is to include other fields in the root document.
I could specify them manually in the projection (like Field1: 1, Field2: 1,...), but as there can be a lot of fields, I was wondering if there exists a way to bring them all automatically.
Thanks in advance !
You can use $addFields instead of $project. It will automatically replace the new field with the existing field.
db.getCollection("Cycles").aggregate([
{ "$addFields": {
"Subcycles": {
"$filter": {
"input": "$Subcycles",
"as": "sub",
"cond": { "$eq": ["$$sub.StateChanged", true] }
}
}
}},
{ "$project" : { "_id": 0 }}
])
You can use $addFields and then use $project to exclude _id field:
db.getCollection('Cycles').aggregate([
{
$addFields: {
'Subcycles' : {
$filter : {
input: '$Subcycles',
as : 'sub',
cond: { $eq: ['$$sub.StateChanged',true]}
}
}
}
},
{
$project: {
_id: 0
}
}
])

MongoDB - Array of object filter with condition

I have a json file that look like this in my collection :
[
{
"change":"00001",
"patchset":"4"
},
//etc
]
Two different object can have the same "change" properties.
So first I want to group them by "change" properties and inside this group I want the highest value of the "patchset" properties. I have managed to do this easily with this command
db.collections.aggregate([{$group:{_id:"$change",patchset_max:{$max:"$patchset"}}}])
but then, and this is where I lost it, with this max patchset, I want to get all the objects where object.patchset = max_patchset but still in the group array.
I tried with $filter and $match and then nested $group but nothing works,
Do you guys have any idea ?
Thanks
You need to use $$ROOT which is a special variable that represents whole document to get all the items for each group and then you can use $addFields to overwrite existing array and $filter to get only those docs that have patchset equal to patchset_max. Try:
db.collection.aggregate([
{
$group: {
_id: "$change",
patchset_max:{$max:"$patchset"},
docs: { $push: "$$ROOT" }
}
},
{
$addFields: {
docs: {
$filter: {
input: "$docs",
as: "doc",
cond: {
$eq: [ "$patchset_max", "$$doc.patchset" ]
}
}
}
}
}
])
Sample playground here
EDIT Answer above is correct, but if on Mongo 3.2, here's an alternative
db.collection.aggregate([{
$group: { _id: "$change", patchset: { $push: "$$ROOT" }, patchset_max:{ $max:"$patchset" } }
},{
$project: {
patchset: {
$filter: {
input: '$patchset',
as: 'ps',
cond: { $eq: ['$$ps.patchset', '$patchset_max'] }
}
}
}
}])

Encapsulate/Transform an Array into an Array of Objects

I am currently in the process of modifying a schema and I need to do a relatively trivial transform using the aggregation framework and a bulkWrite.
I want to be able to take this array:
{
...,
"images" : [
"http://example.com/...",
"http://example.com/...",
"http://example.com/..."
]
}
and aggregate to a similar array where the original value is encapsulated:
{
...,
"images" : [
{url: "http://example.com/..."},
{url: "http://example.com/..."},
{url: "http://example.com/..."}
]
}
This slow query works, but it is ridiculously expensive to unwind an entire collection.
[
{
$match: {}
},
{
$unwind: {
path : "$images",
}
},
{
$group: {
_id: "$_id",
images_2: {$addToSet: {url: "$images"}}
}
},
]
How can this be achieved with project or some other cheaper aggregation?
$map expression should do the job, try this:
db.col.aggregate([
{
$project: {
images: {
$map: {
input: '$images',
as: 'url',
in: {
url: '$$url'
}
}
}
}
}
]);
You don't need to use the bulkWrite() method for this.
You can use the $map aggregation array operator to apply an expression to each element element in your array.
Here, the expression simply create a new object where the value is the item in the array.
let mapExpr = {
"$map": {
"input": "$images",
"as": "imageUrl",
"in": { "url": "$$imageUrl }
}
};
Finally you can use the $out aggregation pipeline operator to overwrite your collection or write the result into a different collection.
Of course $map is not an aggregation pipeline operator so which means that the $map expression must be use in a pipeline stage.
The way you do this depends on your MongoDB version.
The best way is in MongoDB 3.4 using $addFields to change the value of the "images" field in your document.
db.collection.aggregate([
{ "$addFields": { "images": mapExpr }},
{ "$out": "collection }
])
From MongoDB 3.2 backwards, you need to use the $project pipeline stage but you also need to include all the other fields manually in your document
db.collection.aggregate([
{ "$project": { "images": mapExpr } },
{ "$out": "collection }
])