How to project nested array elements with custom field name in MongoDB - mongodb

I have a users collection in MongoDB with this sample row:
[
{
"_id": 1,
"name": "John Doe",
"comments": [
{
"_id": 100,
"content": "Comment 100",
"post": "1000"
},
{
"_id": 101,
"content": "Comment 101",
"post": "1000"
}
]
}
]
I want to project users and comments data after converting _id fields into id. So I used the following query:
db.users.aggregate([
{
$project: {
_id: 0,
id: '$_id',
name:1
comments: {
_id: 0,
id: '$_id',
content: 1,
},
}
}
])
Now the users _id field is successfully converted. But the comments id field is equal to _id of users collection instead of comments.
[
{
"id": 1,
"name": "john Doe",
"comments": [
{
"id": 1,
"content": "comment 100"
},
{
"id": 1,
"content": "comment 101"
},
]
}
]
How can I achieve the right result.

you should use $map in project like this
https://mongoplayground.net/p/H4pueuejYdf
db.collection.aggregate([
{
"$project": {
_id: 0,
id: "$_id",
name: 1,
comments: {
"$map": {
"input": "$comments",
"as": "c",
"in": {
id: "$$c._id",
content: "$$c.content",
post: "$$c.post"
}
}
}
}
}
])

Related

Filter inner documents with array of objects in MongoDB

Have a look at my collections below,
db={
"replies": [
{
"_id": {
"$oid": "60c4814d09488145b72beda9"
},
"post": [
{
"$oid": "5fc67eb5111f570dc3eb7087"
}
],
"likes": [],
"text": "Reply not reported",
"comment": {
"$oid": "60c4813f09488145b72beda8"
},
"__v": 0
},
{
"_id": {
"$oid": "60c4815609488145b72bedaa"
},
"post": [
{
"$oid": "5fc67eb5111f570dc3eb7087"
}
],
"likes": [],
"text": "Reply reported",
"comment": {
"$oid": "60c4813f09488145b72beda8"
},
"__v": 0,
"reportCount": 1,
"reportedUsers": [
{
"$oid": "6252fe50a5cbd65064d4aab8"
}
]
}
],
"comments": [
{
"_id": {
"$oid": "5fca1877111f570dc3eb7088"
},
"replies": [],
"text": "Reported comment",
"post": {
"$oid": "5fc67eb5111f570dc3eb7087"
},
"reportCount": 1,
"reportedUsers": [
{
"$oid": "6252fe50a5cbd65064d4aab8"
}
],
"__v": 0
},
{
"_id": {
"$oid": "60c4813f09488145b72beda8"
},
"replies": [
{
"$oid": "60c4814d09488145b72beda9"
},
{
"$oid": "60c4815609488145b72bedaa"
}
],
"text": "Comment not reported",
"post": {
"$oid": "5fc67eb5111f570dc3eb7087"
},
"__v": 0
}
]
}
I am trying to get comments and replies from post (Object id - 5fc67eb5111f570dc3eb7087). Assume I blocked user with id "6252fe50a5cbd65064d4aab8". In this case the results should not contain the first comment with id "5fca1877111f570dc3eb7088" reportedUsers contains the above mentioned id and the reply with id also need to filter out since it is also reported.
Both comments are comes under post "5fc67eb5111f570dc3eb7087". Can somebody help with the aggregate query for filter comments and replies under post "5fc67eb5111f570dc3eb7087" if the reported users includes user id "6252fe50a5cbd65064d4aab8".
Mongodb playground url
Please let me know any more details needed.
Expected output will be list of comments with expanded list of replies like below,
[
{
"_id": "60c4813f09488145b72beda8",
"replies": [
{
"_id": "60c4814d09488145b72beda9",
"post": "5fc67eb5111f570dc3eb7087",
"likes": [],
"text": "Reply not reported",
"comment": "60c4813f09488145b72beda8"
}
],
"text": "Comment not reported",
"post": "5fc67eb5111f570dc3eb7087"
}
]
Edit:
according to your comment that $oid represents ObjectId, and assuming a post can have several reported user, you can do something like this:
db.comments.aggregate([
{
$match: { "post": ObjectId("5fc67eb5111f570dc3eb7087")}
},
{
$facet: {
reportedUsers: [
{
$match: {reportedUsers: { $ne: []} }
},
{
$project: {
_id: 0,
reportedUsers: 1
}
},
{ $unwind: "$reportedUsers" },
{
$group: {
_id: 0,
rep: { $addToSet: "$reportedUsers" }
}
}
],
ok: [ {$match: {reportedUsers: null}} ]
}
},
{$unwind: "$ok"},
{
$lookup: {
from: "replies",
localField: "ok.replies",
foreignField: "_id",
let: {
reportedUsers: "$reportedUsers"
},
pipeline: [
{
$addFields: {
reportedUsers: {
"$ifNull": [
"$reportedUsers",
[]
]
}
}
}
],
as: "ok.replies"
}
},
{
$project: {
ok: 1,
reportedUsers: {
"$arrayElemAt": [
"$reportedUsers",
0
]
}
}
},
{
$addFields: {
"replies": {
$filter: {
input: "$ok.replies",
as: "item",
cond: {
$eq: [
{
$size: {
"$setIntersection": [
"$$item.reportedUsers",
"$reportedUsers.rep"
]
}
},
0
]
}
}
},
reportedUsers: 0
}
},
{
$project: {
_id: "$ok._id",
post: "$ok.post",
replies: 1,
text: "$ok.text"
}
}
])
As you can see here
This query is $matching all the comments for the required post, then split it to comments without reportedUsers (ok) and group all the reportedUsers, via a $facet step. Then it collects all replies from the replies collection, via $lookup and adding an empty reportedUsers array if it does not exists. Next step is filter the replies and remove all replies that has a reported user that was in our list of reportedUsers grouped before.

Query maximum N records of each group base on a condition in MongoDB?

I have a question regarding querying data in MongoDB. Here is my sample data:
{
"_id": 1,
"category": "fruit",
"userId": 1,
"name": "Banana"
},
{
"_id": 2,
"category": "fruit",
"userId": 2,
"name": "Apple"
},
{
"_id": 3,
"category": "fresh-food",
"userId": 1,
"name": "Fish"
},
{
"_id": 4,
"category": "fresh-food",
"userId": 2,
"name": "Shrimp"
},
{
"_id": 5,
"category": "vegetable",
"userId": 1,
"name": "Salad"
},
{
"_id": 6,
"category": "vegetable",
"userId": 2,
"name": "carrot"
}
The requirements:
If the category is fruit, returns all the records match
If the category is NOT fruit, returns maximum 10 records of each category grouped by user
The category is known and stable, so we can hard-coded in our query.
I want to get it done in a single query. So the result expected should be:
{
"fruit": [
... // All records of
],
"fresh-food": [
{
"userId": 1,
"data": [
// Top 10 records of user 1 with category = "fresh-food"
]
},
{
"userId": 2,
"data": [
// Top 10 records of user 2 with category = "fresh-food"
]
},
...
],
"vegetable": [
{
"userId": 1,
"data": [
// Top 10 records of user 1 with category = "vegetable"
]
},
{
"userId": 2,
"data": [
// Top 10 records of user 2 with category = "vegetable"
]
},
]
}
I've found the guideline to group by each group using $group and $slice, but I can't apply the requirement number #1.
Any help would be appreciated.
You need to use aggregation for this
$facet to categorize incoming data, we categorized into two. 1. Fruit and 2. non_fruit
$match to match the condition
$group first group to group the data based on category and user. Second group to group by its category only
$objectToArray to make the object into key value pair
$replaceRoot to make the non_fruit to root with fruit
Here is the code
db.collection.aggregate([
{
"$facet": {
"fruit": [
{ $match: { "category": "fruit" } }
],
"non_fruit": [
{
$match: {
$expr: {
$ne: [ "$category", "fruit" ]
}
}
},
{
$group: {
_id: { c: "$category", u: "$userId" },
data: { $push: "$$ROOT" }
}
},
{
$group: {
_id: "$_id.c",
v: {
$push: {
uerId: "$_id.u",
data: { "$slice": [ "$data", 3 ] }
}
}
}
},
{ $addFields: { "k": "$_id", _id: "$$REMOVE" } }
]
}
},
{ $addFields: { non_fruit: { "$arrayToObject": "$non_fruit" } }},
{
"$replaceRoot": {
"newRoot": {
"$mergeObjects": [ "$$ROOT", "$non_fruit" ]
}
}
},
{ $project: { non_fruit: 0 } }
])
Working Mongo playground

MongoDB aggregate merging fields

I have a mongo Database I'll like to "join" two of them and then merge some other fields:
Let's see the schemas:
Students Schema (and data):
{
"_id": ObjectId("5fbd564981b1313de790b580"),
"name": "John Doe",
"age": "21",
"image": "https://XXXX/481.png",
"subjects": [
{
"_id": ObjectId("5fbd4e6881b1313de790b56b"),
"passed": true,
},
{
"_id": ObjectId("5fcb63fa8814d96876c687bf"),
}
],
"__v": NumberInt("1"),
}
and Subject schema:
{
"_id": ObjectId("5fbd4e6881b1313de790b56b"),
"course": 3,
"teacher": "John Smith",
"name": "Math",
},
{
"_id": ObjectId("5fcb63fa8814d96876c687bf"),
"name": "IT",
"course": 8,
"teacher": "John Peter",
}
What I'll like to make a query with the subjects (all info) of a student, also if the student have additional fields in subject like passed add it to the subject subdocument.
Here is my query till now:
db.students.aggregate([
{
$match:
{
_id : ObjectId('5fbd564981b1313de790b580')
}
},
{
$lookup :
{
from : "subjects",
localField : "subjects._id",
foreignField : "_id",
as : "FoundSubject"
}
}
]);
which correctly make the "join" but the merge is still missing, I got as result:
{
"_id": ObjectId("5fbd564981b1313de790b580"),
"name": "John Doe",
"age": "21",
"image": "https://XXXX/481.png",
"subjects": [
{
"_id": ObjectId("5fbd4e6881b1313de790b56b"),
"passed": true,
},
{
"_id": ObjectId("5fcb63fa8814d96876c687bf"),
}
],
"__v": NumberInt("1"),
"FoundSubject": [
{
"_id": ObjectId("5fbd4e6881b1313de790b56b"),
"course": 3,
"teacher": "John Smith",
"name": "Math"
},
{
"_id": ObjectId("5fcb63fa8814d96876c687bf"),
"name": "IT",
"course": 8,
"teacher": "John Peter"
}
]
}
but I'll like to have:
{
"_id": ObjectId("5fbd564981b1313de790b580"),
"name": "John Doe",
"age": "21",
"image": "https://XXXX/481.png",
"subjects": [
{
"_id": ObjectId("5fbd4e6881b1313de790b56b"),
"course": 3,
"teacher": "John Smith",
"name": "Math",
"passed": true,
},
{
"_id": ObjectId("5fcb63fa8814d96876c687bf"),
"name": "IT",
"course": 8,
"teacher": "John Peter"
}
],
"__v": NumberInt("1"),
}
with merged data and field "passed" added. How can accomplish that?
I'm new to MongoDB coming from MySQL.
Thanks
You need to merge both objects, add below stage after $lookup,
MongoDB Version From 3.4
$map to iterate loop of students array
$reduce to iterate loop of FoundSubject array, check condition if condition match then return required fields otherwise return initial value
$project to remove FoundSubject from result
{
$addFields: {
subjects: {
$map: {
input: "$subjects",
as: "s",
in: {
$reduce: {
input: "$FoundSubject",
initialValue: {},
in: {
$cond: [
{ $eq: ["$$s._id", "$$this._id"] },
{
_id: "$$this._id",
course: "$$this.course",
name: "$$this.name",
teacher: "$$this.teacher",
passed: "$$s.passed"
},
"$$value"
]
}
}
}
}
}
}
},
{ $project: { FoundSubject: 0 } }
Playground
MongoDB Version From 4.4
$map to iterate loop of students array,
$filter to get matching document from FoundSubject array and $first to get first object from array returned by filter
$mergeObjects to merge current objects with found result object from filter
remove FoundSubject using $$REMOVE
// skipping your stages
{
$addFields: {
FoundSubject: "$$REMOVE",
subjects: {
$map: {
input: "$subjects",
as: "s",
in: {
$mergeObjects: [
"$$s",
{
$first: {
$filter: {
input: "$FoundSubject",
cond: { $eq: ["$$s._id", "$$this._id"] }
}
}
}
]
}
}
}
}
}
Playground

How to match each array field to other field in Mongodb

I have an array as below:
const test = [{
"_id": 1,
"name": "apple",
"car": "ford"
},{
"_id": 2,
"name": "melon",
"car": "ferrari"
},{
"_id": 3,
"name": "perl",
"car": "Renaut"
}]
And there is are documents of Mongodb as below:
[{
"name": "perl", "company": "A"
},{
"name": "melon", "company": "B"
},{
"name": "apple", "company": "C"
},{
"name": "apple", "company": "D"
},{
"name": "perl", "company": "E"
},{
"name": "apple", "company": "F"
}]
And I want to get this result using mongodb aggregate:
[{
"name": "perl", "company": "A", testInform: { "_id": 3, "name": "perl", "car": "Renaut"}
},{
"name": "melon", "company": "B", testInform: { "_id": 2, "name": "melon", "car": "ferrari"}
},{
"name": "apple", "company": "C", testInform: { "_id": 1, "name": "apple", "car": "ford"}
},{
"name": "apple", "company": "D", testInform: { "_id": 1, "name": "apple", "car": "ford"}
},{
"name": "perl", "company": "E", testInform: { "_id": 3, "name": "perl", "car": "Renaut"}
},{
"name": "apple", "company": "F", testInform: { "_id": 1, "name": "apple", "car": "ford"}
}]
I think to use aggregate with $match and $facet, etc., but I don't know exactly how to do this. Could you recommend a solution for this?
Thank you so much for reading this.
$lookup with pipeline keyword
db.demo2.aggregate(
{
$lookup:
{
from: "demo1",
let: { recordName: "$name"},
pipeline: [
{ $match:
{ $expr:
{ $and:
[
{ $eq: [ "$$recordName", "$name" ] },
]
}
}
},
],
as: "testInform"
}
}
)
If the test array data is stored in a collection then acheiving O/P is pretty straightforward $lookup with $project aggregation
$arrayElemAt Why? because the lookup would fetch the joined documents in an array as testInform
db.maindocs.aggregate([
{
$lookup: {
from: "testdocs",
localField: "name",
foreignField: "name",
as: "testInform"
}
},
{
$project: {
_id: 0,
name: 1,
company: 1,
testInform: { $arrayElemAt: ["$testInform", 0] }
}
}
]);
Update based on comments:
The idea is to iterate the cursor from the documents stored in mongodb Array.prototype.find() the object from test which matches the name field, add it to result.
const test = [
{
_id: 1,
name: "apple",
car: "ford"
},
{
_id: 2,
name: "melon",
car: "ferrari"
},
{
_id: 3,
name: "perl",
car: "Renaut"
}
];
const cursor = db.collection("maindocs").find();
const result = [];
while (await cursor.hasNext()) {
const doc = await cursor.next();
const found = test.find(e => e.name === doc.name);
if (found) {
doc["testInform"] = found;
}
result.push(doc);
}
console.info("RESULT::", result);
The aggregation has one stage: Iterate over the test array and get the array element as an object which matches the name field in both the document and the array (using the $reduce operator).
const test = [ { ... }, ... ]
db.test_coll.aggregate( [
{
$addFields: {
testInform: {
$reduce: {
input: test,
initialValue: { },
in: {
$cond: [ { $eq: [ "$$this.name", "$name" ] },
{ $mergeObjects: [ "$$this", "$$value" ] },
"$$value"
]
}
}
}
}
}
] )

MongoDB multiple counts, single document, arrays

I have been searching on stackoverflow and cannot find exactly what I am looking for and hope someone can help. I want to submit a single query, get multiple counts back, for a single document, based on array of that document.
My data:
db.myCollection.InsertOne({
"_id": "1",
"age": 30,
"items": [
{
"id": "1",
"isSuccessful": true,
"name": null
},{
"id": "2",
"isSuccessful": true,
"name": null
},{
"id": "3",
"isSuccessful": true,
"name": "Bob"
},{
"id": "4",
"isSuccessful": null,
"name": "Todd"
}
]
});
db.myCollection.InsertOne({
"_id": "2",
"age": 22,
"items": [
{
"id": "6",
"isSuccessful": true,
"name": "Jeff"
}
]
});
What I need back is the document and the counts associated to the items array for said document. In this example where the document _id = "1":
{
"_id": "1",
"age": 30,
{
"totalIsSuccessful" : 2,
"totalNotIsSuccessful": 1,
"totalSuccessfulNull": 1,
"totalNameNull": 2
}
}
I have found that I can get this in 4 queries using something like this below, but I would really like it to be one query.
db.test1.aggregate([
{ $match : { _id : "1" } },
{ "$project": {
"total": {
"$size": {
"$filter": {
"input": "$items",
"cond": { "$eq": [ "$$this.isSuccessful", true ] }
}
}
}
}}
])
Thanks in advance.
I am assuming your expected result is invalid since you have an object literal in the middle of another object and also you have totalIsSuccessful for id:1 as 2 where it seems they should be 3. With that said ...
you can get similar output via $unwind and then grouping with $sum and $cond:
db.collection.aggregate([
{ $match: { _id: "1" } },
{ $unwind: "$items" },
{ $group: {
_id: "_id",
age: { $first: "$age" },
totalIsSuccessful: { $sum: { $cond: [{ "$eq": [ "$items.isSuccessful", true ] }, 1, 0 ] } },
totalNotIsSuccessful: { $sum: { $cond: [{ "$ne": [ "$items.isSuccessful", true ] }, 1, 0 ] } },
totalSuccessfulNull: { $sum: { $cond: [{ "$eq": [ "$items.isSuccessful", null ] }, 1, 0 ] } },
totalNameNull: { $sum: { $cond: [ { "$eq": [ "$items.name", null ]}, 1, 0] } } }
}
])
The output would be this:
[
{
"_id": "_id",
"age": 30,
"totalIsSuccessful": 3,
"totalNameNull": 2,
"totalNotIsSuccessful": 1,
"totalSuccessfulNull": 1
}
]
You can see it working here