Why "as" in $lookup is replacing the complete set? - mongodb

Let me first introduce you to the 2 collections I am using :
Collection 1 : users
> db.users.find().pretty()
{
"_id" : ObjectId("5ee4e727d04e4b4ac1ef115b"),
"name" : "Ashutosh Tiwari",
"age" : 21,
"email" : "ashutosh#gmail.com"
}
{
"_id" : ObjectId("5ee4e727d04e4b4ac1ef115c"),
"name" : "Maximilian",
"age" : 32,
"email" : "max#yahoo.com"
}
Collection 2 : posts
> db.posts.find().pretty()
{
"_id" : ObjectId("5ee51b7ed9f661cad505fcc6"),
"title" : "First One",
"text" : "Hey this is the first Author",
"author" : ObjectId("5ee4e727d04e4b4ac1ef115c"),
"comments" : [
{
"user" : ObjectId("5ee4e727d04e4b4ac1ef115b"),
"comment" : "This is my comment"
}
]
}
{
"_id" : ObjectId("5ee5353cd9f661cad505fcc8"),
"title" : "First One",
"author" : ObjectId("5ee4e727d04e4b4ac1ef115c"),
"comments" : [
{
"user" : ObjectId("5ee4e727d04e4b4ac1ef115b"),
"comment" : "This is my comment"
}
]
}
I want to have the user inside comments array in 2nd Collection(posts) to be replaced by the user who has written that comment.
I have tried the query below but it is replacing the comments section !
> db.posts.aggregate([
{ $lookup:
{from: "users",
localField:"comments.user",
foreignField:"_id",
as:"comments.user"
}
} ]).pretty()
{
"_id" : ObjectId("5ee51b7ed9f661cad505fcc6"),
"title" : "First One",
"text" : "Hey this is the first Author",
"author" : ObjectId("5ee4e727d04e4b4ac1ef115c"),
"comments" : {
"user" : [
{
"_id" : ObjectId("5ee4e727d04e4b4ac1ef115b"),
"name" : "Ashutosh Tiwari",
"age" : 21,
"email" : "ashutosh#gmail.com"
}
]
}
}
{
"_id" : ObjectId("5ee5353cd9f661cad505fcc8"),
"title" : "First One",
"author" : ObjectId("5ee4e727d04e4b4ac1ef115c"),
"comments" : {
"user" : [
{
"_id" : ObjectId("5ee4e727d04e4b4ac1ef115b"),
"name" : "Ashutosh Tiwari",
"age" : 21,
"email" : "ashutosh#gmail.com"
}
]
}
}
So, here, whole comments section is now replaced whereas I wanted to have the details in comments.user section so I could see the comment and the user who has posted that comment.

you need to unwind the comments array first
your query may look something like this
db.posts.aggregate([
{
$unwind: "$comments" // unwind the comments array to get a stream of documents, each document has only one comment
},
{
$lookup: {
from: "users",
localField: "comments.user",
foreignField: "_id",
as: "comments.user"
}
},
{
$unwind: "$comments.user" // we know there is only one user inside a single comment, so we can unwind this user array to be an object too (as the lookup returns an array)
},
{
$group: { // then do a group by the document _id to get unique documents with comments array instead of the same document duplicated with different comments
_id: "$_id",
author: {
$first: "$author"
},
text: {
$first: "$text"
},
title: {
$first: "$title"
},
comments: {
$push: "$comments"
}
}
}
])
you can test it here
hope it helps

You can handle it in the projection.
db.posts.aggregate([
{ $lookup:
{from: "users",
localField:"comments.user",
foreignField:"_id",
as:"cu"
}
},
{$unwind:{path:"$cu"}},
{
$project:{
"title":1,
"text":1,
"author":1,
"comments":{
user: "$cu",
comment: { $arrayElemAt: [ "$comments.comment", 0 ] },
}
}
}
])

Related

How to query for a subdocument when only _id of the subdocument is given in mongoose?

I have a snapshot of a document present in the database as shown below
{
"_id" : ObjectId("624e105df043d4ad4ee893ee"),
"title" : "First post",
"author" : "Rituparna",
"comments" : [
{
"content" : "First comment",
"author" : "Ritu",
"_id" : ObjectId("624e109b966fbe64b2a80ea9"),
"replies" : [ ]
},
{
"content" : "second comment",
"author" : "Ritu",
"_id" : ObjectId("624e10a4e33f7962d0002f39"),
"replies" : [ ]
}
],
"__v" : 0
}
Now suppose that you are given only the _id of a commen, say the first comment 624e109b966fbe64b2a80ea9
The end result i am looking for is
{
"content" : "First comment",
"author" : "Ritu",
"_id" : ObjectId("624e109b966fbe64b2a80ea9"),
"replies" : [ ]
}
What query do I need to write when I am given only the _id of the comment and the Post model, that is the parent of the subdocument model ?
db.collection.aggregate([
{
$match: { "comments._id": ObjectId("624e109b966fbe64b2a80ea9") }
},
{
$unwind: "$comments"
},
{
$match: { "comments._id": ObjectId("624e109b966fbe64b2a80ea9") }
},
{
$replaceWith: "$comments"
}
])
https://mongoplayground.net/p/WmeoKTg8tUS

Not able to receive expected result in mongodb between collections

I'm trying to get a product list with logged user wishlist in the product list itself. I'm trying $lookup but not getting the expected result.
Product Document:
[
{
"_id" : ObjectId("6044351794bee8b6e0fce48f"),
"sku" : "P003474",
"name" : "Kitchen Wash"
},
{
"_id" : ObjectId("6085584c42ad3c58c5dbc6b7"),
"sku" : "TS0012",
"name" : "T-shirt"
},
{
"_id" : ObjectId("608d20a90e629fcc0d3a93e1"),
"sku" : "Apple1101",
"name" : "Green Apple"
}
]
Wishlist Document:
wishlist:
[
{
"_id" : ObjectId("608d8af12b7556c445f5cd87"),
"product" : ObjectId("6044351794bee8b6e0fce48f"),
"user" : ObjectId("601fb31a60e0a024143e6ea3"),
"isLiked" : false
},
{
"_id" : ObjectId("608d8bad2b7556c445f5cd88"),
"product" : ObjectId("6085584c42ad3c58c5dbc6b7"),
"user" : ObjectId("601fb31a60e0a024143e6ea3"),
"isLiked" : true
}
]
Expected Result:
Expected output:
[
{
"_id" : ObjectId("6044351794bee8b6e0fce48f"),
"sku" : "P003474",
"name" : "Kitchen Wash",
"isLiked": false
},
{
"_id" : ObjectId("6085584c42ad3c58c5dbc6b7"),
"sku" : "TS0012",
"name" : "T-shirt",
"isLiked": true
},
{
"_id" : ObjectId("608d20a90e629fcc0d3a93e1"),
"sku" : "Apple1101",
"name" : "Green Apple"
}
]
The query I'm trying:
db.getCollection('products').aggregate([
{
$lookup: {
from: "wishlists",
localField: "_id",
foreignField: "product",
as: "product"
}
},
{
$unwind:"$product"
}
])
Not able to get the above-declared result
$unwind causing the issues, first problem is it will remove document when product lookup result empty/[] if you don't specify preserveNullAndEmptyArrays : true, see working behaviour in playground
use $lookup with pipeline and match product id and user id condition
i have changed as name to isLiked in $lookup
$addFields to add isLiked parameter it its present, $arrayElemAt to get isLiked element from first element
db.getCollection('products').aggregate([
{
$lookup: {
from: "wishlists",
let: { product: "$_id" },
pipeline: [
{
$match: {
$and: [
{ $expr: { $eq: ["$$product", "$product"] } },
{ user: ObjectId("601fb31a60e0a024143e6ea3") }
]
}
}
],
as: "isLiked"
}
},
{
$addFields: {
isLiked: { $arrayElemAt: ["$isLiked.isLiked", 0] }
}
}
])
Playground

Unable to aggregate two collections using lookup in MongoDB Atlas

I have an orders collection that looks like this:
{
"_id" : "wJNEiSYwBd5ozGtLX",
"orderId" : 52713,
"createdAt" : ISODate("2020-01-31T04:34:13.790Z"),
"status" : "closed",
"orders" : [
{
"_id" : "ziPzwLuZrz9MNkaRT",
"productId" : 10290,
"quantity" : 2
}
]
}
I have an products collection that looks like this
{
"_id" : "238cwwLkZa6gKNN86",
"productId" : 10290,
"title" : "Product Title",
"price" : 9.9
}
I am trying to merge the price information into the orders information.
Something like:
{
"_id" : "wJNEiSYwBd5ozGtLX",
"orderId" : 52713,
"createdAt" : ISODate("2020-01-31T04:34:13.790Z"),
"status" : "closed",
"orders" : [
{
"_id" : "ziPzwLuZrz9MNkaRT",
"productId" : 10290,
"quantity" : 2,
"price": 9.9
}
]
}
If I try a $lookup command on MongoDB Atlas Dashboard like this:
{
from: 'products',
localField: 'orders.productId',
foreignField: 'productId',
as: 'priceInfo'
}
The aggregated output is (not what I wanted):
{
"_id" : "wJNEiSYwBd5ozGtLX",
"orderId" : 52713,
"createdAt" : ISODate("2020-01-31T04:34:13.790Z"),
"status" : "closed",
"orders" : [
{
"_id" : "ziPzwLuZrz9MNkaRT",
"productId" : 10290,
}
],
"priceInfo": [
{
"_id" : "238cwwLkZa6gKNN86",
"productId" : 10290,
"title" : "Product Title",
"price" : 9.9
}
]
}
I do not need a separate priceInfo array. It will be best if I have the product details information merged into the "orders" array. What should be the aggregation lookup syntax to achieve the desired output?
Demo - https://mongoplayground.net/p/bLqcN7tauWU
Read - $lookup $unwind $first $set $push $group
db.orders.aggregate([
{ $unwind: "$orders" }, // break array of orders into individual documents
{
$lookup: { // join
"from": "products",
"localField": "orders.productId",
"foreignField": "productId",
"as": "products"
}
},
{
$set: {
"orders.price": { "$arrayElemAt": [ "$products.price", 0 ] } // set the price
}
},
{
$group: { // group records back
_id: "$_id",
createdAt: { $first: "$createdAt" },
status: { $first: "$status" },
orderId: { $first: "$orderId" },
orders: { $push: "$orders" }
}
}
])

Mongodb Aggregate calculate average and add it to the document

I have websites which contains 2 documents:
{
"_id" : ObjectId("58503934034b512b419a6eab"),
"website" : "https://www.stackoverflow.com",
"name" : "Stack Exchange",
"keywords" : [
"helping",
"C#",
"PYTHON"
]
}
{
"_id" : ObjectId("58503934034b512b419a6eab"),
"website" : "https://www.google.com.com",
"name" : "Stack Exchange",
"keywords" : [
"search",
"engine",
]
}
I also have another seo_tracking which contains:
{
"_id" : ObjectId("587373d6f6325811c8a0b3ad"),
"position" : "2",
"real_url" : "https://www.stackoverflow.com",
"created_at" : ISODate("2017-01-09T11:28:22.104Z"),
"keyword" : "helping"
},
{
"_id" : ObjectId("587373d6f6325811c8a0b3ad"),
"position" : "4",
"real_url" : "https://www.stackoverflow.com",
"created_at" : ISODate("2017-01-09T11:28:22.104Z"),
"keyword" : "C#"
}
etc.. This contains around 100+ documents
What I want to do is is aggregate the seo_tracking with website on the specific URL (www.stackexchange (in websites) would match www.stackoverflow.com in (seo_tracking)) which I can do fine. However, I would like to return for each of the websites the following:
{
"_id" : ObjectId("587373d6f6325811c8a0b3ad"),
"website":"https://www.stackoverflow.com",
"avg_position" : "2"
}
Then for Google etc.. Even if the avg_position is 0 .. I have tried the following:
db.seo_tracking.aggregate([
{
$lookup:
{
from: "websites",
localField: "real_url",
foreignField: "website",
as: "post_websites"
},
},
{
"$group": {
_id:null,
avg_position:{$avg:"$position"}
}
}
])
However, this just produces:
{
"_id" : null,
"avg_position" : 2.0
}
What I need to do is have website and ideally also need the ID
Any ideas to where I'm going wrong here?
You can try something like this. You'll need to $unwind to access the fields from joined collection and change your grouping key to use the _id from joined collection to get average for each website:
db.seo_tracking.aggregate([{
$lookup: {
from: "website",
localField: "real_url",
foreignField: "website",
as: "post_websites"
},
}, {
$unwind: "$post_websites"
}, {
"$group": {
_id: "$post_websites._id",
avg_position: {
$avg: "$position"
},
website: {
$first: "$real_url"
}
}
}])

Sort a match group by id in aggregate

(Mongo newbie here, sorry) I have a mongodb collection, result of a mapreduce with this schema :
{
"_id" : "John Snow",
"value" : {
"countTot" : 500,
"countCall" : 30,
"comment" : [
{
"text" : "this is a text",
"date" : 2016-11-17 00:00:00.000Z,
"type" : "call"
},
{
"text" : "this is a text",
"date" : 2016-11-12 00:00:00.000Z,
"type" : "visit"
},
...
]
}
}
My goal is to have a document containing all the comments of a certain type. For example, a document John snow with all the calls.
I manage to have all the comments for a certain type using this :
db.general_stats.aggregate(
{ $unwind: '$value.comment' },
{ $match: {
'value.comment.type': 'call'
}}
)
However, I can't find a way to group the data received by the ID (for example john snow) even using the $group property. Any idea ?
Thanks for reading.
Here is the solution for your query.
db.getCollection('calls').aggregate([
{ $unwind: '$value.comment' },
{ $match: {
'value.comment.type': 'call'
}},
{
$group : {
_id : "$_id",
comment : { $push : "$value.comment"},
countTot : {$first : "$value.countTot"},
countCall : {$first : "$value.countCall"},
}
},
{
$project : {
_id : 1,
value : {"countTot":"$countTot","countCall":"$countCall","comment":"$comment"}
}
}
])
or either you can go with $project with $filter option
db.getCollection('calls').aggregate([
{
$project: {
"value.comment": {
$filter: {
input: "$value.comment",
as: "comment",
cond: { $eq: [ "$$comment.type", 'call' ] }
}
},
"value.countTot":"$value.countTot",
"value.countCall":"$value.countCall",
}
}
])
In both case below is my output.
{
"_id" : "John Snow",
"value" : {
"countTot" : 500,
"countCall" : 30,
"comment" : [
{
"text" : "this is a text",
"date" : "2016-11-17 00:00:00.000Z",
"type" : "call"
},
{
"text" : "this is a text 2",
"date" : "2016-11-17 00:00:00.000Z",
"type" : "call"
}
]
}
}
Here is the query which is the extension of the one present in OP.
db.general_stats.aggregate(
{ $unwind: '$value.comment' },
{ $match: {
'value.comment.type': 'call'
}},
{$group : {_id : "$_id", allValues : {"$push" : "$$ROOT"}}},
{$project : {"allValues" : 1, _id : 0} },
{$unwind : "$allValues" }
);
Output:-
{
"allValues" : {
"_id" : "John Snow",
"value" : {
"countTot" : 500,
"countCall" : 30,
"comment" : {
"text" : "this is a text",
"date" : ISODate("2016-11-25T10:46:49.258Z"),
"type" : "call"
}
}
}
}
Got my answer looking at this :
How to retrieve all matching elements present inside array in Mongo DB?
using the $addToSet property in the $group one.