I have collection path_test with 2 documents in it
Document 1
{
"_id" : 1,
"tpc" : 5,
"path" : [
{
"nids" : [ 0, 10, 11 ],
"ctc" : 2
},
{
"nids" : [ 0, 10 ],
"ctc" : 2
},
{
"nids" : [ 0, 10, 21 ],
"ctc" : 1
}
]
}
Document 2
{
"_id" : 2,
"tpc" : 5,
"path" : [
{
"nids" : [ 0, 10, 110 ],
"ctc" : 1
},
{
"nids" : [ 0, 10, 11 ],
"ctc" : 2
},
{
"nids" : [ 0, 5 ],
"ctc" : 2
}
]
}
What I'm trying to get as a result are documents with path array in which all elements have nids like [0, 10, *]. Order is important, so [10, 0, *] will be wrong.
It should find Document 1, but not Document 2. Was hoping I can resolve this with a query, before I start using map-reduce or aggregation.
This is what I've tried so far
Query1
db.getCollection('path_test').find( {
"path": { $not: { $elemMatch: { "nids.0": { $nin: [0] }, "nids.1": { $nin: [10] } } } }
});
Query 2
db.getCollection('path_test').find( {
"path.nids": { $not: { $elemMatch: { $nin: [0, 10] } } }
});
but both queries give me results where only 0 is in or where only 10 is in, but I need both and in that exact order.
Is that possible?
not at least one means noone
Query 1
For simplification, lets assign
A = "nids.0": { $ne: 0 }
B = "nids.1": { $ne: 10 }
C = { A, B }
then
{ "path" : { $elemMatch: C } }
will find documents where at least one element in path array satisfies condition C, while
{ "path" : { $not: { $elemMatch: C } } }
will find documents where there are no element in path array that satisfies condition C.
Document 1 and Document 2 don't have elements in their path arrays that satisfy condition C, thus the Query1 output contains both of them. If, f.e, you add to the path array of the Document 1
{ "nids": [ 1, 11, 110], "ctc" : 1 }
then Document 1 will not be in the output of Query 1 becase this added element satisfies C.
Query 2
For simplification, lets assign
C = { $nin: [0, 10] }
then
{ "path.nids" : { $not: { $elemMatch: C } } }
will find documents where there are no element in path.nids array that satisfies condition C.
Document 1 and Document 2 in their path.nids arrays have elements that satisfy condition C, thus the Query 2 output contains neither of them. If, f.e, you add to you collection document
{ "_id" : 6, "tpc" : 5, "path" : [ { "nids" : [ 0, 10 ], "ctc" : 1 } ] }
then it will be in the output of Query 2 because in path.nids array there are no elements that satisfy C.
Solution
In Query 1 replace
{ $elemMatch: { "nids.0": { $nin: [0] }, "nids.1": { $nin: [10] } } }
with
{ $elemMatch: { $or: [ { "nids.0": { $ne: 0 } }, { "nids.1": { $ne: 10 } } ] } }
This new Query will find documents where there are no element in path array that satisfies at least one of conditions A and B. So, it will find Document 1, but not Document 2 (where "nids" : [ 0, 5 ] does not satisfy condition B.
Note that { $ne: 10 } is equivalent to { $nin: [10] }.
Related
Given a MongoDB collection in the following structure
{
"_id" : 1,
"system_id" : "123",
"sub_systems" : [
{
"sub_system_id" : "456",
"status" : "connected",
"messages_relayed" : [ ] // An array of message_ids that have been relayed
}
]}
I'd like to create a query to return how many messages have been relayed by each sub_system. I started with this:
db.messages.aggregate([{
"$project": {
"_id": 0,
"num_of_msgs_relayed": {
"$cond":
{"if": { "$isArray": "$sub_systems.messages_relayed" },
"then": { "$size": "$sub_systems.messages_relayed" },
"else": 0}
}}}]);
To my surprise, the result is:
{ "num_of_msgs_relayed" : 1 }
QUESTION: I expected the query to return a 0 value, since basically I'm projecting the $size of an empty array! What is the reasoning behind this 1?
P.S.: The following command can be used to create the data shown on messages collection:
db.runCommand( {
insert: "messages",
documents: [{'_id': 1, 'system_id': '123', 'sub_systems':[{'status': 'connected', 'messages_relayed': []}]}] }
)
You can try the simplest query below to observe how MongoDB interprets sub_systems.messages_relayed:
db.messages.aggregate([
{ "$project": { "arr": "$sub_systems.messages_relayed"}}
]);
So this query will return an array of arrays since sub_systems is one outer array and messages_relayed is another one. That's why you're getting 1 instead of 0
To "project the size of empty array" you should use $unwind before your project and below aggregation will return 0 instead of 1
db.messages.aggregate([
{ $unwind: "$sub_systems" },
{
$project: {
_id: 0,
num_of_msgs_relayed: {
$cond: {
if: { $isArray: "$sub_systems.messages_relayed" },
then: { $size: "$sub_systems.messages_relayed" },
else: 0
}
}
}
}
])
My Document Structure:
{
"_id" : ObjectId("59edc58af33e9b5988b875fa"),
"Agent" : {
"Name" : "NomanAgent",
"Location" : "Lahore",
"AgentId" : 66,
"Reward" : "Thumb Up",
"Suggestion" : [
"Knowledge",
"Professionalisn"
]
}
}
What I want to achieve in this query:
I want to find the count of each suggestion given by a customer to every agent, it should look something like,
{
"AgentName": "Xyz",
"SuggestionCounts": {
"Knowledge": 2,
"Professionalism": 3,
"Friendliness": 1
}
}
What I have done so far,
db.getCollection('_survey.response').aggregate([
{
$group:{
_id: "$Agent.Name",
Suggestions: {$push:"$Agent.Suggestion"}
}
}
]);
Output:
/* 1 */
{
"_id" : "GhazanferAgent",
"Suggestions" : [
[
"Clarity",
"Effort"
],
[
"Friendliness"
]
]
}
/* 2 */
{
"_id" : "NomanAgent",
"Suggestions" : [
[
"Knowledge",
"Professionalisn"
]
]
}
How I want it to be(As Suggestion in the document is an array and when when i group documents by Agent.Name so it become array of arrays as shown in my output, it want to merge all arrays into single with duplication and then i will find the count of each element in array):
/* 1 */
{
"_id" : "GhazanferAgent",
"SuggestionsCombined" : [
[
"Clarity",
"Effort",
"Friendliness"
]
]
}
/* 2 */
{
"_id" : "NomanAgent",
"SuggestionsCombined" : [
[
"Knowledge",
"Professionalisn"
]
]
}
Thanks in advance!!
One way would be like this - the output structure is not identical to what you suggested but probably close enough:
db.getCollection('_survey.response').aggregate([
{
$unwind: "$Agent.Suggestion" // flatten "Suggestion" array
}, {
$group:{ // group by agent and suggestion
_id: { "AgentName": "$Agent.Name", "Suggestion": "$Agent.Suggestion" },
"Count": { $sum: 1} // calculate count of occurrencs
}
}, {
$group:{
_id: "$_id.AgentName", // group by agent only
"Suggestions": { $push: { "Suggestion": "$_id.Suggestion", "Count": "$Count" } } // create array of "Suggestion"/"Count" pairs per agent
}
}
]);
I would like to either insert a new document with a default value as part of an array, or update that part of the array if the document already exists.
What I thought of was:
db.test.update(
{ "a": 5 },
{ $setOnInsert: { "b": [0, 0] }, $min: { "b.0": 5 } },
{ upsert: true }
);
If I do that, then I get:
Cannot update 'b' and 'b.0' at the same time
Another idea was to remove $setOnInsert and just keep $min, since the minimum between nothing and 5 should be 5.
db.test.update(
{ "a": 5 },
{ $min: { "b.0": 5 } },
{ upsert: true }
);
This doesn't raise an error, but now the document I get is:
{ "a" : 5, "b" : { "0" : 5 } }
I need an array with 5 at position 0 however, not an object with a 0 property.
How can I achieve this?
You can use .bulkWrite() for this, and it's actually a prime use case of why this exists. It only sends "one" actual request to the server and has only one response. It's still two operations, but they are more or less tied together and generally atomic anyway:
db.junk.bulkWrite([
{ "updateOne": {
"filter": { "a": 1 },
"update": { "$setOnInsert": { "b": [ 5,0 ] } },
"upsert": true
}},
{ "updateOne": {
"filter": { "a": 1 },
"update": { "$min": { "b.0": 5 } }
}}
])
Run for the first time will give you an "upsert", note that it's "inserted" and not "modified" in the response:
{
"acknowledged" : true,
"deletedCount" : 0,
"insertedCount" : 0,
"matchedCount" : 1,
"upsertedCount" : 1,
"insertedIds" : {
},
"upsertedIds" : {
"0" : ObjectId("5947c412d6eb0b7d6ac37f09")
}
}
And the document of course looks like:
{
"_id" : ObjectId("5947c412d6eb0b7d6ac37f09"),
"a" : 1,
"b" : [
5,
0
]
}
Then run with a different value to $min as you likely would in real cases:
db.junk.bulkWrite([
{ "updateOne": {
"filter": { "a": 1 },
"update": { "$setOnInsert": { "b": [ 5,0 ] } },
"upsert": true
}},
{ "updateOne": {
"filter": { "a": 1 },
"update": { "$min": { "b.0": 3 } }
}}
])
And the response:
{
"acknowledged" : true,
"deletedCount" : 0,
"insertedCount" : 0,
"matchedCount" : 2,
"upsertedCount" : 0,
"insertedIds" : {
},
"upsertedIds" : {
}
}
Which "matched" 2 but of course $setOnInsert does not apply, so the result is:
{
"_id" : ObjectId("5947c412d6eb0b7d6ac37f09"),
"a" : 1,
"b" : [
3,
0
]
}
Just like it should be
I'm having group of array of element in MongoDB as given below :
{
"_id" : 5,
"quizzes" : [
{
"wk" : 1,
"score" : 10
},
{
"wk" : 2,
"score" : 8
},
{
"wk" : 3,
"score" : 5
}
],
"play" : [
{
"wk" : 2,
"score" : 8
},
{
"wk" : 3,
"score" : 5
}
]
}
I am trying insert new record in array if not present and if record present in that array then update that array record.
Below is my MongoDB query.
db.push.update(
{ _id: 5 },
{ $push: { "quizzes": {"wk" : 6.0,"score" : 8.0},"play": {"wk" : 6.0,"score" : 8.0} } }
)
Every time when i execute this query it inserts new record in array but i want if record present then update that array.
Use $addToSet instead of $push.
db.push.update(
{ _id: 5 },
{ $addToSet: { "quizzes": {"wk": 6.0, "score": 8.0}, "play": {"wk": 6.0, "score": 8.0} } }
)
EDIT:
There is no simple built-in approach for conditional sub-document update in an array field, by specific property. However, a small trick can do the job by executing two commands in sequence.
For example: If we want to update the quizzes field with the object { "wk": 7.0, "score": 8.0 }, we can do it in two steps:
Step-1: $pull out sub-documents from the quizzes array where "wk": 7.0. (Nothing happens if matching sub-document not found).
db.push.update(
{ _id: 5 },
{ $pull: { "quizzes": { "wk": 7.0 } } }
)
Step-2: $addToSet the sub-document.
db.push.update(
{ _id: 5 },
{ $addToSet: { "quizzes": {"wk": 7.0, "score": 8.0} } }
)
You can combine the above two update commands using the bulk.find().update()
I assume what you want to do is,
add a property to an element of the array based on the same element's different property's value.
You can use the updateOne() function with the $(update) operator, please refer the document mongodb documentation for $(update).
Let's say you want to add a property description to the array element where the value of wk is 1 (assuming its some kind of key based on what you have provided)
Input: Your data collection given in the question.
Mongo Command:
db.<your_collection_name>.updateOne(
{
"_id":5,
"quizzes.wk":1
},
{
$set:{
"quizzes.$.description":"I am a new property"
}
});
Output:
{
"_id" : 5,
"quizzes" : [
{
"wk" : 1,
"score" : 10,
"description" : "I am a new property"
},
{
"wk" : 2,
"score" : 8
},
{
"wk" : 3,
"score" : 5
}
],
"play" : [
{
"wk" : 2,
"score" : 8
},
{
"wk" : 3,
"score" : 5
}
]
}
Note: It will only change one entry of an array, the one which is matched first.
Consider following collection in mongoDB :
{a:[4,2,8,71,21]}
{a:[24,2,2,1]}
{a:[4,1]}
{a:[4,2,8,21]}
{a:[2,8,71,21]}
{a:[4,2,8]}
How can I get following results in a most easily:
Getting nth element of array
{a:4}
{a:24}
{a:4}
{a:4}
{a:2}
{a:4}
Getting elements 2 to 4
{a:[8,71,21]}
{a:[2,1]}
{a:[]}
{a:[8,21]}
{a:[71,21]}
{a:[8]}
And other similar queries.
What you are looking for is the $slice projection.
Getting a number of elements from the beginning of an array
You can pass a simple $limit with a number of values to return (eg. 1):
> db.mycoll.find({}, {_id: 0, a: { $slice: 1}})
{ "a" : [ 4 ] }
{ "a" : [ 24 ] }
{ "a" : [ 4 ] }
{ "a" : [ 4 ] }
{ "a" : [ 2 ] }
{ "a" : [ 4 ] }
Getting a range of elements
You can pass an array with parameters of ( $skip, $limit ).
Note: to match your expected output you would have to find elements 3 to 5 (skip the first 2 elements, return the next 3):
> db.mycoll.find({}, {_id: 0, a: { $slice: [2,3]}})
{ "a" : [ 8, 71, 21 ] }
{ "a" : [ 2, 1 ] }
{ "a" : [ ] }
{ "a" : [ 8, 21 ] }
{ "a" : [ 71, 21 ] }
{ "a" : [ 8 ] }
Getting the nth element of array
Pass the number of elements to $skip and a value of 1 for the limit.
For example, to find the second element you need to skip 1 entry:
> db.mycoll.find({}, {_id: 0, a: { $slice: [1,1]}})
{ "a" : [ 2 ] }
{ "a" : [ 2 ] }
{ "a" : [ 1 ] }
{ "a" : [ 2 ] }
{ "a" : [ 8 ] }
{ "a" : [ 2 ] }
Note that the $slice operator:
always returns an array
will return an empty array for documents that match the find criteria but return an empty result for the $slice selection (eg. if you ask for the 5th element of an array with only 2 elements)