$lookup for each element in array mongo aggregation - mongodb

I have users schema with people field containing viewers property which contains an array of ObjectIds as a reference of users collection itself for example
{
"username": "user1",
"password": "password",
"email": "user1#gmail.com",
"people": [{
"id": 1,
"viewers": ["ObjectId....", "ObjectId...."]
},
{
"id": 2,
"viewers": ["ObjectId....", "ObjectId...."]
}
]
}
What I need to do is $lookup for each element inside viewers the problem is when I use below pipeline is pushing the same viewers for each document
{
$unwind: {
path: "$people.viewers",
preserveNullAndEmptyArrays: true
}
},
{
$lookup: {
from: "users",
localField: "people.viewers",
foreignField: "_id",
as: "people"
}
},
{
$unwind: {
path: "$viewers",
preserveNullAndEmptyArrays: true
}
},
{
$project: {
username: 1,
"viewers.username": 1
}
},
{
$group: {
_id: "$_id",
username: { $first: "$username" },
people: { $first: "$people" },
viewers: { $push: "$viewers" }
}
},
{
$project: {
username: 1,
"people.id": 1,
"people.viewers": "$viewers"
}
}
What is the wrong in that aggregation

You can try below aggregation in mongodb 3.4 and above
User.aggregate([
{ "$unwind": { "path": "$people", "preserveNullAndEmptyArrays": true }},
{ "$lookup": {
"from": "users",
"localField": "people.viewers",
"foreignField": "_id",
"as": "people.viewers"
}},
{ "$group": {
"_id": "$_id",
"username": { "$first": "$username" },
"email": { "$first": "$email" },
"people": { "$push": "$people" }
}}
])

Related

Find one user then get their ranking based on their total points using MongoDB

So I got the following data:
Users collection
{
_id: ObjectId("62a2a0422ec90fea68390aaa"),
name: 'Robert Yamashita',
username: 'robyama',
email: 'robert.yamashita#rocketmail.com',
},
{
_id: ObjectId("62a2a0452ec90fea68390aad"),
name: 'Charles X',
username: 'cvx',
email: 'charles.xxx#rocketmail.com',
}
Points collection
{
userId: ObjectId("62a2a0422ec90fea68390aaa"),
action: 'Liked a post',
points: 10,
}
{
userId: ObjectId("62a2a0422ec90fea68390aaa"),
action: 'Liked a post',
points: 10,
}
{
userId: ObjectId("62a2a0452ec90fea68390aad"),
action: 'Liked a comment',
points: 5,
}
I created a pipeline to get the total points of username robyama using the following query:
db.users.aggregate([
{ $match: { username: 'robyama' } },
{
$lookup: {
from: 'points',
localField: '_id',
foreignField: 'user',
as: 'userPoints'
}
},
{
$unwind: '$userPoints'
},
{
$group: {
_id: {
name: '$name',
email: '$email',
username: '$username',
},
count: { $sum: '$userPoints.points' }
}
}
]);
I got the following result:
{
"_id": {
"name": "Robert Yamashita",
"email": "robert.yamashita#rocketmail.com",
"username": "robyama",
},
"count": 20
}
This is exactly what I needed but I wanted to add a ranking field to the returned query since Robert has 20 points and Charles only has 5. So ideally I want the result to be this:
{
"_id": {
"name": "Robert Yamashita",
"email": "robert.yamashita#rocketmail.com",
"username": "robyama",
},
"count": 20
"rank": 1
}
What should I add to my pipeline to get the above output? Any help would be greatly appreciated!
Here's another way to do it. There's only one "$lookup" with one embedded "$group" so it should be fairly efficient. The "$project" seems a bit contrived, but it gives the output in the format you want.
db.users.aggregate([
{
"$match": {
"username": "robyama"
}
},
{
"$lookup": {
"from": "points",
"as": "sortedPoints",
"pipeline": [
{
"$group": {
"_id": "$userId",
"count": {"$sum": "$points"}
}
},
{"$sort": {"count": -1}}
]
}
},
{
"$unwind": {
"path": "$sortedPoints",
"includeArrayIndex": "idx"
}
},
{
"$match": {
"$expr": {
"$eq": ["$_id", "$sortedPoints._id"]
}
}
},
{
"$project": {
"_id": {
"name": "$name",
"username": "$username",
"email": "$email"
},
"count": "$sortedPoints.count",
"rank": {
"$add": ["$idx", 1]
}
}
}
])
Try it on mongoplayground.net.
Well, this is one way of doing it.
Perform join using $lookup and calculate counts for each user.
Sort the elements by counts in desc order.
Group documents by _id as NULL and push them all in an array.
Unwind the array, along with getting row numbers.
Find your required document and calculate the rank using row number.
db.users.aggregate([
{
$lookup: {
from: "points",
localField: "_id",
foreignField: "userId",
as: "userPoints"
}
},
{
$unwind: "$userPoints"
},
{
$group: {
_id: {
name: "$name",
email: "$email",
username: "$username",
},
count: {
$sum: "$userPoints.points"
}
}
},
{
"$sort": {
count: -1
}
},
{
"$group": {
"_id": null,
"docs": {
"$push": "$$ROOT",
}
}
},
{
"$unwind": {
path: "$docs",
includeArrayIndex: "rownum"
}
},
{
"$match": {
"docs._id.username": "robyama"
}
},
{
"$addFields": {
"docs.rank": {
"$add": [
"$rownum",
1
]
}
}
},
{
"$replaceRoot": {
"newRoot": "$docs"
}
}
])
This is the playground link.

How can I get a mongo subset of a collection based on an another collection

I have two collections.
Collection 1 is like an account.
Collection 2 creates a unique association between a user and an account
I am trying to return the accounts for which the user has no association
Collection1 schema
const Collection1Schema = new Schema({
name: { type: String, required: true },
});
Collection1 data
[
{
"_id": "61cf8452fca008360872c9cd",
"name": "Aff 2"
},
{
"_id": "61cf845ffca008360872c9d0",
"name": "AFF 1"
},
{
"_id": "61cf8468fca008360872c9d3",
"name": "Aff 3"
}
]
Collection2 schema
const Collection2Schema = new Schema({
userID: { type: Schema.Types.ObjectId, required: true },
col_1_ID: { type: Schema.Types.ObjectId, required: true },
});
Collection2 data
[
{
"_id": "61e05bb5fe1d8327d4c73663",
"userID": "61cf82dac828bd519cfd38ca",
"col_1_ID": "61cf845ffca008360872c9d0"
},
{
"_id": "61e05c14fe1d8327d4c7367d",
"userID": "61cf82dac828bd519cfd38ca",
"col_1_ID": "61cf8468fca008360872c9d3"
},
{
"_id": "61e05ca0fe1d8327d4c73695",
"userID": "61e05906246ccc41d4ebd30f",
"col_1_ID": "61cf8452fca008360872c9cd"
}
]
This is what I have so far... but it does not return what the user is NOT apart of
I am using Collection2 as the basis in the pipeline
[
{
'$match': {
'userID': new ObjectId('61cf82dac828bd519cfd38ca')
}
}, {
'$lookup': {
'from': 'Collection1',
'localField': 'col_1_ID',
'foreignField': '_id',
'as': 'aa'
}
}, {
'$unwind': {
'path': '$aa',
'preserveNullAndEmptyArrays': true
}
}
]
What I would like to return is all the collection 1 documents ( where userIdD = '61cf82dac828bd519cfd38ca') is NOT associated in collection 2 ... like this :
[
{
"_id": "61cf8452fca008360872c9cd",
"name": "Aff 2"
}
]
UPDATE 1
Here is a playground where another user has joined another account, so the pipeline does not return "Aff 2" like expected
https://mongoplayground.net/p/W6W88_2MaI3
UPDATE 2
Here is a playground that almost does what I want... it's returning duplication "AFF 2" entries.
https://mongoplayground.net/p/nTI3MKNPEmD
try the inversing lookup
https://mongoplayground.net/p/hXAYyv8X461
db.Collection1.aggregate([
{
"$lookup": {
"from": "Collection2",
"localField": "_id",
"foreignField": "col_1_ID",
"as": "joined_docs"
}
},
{
$unwind: {
"path": "$joined_docs"
}
},
{
$match: {
"joined_docs.userID": {
$ne: "61cf82dac828bd519cfd38ca"
}
}
},
{
$project: {
"joined_docs": 0
}
}
])
ANSWER:
after messing around with several mongo playgrounds and digging into a few different pipeline attributes... here is what works:
https://mongoplayground.net/p/xbZeRfVcrZq
Data:
db={
"Collection1": [
{
"_id": "61cf8452fca008360872c9cd",
"name": "Aff 2"
},
{
"_id": "61cf845ffca008360872c9d0",
"name": "AFF 1"
},
{
"_id": "61cf8468fca008360872c9d3",
"name": "Aff 3"
}
],
"Collection2": [
{
"_id": "61e05bb5fe1d8327d4c73663",
"userID": "61cf82dac828bd519cfd38ca",
"col_1_ID": "61cf845ffca008360872c9d0"
},
{
"_id": "61e05c14fe1d8327d4c7367d",
"userID": "61cf82dac828bd519cfd38ca",
"col_1_ID": "61cf8468fca008360872c9d3"
},
{
"_id": "61e05ca0fe1d8327d4c73695",
"userID": "61e05906246ccc41d4ebd30f",
"col_1_ID": "61cf8452fca008360872c9cd"
},
{
"_id": "61e05c14fe1d8327d4c73600",
"userID": "61cf82dac828bd519cfd3111",
"col_1_ID": "61cf8468fca008360872c9d3"
},
{
"_id": "61e05c14fe1d8327d4c73601",
"userID": "61cf82dac828bd519cfd3112",
"col_1_ID": "61cf8452fca008360872c9cd"
},
]
}
Pipeline:
db.Collection1.aggregate([
{
"$lookup": {
"from": "Collection2",
"localField": "_id",
"foreignField": "col_1_ID",
"as": "joined_docs"
}
},
{
$match: {
"joined_docs.userID": {
$ne: "61cf82dac828bd519cfd38ca"
}
}
},
{
$unwind: {
"path": "$joined_docs",
}
},
{
$group: {
_id: "$_id",
"name": {
"$first": "$name"
},
}
}
])
result:
[
{
"_id": "61cf8452fca008360872c9cd",
"name": "Aff 2"
}
]
try this instead:
https://mongoplayground.net/p/HDm2sbdvH88
db.Collection1.aggregate([
{
"$lookup": {
"from": "Collection2",
"localField": "_id",
"foreignField": "col_1_ID",
"as": "joined_docs"
}
},
{
$unwind: {
"path": "$joined_docs"
}
},
{
$group: {
_id: {
account_id: "$_id",
account_name: "$name",
},
user_ids: {
$push: {
"userID": "$joined_docs.userID"
}
}
}
},
{
$match: {
"user_ids.userID": {
$nin: [
"61cf82dac828bd519cfd38ca"
]
}
}
},
{
$project: {
user_ids: 0
}
}
])

mongodb aggregation pipeline not returning proper result and slow

I have three collections users, products and orders , orders type has two possible values "Cash" and "Online". One users can have single/multiple products and products have none/single/multiple orders. I want to text search on users collection on name. Now I want to write a query which will return all matching users on text search highest text score first, it might be possible one user's name is returning top score but don't have any products and orders.
I have written a query but it's not returning users who has text score highest but don't have any products/orders. It's only returning users who has record present in all three collections. And also performance of this query is not great taking long time if a user has lot of products for example more than 3000 products. Any help appreciated.
db.users.aggregate(
[
{
"$match": {
"$text": {
"$search": "john"
}
}
},
{
"$addFields": {
"score": {
"$meta": "textScore"
}
}
},
{
"$sort": {
"Score": {
"$meta": "textScore"
}
}
},
{
"$skip": 0
},
{
"$limit": 6
},
{
"$lookup": {
"from": "products",
"localField": "userId",
"foreignField": "userId",
"as": "products"
}
},
{ $unwind: '$products' },
{
"$lookup": {
"from": "orders",
"let": {
"products": "$products"
},
"pipeline": [
{
"$match": {
"$expr": {
"$and": [
{
"$in": [
"$productId",
["$$products.productId"]
]
},
{
"$eq": [
"$orderType",
"Cash"
]
}
]
}
}
}
],
"as": "orders"
}
},
{ $unwind: 'orders' },
{
$group: {
_id: "$_id",
name: { $first: "$name" },
userId: { $first: "$userId" },
products: { $addToSet: "$products" },
orders: { $addToSet: "$orders" },
score: { $first: "$score" },
}
},
{ $sort: { "score": -1 } }
]
);
Issue:
Every lookup produces an array which holds the matched documents. When no documents are found, the array would be empty. Unwinding that empty array would break the pipeline immediately. That's the reason, we are not getting user records with no products/orders. We would need to preserve such arrays so that the pipeline execution can continue.
Improvements:
In orders lookup, the $eq can be used instead of $in, as we already
unwinded the products array and each document now contains only
single productId
Create an index on userId in products collection to make the query more efficient
Following is the updated query:
db.users.aggregate([
{
"$match": {
"$text": {
"$search": "john"
}
}
},
{
"$addFields": {
"score": {
"$meta": "textScore"
}
}
},
{
"$skip": 0
},
{
"$limit": 6
},
{
"$lookup": {
"from": "products",
"localField": "userId",
"foreignField": "userId",
"as": "products"
}
},
{
$unwind: {
"path":"$products",
"preserveNullAndEmptyArrays":true
}
},
{
"$lookup": {
"from": "orders",
"let": {
"products": "$products"
},
"pipeline": [
{
"$match": {
"$expr": {
"$and": [
{
"$eq": [
"$productId",
"$$products.productId"
]
},
{
"$eq": [
"$orderType",
"Cash"
]
}
]
}
}
}
],
"as": "orders"
}
},
{
$unwind: {
"path":"$orders"
"preserveNullAndEmptyArrays":true
}
},
{
$group: {
_id: "$_id",
name: {
$first: "$name"
},
userId: {
$first: "$userId"
},
products: {
$addToSet: "$products"
},
orders: {
$addToSet: "$orders"
},
score: {
$first: "$score"
}
}
},
{
$sort: {
"score": -1
}
}
]);
To get more information on unwind, please check https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/

How to perform lookup in aggregation when foreignField is in array?

I have two collections:
// users
{
_id: "5cc7c8773861275845167f7a",
name: "John",
accounts: [
{
"_id": "5cc7c8773861275845167f76",
"name": "Name1",
},
{
"_id": "5cc7c8773861275845167f77",
"name": "Name2",
}
]
}
// transactions
{
"_id": "5cc7c8773861275845167f75",
"_account": "5cc7c8773861275845167f76",
}
Using lookup I want to populate _account field in transactions collection with respective element from users.accounts array.
So, I want the final result as:
{
"_id": "5cc7c8773861275845167f75",
"_account": {
"_id": "5cc7c8773861275845167f76",
"name": "Name1",
},
}
I have already tried using this code:
db.transactions.aggregate([
{
$lookup:
{
from: "users.accounts",
localField: "_account",
foreignField: "_id",
as: "account"
}
}
])
In the result account array comes as empty.
What is the correct way to do it ?
You can use below aggregation with mongodb 3.6 and above
db.transactions.aggregate([
{ "$lookup": {
"from": "users",
"let": { "account": "$_account" },
"pipeline": [
{ "$match": { "$expr": { "$in": ["$$account", "$accounts._id"] } } },
{ "$unwind": "$accounts" },
{ "$match": { "$expr": { "$eq": ["$$account", "$accounts._id"] } } }
],
"as": "_account"
}},
{ '$unwind': '$_account' }
])
Try with this
I think case 1 is better.
1)-
db.getCollection('transactions').aggregate([
{
$lookup:{
from:"user",
localField:"_account",
foreignField:"accounts._id",
as:"trans"
}
},
{
$unwind:{
path:"$trans",
preserveNullAndEmptyArrays:true
}
},
{
$unwind:{
path:"$trans.accounts",
preserveNullAndEmptyArrays:true
}
},
{$match: {$expr: {$eq: ["$trans.accounts._id", "$_account"]}}},
{$project:{
_id:"$_id",
_account:"$trans.accounts"
}}
])
2)-
db.getCollection('users').aggregate([
{
$unwind:{
path:"$accounts",
preserveNullAndEmptyArrays:true
}
},
{
$lookup:
{
from: "transactions",
localField: "accounts._id",
foreignField: "_account",
as: "trans"
}
},
{$unwind:"$trans"},
{
$project:{
_id:"$trans._id",
_account:"$accounts"
}
}
])

Merge two $lookup collections in MongoDB

Here i am try to get aggregated result from my challenge collection with challengeusers and challengeusers has the user_id and i used $lookup to join the users too.
When i use this query, i am getting following output.
"challenges": [
{
"_id": "5b7bf6fd87ec106308d7e3c1",
"start_date": "2018-08-09T12:40:21.470Z",
"end_date": "2018-08-05T12:40:21.470Z",
"challnegedusers": [
{
"chalenge_id": "5b7bf6fd87ec106308d7e3c1",
"user_id": "5b75623db457045e3bb12e0a",
"status": 1
},
{
"user_id": "5b75643c0a97791bcc9ed64c",
"status": 1
},
{
"user_id": "5b756144b457045e3bb12e08",
"status": 1
}
],
"users": [
{
"_id": "5b756144b457045e3bb12e08",
"first_name": "XYZ"
},
{
"_id": "5b75623db457045e3bb12e0a",
"first_name": "BAC"
},
{
"_id": "5b75643c0a97791bcc9ed64c",
"first_name": "YTA"
}
]
}
]
But i want the challengeusers and users to merge in a single object.
Most of all i want the status of challengeusers with user's info.
expected output:
"challenges": [
{
"_id": "5b7bf6fd87ec106308d7e3c1",
"start_date": "2018-08-09T12:40:21.470Z",
"end_date": "2018-08-05T12:40:21.470Z",
"challnegedusers": [
{
"user_id": "5b75623db457045e3bb12e0a",
"status": 1,
"first_name": "BAC"
},
{
"user_id": "5b75643c0a97791bcc9ed64c",
"status": 1,
"first_name": "YTA"
},
{
"user_id": "5b756144b457045e3bb12e08",
"status": 1,
"first_name": "XYZ"
}
]
}
]
MongoDB Aggregate Query that i am using.
let challenges = await ChallengeModel.aggregate([
{ $match: criteria },
{ $lookup: {
from: 'challengeusers',
localField: '_id',
foreignField: 'challenge_id',
as: 'challnegedusers'
} },
{ $lookup: {
from: 'appusers',
localField: 'challnegedusers.user_id',
foreignField: '_id',
as: 'users'
} },
{ $sort: {created_at: -1}}
]);
You can try below aggregation in mongodb 3.6
ChallengeModel.aggregate([
{ "$match": criteria },
{ "$lookup": {
"from": "challengeusers",
"let": { "challengeusersId": "$_id" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$challenge_id", "$$challengeusersId" ] } } },
{ "$lookup": {
"from": "appusers",
"let": { "user_id": "$user_id" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$_id", "$$user_id" ] } } },
],
"as": "user"
}},
{ "$unwind": "$user" },
{ "$addFields": { "first_name": "$user.first_name" }},
{ "$project": { "user": 0 }}
],
"as": "challnegedusers"
}}
])
And with your approach you can try this
ChallengeModel.aggregate([
{ "$match": criteria },
{ "$lookup": {
"from": "challengeusers",
"localField": "_id",
"foreignField": "challenge_id",
"as": "challnegedusers"
}},
{ "$unwind": "challnegedusers" },
{ "$lookup": {
"from": "appusers",
"localField": "challnegedusers.user_id",
"foreignField": "_id",
"as": "challnegedusers.user"
}},
{ "$unwind": "challnegedusers.user" },
{ "$addFields": { "challnegedusers.first_name": "$challnegedusers.user.first_name" }},
{ "$sort": { "created_at": -1 }},
{ "$group": {
"_id": "$_id",
"start_date": { "$first": "$start_date" }
"end_date": { "$first": "$end_date" },
"challnegedusers": { "$push": "$challnegedusers" }
}}
])