How to query nested array - node.js

Let's say I have documents like
{
name: "name1",
friends: [
{
name: "name2",
thingsWeDo: [
{
name: "running",
like_it: true
},
{
name: "swimming",
like_it: false
}
]
}
]
}
So if I want to know the name of my friend we both love swimming, how do I do it?
I know the schema design could be better, but if I want to query this schema, how do I do it?
is it possible to chain $elemMatch ?

You should $unwind friends and thingsWeDo in aggregation and then use $match to match your criteria. Check below query :
db.collectionName.aggregate({
"$unwind": "$friends" // first unwind friends array
}, {
"$unwind": "$friends.thingsWeDo" // second unwind friends.thingsWeDo
}, {
"$match": {
"friends.thingsWeDo.name": "swimming", // match your criteria
"friends.thingsWeDo.like_it": false
}
}, {
"$project": {
"name": "$friends.thingsWeDo.name",
"like_it": "$friends.thingsWeDo.like_it"
}
}).pretty()
here unwind two times for large documents query will be slow down. As per my recommendation you should change your schema structure and try to avoid unwinding

Untested, but something like this should do:
Model.find({
$and: [
{ "friends.thingsWeDo.name": 'swimming' },
{ "friends.thingsWeDo.like_it": true }
]
}, function (err, docs) {
});

Related

Sort with Mongoose by the number of respected conditions

I want to make a search function with mongoose, and I have to be able to make a research with multiple fields (with Mongoose, in NodeJS).
So, I do something like this :
const result = await myModel.find({
$or [{condition1: "value1"}, {condition2: "value2"}, etc...]
});
But, I want to sort the result by the number of condition the object returned have. Like :
If I have 2 conditions, I want to display first the objects respecting the 2 conditions, then the objects respecting the 1st condition, and finally the objects respecting the 2nd condition.
Do you guys know how I can do this? :)
Thanks in advance !
================EDIT================
This is the new search function :
/**
* Search function which returns users matching jobs and skills.
*
* #param {Array[String]} jobs
* #param {Array[String]} skills
* #return {Array[UserModel]} users
*/
async search(jobs, skills) {
// Normalized arrays of jobs, skills and fields (to use it in a mongoose request).
const jobSkills = [];
const associatedSkills = [];
const fields = [];
for (const job of jobs) {
jobSkills.push({
$cond: [
{
$eq: ["$jobSkills", job],
},
2,
0,
],
});
fields.push({
jobSkills: job,
});
}
for (const skill of skills) {
associatedSkills.push({
$cond: [
{
$eq: ["$associatedSkills", skill],
},
1,
0,
],
});
fields.push({
associatedSkills: skill,
});
}
// Request to find users matching jobs and skills.
const users = await UserModel.aggregate([
{
$match: {
$or: fields,
},
},
{
$addFields: {
sortField: {
$sum: jobSkills.concat(associatedSkills),
},
},
},
{
$sort: {
sortField: -1,
},
},
]);
return users;
}
Aggregation Log :
Aggregate {
_pipeline: [
{ '$match': [Object] },
{ '$addFields': [Object] },
{ '$sort': [Object] }
],
_model: Model { User },
options: {}
}
In general, a document either matches a query predicate or it doesn't. There isn't really a concept of one document matching "better" than another. So it looks like you'll want to generate a custom value in a new field and sort on that. This will need to be done via an aggregation.
So after the $match, we'll want an $addFields stage that effectively duplicates the query predicates. For each one it will be wrapped in a conditional statement ($cond) where we add 1 for a match or 0 otherwise, e.g.:
{
$cond: [
{
$eq: [
"$condition1",
"value1"
]
},
1,
0
]
}
Then there will be a $sum pulling them together to generate the final score to sort on.
Taken together, the aggregation will look something like this:
db.collection.aggregate([
{
$match: {
$or: [
{
condition1: "value1"
},
{
condition2: "value2"
}
]
}
},
{
$addFields: {
sortField: {
"$sum": [
{
$cond: [
{
$eq: [
"$condition1",
"value1"
]
},
1,
0
]
},
{
$cond: [
{
$eq: [
"$condition2",
"value2"
]
},
1,
0
]
}
]
}
}
},
{
$sort: {
"sortField": -1
}
}
])
Playground demonstration here

Mongoose - How to query field in the last object of an array of objects

I have MongoDB documents structured like this:
{
"_id": "5d8b987f9f8b9f9c8c8b9f9",
"targetsList": [
{
"target": "user",
"statusList": [
{
"date": "2018-01-01",
"type": "OK"
},
{
"date": "2018-01-02",
"type": "FAILD"
}
]
}
]
}
And I want to count all documents that in their "targetList" array, there is an object with "target"=="user" - and also that object conatin on the last element of its "statusList" array, an object with "type" != "FAILD".
Any ideas on how to implement this kind of query?
Mongo playground:
https://mongoplayground.net/p/3bCoHRnh-KQ
In this example, I expected the count to be 1, because only the second object meets the conditions.
An aggregation pipeline
1st step - Filtering out where "targetsList.target": "user"
2nd step - $unwind on targetsList to get it out of array
3rd step - getting the last element of the targetsList.statusList array using $arrayElemAt
4th step - getting the results where that last element is not FAILD
5th step - getting the count
demo - you can try removing parts of the pipeline to see what the intermediate results are
db.collection.aggregate([
{
$match: {
"targetsList.target": "user"
}
},
{
$unwind: "$targetsList"
},
{
$project: {
"targetsList.statusList": {
$arrayElemAt: [
"$targetsList.statusList",
-1
]
},
}
},
{
$match: {
"targetsList.statusList.type": {
$ne: "FAILD"
}
}
},
{
$count: "withoutFailedInLastElemCount"
}
])
Unless it's crucial that the element be the last index, this should work for your case.
db.collection.find({
"targetsList.statusList.type": {
$in: [
"FAILD"
]
}
})
This will retrieve documents where the type value is FAILD. To invert this you can swap $in for $nin.
Updated playground here
Here's another way to do it with a leading monster "$match".
db.collection.aggregate([
{
"$match": {
"targetsList.target": "user",
"$expr": {
"$reduce": {
"input": "$targetsList",
"initialValue": false,
"in": {
"$or": [
"$$value",
{
"$ne": [
{
"$last": "$$this.statusList.type"
},
"FAILD"
]
}
]
}
}
}
}
},
{
"$count": "noFailedLastCount"
}
])
Try it on mongoplayground.net.

MongoDB find $in sorting issue

I need to get docs from MongoDB collection where ID's are in array:
[
'5f80a44d0179262f7c2e6a42',
'5f8c00762fae890e9c4d029c',
'5f802cf8abac1116a46bf9d4'
]
The issue is, docs are not coming in sequence of my array ID's. They are coming (1, 0, 2) for above array ID's.
How can I make them in sequence of my ID's array? I am using, NodeJs + Mongoose.
My code:
var ids = ['5f80a44d0179262f7c2e6a42','5f8c00762fae890e9c4d029c','5f802cf8abac1116a46bf9d4']
Product.find({
_id: {
$in: ids
}
})
I don't think its possible with find(), or any functionality available in MongoDB related to this,
It is possible with aggregate() but this will just fulfil your expected result,
I am not recommending to use this because this will affect performance of query response, this could be a heavy transaction.
$match your conditions
$group by null and make array of all matching objects in root
$addFields to add ids array that we search for
$unwind deconstruct ids array, this will deconstruct in sequence as per we provided in ids array
$project, $reduce input as root array and check condition if id match then return object
$replaceWith to replace root object to root
var ids = [
ObjectId("5f802cf8abac1116a46bf9d4"),
ObjectId("5f8c00762fae890e9c4d029c"),
ObjectId("5f80a44d0179262f7c2e6a42")
];
Product.aggregate([
{ $match: { _id: { $in: ids } } },
{
$group: {
_id: null,
root: { $push: "$$ROOT" }
}
},
{ $addFields: { ids: ids } },
{ $unwind: "$ids" },
{
$project: {
root: {
$reduce: {
input: "$root",
initialValue: {},
in: { $cond: [{ $eq: ["$$this._id", "$ids"] }, "$$this", "$$value"] }
}
}
}
},
{ $replaceWith: "$root" }
])
Playground

How to skip a document based on condition in mongodb aggregation

Let say I have a schema of blog post which contain many keys and one of them is author (ObjectId). Now I have an another collection of Block users which contains two keys: userid (ObjectId) and userWhoHasBeenBlocked (ObjectId).
Now in aggregation I want to skip those collection which has a author equals to the userWhoHasBeenBlocked.
My Approch: First level I have a match query which chcecks the country from which the post has been made. Let say there is a key of a country.
After this I have a $lookup query for block user collection as
{ $match: { country: "usa" } },
{
$lookup:
{
from: "ublocks",
let: { whoHasBeenBlocked: "$author", currentUser: userid },
pipeline: [
{
$match:
{
$expr:
{
$and:
[
{ $eq: ["$blockedUser", "$$whoHasBeenBlocked"] },
]
}
}
},
],
as: "isBlocked"
},
}
},
{ $match: { "$author": { $ne: "$isBlocked.userId" } } }
}
after this I have $projection block. this is not working. How to skip a document within aggregation. I also have pagination after this.

Finding only an item in an array of arrays by value with Mongoose

Here is an example of my Schema with some data:
client {
menus: [{
sections: [{
items: [{
slug: 'some-thing'
}]
}]
}]
}
And I am trying to select it like this:
Schema.findOne({ client._id: id, 'menus.sections.items.slug': 'some-thing' }).select('menus.sections.items.$').exec(function(error, docs){
console.log(docs.menus[0].sections[0].items[0].slug);
});
Of course "docs.menus[0].sections[0].items[0].slug" only works if there is only one thing in each array. How can I make this work if there is multiple items in each array without having to loop through everything to find it?
If you need more details let me know.
The aggregation framework is good for finding things in deeply nested arrays where the positional operator will fail you:
Model.aggregate(
[
// Match the "documents" that meet your criteria
{ "$match": {
"menus.sections.items.slug": "some-thing"
}},
// Unwind the arrays to de-normalize as documents
{ "$unwind": "$menus" },
{ "$unwind": "$menus.sections" },
{ "$unwind": "$menus.sections.items" }
// Match only the element(s) that meet the criteria
{ "$match": {
"menus.sections.items.slug": "some-thing"
}}
// Optionally group everything back to the nested array
// One step at a time
{ "$group": {
"_id": "$_id",
"items": { "$push": "$menus.sections.items.slug" }
}},
{ "$group": {
"_id": "$_id",
"sections": {
"$push": { "items": "$items" }
}
}},
{ "$group": {
"_id": "$_id",
"menus": {
"$push": { "sections": "$sections" }
}
}},
],
function(err,results) {
}
)
Also see the other aggregation operators such as $first for keeping other fields in your document when using $group.

Resources