Aggregate pipeline Match -> Lookup -> Unwind -> Match issue - mongodb

I am puzzled as to why the code below doesn't work. Can anyone explain, please?
For some context: My goal is to get the score associated with an answer option for a survey database where answers are stored in a separate collection from the questions. The questions collection contains an array of answer options, and these answer options have a score.
Running this query:
db.answers.aggregate([
{
$match: {
userId: "abc",
questionId: ObjectId("598be01d4efd70a81c1c5ad4")
}
},
{
$lookup: {
from: "questions",
localField: "questionId",
foreignField: "_id",
as: "question"
}
},
{
$unwind: "$question"
},
{
$unwind: "$question.options"
},
{
$unwind: "$answers"
}
])
I get:
{
"_id" : ObjectId("598e588e0c5e24452c9ee769"),
"userId" : "abc",
"questionId" : ObjectId("598be01d4efd70a81c1c5ad4"),
"answers" : {
"id" : 20
},
"question" : {
"_id" : ObjectId("598be01d4efd70a81c1c5ad4"),
"options" : {
"id" : 10,
"score" : "12"
}
}
}
{
"_id" : ObjectId("598e588e0c5e24452c9ee769"),
"userId" : "abc",
"questionId" : ObjectId("598be01d4efd70a81c1c5ad4"),
"answers" : {
"id" : 20
},
"question" : {
"_id" : ObjectId("598be01d4efd70a81c1c5ad4"),
"options" : {
"id" : 20,
"score" : "4"
}
}
}
All great. If I now add to the original query a match that's supposed to find the answer option having the same id as the answer (e.g. questions.options.id == answers.id), things don't work as I would expect.
The final pipeline is:
db.answers.aggregate([
{
$match: {
userId: "abc",
questionId: ObjectId("598be01d4efd70a81c1c5ad4")
}
},
{
$lookup: {
from: "questions",
localField: "questionId",
foreignField: "_id",
as: "question"
}
},
{
$unwind: "$question"
},
{
$unwind: "$question.options"
},
{
$unwind: "$answers"
},
{
$match: {
"question.options.id": "$answers.id"
}
},
{
$project: {
_id: 0,
score: "$question.options.score"
}
}
])
This returns an empty result. But if I change the RHS of the $match from "$answers.id" to 20, it returns the expected score: 4. I tried everything I could think of, but couldn't get it to work and can't understand why it doesn't work.

I was able to get it to work with the following pipeline:
{
$match: {
userId: "abc",
questionId: ObjectId("598be01d4efd70a81c1c5ad4")
}
},
{
$lookup: {
from: "questions",
localField: "questionId",
foreignField: "_id",
as: "question"
}
},
{
$unwind: "$question"
},
{
$unwind: "$question.options"
},
{
$unwind: "$answers"
},
{
$addFields: {
areEqual: { $eq: [ "$question.options.id", "$answers.id" ] }
}
},
{
$match: {
areEqual: true
}
},
{
$project: {
_id: 0,
score: "$question.options.score"
}
}
I think the reason it didn't work with a direct match is the fact that questions.options.id doesn't actually reference the intended field... I needed to use $questions.options.id which wouldn't work as a LHS of a $match, hence the need to add an extra helper attribute.

Related

How to $lookup by avoiding null values in mongodb aggregate

In here i'm using $lookup to to a left join from other collections, the query works fine but when some records missing values it returns
errmsg : $in requires an array as a second argument, found: null
Heres the querying document structure :
{
"no" : "2020921008981",
"sale" : {
"soldItems" : [
{
"itemId" : "5b55ac7f0550de00210a3b24",
},
{
"itemId" : "5b55ac7f0550de00215584re",
}
],
"bills" : [
{
"billNo" : "2020921053467",
"insurancePlanId" : "160",
},
{
"billNo" : "2020921053467",
"insurancePlanId" : "170",
}
],
"visitIds" : [
5b55ac7f0550de00210a3b24, 5b55ac7f0550de00210a3b24
]
}
}
the query :
db.case.aggregate([
{
$lookup: {
from: "insurance",
let: { ipids: "$sale.bill.insurancePlanId" },
pipeline: [
{
$unwind: "$coveragePlans"
},
{
$match: { $expr: { $in: ["$coveragePlans._id", "$$ipids"] } }
},
{
$project: { _id: 0, name: 1 }
}
],
as: "insurances"
}
},
{
$lookup: {
from: "item",
let: { iid: "$salesOrder.purchaseItems.itemRefId" },
pipeline: [
{
$match: {
$expr: {
$in: ["$_id", {
$map: {
input: "$$iid",
in: { $toObjectId: "$$this" }
}
}
]
}
}
}
],
as: "items"
}
}
])
insurance collection :
{
"_id" : ObjectId("5b55aca20550de00210a6d25"),
"name" : "HIJKL"
"coveragePlans" : [
{
"_id" : "160",
"name" : "UVWZ",
},
{
"_id" : "161",
"name" : "LMNO",
}
]
},
{
"_id" : ObjectId("5b55aca20550de00210a6d25"),
"name" : "WXYZ"
"coveragePlans" : [
{
"_id" : "169",
"name" : "5ABC",
},
{
"_id" : "170",
"name" : "4XYZ",
}
]
}
item collection :
{
"_id" : ObjectId("5b55ac7f0550de00210a3b24"),
"code" : "ABCDE"
},
{
"_id" : ObjectId("5b55ac7f0550de00215584re"),
"code" : "PQRST"
}
How to avoid this and do null checks effectively before pipe-lining into the next stages? Tried with { $match: { "fieldName": { $exists: true, $ne: null } } } but it returns mongo error regarding the format. If its the way to go please mention the stage i should put that.. Thanks in advance
You can use $ifNull operator
let: { ipids: {$ifNull:["$sale.bill.insurancePlanId", [] ]} },
EDIT: To skip empty "$salesOrder.purchaseItems.itemRefId" values
let: { iid: {$filter: {input:"$salesOrder.purchaseItems.itemRefId", cond:{$ne:["$$this", ""]}}} },
You can get around that by not using $in.
It looks like this $map is executed separately for every document in the items collection. If you were to run the map in an $addFields stage, you could used the simple form of lookup to match the added field to _id, which would automagically handle missing, null, and array.
Remove the added field with a $project stage if necessary.
db.case.aggregate([
{$lookup: {
from: "insurance",
let: { ipids: "$sale.bill.insurancePlanId" },
pipeline: [
{$unwind: "$coveragePlans"},
{$match: { $expr: { $in: ["$coveragePlans._id", "$$ipids"] } }},
{$project: { _id: 0, name: 1 }}
],
as: "insurances"
}}
{$addFields:{
matchArray:{$map: {
input: "$$iid",
in: { $toObjectId: "$$this" }
}}
}},
{$lookup: {
from: "item",
localField: "matchArray",
foreignField:"_id",
as: "items"
}},
{$project:{
arrayField: 0
}}
])

Self join query in mongodb and return fields from parent and child documents with condition

Suppose I have multiple documents like these in a collection
Parent document:
{
"_id" : ObjectId("5e86ebd6c2d28863e4e2c920"),
"users" : [],
"name" : "Annual",
"days" : 18,
},
Child documents:
{
"_id" : ObjectId("5e86ec22c2d28863e4e2c921"),
"users" : [
ObjectId("5e58fa20f3bea73c3cb07713"),
ObjectId("5e58fab5f3bea73c3cb07715")
],
"leaveTypeId" : ObjectId("5e86ebd6c2d28863e4e2c920"),
"name" : "Personal",
"days" : 5,
},
{
"_id" : ObjectId("5e86ec22c2d28863e4e2c921"),
"users" : [],
"leaveTypeId" : ObjectId("5e86ebd6c2d28863e4e2c920"),
"name" : "Personal",
"days" : 5,
}
Now I want to build a query like if user found in users array then return name and days from child document otherwise it should return name and days from parent document.
If user_id = ObjectId("5e58fa20f3bea73c3cb07713") then the output should be
{
name: 'Personal',
days: 5
}
If user_id = ObjectId("52fff32rax823vnvy3234es12") then the output should be
{
name: 'Annual',
days: 18
}
Try these aggregation queries :
When it's done on Parent Collection :
db.parent.aggregate([
{
$lookup: {
from: "child",
localField: "_id",
foreignField: "leaveTypeId",
as: "child_docs"
}
},
{
$unwind: "$child_docs"
},
{
$project: {
name: {
$cond: [
{ $in: [ObjectId("5e58fa20f3bea73c3cb07713"), "$child_docs.users"] },
"$child_docs.name",
"$name"
]
},
days: {
$cond: [
{ $in: [ObjectId("5e58fa20f3bea73c3cb07713"), "$child_docs.users"] },
"$child_docs.days",
"$days"
]
}
}
}
]);
Test : MongoDB-Playground
When it's done on Child Collection :
db.child.aggregate([
{
$lookup: {
from: "parent",
localField: "leaveTypeId",
foreignField: "_id",
as: "parent_docs"
}
},
{
$unwind: "$parent_docs"
},
{
$project: {
name: {
$cond: [
{ $in: [ObjectId("5e58fa20f3bea73c3cb07713"), "$users"] },
"$name",
"$parent_docs.name"
]
},
days: {
$cond: [
{ $in: [ObjectId("5e58fa20f3bea73c3cb07713"), "$users"] },
"$days",
"$parent_docs.days"
]
}
}
}
]);
Test : MongoDB-Playground

Mongodb aggretate apply sort to lookup results, and add field index number

The aggregate was executed.
I got the results using lookup, but I need a sort.
In addition, I want to assign an index to the result value.
CollectionA :
{
"_id" : ObjectId("5a6cf47415621604942386cd"),
"contents" : [
ObjectId("AAAAAAAAAAAAAAAAAAAAAAAA"),
ObjectId("BBBBBBBBBBBBBBBBBBBBBBBB")
],
"name" : "jason"
}
CollectionB :
{
"_id" : ObjectId("AAAAAAAAAAAAAAAAAAAAAAAA")
"title" : "a title",
"date" : 2018-01-02
},
{
"_id" : ObjectId("BBBBBBBBBBBBBBBBBBBBBBBB")
"title" : "a title",
"date" : 2018-01-01
}
Query:
db.getCollection('A').aggregate([
{
$match : { "_id" : ObjectId("5a6cf47415621604942386cd") }
},
{
$lookup : {
from: "B",
localField: "contents",
foreignField: "_id",
as: "item"
}
},
{ $sort: { "item.date" : -1 } }
]);
Want Result:
{
"_id" : ObjectId("5a6cf47415621604942386cd"),
"contents" : [
{
"_id" : ObjectId("BBBBBBBBBBBBBBBBBBBBBBBB")
"title" : "a title",
"date" : 2018-01-01,
"index" : 0
},
{
"_id" : ObjectId("AAAAAAAAAAAAAAAAAAAAAAAA")
"title" : "a title",
"date" : 2018-01-02,
"index" : 1
}],
"name" : "jason"
}
The current problem does not apply to the sort.
And I don't know how to designate an index.
Below Aggregation may you. For your desire result.
db.CollectionA.aggregate([
{
$match: { "_id": ObjectId("5a6cf47415621604942386cd") }
},
{
$lookup: {
from: "CollectionB",
let: { contents: "$contents" },
pipeline: [
{
$match: { $expr: { $in: ["$_id", "$$contents"] } }
},
{ $sort: { date: 1 } }
],
as: "contents"
}
},
{
$project: {
contents: {
$map: {
input: { $range: [0, { $size: "$contents" }, 1 ] },
as: "element",
in: {
$mergeObjects: [
{ index: "$$element" },
{ $arrayElemAt: [ "$contents", "$$element" ]}
]
}
}
}
}
}
])
One way to go about it would be to unwind the array, sort it and then group it back
db.A.aggregate([
{
$match: {
"_id": ObjectId("5a6cf47415621604942386cd")
}
},
{
$lookup: {
from: "B",
localField: "contents",
foreignField: "_id",
as: "item"
}
},
{
$unwind: "$item"
},
{
$sort: {
"item.date": -1
}
},
{
$group: {
_id: "$_id",
contents: {
$push: "$item"
}
}
}
])
Another method is, (this is applicable only if the date field corresponds to the document creation date),
db.A.aggregate([
{
$match: {
"_id": ObjectId("5a6cf47415621604942386cd")
}
},
{
$lookup: {
from: "B",
localField: "contents",
foreignField: "_id",
as: "item"
}
},
{
$sort: {
"item": -1
}
}
])
Basically, this sorts on the basis of _id, and since _id is created using the creation date, it should sort accordingly.

How to check $setDifference in two array using mongo-query

UserDetails
{
"_id" : "5c23536f807caa1bec00e79b",
"UID" : "1",
"name" : "A",
},
{
"_id" : "5c23536f807caa1bec00e78b",
"UID" : "2",
"name" : "B",
}
UserProducts
{
"_id" : "5c23536f807caa1bec00e79c",
"UPID" : "100",
"UID" : "1"
},
{
"_id" : "5c23536f807caa1bec00e79c",
"UPID" : "200",
"UID" : "2"
}
Groups
{
"_id" : "5bb20d7556db6915846da55f",
"members" : {
"regularStudent" : [
"200" // UPID
],
}
}
Step 1
I have to take UID from UserDetails check with UserProducts then take UPID from UserProducts
Step 2
we have to check this UPID mapped to Groups collection or not ?.
members.regularStudent we are mapped UPID
Step 3
Suppose UPID not mapped means i want to print the UPID from from UserProducts
I have tried but couldn't complete this, kindly help me out on this.
Expected Output:
["100"]
Note: Expected Output is ["100"] , because UserProducts having UPID 100 & 200 but Groups collection mapped only 200.
My Code
db.UserDetails.aggregate(
{
$lookup: {
from: "UserProducts",
localField: "UID",
foreignField: "UID",
as: "userProduct"
}
},
{ $unwind: "$userProduct" },
{
"$project": { "_id" : 0, "userProduct.UPID" : 1 }
},
{
$group: {
_id: null,
userProductUPIDs: { $addToSet: "$userProduct.UPID" }
}
}
) // returns [ "100", "200" ]
db.Groups.aggregate([
{
$unwind: "$members.regularStudent"
},
{
$group: {
_id: null,
UPIDs: { $addToSet: "$members.regularStudent" }
}
}
]) // returns ["200"]
Now i want to check $setDifference of both array, so i had added below code but returning error like $userProductUPIDs is not defined
db.Groups.aggregate([
{
$unwind: "$members.regularStudent"
},
{
$group: {
_id: null,
UPIDs: { $addToSet: "$members.regularStudent" }
}
},
{
$project: {
members: {
$setDifference: [ $userProductUPIDs , "$members" ]
},
_id : 0
}
}
])
As this is a follow up to one of my previous answers I will try to fix your code. The bottom line is that you need two queries as you can't upgrade your database so the code should look like below:
var queryResult = db.UserDetails.aggregate(
{
$lookup: {
from: "UserProducts",
localField: "UID",
foreignField: "UID",
as: "userProduct"
}
},
{ $unwind: "$userProduct" },
{
"$project": { "_id" : 0, "userProduct.UPID" : 1 }
},
{
$group: {
_id: null,
userProductUPIDs: { $addToSet: "$userProduct.UPID" }
}
});
let userProductUPIDs = queryResult.toArray()[0].userProductUPIDs;
db.Groups.aggregate([
{
$unwind: "$members.regularStudent"
},
{
$group: {
_id: null,
UPIDs: { $addToSet: "$members.regularStudent" }
}
},
{
$project: {
members: {
$setDifference: [ userProductUPIDs , "$UPIDs" ]
},
_id : 0
}
}
]) // should return 100

filtering out documents returned by $lookup in Mongo aggregate

I am running this command:
{ $lookup: { from: "views", localField: "checkedViews._id", foreignField: "id", as: "populatedViews" } },
Unfortunately that is returning all views documents on each upload document, so when I run $project with
populatedViews: {
$size: "$populatedViews"
},
It equals the total amount of views in the entire collection for each upload. Should I use $filter to only include documents that match via the _id, or how can I do that?
An example Video:
{
"_id" : ObjectId("59b5d09980348a3ec3541acc"),
"checkedViews" : [
ObjectId("59b5d0b280348a3ec3541acd"),
ObjectId("59b5d0bf80348a3ec3541ace"),
ObjectId("59b5d1dbd8f37d4ab797fa5c")
]
}
An example checkedView:
{
"_id" : ObjectId("59b5d0b280348a3ec3541acd"),
"updatedAt" : ISODate("2017-09-10T23:54:26.752Z"),
"createdAt" : ISODate("2017-09-10T23:54:26.752Z"),
"video" : ObjectId("59b5d09980348a3ec3541acc"),
"validity" : "real",
"__v" : 0
}
Here's what I have so far:
const oldUploads = await Video.aggregate([
{ $match: {
visibility: { $ne: 'removed' },
'checkedViews.1' : { $exists: true }
}},
{ $lookup: { from: "views", localField: "checkedViews._id", foreignField: "id", as: "populatedViews" } },
{ $project: {
title: 1,
checkedViews: 1,
viewAmount: {
$size: {
$filter: {
input: "$populatedViews",
as: "view",
cond: { $eq: ['$$view.id', '$checkedView.id'] }
}
}
},
populatedViews: {
$size: "$populatedViews"
},
}
},
{
$sort: { viewAmount: -1 }
},
{ $skip: 1 },
{ $limit : 100 }
]);