Mongodb $exists inside $expr in mongodb - mongodb

I want to add multiple conditions on join. Join those docs (of the same collection) who met the following conditions:
Have opposite gender
Have age (IF EXISTS) between the primary doc age preference and primary doc have age (IF EXISTS) between the foreign doc preference (i.e two-way check)
My attempt is the following but has two issues:
$exists can't be used inside $expr idk why
Age query is one way right now
$lookup: {
"from": "appusers",
"let": { 'gen': "$gender",'pref': "$preference" },
"pipeline": [{
$match: {$expr: {
$and: [
{ $ne: ["$gender", "$$gen"]},
{ $or: [
{$exists: {"$age": false}},
{$and: [
{ $gte: ["$age", '$$pref.age_from' ] },
{ $lte: [ "$age", '$$pref.age_to' ] }
]}
]}
]
}}}],
"as": "matches"
}
Example:
Input Docs:
{
name: "person1",
age: 36,
gender: "Male",
preference: {
age_from: 25,
age_to: 35
}
}
{
name: "person2",
age: 18,
gender: "Female",
preference: {
age_from: 25,
age_to: 40
}
}
{
name: "person3",
age: 26,
gender: "Female",
preference: {
age_from: 30,
age_to: 35
}
}
{
name: "person4",
age: 26,
gender: "Female",
preference: {
age_from: 30,
age_to: 40
}
}
Output:
For person 1 the matches array will show only person 4 (and similarly person 4 match will show person 1) i.e.:
{
name: person1,
age: 36,
gender: "Male",
preference: {
age_from: 28,
age_to: 35
},
matches: [
{
name: person4,
...
}
]
}
I have viewed this and this but didn't help

$exists can't be used inside $expr idk why
$expr Allows the use of aggregation expressions within the query language, and $exists is not an aggregation operator,
You just need to correct the 2 things:
put $expr condition inside first $and condition
put $expr in last $and condition
db.appusers.aggregate([
{
$lookup: {
from: "appusers",
let: { gen: "$gender", pref: "$preference" },
pipeline: [
{
$match: {
$and: [
{ $expr: { $ne: ["$gender", "$$gen"] } },
{
$or: [
{ age: { $exists: false } },
{
$expr: {
$and: [
{ $gte: ["$age", "$$pref.age_from"] },
{ $lte: ["$age", "$$pref.age_to"] }
]
}
}
]
}
]
}
}
],
as: "matches"
}
}
])
Playground

For the $exists problem, you can wrap age with $ifNull and use $eq to check for the existence.
For the 2-way age matching, I think you just need to repeat your age matching criteria from person1 to person4 for person4 to person1. Although in your current given test case, no match will be found as person4's age is out of person1's preference.
db.appusers.aggregate([
{
"$match": {
name: "person1"
}
},
{
$lookup: {
"from": "appusers",
"let": {
"a": "$age",
"gen": "$gender",
"pref": "$preference"
},
"pipeline": [
{
$match: {
$expr: {
$and: [
{
$ne: [
"$$gen",
"$gender"
]
},
{
$and: [
{
$or: [
{
$eq: [
{
"$ifNull": [
"$age",
"age-not-exists"
]
},
"age-not-exists"
]
},
{
$and: [
{
$gte: [
"$age",
"$$pref.age_from"
]
},
{
$lte: [
"$age",
"$$pref.age_to"
]
}
]
}
]
},
{
$or: [
{
$eq: [
{
"$ifNull": [
"$$a",
"age-not-exists"
]
},
"age-not-exists"
]
},
{
$and: [
{
$gte: [
"$$a",
"$preference.age_from"
]
},
{
$lte: [
"$$a",
"$preference.age_to"
]
}
]
}
]
}
]
}
]
}
}
}
],
"as": "matches"
}
}
])
Here is the Mongo playground for your reference.

You can use $eq undefined for the field age instead of the $exists
{
"from": "appusers",
"let": { 'gen': "$gender",'pref': "$preference" },
"pipeline": [{
$match: {$expr: {
$and: [
{ $ne: ["$gender", "$$gen"]},
{ $or: [
{$eq: ["$age" , undefined]},
{$and: [
{ $gte: ["$age", '$$pref.age_from' ] },
{ $lte: [ "$age", '$$pref.age_to' ] }
]}
]}
]
}}}],
"as": "matches"
}

Related

MongoDB - Lookup match with condition array of object with string

I have two collections "datasets" and "users".
I tried to lookup datasets.assignedTo = users.id that's working fine. Also, I want to match the field of datasets.firstBillable >= users.prices.beginDate date field are matched to get the current index price value. And also check users.prices.endDate is less than or equal to users.prices.beginDate.
For example:
cgPrices: 45
https://mongoplayground.net/p/YQps9EozlAL
Collections:
db={
users: [
{
id: 1,
name: "Aravinth",
prices: [
{
beginDate: "2022-08-24T07:29:01.639Z",
endDate: "2022-08-31T07:29:01.639Z",
price: 45
}
]
},
{
id: 2,
name: "Raja",
prices: [
{
beginDate: "2022-07-25T07:29:01.639Z",
endDate: "2022-07-30T07:29:01.639Z",
price: 55
}
]
}
],
datasets: [
{
color: "braun, rose gold",
firstBillable: "2022-08-24T07:29:01.639Z",
assignedTo: 1
},
{
color: "beige, silber",
firstBillable: "2022-07-25T07:29:01.639Z",
assignedTo: 2
}
]
}
My current implementation:
db.datasets.aggregate([
{
"$lookup": {
"from": "users",
"as": "details",
let: {
assigned_to: "$assignedTo",
first_billable: "$firstBillable"
},
pipeline: [
{
"$match": {
$expr: {
"$and": [
{
"$eq": [
"$id",
"$$assigned_to"
]
},
{
"$gte": [
"$first_billable",
"$details.prices.beginDate"
]
},
{
"$lte": [
"$first_billable",
"$details.prices.endDate"
]
}
]
}
}
}
]
}
},
{
"$addFields": {
"details": 0,
"cg": {
$first: {
"$first": "$details.prices.price"
}
}
}
}
])
Output i needed:
[
{
"_id": ObjectId("5a934e000102030405000000"),
"assignedTo": 1,
"cg": 45,
"color": "braun, rose gold",
"details": 0,
"firstBillable": "2022-08-24T07:29:01.639Z"
},
{
"_id": ObjectId("5a934e000102030405000001"),
"assignedTo": 2,
"cg": 55,
"color": "beige, silber",
"details": 0,
"firstBillable": "2022-07-25T07:29:01.639Z"
}
]
https://mongoplayground.net/p/YQps9EozlAL
Concerns:
You should compare the date as Date instead of string, hence you are required to convert the date strings to Date before comparing.
In users collection, prices is an array. You need to deconstruct the array to multiple documents first before compare the date fields in price.
The query should be:
db.datasets.aggregate([
{
"$lookup": {
"from": "users",
"as": "details",
let: {
assigned_to: "$assignedTo",
first_billable: {
$toDate: "$firstBillable"
}
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$id",
"$$assigned_to"
]
}
}
},
{
$unwind: "$prices"
},
{
"$match": {
$expr: {
"$and": [
{
"$gte": [
"$$first_billable",
{
$toDate: "$prices.beginDate"
}
]
},
{
"$lte": [
"$$first_billable",
{
$toDate: "$prices.endDate"
}
]
}
]
}
}
}
]
}
},
{
"$addFields": {
"details": 0,
"cg": {
$first: "$details.prices.price"
}
}
}
])
Demo # Mongo Playground

MongoDb query to exclude omission of rows based on criteria

In below example, looking for new partner suggestions for user abc. abc has already sent a request to 123 so that can be ignored. rrr has sent request to abc but rrr is in the fromUser field so rrr is still a valid row to be shown as suggestion to abc
I have two collections:
User collection
[
{
_id: "abc",
name: "abc",
group: 1
},
{
_id: "xyz",
name: "xyyy",
group: 1
},
{
_id: "123",
name: "yyy",
group: 1
},
{
_id: "rrr",
name: "tttt",
group: 1
},
{
_id: "eee",
name: "uuu",
group: 1
}
]
Partnership collection (if users have already partnered)
[
{
_id: "abc_123",
fromUser: "abc",
toUser: "123"
},
{
_id: "rrr_abc",
fromUser: "rrr",
toUser: "abc"
},
{
_id: "xyz_rrr",
fromUser: "xyz",
toUser: "rrr"
}
]
My query below excludes the user rrr but it should not because its not listed in toUser field in the partnership collection corresponding to the user abc.
How to modify this query to include user rrr in this case?
db.users.aggregate([
{
$match: {
group: 1,
_id: {
$ne: "abc"
}
}
},
{
$lookup: {
from: "partnership",
let: {
userId: "$_id"
},
as: "prob",
pipeline: [
{
$set: {
users: [
"$fromUser",
"$toUser"
],
u: "$$userId"
}
},
{
$match: {
$expr: {
$and: [
{
$in: [
"$$userId",
"$users"
]
},
{
$in: [
"abc",
"$users"
]
}
]
}
}
}
]
}
},
{
$match: {
"prob.0": {
$exists: false
}
}
},
{
$sample: {
size: 1
}
},
{
$unset: "prob"
}
])
https://mongoplayground.net/p/utGMeHFRGmt
Your current query does not allow creating an existing connection regardless of the connection direction. If the order of the connection is important use:
db.users.aggregate([
{$match: {
group: 1,
_id: {$ne: "abc"}
}
},
{$lookup: {
from: "partnership",
let: { userId: {$concat: ["abc", "_", "$_id"]}},
as: "prob",
pipeline: [{$match: {$expr: {$eq: ["$_id", "$$userId"]}}}]
}
},
{$match: {"prob.0": {$exists: false}}},
{$sample: {size: 1}},
{$unset: "prob"}
])
See how it works on the playground example
For MongoDB 5 and later, I'd propose the following aggregation pipeline:
db.users.aggregate([
{
$match: {
group: 1,
_id: {
$ne: "abc"
}
}
},
{
$lookup: {
from: "partnership",
as: "prob",
localField: "_id",
foreignField: "toUser",
pipeline: [
{
$match: {
fromUser: "abc",
}
}
]
}
},
{
$match: {
"prob.0": {
$exists: false
}
}
},
{
$unset: "prob"
}
])
The following documents are returned (full result without the $sample stage):
[
{
"_id": "eee",
"group": 1,
"name": "uuu"
},
{
"_id": "rrr",
"group": 1,
"name": "tttt"
},
{
"_id": "xyz",
"group": 1,
"name": "xyyy"
}
]
The main difference is that the lookup connects the collections by the toUser field (see localField, foreignField) and uses a minimal pipeline to restrict the results further to only retrieve the requests from the current user document to "abc".
See this playground to test.
When using MongoDB < 5, you cannot use localField and foreignField to run the pipeline only on a subset of the documents in the * from*
collection. To overcome this, you can use this aggregation pipeline:
db.users.aggregate([
{
$match: {
group: 1,
_id: {
$ne: "abc"
}
}
},
{
$lookup: {
from: "partnership",
as: "prob",
let: {
userId: "$_id"
},
pipeline: [
{
$match: {
$expr: {
$and: [
{
$eq: [
"$fromUser",
"abc"
]
},
{
$eq: [
"$toUser",
"$$userId"
]
}
]
}
}
}
]
}
},
{
$match: {
"prob.0": {
$exists: false
}
}
},
{
$unset: "prob"
}
])
The results are the same as for the upper pipeline.
See this playground to test.
For another, another way, this query starts from the partnership collection, finds which users to exclude, and then does a "$lookup" for everybody else. The remainder is just output formatting, although it looks like you may want to add a "$sample" stage at the end.
db.partnership.aggregate([
{
"$match": {
"fromUser": "abc"
}
},
{
"$group": {
"_id": null,
"exclude": {"$push": "$toUser" }
}
},
{
"$lookup": {
"from": "users",
"let": {
"exclude": {"$concatArrays": [["abc"], "$exclude"]
}
},
"pipeline": [
{
"$match": {
"$expr": {
"$not": {"$in": ["$_id", "$$exclude"]}
}
}
}
],
"as": "output"
}
},
{
"$project": {
"_id": 0,
"output": 1
}
},
{"$unwind": "$output"},
{"$replaceWith": "$output"}
])
Try it on mongoplayground.net.

Mongo DB Join on Primary/Foreign Key

I have two collections, viz: clib and mp.
The schema for clib is : {name: String, type: Number} and that for mp is: {clibId: String}.
Sample Document for clib:
{_id: ObjectId("6178008397be0747443a2a92"), name: "c1", type: 1}
{_id: ObjectId("6178008397be0747443a2a91"), name: "c2", type: 0}
Sample Document for mp:
{clibId: "6178008397be0747443a2a92"}
{clibId:"6178008397be0747443a2a91"}
While Querying mp, I want those clibId's that have type = 0 in clib collection.
Any ideas how this can be achieved?
One approach that I can think of was to use $lookUp, but that doesnt seem to be working. Also, I m not sure if this is anti-pattern for mongodb, another approach is to copy the type from clib to mp while saving mp document.
If I've understood correctly you can use a pipeline like this:
This query get the values from clib where its _id is the same as clibId and also has type = 0. Also I've added a $match stage to not output values where there is not any coincidence.
db.mp.aggregate([
{
"$lookup": {
"from": "clib",
"let": {
"id": "$clibId"
},
"pipeline": [
{
"$match": {
"$expr": {
"$and": [
{
"$eq": [
{
"$toObjectId": "$$id"
},
"$_id"
]
},
{
"$eq": [
"$type",
0
]
}
]
}
}
}
],
"as": "result"
}
},
{
"$match": {
"result": {
"$ne": []
}
}
}
])
Example here
db.mp.aggregate([
{
$lookup: {
from: "clib",
let: {
clibId: "$clibId"
},
pipeline: [
{
$match: {
$expr: {
$and: [
{
$eq: [ "$_id", "$$clibId" ],
}
]
}
}
},
{
$project: { type: 1, _id: 0 }
}
],
as: "clib"
}
},
{
"$unwind": "$clib"
},
{
"$match": {
"clib.type": 0
}
}
])
Test Here

How to properly reference nested field within an array within an aggregate framework (MongoDB)?

I have a collection in the following format:
Collection name: COLL1
{
_id: "a",
list: [
{
_id: "a1",
ranking: 10
},
{
_id: "a2",
ranking: 30
}
...
]
}
{
_id: "b",
list: [
{
_id: "b1",
ranking: 10
},
{
_id: "b2",
ranking: 30
}
...
]
}
When I call: db.getCollection('COLL1').find({"_id": "a","list._id": "a1"}); I can see the results. However, if I call:
db.getCollection('COLL1').aggregate([
{ $match:
{ $expr:
{ $and:
[
{ $eq: ["$_id", "a"] },
{ $eq: ["$list._id", "a1"] }
]
}
}
}
])
Then nothing is returned. Does anyone know why? I think the issue is { $eq: ["$list._id", "a1"] } but I'm not sure what exactly happened here.
I'm trying to get the complete document:
{
_id: "a",
list: [
{
_id: "a1",
ranking: 10
},
{
_id: "a2",
ranking: 30
}
...
]
}
This is part of my aggregate syntax within a $lookup stage, so I have to use aggregate([]) instead of find(). What I'm actually trying to achieve is the following:
...previous stages
{
$lookup:{
from: "COLL1",
let: { local_id: "$_id" }, // this '$_id' is from another collection, not COLL1.
pipeline: [
{ $match:
{ $expr:
{ $and:
[
{ $eq: [ "$_id", "a"] },
{ $eq: [ "$$local_id", "$list._id" ] } // this is where I got in trouble.
]
}
}
}
],
as: "matched_result"
}
},
Thanks a lot!
If you don't need variables from previous pipeline stages, you can just use the same syntax as .find() inside $match
db.getCollection('COLL1').aggregate([
{ $match: { "_id": "a", "list._id": "a1" } }
])
Replacing the second $eq with $in will work just fine. :)
db.getCollection('COLL1').aggregate([
{ $match:
{ $expr:
{ $and:
[
{ $eq: ["$_id", "a"] },
{ $in: ["a1", "$list._id"] }
]
}
}
}
])

Difference between queries with grouped

I'm new to mongodb, and I have 2 queries:
frist:
db.movies.aggregate([
{ "$match": {
$and : [
{ "imdb.rating": { $lt: 7 }},
{$and: [ {"genres": { "$ne": "Crime" } }, {"genres": { "$ne": "Horror" } }]},
{$and: [ {"languages": { "$eq": "English" } }, {"languages": { "$eq": "Japanese" } }]},
{$or: [ {"rated": { "$eq": "PG" } }, {"rated": { "$eq": "G" } }]}
]
}
}
]).itcount()
The result its: 23
And now with this:
db.movies.aggregate([
{ "$match": {
"imdb.rating": { $lt: 7 },
$and: [ {"genres": { "$ne": "Crime" } }, {"genres": { "$ne": "Horror" } }],
$or: [ {"rated": { "$eq": "PG" } }, {"rated": { "$eq": "G" } }],
$and: [ {"languages": { "$eq": "English" } }, {"languages": { "$eq": "Japanese" } }]
}
}
]).itcount()
The result its 25,
But now I can't understand what is the difference between two queries, can help with this?
There is no functional difference. $match implicitly is using $and when you provide more than one expression and a simple key:value expression is shorthand for key: {$eq: value} e.g.
$match: {a:3, b:"buzz"}
is shorthand for:
$match: {$and: [{a:{$eq:3}}, {b:{$eq:"buzz"}} ] }