how to use $regex search in referenced field in mongodb - node.js

i am struggling with a task, which it is I used two schemas
User Schema
{
"first_name": String,
"last_name":String,
"address": String
}
Employee schema
{
user:{
type: ObjectId,
ref: 'User'
},
gross_pay: String,
net_pay: String
tax: String,
}
Well, how can I search first_name using $regex in Employee Schema in this user referenced field? I tried in so many ways, can't get it. how can i resolve it? Thanks in advance

First Approach:
Using $lookup aggregation
Employee.aggregate([
{ "$lookup": {
"from": "users",
"let": { "user": "$user" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$_id", "$$user"] }}},
{ "$project": { "firstName": 1 }}
],
"as": "user"
}},
{ "$unwind": "$user" },
{ "$match": { "user.firstName": { "$regex": your_string, "$options": "i" }}}
])
But it is not the better approach as the $lookup stage is being applied to all the Employee documents and then $match to the users firstName makes the query bit slow.
Second Approach:
First find the users having firstName equal to the match string using $regex and then find the _id in the Employee collection.
const userIds = await (Users.find({ "firstName": { "$regex": your_string, "$options": "i" } })).map(user => user._id)
const employees = await Employee.find({ "user": { "$in": userIds }})
Third Approach:
Keep the single key firstName of the user schema in the employee schema
Employee schema
user: { type: ObjectId, ref: 'User' },
gross_pay: String,
net_pay: String
tax: String
firstName: String
and then use query directly
const userIds = await Employee.find({
"firstName": { "$regex": your_string, "$options": "i" }
})

Related

Display array of documents as objects with id as key

Is there a way to use project or group in mongo to convert an array of documents or subdocuments into an object with documents ObjectId as key and document/subdocument as value?
For example:
const book = Schema({
name: {type: String},
authors: [{type: mongoose.Schema.Types.ObjectId, ref: 'Author'}]
})
const author = Schema({name: {type: String}})
If you query for all books:
Book.find({}).populate('authors').lean().exec()
then you get:
[{
id: 10,
name: 'Book 1',
authors: [{
id: 1,
name: 'Author1'
}]
},...]
but I would like to have this:
[{
id: 10,
name: 'Book 1',
authors: {
1: {id: 1, name: 'Author 1'}
}
},...]
I know that iterating over the objects after querying from mongo will do it but I guess that running the query at mongo is more efficient.
The main consideration here is that the "keys" you want are actually ObjectId values as defined in your schema and not really a "string", which is actually a requirement for a JavaScript Object since all "keys" must be a "string". For JavaScript this really is not much of an issue since JavScript will "stringify" any argument specified as a "key", but it does matter for BSON, which is what MongoDB actually "speaks"
So you can do this with MongoDB, but you need MongoDB 4.x at least in order to support the $convert aggregation operator or it's shortcut method $toString. This also means that rather than populate(), you actually use MongoDB $lookup:
let results = await Books.aggregate([
{ "$lookup": {
"from": Author.collection.name,
"localField": "authors",
"foreignField": "_id",
"as": "authors"
}},
{ "$addFields": {
"authors": {
"$arrayToObject": {
"$map": {
"input": "$authors",
"in": { "k": { "$toString": "$$this._id" }, "v": "$$this" }
}
}
}
}}
])
Or if you prefer the alternate syntax:
let results = await Books.aggregate([
{ "$lookup": {
"from": "authors",
"let": { "authors": "$authors" },
"pipeline": [
{ "$match": { "$expr": { "$in": [ "$_id", "$$authors" ] } } },
{ "$project": {
"_id": 0,
"k": { "$toString": "$_id" },
"v": "$$ROOT"
}}
],
"as": "authors"
}},
{ "$addFields": {
"authors": { "$arrayToObject": "$authors" }
}}
])
Which would return something like:
{
"_id" : ObjectId("5c7f046a7cefb8bff9304af8"),
"name" : "Book 1",
"authors" : {
"5c7f042e7cefb8bff9304af7" : {
"_id" : ObjectId("5c7f042e7cefb8bff9304af7"),
"name" : "Author 1"
}
}
}
So the $arrayToObject does the actual "Object" output where you supply it an array of objects with k and v properties corresponding to key and value. But it must have a valid "string" in "k" which is why you $map over the array content to reformat it first.
Or as the alternate, you can $project within the pipeline argument of $lookup instead of using $map later for exactly the same thing.
With client side JavaScript, the translation is a similar process:
let results = await Books.aggregate([
{ "$lookup": {
"from": Author.collection.name,
"localField": "authors",
"foreignField": "_id",
"as": "authors"
}},
/*
{ "$addFields": {
"authors": {
"$arrayToObject": {
"$map": {
"input": "$authors",
"in": { "k": { "$toString": "$$this._id" }, "v": "$$this" }
}
}
}
}}
*/
])
results = results.map(({ authors, ...rest }) =>
({
...rest,
"authors": d.authors.reduce((o,e) => ({ ...o, [e._id.valueOf()]: e }),{})
})
)
Or with populate()
let results = await Book.find({}).populate("authors");
results = results.map(({ authors, ...rest }) =>
({
...rest,
"authors": (!authors) ? {} : authors.reduce((o,e) => ({ ...o, [e._id.toString()]: e }),{})
})
)
NOTE however that populate() and $lookup are really quite different. MongoDB $lookup is a single request to the server that returns one response. Using populate() actually invokes multiple queries and does the "joining" in client side JavaScript code even if it hides what it is doing from you.

$lookup not returning empty array mongodb

I am creating mongodb with aggregate in which it should join two table so I have used $lookup but query returning me empty array
here is my query
return userScoringModel.aggregate([
{
"$match": {
examId: new ObjectId(req.params.examId),
user_Id: new ObjectId(req.params.userId),
subject: subject,
correctMark: {$lt: 0}
}
},
{
$lookup: {
from: "examPortalQuestions",
localField: "questionId",
foreignField: "_id",
as: "question_text"
}
},
{
"$group": {
"_id": {chapter: '$chapter'},
"questionNumbers": {$push: "$questionNo"},
"question_text": {$push: '$question_text'}
}
},
{
"$project": {
"_id": 0,
"subject": {
"name": "$_id.subject",
"chapter": "$_id.chapter",
"questionNumber": "$questionNumbers",
"question": "$question_text"
}
}
}
]).exec()
and here is output
{
"subject": {
"chapter": "NUMBER TEST AND PAIR FINDING",
"questionNumber": [
"20"
],
"question": [
[]
]
}
},
sample collection
score table:
_id, examid, userid , questionId, chapter
objectId ObjectId ObjectId ObjectId (ref:'questiontable') XYZ
objectId ObjectId ObjectId ObjectId (ref:'questiontable') qwe
Question table :
_id, question,
ObjectId what is abc?
here I am getting question array empty
can someone suggest me how to archive it
thanks in advance

How to match Array elements in Sub-Documents by Criteria

I have following Mongoose schemas :
EmployeeSchema :
var EmployeeSchema = new Schema({
name : String,
employeeDetailsId: {
type: Schema.Types.ObjectId,
ref: 'employeedetails'
}
});
EmployeeDetailSchema :
var EmployeeDetailSchema = new Schema({
employeeId: {
type: Schema.Types.ObjectId,
ref: 'employee'
},
primarySkills: [
{
type: Schema.Types.ObjectId,
ref: 'skills'
}
],
});
SkillsSchema :
var SkillsSchema = new Schema({
name: {
type: String,
required: true
}
});
EmployeeDetailSchema data gets saved on demand, like when a particular Skill is assigned to Employee. Once EmployeeDetail document is saved then corresponding EmployeeDetailID is saved back to EmployeeSchema as employeeDetailsId.
Now there is bi-directional relationship between EmployeeSchema and EmployeeDetailSchema.
NOTE :
Multiple Skills can be associated to an Employee and they are stored as an array of ObjectID's in EmployeeDetails Schema.
UseCase :
I want to fetch all Employees who have particular Skill associated with them, Skill will be input to the Mongoose / Mongo query.
Say input Skill ID is 1234 then i want to fetch all employees who have Skill id 1234 in EmployeeDetail > PrimarySkills array.
Following is the approach which i tried using Mongoose :
EmployeeModel.aggregate([
{
$lookup: {
from: 'employeedetails',
localField: 'employeeDetailsId',
foreignField: '_id',
as: 'details'
}
},
{
$match: {
$and: [
{ "details.primarySkills": { "$exists": true } },
{
"details.primarySkills": {
$in: [mongoose.Types.ObjectId(req.params.skillId)]
}
}
]
}
}
]).exec(function (err, result) {
if (err) return res.send('400', {
message: 'Unable to fetch employees data by status. Please try again later'
});
return res.jsonp(result);
});
Result : Empty array.
I have no clue where am going wrong, need some help.
My bad, original approach which i followed was all fine except a small mistake. I should have used req.query.skillId instead of req.params.skillId
For those wondering the difference b/w query and params, check this answer
This is the final solution, thought it may help others :
EmployeeModel.aggregate([
{
$lookup: {
from: 'employeedetails',
localField: 'employeeDetailsId',
foreignField: '_id',
as: 'details'
}
},
{
$match: {
$and: [
{ "details.primarySkills": { "$exists": true } },
{
"details.primarySkills": {
$in: [mongoose.Types.ObjectId(req.query.skillId)]
}
}
]
}
}
]).exec(function (err, result) {
if (err) return res.send('400', {
message: 'Unable to fetch employees data by status. Please try again later'
});
return res.jsonp(result);
});
One approach that you could take is apply the $lookup to the Skills model on the EmployeeDetails model first and then do another lookup to the Employee model
EmployeeDetails.aggregate([
{ "$match": { "primarySkills": req.query.skillId } },
{ "$unwind": "$primarySkills" }, // skip this pipeline step for MongoDB 3.4 and above
{
"$lookup": {
"from": "skills",// ensure this is the correct collection name
"foreignField": "_id",
"localField": "primarySkills",
"as": "skills"
}
},
{ "$unwind": "$skills" }
{
"$group": {
"_id": "$_id",
"employeeId": { "$first": "$employeeId" },
"primarySkills": { "$push": "$skills" }
}
},
{
"$lookup": {
"from": "employees",// ensure this is the correct collection name
"foreignField": "_id",
"localField": "employeeId",
"as": "employee"
}
}
]).exec(callback);

Mongoose aggregate match if in array

I have a basic document that I am performing an aggregate on to search for the combination of a first and and last name combined:
My document:
{
_id: dmw9294r94
firstName: "John",
lastName: "Smith",
friends: [28enw93hr, 29nwgkn38]
}
My aggregate pipline:
User.aggregate([
{
$project: {
"name" : { $concat : [ "$firstName", " ", "$lastName" ] },
"firstName": "$firstName",
"lastName": "$lastName",
"username": "$username",
"friends": "$friends"}
},{
$match: {
"name": {$regex: new RegExp(req.query.query, "i")
}
}}]).exec(function(err, results){
res.json(results);
}
);
How can I add to this to check if a specific user ID exists within that users friends array?
I tried the following, although had no luck:
$match: {
"name": {$regex: new RegExp(req.query.query, "i"),
"friends" {$eq: myVarHere}
}
As #profesor79 said, you just have to $match for the userid you are interested in:
{$match: {"friends": "user_id_to_find")}
Unless I don't understand the question, it seems to me that $regex is not needed here.

Mongoose error on aggregate group

i have this model:
var Chat = new Schema({
from: String,
to: String,
satopId: String,
createdAt: Date
});
var Chat = mongoose.model('Chat', Chat);
I want do a query to do a query that returns the max created at grouping by to and from field. I tried with:
Chat.aggregate([
{
$group: {
_id: '$to',
from: '$from',
createdAt: {
$max: '$createdAt'
}
}
},
{
$project: {
_id: 1,
createdAt: 1,
from: 1,
to: 1
}
}
], function(err, docs){
})
But this generates this error:
the group aggregate field 'from' must be defined as an expression
inside an object
I don't understand what does it mean. How can i solve it?
Thanks
Anything "outside" if the _id expression in a $group statement requires a "grouping operator" of some sort.
Assuming you are happy with the idea of the "sole" grouping key to be the "to" field in your documents then you probably want something like $first:
Chat.aggregate([
{ "$group": {
"_id": "$to",
"from": { "$first": "$from" },
"createdAt": { "$max": "$createdAt" }
}},
function (err, docs) {
// do something here
}
])
Otherwise if you want "distinct" values on "both" fields then they both belong as a composite value of the grouping key:
Chat.aggregate([
{ "$group": {
"_id": {
"to": "$to",
"from": "$from"
},
"createdAt": { "$max": "$createdAt" }
}},
function (err, docs) {
// do something here
}
])
That's basically how it works. Either it's part of the key or something that needs a grouping operator just like with SQL.
Also note that your $project assertion is not really required as $group already does what you are asking there.

Resources