How to use NOT IN array condition inside mongodb $lookup aggregate - mongodb

I have two collections:
Users:
{
_id: ObjectId('5e11d2d8ad9c4b6e05e55b82'),
name:"vijay"
}
Followers :
{
_id:ObjectId('5ed0c8faac47af698ab9f659'),
user_id:ObjectId('5e11d2d8ad9c4b6e05e55b82'),
following:[
ObjectId(5ee5ca5fac47af698ab9f666'),
ObjectId('5df7c0a66243414ad2663088')
]
created_at:"2020-05-29T08:34:02.959+00:00"
}
I need to list all users who are not in the following array from users table for a particular user, I've come up with the below by writing aggregate function in followers table.
[
{
'$match': {
'user_id': new ObjectId('5e11d2d8ad9c4b6e05e55b82')
}
}, {
'$project': {
'user_id': '$user_id',
'following': '$following'
}
}, {
'$lookup': {
'from': 'users',
'pipeline': [
{
'$match': {
'_id': {
'$nin': [
'$following'
]
}
}
}
],
'as': 'result'
}
}
]
but this is not populating the output I needed.
Can anyone help me with this?
Thanks

You should use $not $in with $expr expression, Because $nin is a query operator not for aggregation expression,
one more fix you need to create variable using let: { following: "$following"} and use inside pipeline $$following, because lookup pipeline will not allow to access fields without reference,
{
$lookup: {
from: "Users",
let: {
following: "$following"
},
pipeline: [
{
$match: {
$expr: {
$not: {
$in: [
"$_id",
"$$following"
]
}
}
}
}
],
as: "result"
}
}
Working Playground: https://mongoplayground.net/p/08OT6NnuYHx

Related

MongoDb aggregation lookup finding document with properties value included in array of another collection

I'm new to mongodb aggregation and I'm not able to extract all docuemnts from a collection that as a field value that is included in an array of another collection.
let's say I have a collection 'users' with documets like:
{
user: 'foo',
urls: ['/url1', '/url2', '/url3']
}
and another collection 'menu' with docuemnts like:
{
name: 'bar',
link: '/url1234',
component: 'layout'
}
{
name: 'baz',
link: '/url454',
component: 'layout'
}
The desired result from the above scenario is
{
name: 'bar',
link: '/url1234'
}
I'm using a pipeline like this but I'm stacked in getting back only the documetns where the url from users collection is included in link field from the menu collection
'$match': {
'user': 'foo'
}
}, {
'$project': {
'urls': 1,
'_id': 0
}
}, {
'$lookup': {
'from': 'menu',
'pipeline': [
{
'$match': {
'component': 'layout'
}
}
],
'as': 'results'
}
}
]
Use $indexOfCP to perform substring search in your sub-pipeline. The $indexOfCP functions will return -1 if the substring is not found. Use $ne to reject such case to obtain substring matched records(i.e. those you desired, as /url1 is a substring of /url1234)
db.users.aggregate([
{
$match: {
user: "foo"
}
},
{
$unwind: "$urls"
},
{
"$lookup": {
"from": "menu",
"let": {
url: "$urls"
},
"pipeline": [
{
"$match": {
$expr: {
$and: [
{
$eq: [
"$component",
"layout"
]
},
{
$ne: [
-1,
{
"$indexOfCP": [
"$link",
"$$url"
]
}
]
}
]
}
}
}
],
"as": "menuLookup"
}
},
{
"$unwind": "$menuLookup"
},
{
"$replaceRoot": {
"newRoot": "$menuLookup"
}
},
{
"$unset": "component"
}
])
Mongo Playground

MongoDb return records only if $lookup cond is occur

I have a class model which has field ref.
I'm trying to fetch only records that match the condition in lookup.
so what i did:
{
$lookup: {
from: 'fields',
localField: "field",
foreignField: "_id",
as: 'FieldCollege',
},
},
{
$addFields: {
"FieldCollege": {
$arrayElemAt: [
{
$filter: {
input: "$FieldCollege",
as: "field",
cond: {
$eq: ["$$field.level", req.query.level]
}
}
}, 0
]
}
}
},
The above code works fine and returning the FieldCollege if the cond is matched.
but the thing is, i wanted to return the class records only if the FieldCollege is not empty.
I'm totally new to mongodb. so i tried something like this:
{
$match: {
'FieldCollege': { $exists: true, $ne: [] }
}
},
Obv this didn't work.
does mongodb support something like this or am i complicating things?
EDIT:
the result from the above code:
"Classes": [
{
"_id": "613245664c6ea614e001fcef",
"name": "test",
"language": "en",
"year_cost": "3232323",
"FieldCollege":[] // with $unwind
}
],
expected Result:
"Classes": [
// FieldCollege is empty
],
I think the good option is to use lookup with pipeline, and see the final version of your query,
$lookup with fields collection and match your both conditions
$limit to result one document
$match FieldCollege is not empty []
$addElemAt to get first element from result FieldCollege
[
{
$lookup: {
from: "fields",
let: { field: "$field" },
pipeline: [
{
$match: {
$and: [
{ $expr: { $eq: ["$$field", "$_id"] } },
{ level: req.query.level }
]
}
},
{ $limit: 1 }
],
as: "FieldCollege"
}
},
{ $match: { FieldCollege: { $ne: [] } } },
{
$addFields: {
FieldCollege: { $arrayElemAt: ["$FieldCollege", 0] }
}
}
]

Mongodb $lookup aggregation returns all documents from foreign index

I have a users index
[{
username: "foo#bar,
roleIds: [ Types.ObjectId("1234") ]
},
{
username: "foo#moo,
}]
With a roles
{
"_id" : ObjectId("60465768f768621ec5828b68"),
"name" : "admin",
"permissionIds" : ObjectId("604657e8e715ss1f2d78b945")
}
and permissions
{
"_id" : ObjectId("604657e8e715ss1f2d78b945"),
"name" : "view-user",
}
When I get the user by username, I want to hydrate the role information. Not all users have roleIds so I need to be able to still return the user regardless of whether they have roleIds.
At the moment the lookup for the roles always returns every item in the roles index!
My idea is that I lookup the roles joining on the index roles by the array roleIds to _ids
Then I pipeline within that lookup to grab the permission information from within the role.
db.getCollection('users').aggregate([
{
$match: {
'username':'foo#bar',
}
},
{
$lookup: {
from: 'roles',
let: {'roleIds': '_id'},
as: 'roles',
pipeline: [{
$lookup: {
from: 'permissions',
let: {'permissionIds': "_id"},
as: 'permissions',
pipeline: [
{
$project: {
name: 1
}
}
]
}
},{
$project: {
name: 1,
permissions: 1
}
}
]
}
}
])
This route just seems to return all documents within the roles index regardless of whether it is actually a join.
Is there something I am immediately doing wrong??
Following things are wrong in your query:
You are passing wrong variables to pipeline inside let in both the lookups.
Missing $match stage inside pipeline which performs the actual join operation.
Initialize empty roleIds with [] using ifNull (since all users don't have that field).
Try this query:
db.getCollection('users').aggregate([
{
$match: {
'username': 'foo#bar',
}
},
{
$lookup: {
from: 'roles',
let: { 'roleIds': { $ifNull: ["$roleIds", []] } },
as: 'roles',
pipeline: [
{
$match: {
$expr: { $in: ["$_id", "$$roleIds"] }
}
},
{
$lookup: {
from: 'permissions',
let: { 'permissionId': "$permissionIds" },
pipeline: [
{
$match: {
$expr: { $eq: ["$_id", "$$permissionId"] }
}
},
{
$project: {
name: 1
}
}
],
as: 'permissions'
}
},
{
$project: {
name: 1,
permissions: 1
}
}
]
}
}
]);

$lookup when foreignField is in nested array

I have two collections :
Student
{
_id: ObjectId("657..."),
name:'abc'
},
{
_id: ObjectId("593..."),
name:'xyz'
}
Library
{
_id: ObjectId("987..."),
book_name:'book1',
issued_to: [
{
student: ObjectId("657...")
},
{
student: ObjectId("658...")
}
]
},
{
_id: ObjectId("898..."),
book_name:'book2',
issued_to: [
{
student: ObjectId("593...")
},
{
student: ObjectId("594...")
}
]
}
I want to make a Join to Student collection that exists in issued_to array of object field in Library collection.
I would like to make a query to student collection to get the student data as well as in library collection, that will check in issued_to array if the student exists or not if exists then get the library document otherwise not.
I have tried $lookup of mongo 3.6 but I didn`t succeed.
db.student.aggregate([{$match:{_id: ObjectId("593...")}}, $lookup: {from: 'library', let: {stu_id:'$_id'}, pipeline:[$match:{$expr: {$and:[{"$hotlist.clientEngagement": "$$stu_id"]}}]}])
But it thorws error please help me in regard of this. I also looked at other questions asked at stackoverflow like. question on stackoverflow,
question2 on stackoverflow but these are comapring simple fields not array of objects. please help me
I am not sure I understand your question entirely but this should help you:
db.student.aggregate([{
$match: { _id: ObjectId("657...") }
}, {
$lookup: {
from: 'library',
localField: '_id' ,
foreignField: 'issued_to.student',
as: 'result'
}
}])
If you want to only get the all book_names for each student you can do this:
db.student.aggregate([{
$match: { _id: ObjectId("657657657657657657657657") }
}, {
$lookup: {
from: 'library',
let: { 'stu_id': '$_id' },
pipeline: [{
$unwind: '$issued_to' // $expr cannot digest arrays so we need to unwind which hurts performance...
}, {
$match: { $expr: { $eq: [ '$issued_to.student', '$$stu_id' ] } }
}, {
$project: { _id: 0, "book_name": 1 } // only include the book_name field
}],
as: 'result'
}
}])
This might not be a very good answer, but if you can change your schema of Library to:
{
_id: ObjectId("987..."),
book_name:'book1'
issued_to: [
ObjectId("657..."),
ObjectId("658...")
]
},
{
_id: "ObjectId("898...")",
book_name:'book2'
issued_to: [
ObjectId("593...")
ObjectId("594...")
]
}
Then when you do:
{
$lookup: {
from: 'student',
localField: 'issued_to',
foreignField: '_id',
as: 'issued_to_students', // this creates a new field without overwriting your original 'issued_to'
}
},
You should get, based on your example above:
{
_id: ObjectId("987..."),
book_name:'book1'
issued_to_students: [
{ _id: ObjectId("657..."), name: 'abc', ... },
{ _id: ObjectId("658..."), name: <name of this _id>, ... }
]
},
{
_id: "ObjectId("898...")",
book_name:'book2'
issued_to: [
{ _id: ObjectId("593..."), name: 'xyz', ... },
{ _id: ObjectId("594..."), name: <name of this _id>, ... }
]
}
You need to $unwind the issued_to from library collection to match the issued_to.student with _id
db.student.aggregate([
{ "$match": { "_id": mongoose.Types.ObjectId(id) } },
{ "$lookup": {
"from": Library.collection.name,
"let": { "studentId": "$_id" },
"pipeline": [
{ "$unwind": "$issued_to" },
{ "$match": { "$expr": { "$eq": [ "$issued_to.student", "$$studentId" ] } } }
],
"as": "issued_to"
}}
])

Complex aggregation query with in clause from document array

Below is the sample MongoDB Data Model for a user collection:
{
"_id": ObjectId('58842568c706f50f5c1de662'),
"userId": "123455",
"user_name":"Bob"
"interestedTags": [
"music",
"cricket",
"hiking",
"F1",
"Mobile",
"racing"
],
"listFriends": [
"123456",
"123457",
"123458"
]
}
listFriends is an array of userId for other users
For a particular userId I need to extract the listFriends (userId's) and for those userId's I need to aggregate the interestedTags and their count.
I would be able to achieve this by splitting the query into two parts:
1.) Extract the listFriends for a particular userId,
2.) Use this list in an aggregate() function, something like this
db.user.aggregate([
{ $match: { userId: { $in: [ "123456","123457","123458" ] } } },
{ $unwind: '$interestedTags' },
{ $group: { _id: '$interestedTags', countTags: { $sum : 1 } } }
])
I am trying to solve the question: Is there a way to achieve the above functionality (both steps 1 and 2) in a single aggregate function?
You could use $lookup to look for friend documents. This stage is usually used to join two different collection, but it can also do join upon one single collection, in your case I think it should be fine:
db.user.aggregate([{
$match: {
_id: 'user1',
}
}, {
$unwind: '$listFriends',
}, {
$lookup: {
from: 'user',
localField: 'listFriends',
foreignField: '_id',
as: 'friend',
}
}, {
$project: {
friend: {
$arrayElemAt: ['$friend', 0]
}
}
}, {
$unwind: '$friend.interestedTags'
}, {
$group: {
_id: '$friend.interestedTags',
count: {
$sum: 1
}
}
}]);
Note: I use $lookup and $arrayElemAt which are only available in Mongo 3.2 or newer version, so check your Mongo version before using this pipeline.