Project nested array element to top level using MongoDB aggregation pipeline - mongodb

I have a groups collection with documents of the form
{
"_id": "g123"
...,
"invites": [
{
"senderAccountId": "a456",
"recipientAccountId": "a789"
},
...
]
}
I want to be able to list all the invites received by a user.
I thought of using an aggregation pipeline on the groups collection that filters all the groups to return only those to which the user has been invited to.
db.groups.aggregate([
{
$match: {
"invites.recipientAccountID": "<user-id>"
}
}
])
Lastly I want to project this array of groups to end up with an array of the form
[
{
"senderAccountId": "a...",
"recipientAccountId": "<user-id>",
"groupId": "g...", // Equal to "_id" field of document.
},
...
]
But I'm missing the "project" step in my aggregation pipeline to bring to the top-level the nested senderAccountId and recipientAccountId fields. I have seen examples online of projections in MongoDB queries and aggregation pipelines but I couldn't find examples for projecting the previously matched element of an array field of a document to the top-level.
I've thought of using Array Update Operators to reference the matched element but couldn't get any meaningful progress using this method.

There are multiple ways to do this, using a combination of unwind and project would work as well. Unwind will create one object for each and project let you choose how you want to structure your result with current variables.
db.collection.aggregate([
{
"$unwind": "$invites"
},
{
"$match": {
"invites.recipientAccountId": "a789"
}
},
{
"$project": {
recipientAccountId: "$invites.recipientAccountId",
senderAccountId: "$invites.senderAccountId",
groupId: "$_id",
_id: 0 // don't show _id key:value
}
}
])
You can also use nimrod serok's $replaceRoot in place of the $project one
{$replaceRoot: {newRoot: {$mergeObjects: ["$invites", {group: "$_id"}]}}}
playground
nimrod serok's solution might be a bit better because mine unwind it first and then matches it but I believe mine is more readable

I think what you want is $replaceRoot:
db.collection.aggregate([
{$match: {"invites.recipientAccountId": "a789"}},
{$set: {
invites: {$first: {
$filter: {
input: "$invites",
cond: {$eq: ["$$this.recipientAccountId", "a789"]}
}
}}
}},
{$replaceRoot: {newRoot: {$mergeObjects: ["$invites", {group: "$_id"}]}}}
])
See how it works on the playground example

Related

Removing item out of nested document array and while also accounting for null/empty document array

I'm new to mongodb and I've been working on this query for quite sometime. I've found solutions using "$project" and "$group" and "$match". Overall goal is if document within nested array "internal" attribute is false, remove it from the array.
$project and $group DO work BUT they then throw of the projection, I don't even see a current projection in this query but once I add in $project or $group it ONLY returns the specific nested document array I'm messing with.
$match won't work because I have cases where the parameter in question that I'm using to remove items from the nested document array is true or false or the array is empty, and $match in different use cases just doesn't return the main document.
Here's an example $group
{ '$unwind': '$notes' },
{
$group: {
_id: "$_id",
notes: {
$push: {
$cond: {
if: { $eq: [ "$notes.internal", false ] },
then: "$$REMOVE",
else: "$notes.internal"
}
}
}
}
You may be able to use $addFields with $filter:
{$addFields: {
notes: {$filter: {
input: "$notes",
as: "item",
cond: {$ne: [ "$$item.internal", false ]}
}}
}}

MongoDB Aggregation: Dedupe by array in subdocuments

I have an aggregation query which calculates records by tag combinations this query is working well however it has one issue which is that it duplicates documents for tag combinations that are in different orders e.g. i could have one document with the tags: ['one', 'two'] and a second document with ['two' 'one'] the rest of the data would be exactly the same.
My first thought would be to do a $group aggregation query and search how to order the arrays in a project query however i cannot find anywhere how to do this. I did see for update queries you can use '$push' however this feature doesnt seem to exist for $project queries.
an example document at this phase is something like this
_id: "sadasdsad"
tags: ['one', 'two'],
total_count:37,
second_count:14,
what would be the best approach to solving this issue?
You can sort your array using $unwind,$sort and finally $group so all your tags are the same before grouping. Example : https://mongoplayground.net/p/EZi04LfY1ff
However, I would try to store those tags already sorted. So you can avoid these steps.
db.collection.aggregate({
"$unwind": "$tag"
},
{
"$sort": {
key: 1,
tag: 1
}
},
{
"$group": {
"_id": "$key",
"tag": {
"$push": "$tag"
}
}
},
{
"$group": {
"_id": "$tag",
"field": {
"$push": "$$ROOT"
}
}
})

How to use two MongoDB aggregations to perform an updateMany

I am trying to write a script that uses 2 aggregates and saves the results as an array to be used for an updateMany.
The first aggregate finds any documents that has a firstTrackingId and a secondTrackingId on it. I save this into an array. This aggregate is working correctly when tested alone.
The second aggregate will use the first aggregate's result array, pulling all documents that have a firstTrackingId from the first aggregate's results. This one will pull any documents that do NOT have a secondTrackingId on it, and save the unique mongo _id/ObjectId to an array.
The updateMany will use all of the results from the second aggregation to update all relevant documents with a status of void.
All these functions are working when I give them hard-coded data, but I can't figure out how to pull the data from the arrays. I am not even sure if I'm "saving" it correctly, or if there is something else I should be doing aside from just initializing the aggregation as an array.
var ids = db.getCollection('Test').aggregate([
{
$match: {
"firstTrackingId": { "$ne": "" },
"secondTrackingId": { "$exists": true }
}
},
{
$group: {
_id: "$firstTrackingId",
}
},
])
var secondIds = db.getCollection('Test').aggregate([
{
$match: {
"firstTrackingId": { $in: ids },
"secondTrackingId": { $exists: false }
}
},
{
$group: {
"_id": "$_id",
}
},
])
db.getCollection('Test').updateMany({
"_id": {
"$in": secondIds
},
}, { $set: {
"status": "VOID"
} })
I tried printing the first aggregation's results out... can't really figure out how... so for the first one if I do:
print(ids.next(ids._id))
I get:
[object BSON]
Which leads me to believe I need to somehow perform an $objectToArray. If anyone has any insight, that'd be awesome. Thank you!
If you are using MongoDB 4.4+, you can do that with a single aggregation pipeline:
match documents with both first and second tracking ID
lookup an array of all documents with the same first tracking ID
unwind the array
consider the array elements as the root document
match to eliminate any that have a second tracking ID
set the desired status field
merge the results with the original collection
{$match: {
firstTrackingId: { $ne: "" },
secondTrackingId: { $exists: true }
}},
{$lookup:{
from: "Test",
localField:"firstTrackingId",
foreignField:"firstTrackingId",
as:"matched"
}},
{$unwind:"$matched"},
{$replaceRoot:{newRoot:"$matched"}},
{$match:{secondTrackingId:{"$exists":false}}},
{$addFields:{status:"VOID"}},
{$merge: {into: "Test"}}

mongodb: aggregates over a list contained in each document

I have currently reading the mongoDb aggregation introduction. The examples show how the aggregation operation is powerful, for example, to sum certain values across a subset of documents in a collection.
What I need is actually a bit different: I need to perform the same operation within a list that is contained in each document of a collection. In this way I would still get an element for each document that is contained in the collection, but the lists that are contained in each document would be collapsed, by summation on a certain field contained in the sub-documents contained in the list.
Is this possible with normal pipeline/aggregation operations in MongoDB?
I discovered that the $unwind operator allows to expand a list contained in a document across several documents.
For example, the following query just expands the sessions list into several documents, that can be used, afterwards, for an aggregation over the Ts field:
db.userStats.aggregate([
{ $match: {"u":{ "$in": [1,2,3,4,5] }}},
{ $unwind: "$sessions" },
{ $group: { _id:"$u" , total: { $sum: "$sessions.Ts"}}},
])
It sounds like you want to do a $project, possibly followed by a $group if you'd prefer to collapse all the results into a single document. Something like:
db.userStats.aggregate([
{ $match: {"u":{ "$in": [1,2,3,4,5] }}},
{ $project: { total: { $sum: "$sessions.Ts"}}},
{ $group: { _id:"$u" , total: { $first: "$total" }}},
])

Get single array from mongoDB collection where the status is current

i want to find accepted bodypart which have status active
i tried this
db.patients.find({
"injury.injurydata.injuryinformation.dateofinjury": {
"$gte": ISODate("2014-05-21T08:00:00Z") ,
"$lt": ISODate("2014-06-03T08:00:00Z")
},
{
"injury.injurydata.acceptedbodyparts":1,
"injury.injurydata.injuryinformation.dateofinjury":1
"injury":{
$elemMatch: {
"injury.injurydata.acceptedbodyparts.status": "current"
}
}
})
but still get both array
If acceptedbodyparts is an array, you can't query acceptedbodyparts.status. If status is a field on the documents contained in the array, you would need to use another $elemMatch clause in your query. So the last part would look something like this:
{"injury":{ "$elemMatch": { "injurydata.acceptedbodyparts": {"$elemMatch": {"status":"current"} }} }}
I also removed the injury. prefix in the first $elemMatch because you're querying data within the injury array.
Note that this will return the entire document with the full array, as long as it contains the document you're searching for. If your intention is to retrieve a particular element in an array, $elemMatch is the wrong approach.
Standard projection will not work with nested arrays or limiting any fields inside arrays. For that you need the aggregation framework:
db.patients.aggregate([
// First match, Matches documents
{ "$match": {
"injury.injurydata.injuryinformation.dateofinjury": {
"$gte": ISODate("2014-05-21T08:00:00Z"),
"$lt": ISODate("2014-06-03T08:00:00Z")
}
}},
// Un-wind the arrays
{ "$unwind": "$injury" },
{ "$unwind": "$injury.injurydata" },
{ "$unwind": "$injury.injurydata.acceptedbodyparts" },
// Now match the required data in the array
{ "$match": {
"injury.injurydata.acceptedbodyparts.status": "current"
}},
// Group only wanted fields
{ "$group": {
"_id": "$_id",
"acceptedbodyparts": {
"$push": "injury.injurydata.acceptedbodyparts"
}
}}
])
You can add in other fields outside of the array either using $first or by akin g them part of the _id in the grouping.
This is just something that is outside of the scope of the standard projection available and the aggregation framework with the extended manipulation capabilities solves this.