Mongo push objects - arrays

I want to push an object to specify name of fields rather than array. I tried $push but I lose informations about field's name inserted in the array.
My collection is :
/* 1 */
{
"_id" : ObjectId("57614a7bd75df17df3013903"),
"O":"aa",
"D":"bb",
"month":1,
"year":2015,
"freq":5
}
/* 2 */
{
"_id" : ObjectId("57614a7bd75df17df3013904"),
"O":"aa",
"D":"bb",
"month":2,
"year":2015,
"freq":5
}
/* 3 */
{
"_id" : ObjectId("57614a7bd75df17df3013905"),
"O":"aa",
"D":"bb",
"month":1,
"year":2016,
"freq":5
}
I want to store all freq corresponding to fields : O and D.
Here is my expected output :
"_id" : ...,
"O" : "aa",
"D" : "bb",
"freq" : {
"2015" : {
"1" : 5,
"2":5
},
"2016" : {
"1" : 5
}
}
}
I tried this :
db.collection.aggregate([
{
'$group':
{
_id:{"O":"$O","D":"$D","Y":"$year"},
"freq" :{$push: "$freq"}
}
},
{
'$group':
{
_id:{"O":"$O","D":"$D"},
"freq" :{$push: "$freq"}
}
})]
but I got an array without informations of year or month.
Thank you

You have used two $group in your query
Your First group query is enough to build the data which you are expecting.
If we are executing the first query
db.stackoverflow.aggregate([
{
'$group':
{
_id:{"O":"$O","D":"$D","Y":"$year"},
"freq" :{$push: "$freq"}
}
}]);
then the result is
{ "_id" : { "O" : "aa", "D" : "bb", "Y" : 2016 }, "freq" : [ 5 ] }
{ "_id" : { "O" : "aa", "D" : "bb", "Y" : 2015 }, "freq" : [ 5, 5 ] }
Now if you execute your second $group query
db.stackoverflow.aggregate([
{
'$group':
{
_id:{"O":"$O","D":"$D"},
"freq" :{$push: "$freq"}
}
}])
then the result is
{ "_id" : { "O" : "aa", "D" : "bb" }, "freq" : [ 5, 5, 5 ] }
Reason:
The values fetched in the first $group query is not passed to the second $group query.
Solution:
Use $project available in the aggregation pipeline which passes along the documents with only the specified fields to the next stage in the aggregation pipeline. The specified fields can be existing fields from the input documents or newly computed fields.
https://docs.mongodb.com/manual/reference/operator/aggregation/project/
Here is the query to get your expected result
db.collection.aggregate([
{
'$group': {
_id: {
"o": "$o",
"d": "$d",
"year": "$year"
},
myArr: {
$push: {
year: "$year",
month: "$month",
freq: "$freq"
}
}
}
},
{
'$group': {
_id: {
"o": "$o",
"d": "$d"
},
myArr1: {
$push: {
year: "$year",
freq: "$myArr"
}
}
}
},
],
{
allowDiskUse: true
})

Related

How to update an array and pull a nested element from same array

With the following document:
{
"_id" : "123",
"firstArray" : [
{
"_id" : "456",
"status" : "open",
"nestedArray" : [
{
"_id" : "100",
"quantity" : 10
},
{
"_id" : "101",
"quantity" : 10
},
{
"_id" : "102",
"quantity" : 10
}
},
{
"_id" : "789",
"status" : "open",
"nestedArray" : [
{
"_id" : "200",
"quantity" : 10
},
{
"_id" : "201",
"quantity" : 10
},
{
"_id" : "202",
"quantity" : 10
}
}
]
}
How can I update the quantity by 20 of the nested ID 101 element and pull the one with the ID 201 from the same MongoDB query ?
I am trying to do that in Java with $set and $pull operator and I'm stuck with the following error:
[BulkWriteError{index=0, code=40, message='Update created a conflict
at 'firstArray.0.nestedArray'', details={}}]
MongoDB doesn’t allow multiple operations on the same property in the same update call. This means that the two operations must happen in two individual queries.
The first solution is you can write 2 seperate queries for both the operations.
The second solution is you can try update with aggregation pipeline, starting from MongoDB 4.2,
$map to iterate loop of firstArray
$filter to iterate loop of nestedArray and remove _id: "201" record
$map to iterate loop of above filtered nestedArray
$cond check condition if _id: "101" then return new quantity otherwise return current
$mergeObjects to merge current object with updated properties
db.collection.update(
{ "firstArray.nestedArray._id": "101" },
[{
$set: {
firstArray: {
$map: {
input: "$firstArray",
in: {
$mergeObjects: [
"$$this",
{
nestedArray: {
$map: {
input: {
$filter: {
input: "$$this.nestedArray",
cond: { $ne: ["$$this._id", "201"] }
}
},
in: {
_id: "$$this._id",
quantity: {
$cond: [
{ $eq: ["$$this._id", "101"] },
20,
"$$this.quantity"
]
}
}
}
}
}
]
}
}
}
}
}
])
Playground

Find all matching elements in the array

Can someone please help me with this query ??
Query >>> Find all warehouses that keep item "Planner" and having in-stock quantity less than 20
This is the sample document in the items collection of the Inventory database :
{
"_id" : ObjectId("6067640da9a907175caaca34"),
"id" : 101,
"name" : "Planner",
"status" : "A",
"height" : 12,
"tags" : [
"mens",
"womens"
],
"warehouses" : [
{
"name" : "Phoenix",
"quantity" : 25
},
{
"name" : "Quickshift",
"quantity" : 15
},
{
"name" : "Poona",
"quantity" : 10
}
]
}
This is what I have tried doing :
db.items.find({"name":"Planner","warehouses.quantity":{"$lt":20}},{"warehouses":1,"_id":0}).pretty()
But it gives me the result as
{
"warehouses" : [
{
"name" : "Phoenix",
"quantity" : 25
},
{
"name" : "Quickshift",
"quantity" : 15
},
{
"name" : "Poona",
"quantity" : 10
}
]
}
Demo - https://mongoplayground.net/p/IpD5ypWSZyt
Use aggregation query
db.collection.aggregate([
{ $match: { "name": "Planner" } },
{ $unwind: "$warehouses" }, // break into individual documents
{ $match: { "warehouses.quantity": { $lt: 20 } } }, // query the data
{ $group: { _id: "_id", warehouses: { $push: "$warehouses" } } } // join them back
])
Demo - https://mongoplayground.net/p/pdTY0IkIqgF
Use $elemMatch only if you think there will be only 1 array element matching per document
The $elemMatch operator matches documents that contain an array field with at least one element that matches all the specified query criteria.
The $elemMatch operator limits the contents of an field from the query results to contain only the first element matching the $elemMatch condition.
db.collection.find({
"name":"Planner",
"warehouses": { "$elemMatch": { "quantity": { $gt: 20 } } }
},
{ "warehouses.$": 1})
https://docs.mongodb.com/manual/reference/method/db.collection.find/#find-projection

Need help in querying mongodb

I have a a few documents that have the following structure. See attached image.
document structure
Each document includes an array of 'FileMeta' objects and each FileMeta object includes an array of 'StatusHistory' objects. I'm trying to get only the FileMetas that contain StatusCode equal to 4 and that the TimeStamp is greater than a certain datetime.
Tried the following query but it only returns the first FileMeta element of each document.
db.getCollection('Collection').find({'ExternalParams.RequestingApplication':'aaa.bbb'},
{ "FileMeta": { $elemMatch: { "StatusHistory":{ $elemMatch:{ "StatusCode": 4, "TimeStamp": { $gt: ISODate("2020-06-28T11:02:26.542Z")} } } } }} )
What am I doing wrong?
here is the document structure:
{
"_id" : ObjectId("5ef84e2ec08abf38b0043ab4"),
"FileMeta" : [
{
"StatusHistory" : [
{
"StatusCode" : 0,
"StatusDesc" : "New File",
"TimeStamp" : ISODate("2020-06-28T11:00:46.286Z")
},
{
"StatusCode" : 2,
"StatusDesc" : "stby",
"TimeStamp" : ISODate("2020-06-28T11:02:20.400Z")
},
{
"StatusCode" : 4,
"StatusDesc" : "success",
"TimeStamp" : ISODate("2020-06-28T11:02:26.937Z")
}
]
},
{
"StatusHistory" : [
{
"StatusCode" : 0,
"StatusDesc" : "New File",
"TimeStamp" : ISODate("2020-06-28T11:00:46.286Z")
},
{
"StatusCode" : 2,
"StatusDesc" : "stby",
"TimeStamp" : ISODate("2020-06-28T11:02:20.617Z")
},
{
"StatusCode" : 4,
"StatusDesc" : "success",
"TimeStamp" : ISODate("2020-06-28T11:02:26.542Z")
}
]
}
],
}
I want to return only the FileMeta objects that include a StatusHistory that match the following conditions: StatusCode = 4 and TimeStamp > SomeDateTime
Sorry for the delay, mate, I've been quite busy lately. Hope you already solved your problem. Anyway, I think that I found the solution.
As you can see on this link, the example shows that by default the $elemMatch operator returns the whole array in case of match on any element.
For instance, consider the following collection:
{ _id: 1, results: [ { product: "abc", score: 10 }, { product: "xyz", score: 5 } ] }
{ _id: 2, results: [ { product: "abc", score: 8 }, { product: "xyz", score: 7 } ] }
{ _id: 3, results: [ { product: "abc", score: 7 }, { product: "xyz", score: 8 } ] }
If you do the following query, for example:
db.survey.find(
{ results: { $elemMatch: { product: "xyz", score: { $gte: 8 } } } }
)
The output will be:
{ "_id" : 3, "results" : [ { "product" : "abc", "score" : 7 }, { "product" : "xyz", "score" : 8 } ] }
Not:
{ "_id" : 3, "results" : [{ "product" : "xyz", "score" : 8 }]}
That said, if you want to return only the document in the array that matches the specified query, you must use the db.collection.aggregate() function with the $unwind and $match operator.
The query below shall give you what you want.
Query:
db.collection.aggregate([
{"$unwind" : "$FileMeta"},
{"$unwind" : "$FileMeta.StatusHistory"},
{
"$match" : {
"FileMeta.StatusHistory.StatusCode" : 4,
"FileMeta.StatusHistory.TimeStamp" : {"$gte" : ISODate("2020-06-28T11:02:26.937Z")}
}
}
]).pretty()
Result:
{
"_id" : ObjectId("5ef84e2ec08abf38b0043ab4"),
"FileMeta" : {
"StatusHistory" : {
"StatusCode" : 4,
"StatusDesc" : "success",
"TimeStamp" : ISODate("2020-06-28T11:02:26.937Z")
}
}
}
One last tip. Consider changing your modeling to something that looks like the unwinded document, and remember that one document should be equivalent to one row in a normal relational database. So avoid storing information that should be on "several rows" on a single document.
Useful links:
The $elemMatch operator.
The $unwind operator.

Calculate weight and skip a specific element Mongodb - Aggregation Framework

I have a collection with array elements containing A,B or C values. I have to calculate a weight of each element value.
The logic of this weight is sample :
We give 1.0 as weight of the last element (dfferente to C), and others 0
If the last element of the array is C, we give it to the previous last element (different to C).
if there is 1 element in the array (A,B or C) we give it 1 as a weight.
This is how my collection looking like :
{
"_id" : ObjectId("5a535c48a4d86ed94a7e8618"),
"myArray" : [
{
"value" : "C"
},
{
"value" : "A
},
{
"value" : "C"
},
{
"value" : "B"
},
{
"value" : "A"
},
{
"value" : "A"
}
]
}
{
"_id" : ObjectId("5a535c48a4d86ed94a7e8619"),
"myArray" : [
{
"value" : "A"
},
{
"value" : "C"
},
{
"value" : "B"
},
{
"value" : "C"
},
{
"value" : "C"
}
]
}
I did some aggregation to make it happen but I can here just to affect 1 to the last (different to C)
and (if the last is C I give 1 to the last one -1) so I have to fixe the case of having C in the last and last-1 and last -2 .. last-n
so I have to fix it to affect 1 to the last one different to C
db.col.aggregate([{'$addFields':{
'myArray_temp':{
'$switch':{
'branches':[{
'case':{'$and':[{'$gt':[{'$size':'$myArray'},1]},{'$eq':[{'$arrayElemAt':['$myArray.value',-1]},'C']}]},
'then':{'$concatArrays':[
{'$map':{
'input':{'$slice':['$myArray',{'$subtract':[{'$size':'$myArray'},2]}]},
'as':'val',
'in':{'value':'$$val.value','weight':0 }
}},
[{'value':{'$arrayElemAt':['$myArray.value',-2]},'weight':1}],
[{'value':{'$arrayElemAt':['$myArray.value',-1]},'weight':0}]
]}
},
{
'case':{'$eq':[{'$size':'$myArray'},1]},
'then':{'$concatArrays':[
[{'value':{'$arrayElemAt':['$myArray.value',0]},'weight':1}]
]}
},
{
'case':{'$and':[{'$gt':[{'$size':'$myArray'},1]},{'$ne':[{'$arrayElemAt':['$myArray.value',-1]},'C']}]},
'then':{'$concatArrays':[
{'$map':{
'input':{'$slice':['$myArray',{'$subtract':[{'$size':'$myArray'},1]}]},
'as':'val',
'in':{'value':'$$val.value','weight':0 }
}},
[{'value':{'$arrayElemAt':['$myArray.value',-1]},'weight':1}]
]}
}
],
'default':{'$concatArrays':[
[{'value':{'$arrayElemAt':['$myArray.value',0]},'weight':1}]
]}
}
}
}}])
The results should be :
{
"_id" : ObjectId("5a535c48a4d86ed94a7e8618"),
"myArray" : [
{
"value" : "C",
"weight": 0
},
{
"value" : "A" ,
"weight": 0
},
{
"value" : "C",
"weight": 0
},
{
"value" : "B" ,
"weight": 0
},
{
"value" : "A",
"weight": 0
},
{
"value" : "A",
"weight": 1
}
]
}
{
"_id" : ObjectId("5a535c48a4d86ed94a7e8619"),
"total" : 4.5,
"myArray" : [
{
"value" : "A",
"weight": 0
},
{
"value" : "C",
"weight": 0
},
{
"value" : "B" ,
"weight": 1 // here we give 1 to the last differente to C
},
{
"value" : "C" ,
"weight": 0 // my code affect 1 here cause it find C in the last and affect to the last-1 element.
},
{
"value" : "C" ,
"weight": 0
}
]
}
We have to skip all the last (C) elements and give the 1 weight to the last not-C element.
Thank you in advance!
Longest aggregation in my career but seems to be working:
db.collection.aggregate([
{
$addFields : {
size: { $size: "$myArray" },
reversed: {
$reverseArray: "$myArray"
}
}
},
{
$addFields: {
otherThanC: {
$filter: {
input: "$reversed",
as: "item",
cond: { $ne: [ "$$item.value", "C" ] }
}
}
}
},
{
$addFields: {
firstOtherThanCIndex : {
$indexOfArray: [ "$reversed", { $arrayElemAt: [ "$otherThanC", 0 ] } ]
}
}
},
{
$unwind: {
path: "$reversed",
includeArrayIndex: "arrayIndex"
}
},
{
$addFields: {
weight: {
$switch: {
branches: [
{ case: { $eq: [ "$size", 1 ] }, then: 1 },
{ case: { $and: [ { $eq: [ "$arrayIndex", 0 ] }, { $eq: [ { $size: "$otherThanC" }, 0 ] } ] } , then: 1},
{ case: { $eq: [ "$arrayIndex", "$firstOtherThanCIndex" ] }, then: 1 }
],
default: 0
}
}
}
},
{
$group: {
_id: "$_id",
myArrayReversed: {
$push: {
value: "$reversed.value",
weight: "$weight"
}
}
}
},
{
$project: {
_id: 1,
myArray: { $reverseArray: "$myArrayReversed" }
}
}
])
Brief description of each pipeline stage:
We need to add two extra fields: $size of and array and second array with reversed items
Second and third steps are to find first (last) item not equal to C using $filter and $arrayElemAt which returns first matching index
Then we can $unwind our reversed array using special syntax which adds index of array when unwinding
That is the moment when we can calculate weight using $switch - simply setting 1 if array has one element or indexes are matching and zero otherwise
Then we just need to reshape the data: grouping back by _id and reversing the array

MongoDB $and operator with nested objects in arrays not working as expected

I have nested array database records styled as such:
{
"_id" : "A",
"foo" : [
{
"_id" : "a",
"date" : ISODate("2017-07-13T23:27:13.522Z")
},
{
"_id" : "b",
"date" : ISODate("2017-08-04T22:36:36.381Z")
},
{
"_id" : "c",
"date" : ISODate("2017-08-23T23:59:40.202Z")
}
]
},
{
"_id" : "B",
"foo" : [
{
"_id" : "d",
"date" : ISODate("2017-07-17T23:27:13.522Z")
},
{
"_id" : "e",
"date" : ISODate("2017-01-06T22:36:36.381Z")
},
{
"_id" : "f",
"date" : ISODate("2017-09-14T23:59:40.202Z")
}
]
},
{
"_id" : "C",
"foo" : [
{
"_id" : "g",
"date" : ISODate("2017-11-17T23:27:13.522Z")
},
{
"_id" : "h",
"date" : ISODate("2017-06-06T22:36:36.381Z")
},
{
"_id" : "i",
"date" : ISODate("2017-10-14T23:59:40.202Z")
}
]
}
When I run the query:
db.bar.find(
{
$and: [
{"foo.date": {$lte: new Date(2017,8,1)}},
{"foo.date": {$gte: new Date(2017,7,1)}}
]
},
{
"_id":1
}
)
I'm returned
{
_id: "A"
},
{
_id: "B"
},
{
_id: "C"
}
Logically I'm asking for only the records where at least one date is between Aug-1 and Sept-1 (Record A), but am getting all records.
I'm thinking it might be referencing different dates on the subdocuments i.e. where foo.1.date > Aug-1 and foo.0.date < Sept-1.
Has anyone else had issue and found a resolution to this?
Your filters are evaluated separately against each subdocument in your array and that's why you're getting all results. For instance for C
element with _id g is gte 1st of August
element with _id h is lte 1st of September
You should use $elemMatch to find date in specified range
db.bar.find(
{ "foo":
{
"$elemMatch":
{ "date":
{
"$gte": new Date(2017,7,1),
"$lte": new Date(2017,8,1)
}
}
}
})
Only A will be returned for this query.
The way you are doing doensn't work as you want for array.
You have to unpack the array and after you compare the values.
db.bar.aggregate(
// Unpack the assignments array
{ $unwind : "$foo" },
// Find the assignments ending after given date
{ $match : {
"foo.date": { $gte: new Date(2017,7,1),$lt: new Date(2017,8,1) }
}}
)
This should also work fine
db.bar.find({"foo.date": { $gte: new Date(2017,7,1),$lt: new Date(2017,8,1) }})

Resources