Combine $facet result with subdocument and then conditionally excluding document - mongodb

Suppose after the $facet stage I have a result including two arrays: roomInfo and hotelInfo.
Which looks like this:
{
"roomInfo": [
{
_id: 'ab1',
booked: 3
},
{
_id: 'ab2',
booked: 1
}
],
"hotelInfo": [
{
name: 'Radison Blue',
roomDetails: [
{
_id: 'ab1',
roomCount: 5
},
{
_id: 'xy1',
roomCount: 5
}
]
},
{
name: 'Intercontinental',
roomDetails: [
{
_id: 'ab2',
roomCount: 5
}
]
}
]
};
Expected Result
I want an output like this:
[
{
name: 'Radison Blue',
roomDetails: [
{
_id: 'ab1',
roomCount: 5,
booked: 3
},
{
_id: 'xy1',
roomCount: 5,
booked: 0
}
]
},
{
name: 'Intercontinental',
roomDetails: [
{
_id: 'ab2',
roomCount: 5,
booked: 1
}
]
}
];
Basically, adding the booked property from roomInfo into the hotelInfo's roomDetails field after matching their ids.
Additionally, after getting the above output result I want to exclude those hotels on which all the rooms(not for a single room) have the value of fields roomCount and booked equal. I want to do this inside the aggregation pipeline stage as I will have to deal with $skip and $limit later on.
How to achieve these use cases?
Thanks!

Basically the approach will be iterating over the hotels and matching each room accordingly, here is a quick working code sample:
db.collection.aggregate([
{
$unwind: "$hotelInfo"
},
{
$project: {
name: "$hotelInfo.name",
"roomDetails": {
$filter: {
input: {
$map: {
input: "$hotelInfo.roomDetails",
as: "info",
in: {
"$mergeObjects": [
"$$info",
{
"$arrayElemAt": [
{
$filter: {
input: "$roomInfo",
as: "room",
cond: {
$eq: [
"$$room._id",
"$$info._id"
]
}
}
},
0
]
}
]
}
}
},
as: "proccessedInfo",
cond: {
$ne: [
"$$proccessedInfo.roomCount",
"$$proccessedInfo.booked"
]
}
}
}
}
}
])
Mongo Playground
With that said you mention you'd like to paginate calls in the future. the current approach does not seem scaleable, because these are "real data points" aka hotels it's fine if your scale is somewhat capped ( no more than several thousands hotels ). but if it's not I recommend you ask another question with the entire pipeline you have so we can adjust it to work better.

Related

How to update a property of the last object of a list in mongo

I would like to update a property of the last objet stored in a list in mongo. For performance reasons, I can not pop the object from the list, then update the property, and then put the objet back. I can not either change the code design as it does not depend on me. In brief am looking for a way to select the last element of a list.
The closest I came to get it working was to use arrayFilters that I found doing research on the subject (mongodb core ticket: https://jira.mongodb.org/browse/SERVER-27089):
db.getCollection("myCollection")
.updateOne(
{
_id: ObjectId('638f5f7fe881c670052a9d08')
},
{
$set: {"theList.$[i].propertyToUpdate": 'NewValueToAssign'}
},
{
arrayFilters: [{'i.type': 'MyTypeFilter'}]
}
)
I use a filter to only update the objets in theList that have their property type evaluated as MyTypeFilter.
What I am looking for is something like:
db.getCollection("maCollection")
.updateOne(
{
_id: ObjectId('638f5f7fe881c670052a9d08')
},
{
$set: {"theList.$[i].propertyToUpdate": 'NewValueToAssign'}
},
{
arrayFilters: [{'i.index': -1}]
}
)
I also tried using "theList.$last.propertyToUpdate" instead of "theList.$[i].propertyToUpdate" but the path is not recognized (since $last is invalid)
I could not find anything online matching my case.
Thank you for your help, have a great day
You want to be using Mongo's pipelined updates, this allows us to use aggregation operators within the update body.
You do however need to consider edge cases that the previous answer does not. (null list, empty list, and list.length == 1)
Overall it looks like so:
db.collection.update({
_id: ObjectId("638f5f7fe881c670052a9d08")
},
[
{
$set: {
list: {
$concatArrays: [
{
$cond: [
{
$gt: [
{
$size: {
$ifNull: [
"$list",
[]
]
}
},
1
]
},
{
$slice: [
"$list",
0,
{
$subtract: [
{
$size: "$list"
},
1
]
}
]
},
[]
]
},
[
{
$mergeObjects: [
{
$ifNull: [
{
$last: "$list"
},
{}
]
},
{
propertyToUpdate: "NewValueToAssign"
}
]
}
]
]
}
}
}
])
Mongo Playground
One option is to use update with pipeline:
db.collection.update(
{_id: ObjectId("638f5f7fe881c670052a9d08")},
[
{$set: {
theList: {
$concatArrays: [
{$slice: ["$theList", 0, {$subtract: [{$size: "$theList"}, 1]}]},
[{$mergeObjects: [{$last: "$theList"}, {propertyToUpdate: "NewValueToAssign"}]}]
]
}
}}
]
)
See how it works on the playground example

how to set particular field in array of object in mongodb aggregation or pipeline query along with conditional update of the other field in document?

{
id:1,
projectName:'Project1'
start:"17-04-2022",
end:"12-05-2020",
tasks:[
{
taskId:1,
taskName:'first task',
taskStart:'20-04-2022',
taskEnd:'25-04-2022'
},
{
taskid:2,
taskName:'second task',
taskStart:'25-04-2022',
taskEnd:'30-04-2022'
},
]
},
{
id:2,
projectName:'Project2'
start:"27-04-2022",
end:"22-05-2020",
tasks:[
{
taskId:1,
taskName:'first task',
taskStart:'30-04-2022',
taskEnd:'12-05-2022'
},
{
taskid:2,
taskName:'second task',
taskStart:'28-04-2022',
taskEnd:'15-05-2022'
},
]
}
how to update field "taskEnd" of tasks array with document id=1 and taskID=1 AND while updating if data from the frontend which is used to update the "taskEnd" exceeds "end" field, update the "end" field also with same value.
Thanks in Advance,
I know how to update specific field for the array of objects and conditional update of field with two separate queries.
can it be done with single query?
You can do this with a pipeline update instead of using an update document.
I'm assuming you are getting the input date in the same format as the other dates.
var input_date = "11-05-2020";
db.collection.update({
id: 1
},
[
{
$addFields: {
end: {
$cond: [
{
$gt: [
{
$dateFromString: {
dateString: "$end"
}
},
{
$dateFromString: {
dateString: input_date
}
}
]
},
"$end",
input_date
]
},
tasks: {
$map: {
input: "$tasks",
as: "elem",
in: {
$cond: [
{
$eq: [
"$$elem.taskId",
1
]
},
{
$setField: {
field: "taskEnd",
input: "$$elem",
value: input_date
}
},
"$$elem"
]
}
}
}
}
}
])

MongoDB push existing field value to another field in an array of objects

If I have a document like this:
db.people.insertOne({name: "Annie", latestScore: 5});
Then based on this answer, I am able to move latestScore to an array field like this:
db.people.updateOne(
{name: 'Annie'},
[
{$set:
{scoreHistory:
{$concatArrays: [
{$ifNull: ["$scoreHistory", []]},
["$latestScore"]
]}
}
},
{ $unset: ["latestScore"] }
]
);
This is the resulting document:
{
_id: ObjectId("61d2737611e48e0d0c30b35b"),
name: 'Annie',
scoreHistory: [ 5 ]
}
Can we perform the same update to objects nested in an array? For example, if the document is like this:
db.people.insertOne({
name: 'Annie',
words: [
{word: "bunny", score: 5},
{word: "pink", score: 5}
]
});
How can we update it to this:
{
name: 'Annie',
words: [
{word: "bunny", scoreHistory: [5]},
{word: "pink", scoreHistory: [5]}
]
}
I know I can iterate and modify the array from the app and update the whole array at once, but I would like to do it using operators like in the first example above.
The website first displays words.word and words.score in rows, clicking on a row shows words.scoreHistory in a popup. I am expecting words array to be less than 500. Any advise on remodelling the schema to simplify the above operation is also welcome!
db.collection.update({
name: "Annie"
},
[
{
$set: {
words: {
$map: {
input: "$words",
as: "m",
in: {
word: "$$m.word",
scoreHistory: {
$concatArrays: [
{
$ifNull: [
"$$m.scoreHistory",
[]
]
},
[
"$$m.score"
]
]
}
}
}
}
}
}
])
mongoplayground

MongoDB query $group is returning null

I am learning MongoDb query and my requirement is to calculate the average time between two dates. I wrote a mongoDB query with project and group stages.
{
project: {
OrderObject:1,
date:1
}
},
{
group: {
objectId: '$OrderObject.pharmacy.companyName',
count: {
$sum: 1
},
duration: {
$avg: {
$abs: {
$divide: [
{
$subtract: [
{
$arrayElemAt: [
'$date',
0
]
},
{
$arrayElemAt: [
'$date',
1
]
}
]
},
60000
]
}
}
},
OrderIDs: {
$addToSet: '$OrderObject.orderID'
},
pharmacyName: {
$addToSet: '$OrderObject.pharmacy.companyName'
},
}
}
The output I get is
{
count: 3,
duration: 54.53004444444445,
OrderIDs: [ 'ABCDE', 'EWQSE', 'ERTRE' ],
pharmacyName: [ 'pharmacy business Name' ],
objectId: null
},
Can someone please tell me why objectId is null in this case but the value is printed in pharmacyName field. I am using this pipeline in parse server as query.aggregate(pipeline, {useMasterKey:true})
The my expectation is pharmacyName === objectId
Most probably your nested element here is with different name:
OrderObject.name.companyName
but this is not an issue for the $group stage since it make the aggregation for all elements( in total 3) in the collection when the _id is null and do not give you any errors ...
It is also interesing why in your output the "pharmacyName" appear simply as "name" ? ;)

MongoDB retrun array of Items from other arrays, with reduced datapoints

I have a user object that looks (roughly) like the example below - a user containg Arrays of Objects for "autos" "boats" and "planes" - each object COULD have an image URL, but many do not, I want an array of ALL the image URLs form a user (regardless of which group they are in) and I want SOME but NOT ALL of the object that is associated with that image.
DATA MODEL
{
user: STRING,
autos: [
{
make: STRING,
model: STRING,
year: NUMBER,
price: NUMBER,
image: STRING
}
],
boats: [
{
make: STRING,
model: STRING,
year: NUMBER,
price: NUMBER,
image: STRING
}
],
planes: [
{
make: STRING,
model: STRING,
year: NUMBER,
price: NUMBER,
image: STRING
}
]
}
This is a reduced "object" in each of these arrays, the real data contains many points that are specific to each of these TYPES, and I cannot change the model at this point.
So I am able to pull back an array of Image URLs from the arrays, and concat them into a single array, But then I just have URLS... what I want is the "make" and "model" and "image URL" for each
Here is the Query I've got
query = [
{
$match: matchCriteria
},
{
$project: {
_id: 0,
image: {
$filter: {
input: {
$concatArrays: [
'$autos.image',
'$boats.image',
'$planes.image'
]
},
cond: {
$and: [ // skip records without image data
{ $eq: [{ $type: "$$this" }, "string"] },
{ $ne: ["$$this", ""] }
]
}
}
}
}
}
];
this gives me data looking like this
"data": [
"https://www.yourPics.com/see?image=image4",
"https://www.yourPics.com/see?image=image5",
"https://www.yourPics.com/see?image=image6",
"https://www.yourPics.com/see?image=image1",
"https://www.yourPics.com/see?image=image2",
"https://www.yourPics.com/see?image=image3",
"https://www.yourPics.com/see?image=image7",
"https://www.yourPics.com/see?image=image8",
"https://www.yourPics.com/see?image=image9",
"https://www.yourPics.com/see?image=image10",
"https://www.yourPics.com/see?image=image11"
],
But I WANT something that looks like this
"data": [
{
make:"some make4",
model:"some model4",
image: "https://www.yourPics.com/see?image=image4"
},
{
make:"some make5",
model:"some model5",
image: "https://www.yourPics.com/see?image=image5"
},
{
make:"some make6",
model:"some model6",
image: "https://www.yourPics.com/see?image=image6"
},
...
],
Any help is greatly appreciated.
I'm not sure why you're concating just the image attribute of those objects. Doing that, you will certainly end up with just an array of images. Why not concat the entire arrays? As in:
$concatArrays: ["$autos", "$boats"]
So I have put this Playground together for you. Using this project:
{
$project: {
items: {
$filter: {
input: {
$concatArrays: [
"$autos",
"$boats"
]
},
as: "items",
cond: {
$and: [
{
$ne: [
"$$items.image",
undefined
]
},
{
$ne: [
"$$items.image",
""
]
}
]
}
}
}
}
}
You will get this result:
[
{
"_id": ObjectId("5a934e000102030405000000"),
"items": [
{
"image": "https://pont.com",
"make": "Pontiac",
"model": "Sunfire",
"year": "2004"
},
{
"image": "https://aluma.com",
"make": "Aluma",
"model": "X4",
"price": "100,000",
"year": "2021"
}
]
}
]
From here, excluding the keys you don't want (e.g year, price) is a trivial matter. Just project only what you want:
{
$project: {
_id: 0,
"items.make": 1,
"items.model": 1,
"items.image": 1
}
}
Playground: https://mongoplayground.net/p/qFIwLhuXR84