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" }
Related
I have a customers collection such as;
{
"_id" : ObjectId("5de8c07dc035532b489b2e23"),
"name" : "sam",
"orders" : [{"ordername" : "cola"},{"ordername" : "cheesecake"}]
}
And waiters collection such as;
{
"_id" : ObjectId("5de8bc24c035532b489b2e20"),
"waiter" : "jack",
"products" : [{"name" : "cola", "price" : "4"},
{"name" : "water", "price" : "2"},
{"name" : "coffee", "price" : "8" }]
}
{
"_id" : ObjectId("5de8bdc7c035532b489b2e21"),
"waiter" : "susan",
"products" : [{"name" : "cheesecake", "price" : "12" },
{"name" : "apple pie", "price" : "14" }]
}
I want to join the objects from waiters collection into the customers collection by matching "products.name" and "orders.ordername". But, the result includes the whole document from the waiters collection, however, I want only the matched objects inside the document. Here is what I want;
ordered:[
{"name" : "cola", "price" : "4"},
{"name" : "cheesecake", "price" : "12" },
]
I tried $lookup with and without pipeline, and filter but could not get this result. Thanks in advance.
You had the right idea, we just have to "massage" the data a bit due to its structure like so:
db.collection.aggregate([
{
$addFields: {
"orderNames":
{
$reduce: {
input: "$orders",
initialValue: [],
in: {$concatArrays: [["$$this.ordername"], "$$value"]}
}
}
}
},
{
$lookup:
{
from: "waiters",
let: {orders: "$orderNames"},
pipeline: [
{
$unwind: "$products"
},
{
$match:
{
$expr:{$in: ["$products.name", "$$orders"]},
}
},
{
$group: {
_id: "$products.name",
price: {$first: "$products.price"}
}
},
{
$project: {
_id: 0,
price: 1,
name: "$_id"
}
}
],
as: "ordered"
}
}
])
It feels like you could benefit from a new collection of mapping items to prices. Could potentially save you a lot of time.
I have data like the following,
Student | Subject
A | Language
A | Math
B | Science
A | Arts
C | Biology
B | History
and so on...
I want to fetch the students who has same name but enrolled in two different subjects Language & Math only.
I tried to use the query:
$group:{
_id:"$student",
sub:"{$addToSet:"$subject"}
},
$match:{
sub:{$in:["Language","Math"]}
}
But I am getting no documents to preview in MongoDB Compass. I am working in a VM machine, Compass is able to group only biology, history, science, arts only but not able to group language and math. I wanted to get A as my output.
Thanks in loads.
The collection data and the expected output:
{ Student:"A", Subject:"Language" },
{ Student:"A", Subject:"Math" },
{ Student:"B", Subject:"Science" },
{ Student:"A", Subject:"Arts" },
{ Student:"C", Subject:"Biology" },
{ Student:"B", Subject:"History" }
I am looking to get A as my output.
You are almost there, just need some tweak to your aggregation pipeline:
const pipeline = [
{
$group:
{
_id: '$Student', // Group students by name
subjects: {
$addToSet: '$Subject', // Push all the subjects they take uniquely into an array
},
},
},
{
// Filter for students who only offer Language and Mathematics
$match: { subjects: { $all: ['Language', 'Math'], $size: 2 } },
},
];
db.students.aggregate(pipeline);
That should give an output array like this:
[
{ "_id" : studentName1 , "subjects" : [ "Language", "Math" ] },
{ "_id" : studentName2 , "subjects" : [ "Language", "Math" ] },
....
]
You have to use an Aggregation operator, $setIsSubset. The $in (aggregation) operator is used to check an array for one value only. I think you are thinking of $in (query operator)..
The Query:
db.student_subjects.aggregate( [
{ $group: {
_id: "$student",
studentSubjects: { $addToSet: "$subject" }
}
},
{ $project: {
subjectMatches: { $setIsSubset: [ [ "Language", "Math" ], "$studentSubjects" ] }
}
},
{ $match: {
subjectMatches: true
}
},
{ $project: {
matched_student: "$_id", _id: 0
}
}
] )
The Result:
{ "matched_student" : "A" }
NOTES:
If you replace [ "Language", "Math" ] with [ "History" ], you will get the result: { "matched_student" : "B" }.
You can also try and see other set operators (aggregation), like the $allElementsTrue. Use the best one that suits your application.
[ EDIT ADD ]
Sample data for student_subjects collection:
{ "_id" : 1, "student" : "A", "subject" : "Language" }
{ "_id" : 2, "student" : "A", "subject" : "Math" }
{ "_id" : 3, "student" : "B", "subject" : "Science" }
{ "_id" : 4, "student" : "A", "subject" : "Arts" }
{ "_id" : 5, "student" : "C", "subject" : "Biology" }
{ "_id" : 6, "student" : "B", "subject" : "History" }
The Result After Each Stage:
1st Stage: $group
{ "_id" : "C", "studentSubjects" : [ "Biology" ] }
{ "_id" : "B", "studentSubjects" : [ "History", "Science" ] }
{ "_id" : "A", "studentSubjects" : [ "Arts", "Math", "Language" ] }
2nd Stage: $project
{ "_id" : "C", "subjectMatches" : false }
{ "_id" : "B", "subjectMatches" : false }
{ "_id" : "A", "subjectMatches" : true }
3rd Stage: $match
{ "_id" : "A", "subjectMatches" : true }
4th Stage: $project
{ "matched_student" : "A" }
We have nested document and trying to group by array element. Our document structure looks like
/* 1 */
{
"_id" : ObjectId("5a690a4287e0e50010af1432"),
"slug" : [
"true-crime-the-10-most-infamous-american-murder-mysteries",
"10-most-infamous-american-murder-mysteries"
],
"tags" : [
{
"id" : "59244aa6b1be5055278e9b5b",
"name" : "true crime",
"_id" : "59244aa6b1be5055278e9b5b"
},
{
"id" : "5924524db1be5055278ebd6e",
"name" : "Occult Museum",
"_id" : "5924524db1be5055278ebd6e"
},
{
"id" : "5a690f0fc1a72100110c2656",
"_id" : "5a690f0fc1a72100110c2656",
"name" : "murder mysteries"
},
{
"id" : "59244d71b1be5055278ea654",
"name" : "unsolved murders",
"_id" : "59244d71b1be5055278ea654"
}
]
}
We want to find list of all slugs group by tag name. I am trying with following and it gets result but it isn't accurate. We have hundreds of records with each tag but i only get few with my query. I am not sure what i am doing wrong here.
Thanks in advance.
// Requires official MongoShell 3.6+
db.getCollection("test").aggregate(
[
{
"$match" : {
"item_type" : "Post",
"site_id" : NumberLong(2),
"status" : NumberLong(1)
}
},
{$unwind: "$tags" },
{
"$group" : {
"_id" : {
"tags᎐name" : "$tags.name",
"slug" : "$slug"
}
}
},
{
"$project" : {
"tags.name" : "$_id.tags᎐name",
"slug" : "$_id.slug",
"_id" : NumberInt(0)
}
}
],
{
"allowDiskUse" : true
}
);
Expected output is
TagName Slug
----------
true crime "true-crime-the-10-most-infamous-american-murder-mysteries",
"10-most-infamous-american-murder-mysteries"
"All records where tags true crime"
Instead of using slug as a part of _id you should use $push or $addToSet to accumulate them, try:
db.test.aggregate([
{
$unwind: "$tags"
},
{
$unwind: "$slug"
},
{
$group: {
_id: "$tags.name",
slugs: { $addToSet: "$slug" }
}
},
{
$project: {
_id: 1,
slugs: {
$reduce: {
input: "$slugs",
initialValue: "",
in: {
$concat: [ "$$value", ",", "$$this" ]
}
}
}
}
}
])
EDIT: to get comma separated string for slugs you can use $reduce with $concat
Output:
{ "_id" : "murder mysteries", "slugs" : ",10-most-infamous-american-murder-mysteries,true-crime-the-10-most-infamous-american-murder-mysteries" }
{ "_id" : "Occult Museum", "slugs" : ",10-most-infamous-american-murder-mysteries,true-crime-the-10-most-infamous-american-murder-mysteries" }
{ "_id" : "unsolved murders", "slugs" : ",10-most-infamous-american-murder-mysteries,true-crime-the-10-most-infamous-american-murder-mysteries" }
{ "_id" : "true crime", "slugs" : ",10-most-infamous-american-murder- mysteries,true-crime-the-10-most-infamous-american-murder-mysteries" }
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 ] } } }
]);
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.