MongoDB - Move some fields from the array into another array - mongodb

I have this simplified MongoDB document and would like to change something because there is quite a lot of redundant data. This field "activeUsersLookup" is the result of aggregation which returns data I'd like to put inside the first users array.
First id:
"_id": "80b1565a-faf4-4e68-9bd6-8344060e8d3a" matches
id from activeUsersLookup the same story is with user IDs.
[{
"_id": "80b1565a-faf4-4e68-9bd6-8344060e8d3a",
"users": [
{
"_id": "eaa946da-2708-443e-ab4c-b6db357050ca",
"lastactive": {
"$date": {
"$numberLong": "1637922656000"
}
}
},
{
"_id": "4972ba13-6f4e-4943-be07-15802e22e0dd",
"lastactive": {
"$date": {
"$numberLong": "1653286066000"
}
}
},
{
"_id": "6c4a62ce-c6c6-430f-a0cd-d348ec77dbb2",
"lastactive": {
"$date": {
"$numberLong": "1558623982000"
}
}
}
],
"activeUsersLookup": [
{
"_id": "80b1565a-faf4-4e68-9bd6-8344060e8d3a",
"users": [
{
"_id": "eaa946da-2708-443e-ab4c-b6db357050ca",
"activities": 2
},
{
"_id": "6c4a62ce-c6c6-430f-a0cd-d348ec77dbb2",
"activities": 1
}
],
"sumOfActivities": 3
}
]
}]
So more or less the final document should look like this:
[{
"_id": "80b1565a-faf4-4e68-9bd6-8344060e8d3a",
"users": [
{
"_id": "eaa946da-2708-443e-ab4c-b6db357050ca",
"lastactive": {
"$date": {
"$numberLong": "1637922656000"
}
},
"activities": 2
},
{
"_id": "4972ba13-6f4e-4943-be07-15802e22e0dd",
"lastactive": {
"$date": {
"$numberLong": "1653286066000"
}
},
"activities": 0
},
{
"_id": "6c4a62ce-c6c6-430f-a0cd-d348ec77dbb2",
"lastactive": {
"$date": {
"$numberLong": "1558623982000"
}
},
"activities": 1
},
"sumOfActivities": 3
]
}]
I've tried with:
{
$addFields: {
'licenses.activities': '$activeUsersLookup.users.activities'
}
}
But this gives me an empty array so I must be doing something wrong.
The next stage would be to sum all those activities as sumOfActivities and the last stage would be unset activeUsersLookup.
What magic tricks must I do to have the needed result? :)

I don't think the expected result you posted for the "sumOfActivities": 3 in the users array is valid.
Assume that you are trying to achieve the result as below:
[{
"_id": "80b1565a-faf4-4e68-9bd6-8344060e8d3a",
"users": [...],
"sumOfActivities": 3
}]
The query is a bit long:
$set - Set activeUsersLookup field as object.
1.1. $first - Get the first document from 1.2.
1.2. $filter - Filter document(s) from activeUsersLookup by matching _id for the document in activeUsersLookup with _id (root document).
$set
2.1. - Set users array.
2.1.1. $map - Iterate the documents in users array and return a new array.
2.1.2. $mergeObjects - Merge current documents with the documents with activities field.
2.1.3. $ifNull - Set activities as 0 if no result returned from 2.1.4.
2.1.4. $getField - Get the activities field from the result 2.1.5.
2.1.5. $first - Get the first document from the result 2.1.6.
2.1.6. $filter - Filter the activeUsersLookup.users documents by matching _id for the document (users array) with _id for the current document.
2.2. Set sumOfActivities field.
$unset - Remove activeUsersLookup field.
db.collection.aggregate([
{
$set: {
activeUsersLookup: {
$first: {
$filter: {
input: "$activeUsersLookup",
cond: {
$eq: [
"$$this._id",
"$_id"
]
}
}
}
}
}
},
{
$set: {
users: {
$map: {
input: "$users",
as: "user",
in: {
$mergeObjects: [
"$$user",
{
activities: {
"$ifNull": [
{
"$getField": {
"field": "activities",
"input": {
$first: {
$filter: {
input: "$activeUsersLookup.users",
cond: {
$eq: [
"$$this._id",
"$$user._id"
]
}
}
}
}
}
},
0
]
}
}
]
}
}
},
sumOfActivities: "$activeUsersLookup.sumOfActivities"
}
},
{
$unset: "activeUsersLookup"
}
])
Sample Mongo Playground

Related

MongoDB Aggregate Query to find the documents with missing values

I am having a huge collection of objects where the data is stored for different employees.
{
"employee": "Joe",
"areAllAttributesMatched": false,
"characteristics": [
{
"step": "A",
"name": "house",
"score": "1"
},
{
"step": "B",
"name": "car"
},
{
"step": "C",
"name": "job",
"score": "3"
}
]
}
There are cases where the score for an object is completely missing and I want to find out all these details from the database.
In order to do this, I have written the following query, but seems I am going wrong somewhere due to which it is not displaying the output.
I want the data in the following format for this query, so that it is easy to find out which employee is missing the score for which step and which name.
db.collection.aggregate([
{
"$unwind": "$characteristics"
},
{
"$match": {
"characteristics.score": {
"$exists": false
}
}
},
{
"$project": {
"employee": 1,
"name": "$characteristics.name",
"step": "$characteristics.step",
_id: 0
}
}
])
You need to use $exists to check the existence
playground
You can use $ifNull to handle both cases of 1. the score field is missing 2. score is null.
db.collection.aggregate([
{
"$unwind": "$characteristics"
},
{
"$match": {
$expr: {
$eq: [
{
"$ifNull": [
"$characteristics.score",
null
]
},
null
]
}
}
},
{
"$group": {
_id: null,
documents: {
$push: {
"employee": "$employee",
"name": "$characteristics.name",
"step": "$characteristics.step",
}
}
}
},
{
$project: {
_id: false
}
}
])
Here is the Mongo playground for your reference.

MongoDB aggregate filter returns null

In MongoDB, I have a messages' collection (find it below):
I'm interested in querying the parent document by id, and say filtering contactedNumberMessages to include only incoming messages (those having direction "in") so I wrote the following code with Mongoose, however contactedNumberMessages is null in the returned data, any clue as to why I'm getting null? Thank you
Messages.aggregate([
{
$match: {
_id: id
}
},
{
$project: {
messaging: {
ourNumber: 1,
messages: {
contact: 1,
contactedNumberMessages: {
$filter: {
input: "$contactedNumberMessages",
as: "message",
cond: {
$eq: ["$$message.direction", "out"]
}
}
}
}
}
}
}
]);
{
"_id": {
"$oid": "612f4e32aa56064f1608c2eb"
},
"messaging": [
{
"ourNumber": "+15123568549",
"messages": [
{
"contact": "+21629000111",
"contactedNumberMessages": [
{
"direction": "out",
"content": "Hello!",
"when": {
"$date": "2021-09-23T23:00:00.000Z"
},
"nature": "SMS"
},
{
"direction": "in",
"content": "Hi!",
"when": {
"$date": "2021-09-23T23:00:00.000Z"
},
"nature": "SMS"
}
]
}
]
}
]
}
pls refer to example here: https://mongoplayground.net/p/9toRoa_5IE9
you should use something like below in aggregation:
[{$match: {
_id: ObjectId('612f4e32aa56064f1608c2eb')
}}, {$unwind: {
path: '$messaging',
}}, {$unwind: {
path: '$messaging.messages',
}}, {$project: {
messaging: {
ourNumber: 1,
messages: {
contact: 1,
contactedNumberMessages: {
$filter: {
input: "$messaging.messages.contactedNumberMessages",
as: "message",
cond: {
$eq: ["$$message.direction", "out"]
}
}
}
}
}
}}]
As you have nested array within array and sub array that filter stage was not getting correct output, i have added unwind to get the normal array for field:messaging.messages.contactedNumberMessages
if needed you can again do groupby to ensure you get document in expected format as after unwind it will create multiple documents in aggregation for each documents in array which in unwinded.

mongodb - find previous and next document in aggregation framework

After applying a long pipeline to my collection I can obtain something like this:
{
{
"_id": "main1",
"title": "First",
"code": "C1",
"subDoc": {
"active": true,
"sub_id": "main1sub1",
"order": 1
}
},
{
"_id": "main2",
"title": "Second",
"code": "C2",
"subDoc": {
"active": true,
"sub_id": "main2sub1",
"order": 1
}
},
{
"_id": "main3",
"title": "Third",
"code": "C3",
"subDoc": {
"active": false,
"sub_id": "main3sub1",
"order": 1
}
}
}
The documents are already in the correct order. Now I have to find the document immediately preceding or following the one corresponding to a given parameter. For example, if I know { "code" : "C2" } I have to retrieve the previous document (example document with "code" : "C1").
I only need to get that document, not the others.
I know how to do it using the find () method and applying sort () and limit () in sequence, but I want to get the document directly in the aggregation pipeline, adding the necessary stages to do it.
I've tried some combinations of $ indexOfArray and $ arrayElemAt, but the first problem I encounter is that I don't have an array, it's just documents.
The second problem is that the parameter I know might sometimes be inside the subdocument, for example {"sub_id": "main3sub1"}, and again I should always get the previous or next parent document as a response (in the example, the pipeline should return document "main2" as previous document)
I inserted the collection in mongoplayground to be able to perform the tests quickly:
mongoplayground
Any idea?
If you want to retrieve only the previous document, use the following query:
First Approach:
Using $match,$sort,$limit
db.collection.aggregate([
{
$match: {
code: {
"$lt": "C2"
}
}
},
{
"$sort": {
code: -1
}
},
{
$limit: 1
}
])
MongoDB Playground
Second Approach:
As specified by # Wernfried Domscheit,
Converting to array and then using $arrayElemAt
db.collection.aggregate([
{
$group: {
_id: null,
data: {
$push: "$$ROOT"
}
}
},
{
$addFields: {
"index": {
$subtract: [
{
$indexOfArray: [
"$data.code",
"C2"
]
},
1
]
}
}
},
{
$project: {
_id: 0,
data: {
$arrayElemAt: [
"$data",
"$index"
]
}
}
},
{
$replaceRoot: {
newRoot: "$data"
}
}
])
MongoDB Playground

How can I set self join in mongodb

db={
comments: [
{
"_id": ObjectId("5f364189f412c01fd01abab3"),
"content": "Comment 1",
"parent_comment_id": "",
"date": 1592461538923
},
{
"_id": ObjectId("5f364642f412c01fd01abeu4"),
"content": "Replied",
"parent_comment_id": "5f364189f412c01fd01abab3",
"date": 1592461538926
},
{
"_id": ObjectId("5f364642f412c01fd01abtx5"),
"content": "fresh comment",
"parent_comment_id": "",
"date": 1592461538929
}
]
}
How can I achieve self join in mongodb based on parent_comment_id.
is it possible in mongodb as like mysql ?
Using aggregation aggregate(),
$addFields for convert parent_comment_id to object if not empty, if its already an object id then skip this pipeline
db.comments.aggregate([
{
$addFields: {
parent_comment_id: {
$cond: {
if: { $eq: ["$parent_comment_id", ""] },
then: "$parent_comment_id",
else: { $toObjectId: "$parent_comment_id" }
}
}
}
},
$lookup to join with self collection, and use lookup with pipeline to match condition
$match parent_comment_id to _id
{
"$lookup": {
from: "comments",
le": { pid: "$parent_comment_id" },
as: "parentComment",
pipeline: [
{
$match: {
$expr: { $eq: ["$$pid", "$_id" ] }
}
}
]
}
},
$unwind to deconstruct parentComment because its an array and we need an object
preserveNullAndEmptyArrays to ignore empty parentComment array
{
$unwind: {
path: "$parentComment",
preserveNullAndEmptyArrays: true
}
}
])
Playground

MongoDB: single find request to return data from different documents with different fields

I have this collection:
{
"name": "Leonardo",
"height": "180",
"weapon": "sword",
"favorite_pizza": "Hawai"
},
{
"name": "Donatello",
"height": "181",
"weapon": "stick",
"favorite_pizza": "Pepperoni"
},
{
"name": "Michelangelo",
"height": "182",
"weapon": "nunchucks",
"favorite_pizza": "Bacon"
},
{
"name": "Raphael",
"height": "183",
"weapon": "sai",
"favorite_pizza": "Margherita"
}
With using one query I want this result (ordered by height):
{
"name": "Leonardo",
"height": "180",
"weapon": "sword",
"favorite_pizza": "Hawai"
},
{
"name": "Donatello",
},
{
"name": "Michelangelo",
},
{
"name": "Raphael",
}
So the query needs to first get the document which has smallest height field and then get all contents of that document, then it needs to get all other documents and return only name field of those documents, while ordering those documents by height.
Change your height to numeric for correct sorting and you can try below aggregation in 3.4 pipeline.
The query $sorts the document by "height" ascending followed by $group to create two fields, "first" field which has the smallest height record ($$ROOT to access the whole document) and "allnames" to record all names.
$project with $slice + $concatArrays to replace the "allnames" array first element with the smallest height document and get the updated array.
$unwind with $replaceRoot to promote all the docs to top level.
db.colname.aggregate([
{"$sort":{
"height":1
}},
{"$group":{
"_id":null,
"first":{"$first":"$$ROOT"},
"allnames":{"$push":{"name":"$name"}}
}},
{"$project":{
"data":{"$concatArrays":[["$first"],{"$slice":["$allnames",1,{"$size":"$allnames"}] } ]}
}},
{"$unwind":"$data"},
{"$replaceRoot":{"newRoot":"$data"}}
])
Just for completeness reasons...
#Veeram's answer is probably the better choice (I have a feeling it should be faster and easier to understand) but you can achieve the same result using a slightly simpler $group stage followed by slightly more complex $project stage using $reduce:
collection.aggregate([{
$sort: {
"height": 1
}
}, {
$group: {
"_id":null,
"allnames": {
$push: "$$ROOT"
}
}
}, {
$project: {
"data": {
$reduce: {
input: "$allnames",
initialValue: null,
in: {
$cond: [{
$eq: [ "$$value", null ] // if it's the first time we come here
},
[ "$$this" ], // we include the entire document
{
$concatArrays: [ // else we concat
"$$value", // the already concatenated values
[ { "name": "$$this.name" } ] // with the "name" of the currently looked at document
]
}]
}
}
}
}
}, {
$unwind: "$data"
}, {
$replaceRoot: {
"newRoot": "$data"
}
}])
Alternatively - as pointed out by #Veeram in the comment below - , it's possible to write the $reduce in this way:
$project: {
"data": {
$reduce: {
input: { "$slice": [ "$allnames", 1, { $size: "$allnames" } ] }, // process everything in the "allnames" array except for the first item
initialValue: { "$slice": [ "$allnames", 1 ] }, // start with the first item
in: { $concatArrays: [ "$$value", [ { "name": "$$this.name" } ] ]} // and keep appending the "name" field of all other items only
}
}
}