MongoDB aggregate field in array of objects - mongodb

I'm trying to solve a problem for some time now but with no luck, unfortunately.
So I'm refactoring some old code (which used the all known get each doc query and for loop over it) and I'm trying to aggregate the results to remove the thousands of calls the BE is making.
The current doc looks like this
{
"_id" : ObjectId("5c176fc65f543200019f8d66"),
"category" : "New client",
"description" : "",
"createdById" : ObjectId("5c0a858da9c0f000018382bb"),
"createdAt" : ISODate("2018-12-17T09:43:34.642Z"),
"sentAt" : ISODate("2018-12-17T09:44:25.902Z"),
"scheduleToBeSentAt" : ISODate("2018-01-17T11:43:00.000Z"),
"recipients" : [
{
"user" : ObjectId("5c0a858da9c0f000018382b5"),
"status" : {
"approved" : true,
"lastUpdated" : ISODate("2018-01-17T11:43:00.000Z")
}
},
{
"user" : ObjectId("5c0a858da9c0f000018382b6"),
"status" : {
"approved" : true,
"lastUpdated" : ISODate("2018-01-17T11:43:00.000Z")
}
},
],
"recipientsGroup" : "All",
"isActive" : false,
"notificationSent" : true
}
The field recipients.user is an objectID of a user from the Users collection.
What is the correct way to modify this so the result will be
{
"_id": ObjectId("5c176fc65f543200019f8d66"),
"category": "New client",
"description": "",
"createdById": ObjectId("5c0a858da9c0f000018382bb"),
"createdAt": ISODate("2018-12-17T09:43:34.642Z"),
"sentAt": ISODate("2018-12-17T09:44:25.902Z"),
"scheduleToBeSentAt": ISODate("2018-01-17T11:43:00.000Z"),
"recipients": [{
"user": {
"_id": ObjectId("5c0a858da9c0f000018382b5"),
"title": "",
"firstName": "Monique",
"lastName": "Heinrich",
"qualification": "Management",
"isActive": true
},
"status": {
"approved": true,
"lastUpdated": ISODate("2018-01-17T11:43:00.000Z")
}
},
{
"user": {
"_id": ObjectId("5c0a858da9c0f000018382b6"),
"title": "",
"firstName": "Marek",
"lastName": "Pucelik",
"qualification": "Management",
"isActive": true
},
"status": {
"approved": true,
"lastUpdated": ISODate("2018-01-17T11:43:00.000Z")
}
},
],
"recipientsGroup": "All",
"isActive": false,
"notificationSent": true
}
An aggregation is a powerful tool but sometimes the simple solution makes your brain hurt.....
I tried something like this but with no luck also.
db.getCollection('Protocols').aggregate([
{
$lookup: {
from: "Users",
localField: "recipients.user",
foreignField: "_id",
as: "users"
}
},
{
$project: {
"recipients": {
"status": 1,
"user": {
$filter: {
input: "$users",
cond: { $eq: ["$$this._id", "$user"] }
}
},
}
}
}
])

You can use the $lookup operator in your aggregation pipeline
https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/
But for performance reason you'd rather duplicate user object in your recipents array to avoid such complex queries.

Related

Stucking with nested array lookup in MongoDB

I'm trying to lookup query from nested array in mongodb and I'm getting stuck.
I have total threee collections.
(1) Channel (Parent)
(2) ChannelThreads (Children)
(3) Users
Channel Collection:
{
"_id" : ObjectId("61efcbdc1aa27f83da47c93f"),
"tags" : [],
"slug_history" : [
"iny1Xik"
],
"title" : "Pirate Chat",
"settingId" : ObjectId("61408586b719c8ce89f08674"),
"status" : "published",
"lockedPageContent" : "",
"slug" : "iny1Xik",
"createdAt" : ISODate("2022-01-25T10:07:24.144Z"),
"updatedAt" : ISODate("2022-01-25T10:07:24.144Z"),
"__v" : 0
}
Channel Thread Collection:
{
"_id" : ObjectId("61efcd5df82318884746eb80"),
"threadImage" : [],
"parentId" : null,
"channelId" : ObjectId("61efcbdc1aa27f83da47c93f"),
"authorId" : ObjectId("6177de8f8a5fd72a4f37b7db"),
"threadText" : "New Message",
"reactions" : [
{
"authors" : [
ObjectId("3687de8f8a5fd72a4f37b7bg")
],
"_id" : ObjectId("61ef856432753c196382c37d"),
"icon" : "&#128528"
}
],
"createdAt" : ISODate("2022-01-25T10:13:49.033Z"),
"updatedAt" : ISODate("2022-01-25T10:13:49.033Z"),
"__v" : 0
}
User Collection:
{
"_id" : ObjectId("6177de8f8a5fd72a4f37b7db"),
"image" : "",
"tags" : [],
"pushTokens" : [],
"lastLogin" : ISODate("2022-01-25T10:08:19.055Z"),
"firstName" : "dinesh",
"lastName" : "patel",
"email" : "dineshpatel#example.com",
"infusionSoftId" : "784589",
"role" : "user",
"__v" : 0,
"settings" : {
"commentNotification" : false,
"commentReplyNotification" : true
}
}
I'm trying to implement lookup for authors of thread reactions.
Expected Output:
{
"_id": ObjectId("61efcbdc1aa27f83da47c93f"),
"tags": [],
"slug_history": [
"iny1Xik"
],
"title": "Pirate Chat",
"settingId": ObjectId("61408586b719c8ce89f08674"),
"status": "published",
"lockedPageContent": "",
"slug": "iny1Xik",
"createdAt": ISODate("2022-01-25T10:07:24.144Z"),
"updatedAt": ISODate("2022-01-25T10:07:24.144Z"),
"__v": 0,
"threads": [
{
"_id": ObjectId("61efcd5df82318884746eb80"),
"threadImage": [],
"parentId": null,
"channelId": ObjectId("61efcbdc1aa27f83da47c93f"),
"authorId": {
"_id": ObjectId("6177de8f8a5fd72a4f37b7db"),
"image": "",
"tags": [],
"pushTokens": [],
"lastLogin": ISODate("2022-01-25T10:08:19.055Z"),
"firstName": "dinesh",
"lastName": "patel",
"email": "dineshpatel#example.com",
"infusionSoftId": "something",
"role": "user",
"__v": 0,
"settings": {
"commentNotification": false,
"commentReplyNotification": true
}
},
"threadText": "New Message",
"reactions": [
{
"authors": [
{
"_id": ObjectId("3687de8f8a5fd72a4f37b7bg"),
"image": "",
"tags": [],
"pushTokens": [],
"lastLogin": ISODate("2022-01-25T10:08:19.055Z"),
"firstName": "kayle",
"lastName": "hell",
"email": "kylehell#example.com",
"infusionSoftId": "8475151",
"role": "user",
"__v": 0,
"settings": {
"commentNotification": false,
"commentReplyNotification": true
}
}
],
"_id": ObjectId("61ef856432753c196382c37d"),
"icon": "&#128528"
}
],
"createdAt": ISODate("2022-01-25T10:13:49.033Z"),
"updatedAt": ISODate("2022-01-25T10:13:49.033Z"),
"__v": 0
}
]
}
How can write lookup query for reaction authors.
Thanks in advance!!
You can try nested lookup,
$lookup with channel thread collection, pass channel id in let
$match to match channelId condition
$lookup with user collection to get author info for authorId
$lookup with user collection to get reactions's authors info
$arrayElemAt to get first element from authorId
$map to iterate loop of reactions array, $filter to iterate loop of users and get matching author user info from users array,
$mergeObjects to merge authors and current object properties
$$REMOVE to remove users field because it is not needed now
db.channel.aggregate([
{
$lookup: {
from: "channelThread",
let: { channelId: "$_id" },
pipeline: [
{ $match: { $expr: { $eq: ["$$channelId", "$channelId"] } } },
{
$lookup: {
from: "user",
localField: "authorId",
foreignField: "_id",
as: "authorId"
}
},
{
$lookup: {
from: "user",
localField: "reactions.authors",
foreignField: "_id",
as: "users"
}
},
{
$addFields: {
authorId: { $arrayElemAt: ["$authorId", 0] },
reactions: {
$map: {
input: "$reactions",
as: "r",
in: {
$mergeObjects: [
"$$r",
{
authors: {
$filter: {
input: "$users",
cond: { $in: ["$$this._id", "$$r.authors"] }
}
}
}
]
}
}
},
users: "$$REMOVE"
}
}
],
as: "threads"
}
}
])
Playground

Is there a Mongo function for filtering all nested/subdocuments based on a field?

I have some documents in MongoDB that have other nested documents all with an "active" field.
For instance :
{
"id": "PRODUCT1",
"name": "Product 1",
"active": true,
"categories": [
{
"id": "CAT-1",
"active": true,
"subcategories": [
{
"id": "SUBCAT-1",
"active": false
},
{
"id": "SUBCAT-2",
"active": true
}
]
},
{
"id": "CAT-2",
"active": false,
"subcategories": [
{
"id": "SUBCAT-3",
"active": true
}
]
}
]
}
Is there a way to find all documents but only keep the "active" nested documents.
This is the result I'd like :
{
"id": "PRODUCT1",
"name": "Product 1",
"active": true,
"categories": [
{
"id": "CAT-1",
"active": true,
"subcategories": [
{
"id": "SUBCAT-2",
"active": true
}
]
}
]
}
Knowing that I do NOT know the document schema beforehand. That's why I need a sort of conditioned wildcard projection... (ie *.active=true). Is this possible or this HAS to be done serverside ?
Use $redact.
db.collection.aggregate(
[
{ $redact: {
$cond: {
if: { $eq:["$active", true] },
then: "$$DESCEND",
else: "$$PRUNE"
}
}
}
]
);
https://mongoplayground.net/p/7UMphkH5OWn
//actual code out from mongo shell 4.2 on windows
//sample document as shared in problem statement, query to find the document from //collection
> db.products.find().pretty();
{
"_id" : ObjectId("5f748ee5377e73757bb7ceac"),
"id" : "PRODUCT1",
"name" : "Product 1",
"active" : true,
"categories" : [
{
"id" : "CAT-1",
"active" : true,
"subcategories" : [
{
"id" : "SUBCAT-1",
"active" : false
},
{
"id" : "SUBCAT-2",
"active" : true
}
]
},
{
"id" : "CAT-2",
"active" : false,
"subcategories" : [
{
"id" : "SUBCAT-3",
"active" : true
}
]
}
]
}
//verify mongo shell version no. for reference
> db.version();
4.2.6
//using aggregate and $unwind you can query the inner array elements as shown below
> db.products.aggregate([
... {$unwind: "$categories"},
... {$unwind: "$categories.subcategories"},
... {$match:{"active":true,
... "categories.active":true,
... "categories.subcategories.active":true}}
... ]).pretty();
{
"_id" : ObjectId("5f748ee5377e73757bb7ceac"),
"id" : "PRODUCT1",
"name" : "Product 1",
"active" : true,
"categories" : {
"id" : "CAT-1",
"active" : true,
"subcategories" : {
"id" : "SUBCAT-2",
"active" : true
}
}
}
>
You'll be able to achieve this with a few $map, $reduce and $filter stages.
db.collection.aggregate([
{
"$addFields": {
"categories": {
"$filter": {
"input": "$categories",
"cond": {
$eq: [
"$$this.active",
true
]
}
}
}
}
},
{
"$addFields": {
"categories": {
"$map": {
"input": "$categories",
"in": {
"$mergeObjects": [
"$$this",
{
"subcategories": {
"$filter": {
"input": "$$this.subcategories",
"cond": {
$eq: [
"$$this.active",
true
]
}
}
}
}
]
}
}
}
}
}
])
Executing the above will give you the following result based on your input
[
{
"_id": ObjectId("5a934e000102030405000000"),
"active": true,
"categories": [
{
"active": true,
"id": "CAT-1",
"subcategories": [
{
"active": true,
"id": "SUBCAT-2"
}
]
}
],
"id": "PRODUCT1",
"name": "Product 1"
}
]
https://mongoplayground.net/p/fkkby-eibx2

Need a Mongo query to generate a particular result-set with aggregation

I am new to mongodb, I have a requirement and would like to know how to generate custom resultset using Mongo aggregate operator. Any help would be appreciated.
Need to group the collection by "company" and "status" and would need to produce resultset given below.
Collection
[
{
"company": "google",
"status": "active",
"offer": {
"job": "developer",
"salary": 10000.00
},
},
{
"company": "google",
"status": "active",
"offer": {
"job": "designer",
"salary": 500000.00
},
},
{
"company": "amazon",
"status": "inactive",
"offer": {
"job": "designer",
"salary": 500000.00
},
}
]
Expected Result-Set
[
{
"company" : "google",
"report" : [{
"status" : "active",
"totalSalary" : 60000
},
{
"status" : "inactive",
"totalSalary" : 0
}]
},
{
"company" : "amazon",
"report" : [{
"status" : "active",
"totalSalary" : 0
},
{
"status" : "inactive",
"totalSalary" : 500000.00
}]
}
]
You should 100% check the official documentation on aggregates, it's a bit complicated at first but once you get the hang of it they're great. I also recommend you https://mongoplayground.net/, it's a great site for doing this kind of tests.
What you're looking for is something like this
db.collection.aggregate([
{
$group: {
_id: {
company: "$company"
},
report: {
$addToSet: "$offer"
}
}
}
])
You can test it here. You also probably want to rename the resulting _id field that's mandatory in a group aggregate. You can find how to do that here

How to join MongoDB with DBRef

I have two documents with a DBRef relation between those documents.
Task document:
{
"_id": 77,
"title": "Test title",
"status": "in-progress",
"reporter": {
"$ref": "User",
"$id": ObjectId("5daf022549a36e319879f357"),
"$db": "test"
},
"priority": "high",
"project": {
"$ref": "Project",
"$id": 30,
"$db": "gsc"
}
}
User Document:
{
"_id": ObjectId("5daf022549a36e319879f357"),
"username": "user1",
"email": "test#gmail.com",
"is_active": true,
"firstName": "user-1"
}
I tried below query but I didn't get proper result
db.Task.findOne($lookup:
{
from: User,
localField: reporter,
foreignField: reporter._id,
as: User_Task
}
)
How to perform the JOIN? Also, want to both document all data?
I want data from Task Document and suggest how to join with project field.
Need this kind of result:
{
"_id" : 77,
"title" : "Test title",
"status" : "in-progress"
"reporter" :
{
"_id" : ObjectId("5daf022549a36e319879f357"),
"username" : "user1",
"email" : "test#gmail.com"
"is_active" : true,
"firstName" : "user-1"
},
"priority" : "high",
"project" : {}
}
A simple $lookup should do the trick. You just need to supply reporter.$id to the field localField
db.task.aggregate([
{
"$lookup": {
"from": "user",
"localField": "reporter.$id",
"foreignField": "_id",
"as": "reporter"
}
},
{
"$unwind": "$reporter"
}
])
Here is the Mongo playground for your reference.

mongo $unwind and $group

I have two collections. One of which I wish to add a reference to the other and have it populated on return.
Here is an example json I am trying to achieve as the result:
{
"title": "Some Title",
"uid": "some-title",
"created_at": "1412159926",
"updated_at": "1412159926",
"id": "1",
"metadata": {
"date": "2016-10-17",
"description": "a description"
},
"tags": [
{
"name": "Tag 1",
"uid": "tag-1"
},
{
"name": "Tag 2",
"uid": "tag-2"
},
{
"name": "Tag 3",
"uid": "tag-3"
}
]
}
Here is the mongo query I have which gets my close, but it nests the original body of the item within the _id object.
db.tracks.aggregate([{
$unwind: "$tags"
}, {
$lookup: {
from: "tags",
localField: "tags",
foreignField: "_id",
as: "tags"
}
}, {
$unwind: "$tags"
}, {
$group: {
"_id": {
"title": "$title",
"uid": "$uid",
"metadata": "$metadata"
},
"tags": {
"$push": "$tags"
}
}
}])
So the result is this:
{
"_id" : {
"title" : "Some Title",
"uid" : "some-title",
"metadata" : {
"date" : "2016-10-17",
"description" : "a description"
}
},
"tags" : [
{
"_id" : ObjectId("580499d06fe29ce7093fb53a"),
"name" : "Tag 1",
"uid" : "tag-1"
},
{
"_id" : ObjectId("580499d06fe29ce7093fb53b"),
"name" : "Tag 2",
"uid" : "tag-2"
}
]
}
Is there a way to achieve the desired output? Also is there a way to not have to define in the $group all the fields which I wish to return, I would like to return the original Object but with the referenced documents in the tags array.
Since you had initially pivoted your original documents on the tags array field which means the documents will be denormalized, your $group pipeline should
use the _id field as its _id key and access the other fields using the $first or $last operator.
The group pipeline operator is similar to the SQL's GROUP BY clause. In SQL, you can't use GROUP BY unless you use any of the aggregation functions. The same way, we have to use an aggregation function in MongoDB as well, so unfortunately there is no other way of not having to define in the $group pipeline all the fields which you wish to return apart from using the $first or $last operator on each field:
db.tracks.aggregate([
{ "$unwind": "$tags" },
{
"$lookup": {
"from": "tags",
"localField": "tags",
"foreignField": "_id",
"as": "resultingArray"
}
},
{ "$unwind": "$resultingArray" },
{
"$group": {
"_id": "$_id",
"title": { "$first": "$title" },
"uid": { "$first": "$uid" },
"created_at": { "$first": "$created_at" },
"updated_at": { "$first": "$updated_at" },
"id": { "$first": "$id" },
"metadata": { "$first": "$metadata" },
"tags": { "$push": "$resultingArray" }
}
}
])
One trick I always use whenever I want to debug a pipeline that's giving unexpected results is to run the aggregation with just the first pipeline operator. If that gives the expected result, add the next.
In the answer above, you'd first try aggregating just the $unwind; if that works, add the $lookup. This can help you narrow down which operator is causing issues. In this case, you could run the pipeline with just the first three steps since you believe the $group is the one causing issues and then inspect the resulting documents from that pipeline:
db.tracks.aggregate([
{ "$unwind": "$tags" },
{
"$lookup": {
"from": "tags",
"localField": "tags",
"foreignField": "_id",
"as": "resultingArray"
}
},
{ "$unwind": "$resultingArray" }
])
which yields the output
/* 1 */
{
"_id" : ObjectId("5804a6c900ce8cbd028523d9"),
"title" : "Some Title",
"uid" : "some-title",
"created_at" : "1412159926",
"updated_at" : "1412159926",
"id" : "1",
"metadata" : {
"date" : "2016-10-17",
"description" : "a description"
},
"resultingArray" : {
"name" : "Tag 1",
"uid" : "tag-1"
}
}
/* 2 */
{
"_id" : ObjectId("5804a6c900ce8cbd028523d9"),
"title" : "Some Title",
"uid" : "some-title",
"created_at" : "1412159926",
"updated_at" : "1412159926",
"id" : "1",
"metadata" : {
"date" : "2016-10-17",
"description" : "a description"
},
"resultingArray" : {
"name" : "Tag 2",
"uid" : "tag-2"
}
}
/* 3 */
{
"_id" : ObjectId("5804a6c900ce8cbd028523d9"),
"title" : "Some Title",
"uid" : "some-title",
"created_at" : "1412159926",
"updated_at" : "1412159926",
"id" : "1",
"metadata" : {
"date" : "2016-10-17",
"description" : "a description"
},
"resultingArray" : {
"name" : "Tag 3",
"uid" : "tag-3"
}
}
From inspection you will see that for each input document, the last pipeline outputs 3 documents where 3 is the number of array elements in the computed field resultingArray and they all have a common _id and the other fields with the exception of the resultingArray field which is different, thus you get your desired results by adding a pipeline that groups the documents by the _id field and subsequently getting the other fields with $first or $last operator, as in the given solution:
db.tracks.aggregate([
{ "$unwind": "$tags" },
{
"$lookup": {
"from": "tags",
"localField": "tags",
"foreignField": "_id",
"as": "resultingArray"
}
},
{ "$unwind": "$resultingArray" },
{
"$group": {
"_id": "$_id",
"title": { "$first": "$title" },
"uid": { "$first": "$uid" },
"created_at": { "$first": "$created_at" },
"updated_at": { "$first": "$updated_at" },
"id": { "$first": "$id" },
"metadata": { "$first": "$metadata" },
"tags": { "$push": "$resultingArray" }
}
}
])