Mongo DB aggrregation on multiple arrays - mongodb

I want to retrieve from my cart items and bundles that are not deleted.
my cart looks like this:
{
"_id": "589474849d7b3f439797faf1",
"bundles": [{
"id": "57c98e25298cd0f908021c12",
"serial": "xxxx",
"status": ""
}],
"items": [{
"id": "589de9a690d632ccbc10cd64",
"status": "deleted",
"quantity": 1,
"serial": "fffff"
}]
}
What I tried was:
[
{$match: condition},
{$unwind: {"path": "$items", "preserveNullAndEmptyArrays": true}},
{$unwind: {"path": "$bundles", "preserveNullAndEmptyArrays": true}},
{$match: {"items.status": {$ne: "deleted"}}},
{$match: {"bundles.status": {$ne: "deleted"}}},
{
"$group": {
"_id": "$_id",
currency: {$first: "$currency"},
tenant: {$first: "$tenant"},
user: {$first: "$user"},
"items": {"$addToSet": "$items"},
"bundles": {"$addToSet": "$bundles"}
}
}
];
It works fine for all cases except when there is only one deleted item, and multiple bundles. The query return no bundles at all
expected output:
{
"_id": "589474849d7b3f439797faf8",
"items": [{
"id": "589de9a690d632ccbc10cd64",
"quantity": 1,
"serial": "fff"
}, {
"id": "589de9a690d632ccbc10c55",
"quantity": 1,
"serial": "xxx"
}],
"bundles": [{
"id": "57c98e25298cd0f908021c12",
"serial": "pppp"
}]
}

Oh thanks guys I found the solution, filter will do the trick
[{
$match: "condition"
}, {
$project: {
items: {
$filter: {
input: "$items",
as: "item",
cond: {
$ne: ["$$item.status", "deleted"]
}
}
},
bundles: {
$filter: {
input: "$bundles",
as: "bundle",
cond: {
$ne: ["$$bundle.status", "deleted"]
}
}
}
}
}]

Related

Mongodb Aggregations - Group by date including condition

I have a series of documents gathered by aggregation grouping. This is the result for one document:
{
"_id": {
"ip": "79.xxx.xxx.117",
"myDate": "2022-10-19"
},
"date": "2022-10-19",
"allVisitedPages": [
{
"page": "/",
"time": {
"time": "2022-10-19T11:35:44.655Z",
"tz": "-120",
"_id": "634fe1100a011986b7137da0"
}
},
{
"page": "/2",
"time": {
"time": "2022-10-19T12:14:29.536Z",
"tz": "-120",
"_id": "634fea257acb264f23d421f1"
}
},
{
"page": "/",
"time": {
"time": "2022-10-19T15:37:30.002Z",
"tz": "-120",
"_id": "634fea266001ea364eeb38ea"
}
},
],
"visitedPages": 3,
"createdAt": "2022-10-19T11:35:44.920Z"
},
I want to get this (in this case 2 documents as the time difference between array position 2 and 3 is greater than 2 hours):
{
"_id": {
"ip": "79.xxx.xxx.117",
"myDate": "2022-10-19"
},
"date": "2022-10-19",
"allVisitedPages": [
{
"page": "/",
"durationInMinutes": "39",
"time": {
"time": "2022-10-19T11:35:44.655Z",
"tz": "-120",
"_id": "634fe1100a011986b7137da0"
}
},
{
"page": "/2",
"durationInMinutes": "2",
"time": {
"time": "2022-10-19T12:14:29.536Z",
"tz": "-120",
"_id": "634fea257acb264f23d421f1"
}
}
],
"visitedPages": 2,
},
{
"_id": {
"ip": "79.xxx.xxx.117",
"myDate": "2022-10-19"
},
"date": "2022-10-19",
"allVisitedPages": [
{
"page": "/",
"durationInMinutes": "2",
"time": {
"time": "2022-10-19T15:37:30.002Z",
"tz": "-120",
"_id": "634fea266001ea364eeb38ea"
}
},
],
"visitedPages": 1,
},
I want to get a new grouping document if the time between an array position and the following array position is greater than 2 hours. On the last array position it show always show "2".
I tried $divide and $datediff. But this is not possible on the group stage as it's an unary operator. An approach I tried is to calculate the sum of start and end time by dividing. But how to execute this on an array level on the group stage? Maybe someone could point me in the right direction if possible at all?
You can group and then reduce, but another option is to use $setWindowFields to calculate your grouping index before grouping:
db.collection.aggregate([
{$setWindowFields: {
partitionBy: {$concat: ["$ip", "$date"]},
sortBy: {"time.time": 1},
output: {prevtime: {
$push: "$time.time",
window: {documents: [-1, "current"]}
}}
}},
{$addFields: {
minutesDiff: {
$toInt: {
$dateDiff: {
startDate: {$first: "$prevtime"},
endDate: {$last: "$prevtime"},
unit: "minute"
}
}
}
}},
{$addFields: {deltaIndex: {$cond: [{$gt: ["$minutesDiff", 120]}, 1, 0]}}},
{$setWindowFields: {
partitionBy: {$concat: ["$ip", "$date"]},
sortBy: {"time.time": 1},
output: {
groupIndex: {
$sum: "$deltaIndex",
window: {documents: ["unbounded", "current"]}
},
duration: {
$push: "$minutesDiff",
window: {documents: ["current", 1]}
}
}
}
},
{$set: {
duration: {
$cond: [
{$and: [
{$eq: [{$size: "$duration"}, 2]},
{$lte: [{$last: "$duration"}, 120]}
]},
{$last: "$duration"},
2
]
}
}},
{$group: {
_id: {ip: "$ip", myDate: "$date", groupIndex: "$groupIndex"},
date: {$first: "$date"},
allVisitedPages: {$push: {page: "$page", time: "$time", duration: "$duration"}},
visitedPages: {$sum: 1}
}},
{$unset: "_id.groupIndex"}
])
See how it works on the playground example

how to transform data using aggregate function mongodb

How to transform data base on parent_id within self join ? Is this possible make the result as expected. Please help on this thanks
db={
post: [
{
"_id": ObjectId("59f9c5629f75813e21a6fe34"),
"parent_id": "0",
"name": "main_category",
"short_desc": "",
"long_desc": "",
"slug": "main_category",
"status": true,
"createdAt": ISODate("2017-11-01T13:00:18.714Z"),
"updatedAt": ISODate("2019-02-19T07:31:20.967Z")
},
{
"_id": ObjectId("59f9c5629f75813e21a6fe73"),
"parent_id": "59f9c5629f75813e21a6fe34",
"name": "sub_category",
"short_desc": "",
"long_desc": "",
"slug": "sub_category",
"status": true,
"createdAt": ISODate("2017-11-01T13:00:18.714Z"),
"updatedAt": ISODate("2019-02-19T07:31:20.967Z")
},
{
"_id": ObjectId("59f9c5629f75813e21a6fe33"),
"parent_id": "59f9c5629f75813e21a6fe73",
"name": "sub_category1",
"short_desc": "",
"long_desc": "",
"slug": "sub_category1",
"status": true,
"createdAt": ISODate("2017-11-01T13:00:18.714Z"),
"updatedAt": ISODate("2019-02-19T07:31:20.967Z")
}
]
}
output should like this. If any more category does not belongs to anything it should stay blank
[
{
mainCategory: 'main_category',
subCategory1: 'sub_category',
subCategory2: 'sub_category1',
subCategory3: '',
subCategory4: '',
subCategory5: ''
}, {
mainCategory: '{if any}',
subCategory1: '{if any}',
subCategory2: '{if any}',
subCategory3: '',
subCategory4: '',
subCategory5: ''
}
];
Any hope to get this stat. ?
$graphLookup reads from the collection specified by its from argument, not from the documents in the pipeline.
In the pipeline you created to change the datatype, use a $merge stage to update the existing documents:
db.post.aggregate([
{$addFields: {
parent_oid: {
$cond: {
if: {$eq: ["$parent_id","0"]},
then: "$parent_id",
else: {$toObjectId: "$parent_id"}
}
}
}
},
{$merge: "post"}
])
Then you can use $graphLookup to form the lists, and transform them to the shape you need:
db.post.aggregate([
{$match: {parent_id: "0" }},
{"$graphLookup": {
"from": "post",
"startWith": "$_id",
"connectFromField": "_id",
"connectToField": "parent_oid",
"as": "response"
}},
{$unwind: "$response"},
{$group: {
_id: "$_id",
main_category: {$first: "$slug"},
subCategories: {$push: {
k: "$response.name",
v: "$response.slug"
}}
}
},
{$replaceRoot: {
newRoot: {
$mergeObjects: [
{mainCategory: "$main_category"},
{$arrayToObject: "$subCategories"}
]
}
}}
])
Output from the sample data:
[
{
"mainCategory": "main_category",
"sub_category": "sub_category",
"sub_category1": "sub_category1"
}
]
Playground

Find text in array of object and return only relevant items (aggregate & project)

I need to query mongoDB (mongoose) in very specific way. Here is the challenge:
My schema:
const eventSchema = new Schema(
{ name: { type: 'String' },
sessions: {
type: [
{
id: { type: 'Number' },
name: { type: 'String' }
}
]
}
});
Actual data
{"_id": "234", "name": "ng-nl", "sessions":[
{"id":"1", "name": "Testing Angular 4 Workshop"},
{"id":"2", "name": "Angular 4 and Firebase"}
]}
{"_id": "896", "name": "ng-conf 2037", "sessions":[
{"id":"1", "name": "How Elm Powers React"},
{"id":"2", "name": "Angular and React together"}
]}
The challenge:
When searching for string "angular"
I would like to get following data (the structure may be different):
{"_id": "234", session: {"id":"1", "name": "Testing Angular 4 Workshop"}}
{"_id": "234", session: {"id":"2", "name": "Angular 4 and Firebase"}}
{"_id": "896", session: {"id":"2", "name": "Angular and React together"}}
I've tried
{sessions.name: {$regex: 'angular', $options: 'i'}}
works correctly but returns full objects, I need only relevant sessions.
I've thought to do something with
db.test.aggregate([
{
$project: {
sessions: {
$filter: {
input: "$sessions",
as: "session",
cond: { $match: [{"$$session.name": {$regex: 'albert', $options: 'i'}}] }
}
}
}
}
])
But I have an error "MongoError: Unrecognized expression '$match'".
Any suggestions?
You can use $unwind and $match to get the expected result like this:
db.test.aggregate([
{
"$unwind": "$sessions"
},
{
$match: {
"sessions.name": {
"$regex": "angular",
"$options": "i"
}
}
}
])
Output
[
{ "_id": "234","name": "ng-nl","sessions": {"id": "1","name": "Testing Angular 4 Workshop"}},
{"_id": "234", "name": "ng-nl","sessions": {"id": "2","name": "Angular 4 and Firebase"}},
{ "_id": "896","name": "ng-conf 2037","sessions": {"id": "2","name": "Angular and React together"}}
]
In your expected output above, you have excluded outer name field. If you don't want to show the all fields in the result you can use $project like this:
db.test.aggregate([
{
"$unwind": "$sessions"
},
{
$match: {
"sessions.name": {
"$regex": "angular",
"$options": "i"
}
}
},
{
$project: {
_id: "$_id",
"sessions": 1
}
}
])
Output
[
{"_id": "234","sessions": {"id": "1","name": "Testing Angular 4 Workshop"}},
{"_id": "234","sessions": {"id": "2","name": "Angular 4 and Firebase"}},
{"_id": "896","sessions": {"id": "2","name": "Angular and React together"}}
]
Try $unwind like this
db.test.aggregate([
{
$project: {
sessions: {
$filter: {
input: "$sessions",
as: "session",
cond: { $match: [{"$$session.name": {$regex: 'albert', $options: 'i'}}] }
}
}
}
},
{
$unwind:"$sessions"
}
])

How to Pull Out nested Objects in Mongodb?

I'm trying to make a query by merging from another collection, but there are obstacles when the query is run, the data generated is not what I imagined
i have the data like this
{
"_id": "5ce8981a46039c14a4ec32d1",
"name": "Monkey D Luffy",
"email": "aaa#aaa.com",
"status": "not verified",
"password": "$2a$10$ayluBIsOOelBTIk.69GjHubgQemr6dJfgBUELNusCOaUGLpS/qKs6",
"metas": {
"role": "admin",
"smartphone": "ios",
"address": "konoha",
"hobby": "eat ramen"
}
},
and i want pull out metas from nested document :
{
"_id": "5ce8981a46039c14a4ec32d1",
"name": "Monkey D Luffy",
"email": "aaa#aaa.com",
"status": "not verified",
"password": "$2a$10$ayluBIsOOelBTIk.69GjHubgQemr6dJfgBUELNusCOaUGLpS/qKs6",
"role": "admin",
"smartphone": "ios",
"address": "konoha",
"hobby": "eat ramen"
},
if any duplicate from my question pls suggest me, because I didn't find the same question, mostly using arrays.
and here is my query:
db.accounts.aggregate([
{
$lookup: {
from: "account_meta",
localField: "_id",
foreignField: "account_id",
as: "metas"
}
},
{ "$unwind": "$metas" },
{
$group: {
_id: "$_id",
name: {$first:"$name"},
status: {$first: "$status"},
email: {$first: "$email"},
password: {$first: "$password"},
data: {
"$push": {
"k" : "$metas.key",
"v": "$metas.value"
}
}
}
},
{
$project: {
"_id": "$_id",
"name": "$name",
"email": "$email",
"status": "$status",
"password": "$password",
"metas" :{
$arrayToObject: "$data"
}
}
},
{
"$replaceRoot": {
"newRoot":
{
"$mergeObjects": [ {$arrayToObject: "$data"}, "$$ROOT"]
},
}
},
])
i just edit some code from my $mergeObject:
{
"$replaceRoot": {
"newRoot":
{
"$mergeObjects": [ "$metas", "$$ROOT"]
},
}
},
{$project: { metas: 0} }

Projecting specific fields present inside an array, based on the value of some other field

Overview :
The documents, that I'm working upon, have two nested arrays in them - contentMetaData & text_content.
Within contentMetaData, we have the text_content and content_flag. Based on the value of the content_flag, I need to hide specific field within the text_content.
Requirement :
If the content_flag is true, text_content should have a single child - the text_note.
If the content_flag is false, text_content should have a single child - the text_description.
The structure and other details need to be preserved.
Documents SHOULD NOT be updated; the values need to be only hidden during projection.
Version Used : Mongo 2.6
Sample Document :
{
"_id": ObjectId("56f8dd19e4b0365115927b0f"),
"contentId": "cbc91805-2faa-4eff-8f84-02547173c152",
"contentMetaData": [
{
"_id": "1574b58f-b7fa-4cd5-b34f-98beeb657c97",
"name": "text_content",
"attributes": [],
"children": [
{
"_id": "97340ecf-fdbd-41e5-a6b2-01cc542f16ee",
"name": "text_note",
"value": "abc",
"type": "java.lang.String",
"attributes": [],
"children": [],
"noOfChildren": 0,
"positionIndex": 1
},
{
"_id": "19c5a3fb-54a2-4368-a89d-ea1d2554402d",
"name": "text_description",
"value": "def",
"type": "java.lang.String",
"attributes": [],
"children": [],
"noOfChildren": 0,
"positionIndex": 2
}
],
"noOfChildren": 2,
"positionIndex": 1
},
{
"_id": "4e8ef7c9-cffd-4b36-9109-89b263dff3c8",
"name": "content_flag",
"value": "true",
"type": "java.lang.String",
"attributes": [],
"children": [],
"noOfChildren": 0,
"positionIndex": 2
}
]
}
Sample Output :
{
"_id": ObjectId("56f8dd19e4b0365115927b0f"),
"contentId": "cbc91805-2faa-4eff-8f84-02547173c152",
"contentMetaData": [
{
"_id": "1574b58f-b7fa-4cd5-b34f-98beeb657c97",
"name": "text_content",
"attributes": [],
"children": [
{
"_id": "97340ecf-fdbd-41e5-a6b2-01cc542f16ee",
"name": "text_note",
"value": "abc",
"type": "java.lang.String",
"attributes": [],
"children": [],
"noOfChildren": 0,
"positionIndex": 1
}
],
"noOfChildren": 2,
"positionIndex": 1
},
{
"_id": "4e8ef7c9-cffd-4b36-9109-89b263dff3c8",
"name": "content_flag",
"value": "true",
"type": "java.lang.String",
"attributes": [],
"children": [],
"noOfChildren": 0,
"positionIndex": 2
}
]
}
I attempted using $map but it didn't work. I tried using $unwind, but was unable to $push the data back, in the desired format.
Sample Mongo Code :
db.content.aggregate([
{
$project: {
_id: 1,
contentId: 1,
contentMetaData: 1
tempMetaData: "$contentMetaData"
}
},
{
$unwind: "$contentMetaData"
},
{
$match: {
"contentMetaData.name": "content_flag"
}
},
{
$project: {
_id: 1,
contentId: 1,
contentMetaData: "$tempMetaData",
content_flag_value: "$contentMetaData.value"
}
},
{
$project: {
_id: 1,
contentId: 1,
contentMetaData: 1,
tempMetaData: "$contentMetaData",
content_flag_value: 1
}
},
{
$unwind: "$contentMetaData"
},
{
$match: {
"contentMetaData.name": "text_content"
}
},
{
$project: {
_id: 1,
contentId: 1,
contentMetaData: 1,
tempMetaData: "$contentMetaData",
content_flag_value: 1,
text_content : "$contentMetaData.children",
temp_text_content: "$text_content"
}
},
{
$unwind: "$text_content"
},
{
$group:{
_id:"$_id",
contentId:{$first:"$contentId"},
text_content:
{$max:
{$cond:
[
{$eq: ["$content_flag_value", "true"]},
{$cond:
[{$or:[
{$eq: ["$text_content.name","wk_link_url"]},
{$eq: ["$text_content.name","wk_link_description"]}
]},
"$text_content",
null]
},
null
]
}
},
contentMetaData:{$first:"$contentMetaData"}
}
},
{
$group:{
_id:"$_id",
contentId:{$first:"$contentId"},
contentMetaData:{$push:{"text_content":"$text_content"}}
}
},
{
$project: {
_id: 0,
contentId: 1,
contentMetaData: 1
}
}]).pretty()
I'm new to Mongo. Can somebody help me out with this?
You can try the below aggregation.
$map in combination with $setDifference to extract text_content and content_flag array.
$unwind to content_flag document.
$map to keep the current values in text_content and $map in combination with $setDifference to filter the children on the criteria.
$setUnion to join back the text_content and content_flag array into contentMetaData
db.collection.aggregate({
$project: {
_id: 1,
contentId: 1,
text_content: {
"$setDifference": [{
"$map": {
"input": "$contentMetaData",
"as": "text",
"in": {
"$cond": [{
$eq: ['$$text.name', "text_content"]
},
"$$text",
false
]
}
}
},
[false]
]
},
content_flag: {
"$setDifference": [{
"$map": {
"input": "$contentMetaData",
"as": "content",
"in": {
"$cond": [{
$eq: ['$$content.name', "content_flag"]
},
"$$content",
false
]
}
}
},
[false]
]
}
}
}, {
$unwind: "$content_flag"
}, {
$project: {
"_id": 1,
contentId: 1,
"contentMetaData": {
$setUnion: [{
$map: {
input: "$text_content",
as: "text",
in: {
"_id": "$$text._id",
"name": "$$text.name",
"attributes": "$$text.attributes",
"noOfChildren": "$$text.noOfChildren",
"positionIndex": "$$text.positionIndex",
"children": {
"$setDifference": [{
"$map": {
"input": "$$text.children",
"as": "child",
"in": {
"$cond": [{
"$cond": [{
$eq: ["$content_flag.value", "true"]
}, {
$eq: ["$$child.name", "text_note"]
}, {
$eq: ["$$child.name", "text_description"]
}]
},
"$$child",
false
]
}
}
},
[false]
]
}
}
}
},
["$content_flag"]
]
}
}
})
Update:
$map in combination with $setDifference to extract content_flag array.
$unwind to content_flag document.
$redact to go through a document level at a time and look for name field recursively and perform $$DESCEND and $$PRUNE on the criteria.
$project to format the final response.
db.collection.aggregate({
$project: {
_id: 1,
contentId: 1,
contentMetaData: 1,
content_flag: {
"$setDifference": [{
"$map": {
"input": "$contentMetaData",
"as": "content",
"in": {
"$cond": [{
$eq: ['$$content.name', "content_flag"]
},
"$$content",
false
]
}
}
},
[false]
]
}
}
}, {
$unwind: "$content_flag"
}, {
$redact: {
$cond: [{
$or: [{
$eq: ["$name", "text_content"]
}, {
$not: "$name"
}, {
$eq: ["$name", "content_flag"]
}, {
$and: [{
$eq: ["$name", "text_note"]
}, {
$eq: ["$$ROOT.content_flag.value", "true"]
}]
}, {
$and: [{
$eq: ["$name", "text_description"]
}, {
$eq: ["$$ROOT.content_flag.value", "false"]
}]
}]
},
"$$DESCEND",
"$$PRUNE"
]
}
}, {
$project: {
_id: 1,
contentId: 1,
contentMetaData: 1
}
});