Project Fields in Array of Array Objects in Mongodb - mongodb

I have this document:
[
{
"name": "Report1",
"specifications": [
{
"parameters": [
{
"name": "feature",
"value": [
"13"
]
},
{
"name": "security",
"value": [
"XXXX-695"
]
},
{
"name": "imageURL",
"value": [
"football.jpg"
],
}
]
}
]
},
{
"name": "Report2",
"specifications": [
{
"parameters": [
{
"name": "feature",
"value": [
"67"
]
},
{
"name": "imageURL",
"value": [
"basketball.jpg"
],
},
{
"name": "security",
"value": [
"XXXX-123"
]
}
]
}
]
}
]
I want to obtain specifications[0].parameters.value[0] where parameters.name = "imageUrl". Like that:
[
{
"imageparam": "football.jpg",
"name": "Report1"
},
{
"imageparam": "basketball.jpg",
"name": "Report2"
}
]
I use MongoDB 3.6.3 with MongoDB Compass. I want to use aggregation (to add a pipeline in MongoDb Compass) so I could write finally this aggregation but it has 5 $project stage. Is there any more efficient or better solution:
db.collection.aggregate([{$project: {
_id: 0,
name: 1,
specifications: {$arrayElemAt: ["$specifications", 0]}
}}, {$project: {
name: 1,
imageparam: {
$filter: {
input: '$specifications.parameters',
as: 'param',
cond: {
$eq: [
'$$param.name',
'imageURL'
]
}
}
}
}}, {$project: {
name: 1,
imageparam: {$arrayElemAt: ["$imageparam",0]}
}}, {$project: {
name: 1,
imageparam: "$imageparam.value"
}}, {$project: {
name: 1,
imageparam: {$arrayElemAt: ["$imageparam",0]}
}}])
This is playground.

You can reduce it to 2 stages, might be there will be other options as well,
pass directly $arrayElemAt of specifications.parameters to $filter input and find the matching value for imageURL
use can use $addFields or $set stage to get first element from return result
db.collection.aggregate([
{
$project: {
_id: 0,
name: 1,
imageparam: {
$arrayElemAt: [
{
$filter: {
input: { $arrayElemAt: ["$specifications.parameters", 0] },
cond: { $eq: ["$$this.name", "imageURL"] }
}
},
0
]
}
}
},
{
$addFields: {
imageparam: { $arrayElemAt: ["$imageparam.value", 0] }
}
}
])
Playground
The second option you can do it in single stage using $let to bind the variables for use in the specified expression,
db.collection.aggregate([
{
$project: {
_id: 0,
name: 1,
imageparam: {
$let: {
vars: {
param: {
$arrayElemAt: [
{
$filter: {
input: { $arrayElemAt: ["$specifications.parameters", 0] },
cond: { $eq: ["$$this.name", "imageURL"] }
}
},
0
]
}
},
in: { $arrayElemAt: ["$$param.value", 0] }
}
}
}
}
])
Playground

Related

MongoDB - Lookup match with condition array of object with string

I have two collections "datasets" and "users".
I tried to lookup datasets.assignedTo = users.id that's working fine. Also, I want to match the field of datasets.firstBillable >= users.prices.beginDate date field are matched to get the current index price value. And also check users.prices.endDate is less than or equal to users.prices.beginDate.
For example:
cgPrices: 45
https://mongoplayground.net/p/YQps9EozlAL
Collections:
db={
users: [
{
id: 1,
name: "Aravinth",
prices: [
{
beginDate: "2022-08-24T07:29:01.639Z",
endDate: "2022-08-31T07:29:01.639Z",
price: 45
}
]
},
{
id: 2,
name: "Raja",
prices: [
{
beginDate: "2022-07-25T07:29:01.639Z",
endDate: "2022-07-30T07:29:01.639Z",
price: 55
}
]
}
],
datasets: [
{
color: "braun, rose gold",
firstBillable: "2022-08-24T07:29:01.639Z",
assignedTo: 1
},
{
color: "beige, silber",
firstBillable: "2022-07-25T07:29:01.639Z",
assignedTo: 2
}
]
}
My current implementation:
db.datasets.aggregate([
{
"$lookup": {
"from": "users",
"as": "details",
let: {
assigned_to: "$assignedTo",
first_billable: "$firstBillable"
},
pipeline: [
{
"$match": {
$expr: {
"$and": [
{
"$eq": [
"$id",
"$$assigned_to"
]
},
{
"$gte": [
"$first_billable",
"$details.prices.beginDate"
]
},
{
"$lte": [
"$first_billable",
"$details.prices.endDate"
]
}
]
}
}
}
]
}
},
{
"$addFields": {
"details": 0,
"cg": {
$first: {
"$first": "$details.prices.price"
}
}
}
}
])
Output i needed:
[
{
"_id": ObjectId("5a934e000102030405000000"),
"assignedTo": 1,
"cg": 45,
"color": "braun, rose gold",
"details": 0,
"firstBillable": "2022-08-24T07:29:01.639Z"
},
{
"_id": ObjectId("5a934e000102030405000001"),
"assignedTo": 2,
"cg": 55,
"color": "beige, silber",
"details": 0,
"firstBillable": "2022-07-25T07:29:01.639Z"
}
]
https://mongoplayground.net/p/YQps9EozlAL
Concerns:
You should compare the date as Date instead of string, hence you are required to convert the date strings to Date before comparing.
In users collection, prices is an array. You need to deconstruct the array to multiple documents first before compare the date fields in price.
The query should be:
db.datasets.aggregate([
{
"$lookup": {
"from": "users",
"as": "details",
let: {
assigned_to: "$assignedTo",
first_billable: {
$toDate: "$firstBillable"
}
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$id",
"$$assigned_to"
]
}
}
},
{
$unwind: "$prices"
},
{
"$match": {
$expr: {
"$and": [
{
"$gte": [
"$$first_billable",
{
$toDate: "$prices.beginDate"
}
]
},
{
"$lte": [
"$$first_billable",
{
$toDate: "$prices.endDate"
}
]
}
]
}
}
}
]
}
},
{
"$addFields": {
"details": 0,
"cg": {
$first: "$details.prices.price"
}
}
}
])
Demo # Mongo Playground

group an array into subarrays in a project stage

I want to split the following array according to the group-value. I know I can do this using $unwind and $group. Is there any way to this in a single $project-stage?
Input
{
"_id": 1,
"some_field": "some_value",
"array": [
{
"group": "a",
"subgroup": "aa",
"value": 1
},
{
"group": "b",
"subgroup": "bb",
"value": 2
},
{
"group": "a",
"subgroup": "ab",
"value": 2
}
]
}
desired output:
{
"_id": 1,
"some_field": "some_value",
"array": [
{
"group": "a",
"values": [
{
"subgroup": "aa",
"value": 1
},
{
"subgroup": "ab",
"value": 2
}
]
},
{
"group": "b",
"values": [
{
"subgroup": "bb",
"value": 2
}
]
}
]
}
Try this: https://mongoplayground.net/p/pFn3tLtAG4D
$set: {
_id: "$_id",
some_field: "$some_field",
array: {
$map: {
input: {
$setUnion: [
"$array.group"
]
},
in: {
group: "$$this",
values: {
$map: {
input: {
$filter: {
input: "$array",
as: "elem",
cond: {
$eq: [
"$$elem.group",
"$$this"
]
}
}
},
as: "vals",
in: {
subgroup: "$$vals.subgroup",
value: "$$vals.value"
}
}
}
}
}
}
}
This is far from a single project stage, but it does produce the desired output from the given input.
db.collection.aggregate([
{'$match': {'_id': 1}},
{'$unwind': '$array'},
{'$project': {'array': {'group': '$array.group', 'values': '$array'},
'some_field': 1,
'my_id': '$_id'}},
{'$unset': 'array.values.group'},
{'$group': {'_id': '$array.group',
'values': {'$push': '$array.values'},
'some_field': {'$first': '$some_field'},
'my_id': {'$first': '$my_id'}}},
{'$set': {'array': {'group': '$_id', 'values': '$values'}}},
{'$unset': 'values'},
{'$group': {'_id': '$my_id',
'array': {'$push': '$array'},
'some_field': {'$first': '$some_field'}}}
])
Try it on mongoplayground.net.
It is doable, it's definitely not clean or sexy.
My approach is to use $reduce and $mergeObjects, we'll iterate over the array and keep reconstructing the result.
The main issue that plagues this approach is this feature that doesn't allow to $concatArrays expressions, so we have to use some very ugly workarounds.
Anyways here is how you can achieve this:
db.collection.aggregate([
{
$project: {
_id: 1,
some_field: 1,
array: {
$map: {
input: {
"$objectToArray": {
$reduce: {
input: "$array",
initialValue: {},
in: {
"$mergeObjects": [
"$$value",
{
"$arrayToObject": [
[
{
k: "$$this.group",
v: {
$map: {
input: {
"$concatArrays": [
[
"$$this"
],
{
$map: {
input: {
$filter: {
input: {
"$objectToArray": "$$value"
},
as: "filterItem",
cond: {
$eq: [
"$$filterItem.k",
"$$this.group"
]
}
}
},
as: "mapItem",
in: "$$mapItem.v"
}
},
]
},
as: "map2Item",
in: {
$cond: [
{
"$isArray": "$$map2Item"
},
{
$arrayElemAt: [
"$$map2Item",
0
]
},
"$$map2Item"
]
}
}
}
}
]
]
}
]
}
}
}
},
as: "item",
in: {
group: "$$item.k",
values: "$$item.v"
}
}
}
}
}
])
Mongo Playground

MongoDB compare endTime with startTime of next document

I have a similar collection where I have sort them by their startTime:
{"name": 'A', "startTime": '1634626355', "endTime": '1634631405'}
{"name": 'A', "startTime": '1634631406', "endTime": '1634631864'}
{"name": 'A', "startTime": '1634631865', "endTime": '1634656048'}
{"name": 'A', "startTime": '1634712642', "endTime": '1634718856'}
How can I compare the documents such that if the document endTime and the next document startTime duration is less than 5 minutes, merge it.
This is the result I'm trying to achieve (The 1st 3 documents are merged into 1 where it uses the startTime of the 1st document and the endTime of the 3rd document):
{"name": 'A', "startTime": '1634626355', "endTime": '1634656048'}
{"name": 'A', "startTime": '1634712642', "endTime": '1634718856'}
Thanks
First of all, you should never store date/time values as string, it's a design flaw. Store always proper Date object.
This solution works without self-lookup, so it may perform better:
db.collection.aggregate([
{
$set: {
startDateTime: { $toDate: { $multiply: ["$startTime", 1000] } },
endDateTime: { $toDate: { $multiply: ["$endTime", 1000] } }
},
},
{ $sort: { startDateTime: 1 } },
{ $group: { _id: null, data: { $push: "$$ROOT" } } },
{
$set: {
data: {
$reduce: {
input: "$data",
initialValue: [],
in: {
$cond: {
if: {
$or: [
{ $eq: [{ $size: "$$value" }, 0] }, // for the initail element
{
$gt: [
{
$dateDiff: { // calculate difference
endDate: "$$this.startDateTime",
startDate: { $last: "$$value.endDateTime" },
unit: "minute"
}
},
5 // more than 5 Minutes
]
}
]
},
then: { $concatArrays: ["$$value", ["$$this"]] }, // append new element
else: {
$map: {
input: "$$value",
as: "data",
in: {
$cond: {
if: { $eq: ["$$data._id", { $last: "$$value._id" }] }, // find last element
then: { // update last element
$mergeObjects: [
"$$data",
{ endDateTime: "$$this.endDateTime" },
{ endTime: "$$this.endTime" }
]
},
else: "$$data"
}
}
}
}
}
}
}
}
}
},
// some cosmetic
{ $unwind: "$data" },
{ $replaceRoot: { newRoot: "$data" } }
])
Mongo Playground
You can use $lookup in an aggregation pipeline to find out the documents that you need to remove. Then, perform a forEach to remove them.
db.collection.aggregate([
{
$addFields: {
endDateTime: {
"$toDate": {
"$multiply": [
{
$toLong: "$endTime"
},
1000
]
}
}
},
},
{
"$lookup": {
"from": "collection",
let: {
end: "$endDateTime"
},
pipeline: [
{
"$addFields": {
startDateTime: {
"$toDate": {
"$multiply": [
{
$toLong: "$startTime"
},
1000
]
}
}
}
},
{
$match: {
$expr: {
$and: [
{
$lte: [
{
$subtract: [
"$startDateTime",
"$$end"
]
},
300000
]
},
{
$lte: [
"$$end",
"$startDateTime"
]
}
]
}
}
}
],
"as": "lessThan5min"
}
},
{
"$unwind": "$lessThan5min"
},
{
"$replaceRoot": {
"newRoot": "$lessThan5min"
}
}
]).forEach(function(doc){
db.collection.remove({ "_id": doc._id });
});
Here is the Mongo playground to find out the documents that you need to remove for your reference.

MongoDb Create Aggregate Create query

I have 3 table users,shifts,temporaryShifts,
shifts:[{_id:ObjectId(2222),name:"Morning"},{_id:ObjectId(454),name:"Night"}]
users:[{_id:ObjectId(123),name:"Albert",shift_id:ObjectId(2222)}]
temporaryShifts:[
{_id:2,userId:ObjectId(123),shiftId:ObjectId(454),type:"temporary",date:"2020-02-01"},
{_id:987,userId:ObjectId(123),shiftId:ObjectId(454),type:"temporary",date:"2020-02-03"},
{_id:945,userId:ObjectId(123),shiftId:ObjectId(454),type:"temporary",date:"2020-02-08"},
{_id:23,userId:ObjectId(123),shiftId:ObjectId(454),date:"2020-02-09"}]
i want to make a mongoose aggregate query then give me result :
get result between two dates for example :2020-02-01 2020-02-05,
resullts is :
[
{_id:ObjectId(123),name:"Albert",shift:[
{_id:2,shiftId:ObjectId(454),type:"temporary",date:"2020-02-01"},
{_id:2,shiftId:ObjectId(2222),type:"permanent",date:"2020-02-02"},
{_id:2,shiftId:ObjectId(454),type:"temporary",date:"2020-02-03"},
{_id:2,shiftId:ObjectId(2222),type:"permanent",date:"2020-02-04"},
{_id:2,shiftId:ObjectId(2222),type:"permanent",date:"2020-02-05"},
]}
]
in result type temporary mean selected date in table temporaryShift document available else type permanent
MongoPlayGround You Can edit
You can first project a date range array using $range, in your example it will be like [2020-02-01, 2020-02-02, 2020-02-03, 2020-02-04, 2020-02-05], then you can use the array to perform $lookup
db.users.aggregate([
{
$limit: 1
},
{
"$addFields": {
"startDate": ISODate("2020-02-01"),
"endDate": ISODate("2020-02-05")
}
},
{
"$addFields": {
"dateRange": {
"$range": [
0,
{
$add: [
{
$divide: [
{
$subtract: [
"$endDate",
"$startDate"
]
},
86400000
]
},
1
]
}
]
}
}
},
{
"$addFields": {
"dateRange": {
$map: {
input: "$dateRange",
as: "increment",
in: {
"$add": [
"$startDate",
{
"$multiply": [
"$$increment",
86400000
]
}
]
}
}
}
}
},
{
"$unwind": "$dateRange"
},
{
"$project": {
"name": 1,
"shiftId": 1,
"dateCursor": "$dateRange"
}
},
{
"$lookup": {
"from": "temporaryShifts",
"let": {
dateCursor: "$dateCursor",
shiftId: "$shiftId"
},
"pipeline": [
{
"$addFields": {
"parsedDate": {
"$dateFromString": {
"dateString": "$date",
"format": "%Y-%m-%d"
}
}
}
},
{
$match: {
$expr: {
$and: [
{
$eq: [
"$$dateCursor",
"$parsedDate"
]
}
]
}
}
}
],
"as": "temporaryShiftsLookup"
}
},
{
"$unwind": {
path: "$temporaryShiftsLookup",
preserveNullAndEmptyArrays: true
}
},
{
$project: {
shiftId: 1,
type: {
"$ifNull": [
"$temporaryShiftsLookup.type",
"permanent"
]
},
date: "$dateCursor"
}
}
])
Here is the Mongo Playground for your reference.

Mongo Query to fetch distinct nested documents

I need to fetch distinct nested documents.
Please find the sample document:
{
"propertyId": 1001820437,
"date": ISODate("2020-07-17T00:00:00.000Z"),
"HList":[
{
"productId": 123,
"name": "Dubai",
"tsh": true
}
],
"PList":[
{
"productId": 123,
"name": "Dubai",
"tsh": false
},
{
"productId": 234,
"name": "India",
"tsh": true
}
],
"CList":[
{
"productId": 234,
"name": "India",
"tsh": false
}
]
}
Expected result is:
{
"produts":[
{
"productId": 123,
"name": "Dubai"
},
{
"productId": 234,
"name": "India"
}
]
}
I tried with this query:
db.property.aggregate([
{
$match: {
"propertyId": 1001820437,
"date": ISODate("2020-07-17T00:00:00.000Z")
}
},
{
"$project": {
"_id": 0,
"unique": {
"$filter": {
"input": {
"$setDifference": [
{
"$concatArrays": [
"$HList.productId",
"$PList.productId",
"$CList.productId"
]
},
[]
]
},
"cond": {
"$ne": [ "$$this", "" ]
}
}
}
}
}
]);
Is $setDifference aggregation is correct choice here?
My query returns only unique product ids but i need a productId with name.
Could someone help me to solve this?
Thanks in advance
You can use $projectfirst to get rid of tsh field and then run $setUnion which ignores duplicated entries:
db.collection.aggregate([
{
$project: {
"HList.tsh": 0,
"PList.tsh": 0,
"CList.tsh": 0,
}
},
{
$project: {
products: {
$setUnion: [ "$HList", "$PList", "$CList" ]
}
}
}
])
Mongo Playground
The following two aggregations return the expected and same result (you can use any of the two):
db.collection.aggregate( [
{
$project: {
_id: 0,
products: {
$reduce: {
input: { $setUnion: [ "$HList", "$PList", "$CList" ] },
initialValue: [],
in: {
$setUnion: [ "$$value", [ { productId: "$$this.productId", name: "$$this.name" } ] ]
}
}
}
}
}
] )
This one is little verbose:
db.collection.aggregate( [
{
$project: { list: { $setUnion: [ "$HList", "$PList", "$CList" ] } }
},
{
$unwind: "$list"
},
{
$group: {
_id: null,
products: { $addToSet: { "productId": "$list.productId", "name": "$list.name" } }
}
},
{
$project: { _id: 0 }
}
] )
db.collection.aggregate([
{
$match: {
"propertyId": 1001820437,
"date": ISODate("2020-07-17T00:00:00.000Z")
}
},
{
$project: {
products: {
$filter: {
input: { "$setUnion" : ["$CList", "$HList", "$PList"] },
as: 'product',
cond: {}
}
}
}
},
{
$project: {
"_id":0,
"products.tsh": 1,
"products.name": 1,
}
},
])