Project first item in an array to new field (MongoDB aggregation) - mongodb

I am using Mongoose aggregation (MongoDB version 3.2).
I have a field users which is an array. I want to $project first item in this array to a new field user.
I tried
{ $project: {
user: '$users[0]',
otherField: 1
}},
{ $project: {
user: '$users.0',
otherField: 1
}},
{ $project: {
user: { $first: '$users'},
otherField: 1
}},
But neither works.
How can I do it correctly? Thanks

Update:
Starting from v4.4 there is a dedicated operator $first:
{ $project: {
user: { $first: "$users" },
otherField: 1
}},
It's a syntax sugar to the
Original answer:
You can use arrayElemAt:
{ $project: {
user: { $arrayElemAt: [ "$users", 0 ] },
otherField: 1
}},

If it is an array of objects and you want to use just single object field, ie:
{
"users": [
{name: "John", surname: "Smith"},
{name: "Elon", surname: "Gates"}
]
}
you can use:
{ $project: { user: { $first: "$users.name" } }
Edit (exclude case - after comment from #haytham)
In order to exclude a single field from a nested document in array you have to do 2 projections:
{ $project: { user: { $first: "$users" } }
Which return whole first object, and then exclude field you do not want, ie:
{ $project: { "user.name" : 0 }

Starting Mongo 4.4, the aggregation operator $first can be used to access the first element of an array:
// { "users": ["Jean", "Paul", "Jack"] }
// { "users": ["Claude"] }
db.collection.aggregate([
{ $project: { user: { $first: "$users" } } }
])
// { "user" : "Jean" }
// { "user" : "Claude" }

Related

mongodb - check a collection for mutual likes

I had a query that worked before when likes collection had users stored as from and to fields. The query below checked for the two fields and if like was true.
Since then I changed the way users are stored in the likes collection.
.collection('likes')
.aggregate([ {$match: {$and : [{$or: [{from: userId}, {to: userId}]}, {like: true}]}},
{$group: {
_id: 0,
from: {$addToSet: "$from"},
to: {$addToSet: "$to"},
}
},
{$project: {
_id: 0,
users: {
$filter: {
input: {$setIntersection: ["$from", "$to"]},
cond: {$ne: ["$$this", userId]}
}
}
}
},
This is how likes collection used to store data (and above query worked) in figuring out mutual likers for a userId passed in req.body
{
"_id": "xw5vk1s_PpJaal46di",
"from": "xw5vk1s",
"to": "PpJaal46di"
"like": true,
}
and now I changed users to an array.
{
"_id": "xw5vk1s_PpJaal46di",
"users": [
"xw5vk1s",//first element equivalent to from
"PpJaal46di"//equivalent to previously to
],
"like": true,
}
I am not sure how to modify the query to now check for array elements in users field now that from and to is not where the two users liking each other are stored.
For MongoDB v5.2+, you can use $sortArray to create a common key for the field users and $group to get the count. You will get mutual likes by count > 1.
db.collection.aggregate([
{
$match: {
users: "xw5vk1s"
}
},
{
$group: {
_id: {
$sortArray: {
input: "$users",
sortBy: 1
}
},
count: {
$sum: 1
}
}
},
{
"$match": {
count: {
$gt: 1
}
}
}
])
Mongo Playground
Prior to MongoDB v5.2, you can create sorted key for grouping through $unwind and $sort then $push back the users in $group.
db.collection.aggregate([
{
$match: {
users: "xw5vk1s"
}
},
{
"$unwind": "$users"
},
{
$sort: {
users: 1
}
},
{
$group: {
_id: "$_id",
users: {
$push: "$users"
},
like: {
$first: "$like"
}
}
},
{
"$group": {
"_id": "$users",
"count": {
"$sum": 1
}
}
},
{
"$match": {
count: {
$gt: 1
}
}
}
])
Mongo Playground

clone and rename a field of an array of subdocuments in Mongo

I've got a collection like this:
{
name: "A Name",
answers: [
{order: 1},
{order: 2},
{order: 3}
]
}
What I want to do is to add a new filed id to each element of the answers array based on the value of the order property - I want just to clone it, so the output is
{
name: "A Name",
answers: [
{order: 1, id: 1},
{order: 2, id: 2},
{order: 3, id: 3}
]
}
I looked at this post and this one too, but I don't know how to combine them to work properly for subdocuments.
In MongoDB documentation for the aggregate method, I found a simple example of how to update embedded documents here, but I have no idea how to use the order property instead of a fixed term. The following tries seem not to work as I need:
db.collection.aggregate([
{
$addFields: {
"answers.id": "answers.$.order"
}
}
])
db.collection.aggregate([
{
$addFields: {
"answers.id": "$answers.order"
}
}
])
Is it possible to achieve the expected result with the `aggregate method?
Demo - https://mongoplayground.net/p/M79MV6-Zp4C
db.collection.aggregate([
{
$set: {
answers: {
$map: {
input: "$answers",
as: "answer",
in: { $mergeObjects: [ "$$answer", { id: "$$answer.order" } ]
}
}
}
}
}
])
Updated Demo - https://mongoplayground.net/p/iyqIPGQ5-ld
db.collection.aggregate([
{ $unwind: "$answers" },
{
$group: {
_id: "$_id",
answers: { $push: { order: "$answers.order", id: "$answers.order" } },
name: { $first: "$name" } // preserve properties add them to the group pick 1st value
}
}
])
Demo - https://mongoplayground.net/p/Ln5CcmT-Kkm
db.collection.aggregate([
{ $unwind: "$answers" }, // break into individuals documents
{ $addFields: { "answers.id": "$answers.order" } }, // copy order value to id
{ $group: { _id: "$_id", answers: { $push: "$answers" } } } // join and group it back
])
If you want to sort n get id from index
Demo - https://mongoplayground.net/p/6U_sRYWHtDR
db.collection.aggregate([
{ $sort: { "answers.order": 1 } },
{ $unwind: { path: "$answers", includeArrayIndex: "index" } },
{ $group: { _id: "$_id", answers: { "$push": { order: "$answers.order", id: { $add: [ "$index", 1 ] } } } } }
])

mongodb: sort nested array using dynamic parmeter of field name

Assume in an aggregation pipeline one of the steps produces the following results:
{
customer: "WN",
sort_category: "category_a",
locations: [
{
city: "Elkana",
category_a: 11904.0,
category_b: 74.0,
category_c: 657.0,
},
{
city: "Haifa",
category_a: 20.0,
category_b: 841.0,
category_c: 0,
},
{
city" : "Jerusalem",
category_a: 451.0,
category_b: 45.0,
category_c: 712.0,
}
]
}
{
...
}
The next step is to sort the list of the nested objects of each document in the collection.
The list of the nested objects should be sorted by dynamic parameter containing the field name.
For example - the list of locations should be sorted by the value of category_a.
category_a is parmeter given in sort_category field.
Here is a solution without use $function:
db.tests.aggregate([
{$unwind:"$locations"},
{$project: {
_id: 1,
customer: 1,
sort_category: 1,
locations: 1,
locationsKV:{$objectToArray:"$locations"}
}
},
{$unwind:"$locationsKV"},
{$project:{
_id: 1,
customer: 1,
sort_category: 1,
locations: 1,
locationsKV: 1,
category: {
$cond:[{$eq: ["$sort_category","$locationsKV.k"]},
"$locationsKV.v", 0]},
agg: {
$cond: [{$eq: ["$sort_category","$locationsKV.k"]}, true, false]
}
}
},
{$match: {agg: true}},
{$sort: {category: 1}},
{$group: {
_id: "$_id",
customer: {$first: "$customer"},
sort_category: {$first: "$sort_category"},
locations: {$push: "$locations"}
}
},
{$project:{ _id: 0}}
])
You can try custom way,
$addFields will add one copy field named category of key sort_category inside every object in locations array using $map and $reduce
$unwind deconstruct locations array
$sort by category field that we have added in locations array
$project to remove category field
$group by _id and reconstruct locations array
$project to remove _id field
db.collection.aggregate([
{
$addFields: {
locations: {
$map: {
input: "$locations",
as: "l",
in: {
$mergeObjects: [
"$$l",
{
category: {
$reduce: {
input: { $objectToArray: "$$l" },
initialValue: null,
in: {
$cond: [{ $eq: ["$$this.k", "$sort_category"] }, "$$this.v", "$$value"]
}
}
}
}
]
}
}
}
}
},
{ $unwind: "$locations" },
{ $sort: { "locations.category": 1 } },
{ $project: { "locations.category": 0 } },
{
$group: {
_id: "$_id",
customer: { $first: "$customer" },
sort_category: { $first: "$sort_category" },
locations: { $push: "$locations" }
}
},
{ $project: { _id: 0 } }
])
Playground
If you are planing to upgrade your MongoDB to v4.4 or also this will helpful for others, The $function is a option for custom operation and user defined operation.
There are 3 properties:
body The function definition. You can specify the function definition as either BSON type Code or String, define our own function using function(){, we have passed locations array and sort_category that is dynamic field, the function logic is sort by descending order
args Arguments passed to the function body
lang The language used in the body. You must specify lang: "js"
db.collection.aggregate([
{
$addFields: {
locations: {
$function: {
body: function(locations, sort_category){
return locations.sort(function(a, b){
// DESCENDING ORDER
return b[sort_category] - a[sort_category]
// ASCENDING ORDER
// return a[sort_category] - b[sort_category]
})
},
args: ["$locations", "$sort_category"],
lang: "js"
}
}
}
}
])
For more guidelines about restrictions and considerations Refer.

How do you convert an array of ObjectIds into an array of embedded documents with a field containing the original array element value

I have a collection of documents where one of the fields is currently an array of ObjectId items.
{
_id: ObjectId(...),
user: "jdoe",
docs: [
ObjectId(1),
ObjectId(2),
...
]
}
{
_id: ObjectId(...),
user: "jsmith",
docs: [
ObjectId(3),
ObjectId(4),
...
]
}
How can I update all of the documents in my collection to convert the docs field into an array of objects that contain a "docID" field equal to the original element value?
For example, I'd want my documents to end up looking like:
{
_id: ObjectId(...),
user: "jdoe",
docs: [
{ docID: ObjectId(1) },
{ docID: ObjectId(2) },
...
]
}
{
_id: ObjectId(...),
user: "jsmith",
docs: [
{ docID: ObjectId(3)},
{ docID: ObjectId(4)},
...
]
}
I'm hoping there is a command that I can run from the shell such as:
db.getCollection('myCollection').update(
{},
{
$set: {
'docs.$[]: { docID: '$$VALUE'}
}
},
{multi: true }
);
But I can't figure out how to reference the original value of the element.
Update:
I'm marking #mickl with the correct answer since it got me on the correct track. Below is the final aggregate that I ended up with which only changes the docs field if it is an array of object IDs, otherwise the existing value is left as-is, including documents that don't have a docs field.
db.getCollection('myCollection').aggregate([
{ $addFields: {
'docs': { $cond: {
if : { $eq: [{ $type: { $arrayElemAt: [ '$docs', 0]} }, "objectId"]},
then: { $map: {
input: '$docs',
in: { tocID: '$$this'}
}},
else : '$docs'
}}
}},
{ $out: "myCollection" }
])
You can use $map to reshape your data and $out to replace existing collection with aggregation result:
db.col.aggregate([
{
$addFields: {
docs: {
$map: {
input: "$docs",
in: { docID: "$$this" }
}
}
}
},
{ $out: "col" }
])

Retrieving a count that matches specified criteria in a $group aggregation

So I am looking to group documents in my collection on a specific field, and for the output results of each group, I am looking to include the following:
A count of all documents in the group that match a specific query (i.e. a count of documents that satisfy some expression { "$Property": "Value" })
The total number of documents in the group
(Bonus, as I suspect that this is not easily accomplished) Properties of a document that correspond to a $min/$max accumulator
I am very new to the syntax used to query in mongo and don't quite understand how it all works, but after some research, I've managed to get it down to the following query (please note, I am currently using version 3.0.12 for my mongo db, but I believe we will upgrade in a couple of months time):
db.getCollection('myCollection').aggregate(
[
{
$group: {
_id: {
GroupID: "$GroupID",
Status: "$Status"
},
total: { $sum: 1 },
GroupName: { $first: "$GroupName" },
EarliestCreatedDate: { $min: "$DateCreated" },
LastModifiedDate: { $max: "$LastModifiedDate" }
}
},
{
$group: {
_id: "$_id.GroupID",
Statuses: {
$push: {
Status: "$_id.Status",
Count: "$total"
}
},
TotalCount: { $sum: "$total" },
GroupName: { $first: "$GroupName" },
EarliestCreatedDate: { $min: "$EarliestCreatedDate" },
LastModifiedDate: { $max: "$LastModifiedDate" }
}
}
]
)
Essentially what I am looking to retrieve is the Count for specific Status values, and project them into one final result document that looks like the following:
{
GroupName,
EarliestCreatedDate,
EarliestCreatedBy,
LastModifiedDate,
LastModifiedBy,
TotalCount,
PendingCount,
ClosedCount
}
Where PendingCount and ClosedCount are the total number of documents in each group that have a status Pending/Closed. I suspect I need to use $project with some other expression to extract this value, but I don't really understand the aggregation pipeline well enough to figure this out.
Also the EarliestCreatedBy and LastModifiedBy are the users who created/modified the document(s) corresponding to the EarliestCreatedDate and LastModifiedDate respectively. As I mentioned, I think retrieving these values will add another layer of complexity, so if there is no practical solution, I am willing to forgo this requirement.
Any suggestions/tips would be very much appreciated.
You can try below aggregation stages.
$group
Calculate all the necessary counts TotalCount, PendingCount and ClosedCount for each GroupID
Calculate $min and $max for EarliestCreatedDate and LastModifiedDate respectively and push all the fields to CreatedByLastModifiedBy to be compared later for fetching EarliestCreatedBy and LastModifiedBy for each GroupID
$project
Project all the fields for response
$filter the EarliestCreatedDate value against the data in the CreatedByLastModifiedBy and $map the matching CreatedBy to the EarliestCreatedBy and $arrayElemAt to convert the array to object.
Similar steps for calculating LastModifiedBy
db.getCollection('myCollection').aggregate(
[{
$group: {
_id: "$GroupID",
TotalCount: {
$sum: 1
},
PendingCount: {
$sum: {
$cond: {
if: {
$eq: ["Status", "Pending"]
},
then: 1,
else: 0
}
}
},
ClosedCount: {
$sum: {
$cond: {
if: {
$eq: ["Status", "Closed "]
},
then: 1,
else: 0
}
}
},
GroupName: {
$first: "$GroupName"
},
EarliestCreatedDate: {
$min: "$DateCreated"
},
LastModifiedDate: {
$max: "$LastModifiedDate"
},
CreatedByLastModifiedBy: {
$push: {
CreatedBy: "$CreatedBy",
LastModifiedBy: "$LastModifiedBy",
DateCreated: "$DateCreated",
LastModifiedDate: "$LastModifiedDate"
}
}
}
}, {
$project: {
_id: 0,
GroupName: 1,
EarliestCreatedDate: 1,
EarliestCreatedBy: {
$arrayElemAt: [{
$map: {
input: {
$filter: {
input: "$CreatedByLastModifiedBy",
as: "CrBy",
cond: {
"$eq": ["$EarliestCreatedDate", "$$CrBy.DateCreated"]
}
}
},
as: "EaCrBy",
in: {
"$$EaCrBy.CreatedBy"
}
}
}, 0]
},
LastModifiedDate: 1,
LastModifiedBy: {
$arrayElemAt: [{
$map: {
input: {
$filter: {
input: "$CreatedByLastModifiedBy",
as: "MoBy",
cond: {
"$eq": ["$LastModifiedDate", "$$MoBy.LastModifiedDate"]
}
}
},
as: "LaMoBy",
in: {
"$$LaMoBy.LastModifiedBy"
}
}
}, 0]
},
TotalCount: 1,
PendingCount: 1,
ClosedCount: 1
}
}]
)
Update for Version < 3.2
$filter is also not available in your version. Below is the equivalent.
The comparison logic is the same and creates an array with for every non matching entry the value of false or LastModifiedBy otherwise.
Next step is to use $setDifference to compare the previous array values with array [false] which returns the elements that only exist in the first set.
LastModifiedBy: {
$setDifference: [{
$map: {
input: "$CreatedByLastModifiedBy",
as: "MoBy",
in: {
$cond: [{
$eq: ["$LastModifiedDate", "$$MoBy.LastModifiedDate"]
},
"$$MoBy.LastModifiedBy",
false
]
}
}
},
[false]
]
}
Add $unwind stage after $project stage to change to object
{$unwind:"$LastModifiedBy"}
Similar steps for calculating EarliestCreatedBy