Retrieve the second element of array for each document - MongoDB - mongodb

I have a MongoDB collection, called bios, that contains documents similar to these:
{
"_id" : ObjectId("51df07b094c6acd67e492f41"),
"name" : {
"first" : "John",
"last" : "McCarthy"
},
"birth" : ISODate("1927-09-04T04:00:00Z"),
"death" : ISODate("2011-12-24T05:00:00Z"),
"contribs" : [
"Lisp",
"Artificial Intelligence",
"ALGOL"
]
},
{
"_id" : 3,
"name" : {
"first" : "Grace",
"last" : "Hopper"
},
"title" : "Rear Admiral",
"birth" : ISODate("1906-12-09T05:00:00Z"),
"death" : ISODate("1992-01-01T05:00:00Z"),
"contribs" : [
"UNIVAC",
"compiler",
"FLOW-MATIC",
"COBOL"
]
}
My target is to retrieve the second element of the array contribs for each document in bios collection.
Using the new aggregation pipeline operator $filter I run the following query:
> db.bios.aggregate([
{
$match: {"contribs.2":{"$exists":1}}},
{
$project:{contribs:
{
$filter:{input:"$contribs", as: "contribs", cond:{}}},_id:0}}])
With my query, the output is:
{ "contribs" : [ "Lisp", "Artificial Intelligence", "ALGOL" ] }
{ "contribs" : [ "UNIVAC", "compiler", "FLOW-MATIC", "COBOL" ] }
that is not just the second element of the array contribs but a projection on contribs array when its second element exists.

did you try $elementAt ?
db.bios.aggregate([
{ $match: {"contribs.1": { "$exists": 1 } }},
{ $project: { contribs: { $arrayElemAt: [ "$contribs", 1 ] } } }
]);

Related

Mongodb use slice and elementmatch

I am trying to retrieve elements in an array in mongo db. I would like to retrieve the 15 first elements which do not match a pattern
So let's imagine I have
{
"_id" : ObjectId("s4dcsd5s4d6c54s6d"),
"items" : [
{
type : "TYPE_1",
text : "blablabla"
},
{
type : "TYPE_2",
text : "blablabla"
},
{
type : "TYPE_3",
text : "blablabla"
},
{
type : "TYPE_1",
text : "blablabla"
},
{
type : "TYPE_2",
text : "blablabla"
},
{
type : "TYPE_1",
text : "blablabla"
}
]
}
So currently I have more element to match compared to the element to not match that's why I use nin. but it is to simplifiy
If I use
db.history.find({ "_id" : ObjectId("s4dcsd5s4d6c54s6d")}, { "items" : { "$elemMatch" : { "type" : { "$nin" : [ "TYPE_2" , "TYPE_3"]}}}, "items" : { $slice : [0, 2]}}).pretty()
It seems that the element match is not taken into account (inverse as well if i put element match after slice)
Then if I do:
db.history.find({ "_id" : ObjectId("s4dcsd5s4d6c54s6d")}, { "items" : { "$elemMatch" : { "type" : { "$nin" : [ "TYPE_2" , "TYPE_3"]}}, $slice : [0, 2]}}).pretty()
An error is thrown by mongo
Do you know how I can do?
Thanks a lot
You can't use $elemMatch for your case since it will only return the first element. From documentation :
$elemMatch The $elemMatch operator limits the contents of an
field from the query results to contain only the first element
matching the $elemMatch condition.
You can do an aggregation query which will do the following:
match your _id
unwind your items array to have one record per items in the array
match the types $nin your array [ "TYPE_2" , "TYPE_3"]
limit the number of result
The query is :
db.history.aggregate([{
$match: {
_id: ObjectId("s4dcsd5s4d6c54s6d")
}
}, {
$unwind: '$items'
}, {
$match: {
'items.type': { '$nin': ["TYPE_2", "TYPE_3"] }
}
},
{ $limit: 2 }
])
It gives :
{ "_id" : "s4dcsd5s4d6c54s6d", "items" : { "type" : "TYPE_1", "text" : "blablabla" } }
{ "_id" : "s4dcsd5s4d6c54s6d", "items" : { "type" : "TYPE_1", "text" : "blablabla" } }
You will need to use aggregation for restricting the array in the form you have. Use $filter to apply the condition and $slice to limit the array elements.
db.history.aggregate([{
$match: {
_id: ObjectId("586309d6772c68234445f2a5")
}
}, {
"$project": {
"items": {
"$slice": [{
"$filter": {
"input": "$items",
"as": "item",
"cond": {
"$and": [{
$ne: ["$$item.type", "TYPE_2"]
}, {
$ne: ["$$item.type", "TYPE_3"]
}]
}
}
},
2
]
}
}
}])
Sample Output:
{ "_id" : ObjectId("586309d6772c68234445f2a5"), "items" : [ { "type" : "TYPE_1", "text" : "blablabla" }, { "type" : "TYPE_1", "text" : "blablabla" } ] }

MongoDB - Fetch an object from deep subdocuments

How to query an object from an Array inside an Array, and get it as a top-level object? For example, consider the following record.
{
"subjects": [
{
"name": "English",
"teachers": [
{
"name": "Mark" /* Trying to get this object*/
},
{
"name": "John"
}
]
}
]
}
I am trying to get the following object out as the top-level object.
{
"name": "Mark"
}
You need to use the aggregation framework to do exactly what you're asking for.
Here I entered the document you gave into collection: foo.
> db.foo.find().pretty()
{
"_id" : ObjectId("57ceed3d31484d5b491eaae9"),
"subjects" : [
{
"name" : "English",
"teachers" : [
{
"name" : "Mark"
},
{
"name" : "John"
}
]
}
]
}
Using $unwind to unravel our array we then enter our first stage of the aggregation pipeline:
> db.foo.aggregate([
... {$unwind: "$subjects"}
... ]).pretty()
{
"_id" : ObjectId("57ceed3d31484d5b491eaae9"),
"subjects" : {
"name" : "English",
"teachers" : [
{
"name" : "Mark"
},
{
"name" : "John"
}
]
}
}
Subjects was an array of length 1 so the only difference here is one less set of [] array brackets.
We need to unwind again.
> db.foo.aggregate([
... {$unwind: "$subjects"},
... {$unwind: "$subjects.teachers"}
... ]).pretty()
{
"_id" : ObjectId("57ceed3d31484d5b491eaae9"),
"subjects" : {
"name" : "English",
"teachers" : {
"name" : "Mark"
}
}
}
{
"_id" : ObjectId("57ceed3d31484d5b491eaae9"),
"subjects" : {
"name" : "English",
"teachers" : {
"name" : "John"
}
}
}
Now we turned our array of length '2' into two separate documents. The first one with subjects.teachers.name = Mark and the second with subjects.teachers.name = John.
We only want to return the case where name = Mark so we need to add a $match stage to our pipeline.
> db.foo.aggregate([
... {$unwind: "$subjects"},
... {$unwind: "$subjects.teachers"},
... {$match: {"subjects.teachers.name": "Mark"}}
... ]).pretty()
{
"_id" : ObjectId("57ceed3d31484d5b491eaae9"),
"subjects" : {
"name" : "English",
"teachers" : {
"name" : "Mark"
}
}
}
Ok! Now we are only matching on the case where name: Mark.
Let's add a $project case to shape our input how we want.
> db.foo.aggregate([
... {$unwind: "$subjects"},
... {$unwind: "$subjects.teachers"},
... {$match: {"subjects.teachers.name": "Mark"}},
... {$project: {"name": "$subjects.teachers.name", "_id": 0}}
... ]).pretty()
{ "name" : "Mark" }

How to check if nested arrays are ALL empty in mongodb?

I have something like below:
{
"_id" : "1",
"firstArray" : [
{
"_id" : "11",
"secondArray" : [ ]
},
{
"_id" : "12",
"secondArray" : [ ]
},
{
"_id" : "13",
"secondArray" : [ { "type" : "somthing" } ]
}
]
},
{
"_id" : "2",
"firstArray" : [
{
"_id" : "21",
"secondArray" : [ ]
},
{
"_id" : "22",
"secondArray" : [ ]
}
]
}
I need a mongodb query to find documents which ALL of the nested secondArrays are empty? the query should return second document and not the first one.
to solve that, we need to check size of arr2, but to enable that we need first to unwind arr1.
Please find below aggregation framework snippet which solves this problem,
db.pmoubed.aggregate([{
$unwind : "$firstArray"
}, {
$project : {
_id : 1,
firstArray : 1,
isNotEmpty : {
$size : "$firstArray.secondArray"
}
}
}, {
$group : {
_id : "$_id",
isNotEmpty : {
$sum : "$isNotEmpty"
},
firstArray : {
$push : "$firstArray"
}
}
}, {
$match : {
"isNotEmpty" : 0
}
}
])
Any comments welcome

MongoDB find query using $and and $nor operator

I have a collection as below
{
"_id" : ObjectId("55bec0793bed809b2cb658ad"),
"lock_1" : ["desc_1","desc_2"],
"lock_2" : ["desc_1"]
},
{
"_id" : ObjectId("55bec0793bed809b2cb658ab"),
"lock_1" : ["desc_1","desc_2","desc_3"],
"lock_2" : ["desc_1"]
}
{
"_id" : ObjectId("55bec0793bed809b2cb658ac"),
"lock_1" : ["desc_1"],
"lock_2" : []
},
{
"_id" : ObjectId("55bec0793bed809b2cb658ae"),
"lock_1" : [],
"lock_2" : ["desc_1"]
},
{
"_id" : ObjectId("55bec0793bed809b2cb658aj"),
"lock_1" : ["desc_3","desc_4"],
"lock_2" : ["desc_5"]
},
{
"_id" : ObjectId("55bec0793bed809b2cb658ak"),
"lock_1" : [],
"lock_2" : []
}
I have retrieved all the documents having "desc_1" in both "lock_1" and "lock_2" array by using below query, returning first two documents, which is correct.
db.locks.find( { $and: [ {lock_1 : "desc_1"} , {lock_2 : "desc_1"} ] } )
Now, I am trying to get documents which does not satisfy above condition. I tried using below query but it is returning last two documents.
db.locks.find( { $nor: [ {lock_1 : "desc_1"} , {lock_2 : "desc_1"} ] } )
How to retrieve documents where "desc_1" is either not present or present in one of the arrays?
What you need here is the $or operator which performs a logical OR operation and the $ne operator to test the contents of fields directly.
db.locks.find({
"$or": [
{ "lock_1": { "$ne": "desc_1" } },
{ "lock_2": { "$ne": "desc_1" } }
]
})
db.locks.find({
$or: [
{ lock_1 : { $nin: [ "desc_1" ] }} ,
{ lock_2 : { $nin: [ "desc_1" ] }}
]
})
reference:
$or
$nin

MongoDB Aggregation Projection

If I have a collection as follows:
db.cafe.insert({name: "Cafe1", customers: [{name: "David", foods: [{name : "cheese"}, {name: "beef"}]}, {name: "Bill", foods: [{name: "fish"}]} ]})
db.cafe.find().pretty()
{
"_id" : ObjectId("54f5ae58baed23b7a34fccb6"),
"name" : "Cafe1",
"customers" : [
{
"name" : "David",
"foods" : [
{
"name" : "cheese"
},
{
"name" : "beef"
}
]
},
{
"name" : "Bill",
"foods" : [
{
"name" : "fish"
}
]
}
]
}
How can I extract an array containing just the food objects for people called "David".
Desired output is just the array of foods, i.e:
[{name: "cheese"}, {name: "beef"}]
I have tried an aggregation pipeline that unwinds the cafes customers, then matches on name then projects the food, e.g:
db.cafe.aggregate( [{$unwind : "$customers"}, {$match : {"customers.name": "David"}}, {$project : {"customers.foods": 1, _id : 0}
}] ).pretty()
{
"customers" : {
"foods" : [
{
"name" : "cheese"
},
{
"name" : "beef"
}
]
}
}
This seems close to the desired result, however, I'm left with the issue that the foods I want are referenced as an array under the property customers.foods. I would like the result to directly be:
[
{
"name" : "cheese"
},
{
"name" : "beef"
}
]
is there a way I can achieve the desired output?
You are doing your projection wrong.
db.cafe.aggregate( [
{ "$match" : { "customers.name": "David" }},
{ "$unwind" : "$customers" },
{ "$project" : { "foods": "$customers.foods", "_id": 0 }}
])
Output
{ "foods" : [ { "name" : "cheese" }, { "name" : "beef" } ] }
You can also get (something very, very close to) your desired output with a regular query:
> db.cafe.find({ "customers.name" : "David" }, { "customers.$.foods" : 1, "_id" : 0 })
{ "customers" : [ { "name" : "David", "foods" : [ { "name" : "cheese" }, { "name" : "beef" } ] } ] }
Customers will be an array containing just the first object with name : "David". You should prefer this approach to the aggregation as it's vastly more performant. You can extract the foods array in client code.