Get documents from an array in MongoDB [duplicate] - arrays

This question already has answers here:
Retrieve only the queried element in an object array in MongoDB collection
(18 answers)
Closed 5 years ago.
The community reviewed whether to reopen this question 4 months ago and left it closed:
Original close reason(s) were not resolved
I have array in subdocument like this
{
"_id" : ObjectId("512e28984815cbfcb21646a7"),
"list" : [
{
"a" : 1
},
{
"a" : 2
},
{
"a" : 3
},
{
"a" : 4
},
{
"a" : 5
}
]
}
Can I filter subdocument for a > 3
My expect result below
{
"_id" : ObjectId("512e28984815cbfcb21646a7"),
"list" : [
{
"a" : 4
},
{
"a" : 5
}
]
}
I try to use $elemMatch but returns the first matching element in the array
My query:
db.test.find( { _id" : ObjectId("512e28984815cbfcb21646a7") }, {
list: {
$elemMatch:
{ a: { $gt:3 }
}
}
} )
The result return one element in array
{ "_id" : ObjectId("512e28984815cbfcb21646a7"), "list" : [ { "a" : 4 } ] }
and I try to use aggregate with $match but not work
db.test.aggregate({$match:{_id:ObjectId("512e28984815cbfcb21646a7"), 'list.a':{$gte:5} }})
It's return all element in array
{
"_id" : ObjectId("512e28984815cbfcb21646a7"),
"list" : [
{
"a" : 1
},
{
"a" : 2
},
{
"a" : 3
},
{
"a" : 4
},
{
"a" : 5
}
]
}
Can I filter element in array to get result as expect result?

Using aggregate is the right approach, but you need to $unwind the list array before applying the $match so that you can filter individual elements and then use $group to put it back together:
db.test.aggregate([
{ $match: {_id: ObjectId("512e28984815cbfcb21646a7")}},
{ $unwind: '$list'},
{ $match: {'list.a': {$gt: 3}}},
{ $group: {_id: '$_id', list: {$push: '$list.a'}}}
])
outputs:
{
"result": [
{
"_id": ObjectId("512e28984815cbfcb21646a7"),
"list": [
4,
5
]
}
],
"ok": 1
}
MongoDB 3.2 Update
Starting with the 3.2 release, you can use the new $filter aggregation operator to do this more efficiently by only including the list elements you want during a $project:
db.test.aggregate([
{ $match: {_id: ObjectId("512e28984815cbfcb21646a7")}},
{ $project: {
list: {$filter: {
input: '$list',
as: 'item',
cond: {$gt: ['$$item.a', 3]}
}}
}}
])
$and:
get data between 0-5:
cond: {
$and: [
{ $gt: [ "$$item.a", 0 ] },
{ $lt: [ "$$item.a", 5) ] }
]}

Above solution works best if multiple matching sub documents are required.
$elemMatch also comes in very use if single matching sub document is required as output
db.test.find({list: {$elemMatch: {a: 1}}}, {'list.$': 1})
Result:
{
"_id": ObjectId("..."),
"list": [{a: 1}]
}

Use $filter aggregation
Selects a subset of the array to return based on the specified
condition. Returns an array with only those elements that match the
condition. The returned elements are in the original order.
db.test.aggregate([
{$match: {"list.a": {$gt:3}}}, // <-- match only the document which have a matching element
{$project: {
list: {$filter: {
input: "$list",
as: "list",
cond: {$gt: ["$$list.a", 3]} //<-- filter sub-array based on condition
}}
}}
]);

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

Trying to filter values of an array within an array in MongoDB

I am new to MongoDB and I'm trying to filter values of an array within an array. An example of the schema is below. The schema is basically a dump of a 3 tiered Dictionary with a simple object of scalars as the leaf node.
The "I" member contains an array of documents (outer array) of key-value pairs with a string key (k), and the value (v) is an array of documents (middle array) of key-value pairs with a date as the key and value is another dictionary, which isn't part of this question.
Basically, what I need to do is retrieve the most recent data from the middle array (Date, key-value) for a given value of the outer array (string, key-value).
(Collection Sample)
{
"_id" : ObjectId("5eacfbe62758834aefdec003"),
"UserId" : UUID("46942978-29f4-4521-9932-840cead6743e"),
"Data" : {
"I" : [
{
"k" : "LRI39",
"v" : [
{
"k" : ISODate("2020-03-11T20:24:41.591Z"),
"v" : [
{
"k" : ISODate("2020-03-11T20:24:41.594Z"),
"v" : {
"Source" : 1,
"Value" : 19
}
}
]
},
{
"k" : ISODate("2020-01-22T11:37:23.393Z"),
"v" : [
{
"k" : ISODate("2020-01-22T11:37:23.412Z"),
"v" : {
"Source" : 1,
"Value" : 20
}
}
]
}
]
},
...
]
}
}
I have been able to generate a document which is basically what you see from "Data" to the end of the sample, being the entire record for LRI39, using:
db.threetier.aggregate([
{
$project: {
"Data.I": {
$filter: {
input: "$Data.I",
as: "item",
cond: {
$eq: [ "$$item.k", "LRI39" ]
}
}
}
}
}
])
However, no matter what I do, I cannot seem to return any subset of the records of the middle array: I get the 2020-03-11 and 2020-01-22 elements or I get nothing.
I have tried adding stages like the below to the projection above, figuring that I would get 1 record (the 2020-01-22 record) but I get both. If I change the date to be in 2019, I get nothing (as expected).
$project: {
"Data.I.v": {
$filter: {
input: "$Data.I.v",
as: "stamp",
cond: { $lt: [ "$$stamp.k", ISODate("2020-02-14T00:00:00Z") ] }
}
}
}
I have also tried:
{ $match: { $expr: { $lt: [ "Data.I.v.k", ISODate("2020-02-14T00:00:00Z") ] } } }
but that returns no results at all (probably because $match works on documents not arrays) as well as trying to unwind the array using $unwind: "$Data.I.v" before the match, but that returns nothing as well.
It seems like I am missing something fundamental here. I do realize that Mongo is designed (I think) to have those array items as documents, but I do want to see if this will work as is.
You will need to unwind both Data.I and Data.I.v, so that you can consider each of the sub-elements separately.
Then reverse sort by the date field.
Group by the _id and key, selecting only the first document in each group.
Finally, replaceRoot so the return is just the selected document.
db.collection.aggregate([
{$unwind: "$Data.I"},
{$unwind: "$Data.I.v"},
{$sort: {"Data.I.v.k": -1}},
{$group: {
_id: {
_id: "$_id",
key: "$Data.I.k"
},
document: {$first: "$$ROOT"}
}},
{$replaceRoot: {newRoot: "$document"}}
])
Playground

Sort by deep document field in MongoDb

I have a collection called Visitor which has an array of chats and each array has a document called user.
I need to find some documents on this collection and sort them by if they have some specific user in their chats first.
The path for the user id is:
chats.user._id
where:
chats // array
user // document
_id // ObjectId
The below script does sort the documents correctly, however, it expands the chats array and multiplies the document for each chat in the array.
I only need the sorting, so can I sort and not use the unwind pipeline or make it somehow not multiply the documents?
db.getCollection('Visitor').aggregate([
{$unwind: "$chats"},
{ $match: {'event._id':ObjectId('5c942a3591deb389bfd92579'), 'chats.enabled': {$exists: true}}},
{
"$project": {
"_id": 1,
"chats.user._id": 1,
"weight": {
"$cond": [
{ "$eq": [ "$chats.user._id", ObjectId("5c942a3591deb389bfd92579") ] },
10,
0
]
}
}
},
{ "$sort": { "weight": -1 } },
])
EDIT: I don't need to sort the inner array, but sort the find command by checking if a specific user is in the chats array.
Some sample of Visitor collection:
[
{
"_id" : ObjectId("5c9a3a1bd86e0ba64106e90e"),
"event" : {
"_id" : ObjectId("5c942a3591deb389bfd92579")
},
"chats" : [
{
"enabled" : false,
"user" : {
"_id" : ObjectId("5c81232f09a923b559763418")
},
"_id" : ObjectId("5c9a3a1bd86e0ba64106e915")
}
]
},
{
"_id" : ObjectId("5c9a3a35d86e0ba64106e950"),
"event" : {
"_id" : ObjectId("5c942a3591deb389bfd92579")
},
"chats" : [
{
"enabled" : true,
"user" : {
"_id" : ObjectId("5c81232f09a923b559763418")
},
"_id" : ObjectId("5c9a3a35d86e0ba64106e957")
},
{
"enabled" : true,
"user" : {
"_id" : ObjectId("5c942a3591deb389bfd92579")
},
"_id" : ObjectId("5c9a3a34d86e0ba64106e91d")
}
]
}
]
In the above sample, I need to make the second document to be sorted first because it has the user with the _id ObjectId("5c942a3591deb389bfd92579").
The problem here is that using $unwind you modify initial structure of your documents (you will get one document per chats. I would suggest using $map to get an array of weights based on specified userId and then you can use $max to get final weight
db.col.aggregate([
{ $match: {'event._id':ObjectId('5c942a3591deb389bfd92579'), 'chats.enabled': {$exists: true}}},
{
"$project": {
"_id": 1,
"chats.user._id": 1,
"weight": {
$max: { $map: { input: "$chats", in: { $cond: [ { $eq: [ "$$this.user._id", ObjectId("5c942a3591deb389bfd92579") ] }, 10, 0 ] } } }
}
}
},
{ "$sort": { "weight": -1 } },
])

Why does MongoDB $size returns 1 for an empty sub-array?

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
}
}
}
}
])

Find Query - Filter by array size after $elemMatch

Is it possible to return records based on the size of the array after $elemMatch has filtered it down?
For example, if I have many records in a collection like the following:
[
{
contents: [
{
name: "yorkie",
},
{
name: "dairy milk",
},
{
name: "yorkie",
},
]
},
// ...
]
And I wanted to find all records in which their contents field contained 2 array items with their name field equal to "yorkie", how would I do this? To clarify, the array could contain other items, but the criteria is met so long as 2 of those array items have the matching field:value.
I'm aware I can use $elemMatch (or contents.name) to return records where the array contains at least one item matching that name, and I'm aware I can also use $size to filter based on the exact number of array items in the record's field. Is there a way that they can be both combined?
Not in a find query, but it can be done with an aggregation:
db.test.aggregate([
{ "$match" : { "contents.name" : "yorkie" } },
{ "$unwind" : "$contents" },
{ "$match" : { "contents.name" : "yorkie" } },
{ "$group" : { "_id" : "$_id", "sz" : { "$sum" : 1 } } }, // use $first to include other fields
{ "$match" : { "sz" : { "$gte" : 2 } } }
])
I interpreted
the criteria is met so long as 2 of those array items have the matching field:value
as meaning the criteria is met if at least 2 array items have the matching value in name.
I know this thread is old, but today you can just use find
db.test.find({
"$expr": {
"$gt": [
{
"$reduce": {
"input": "$contents",
"initialValue": 0,
"in": {
"$cond": {
"if": {
"$eq": ["$$this.name", 'yorkie']
},
"then": {
"$add": ["$$value", 1]
},
"else": "$$value"
}
}
}
},
1
]
}
})
The reduce will do the trick here, and will return the number of objects that match the criteria

Resources