Mongodb $and operator doesn't work as expected - javascript

I have a basic social media app that allows users to follow each other. When need to find some specific persons "followers", i look for users who have the id of this specific person in their "following"s;
{
$and: [
{
$and: [
{
"following.userId": mongoose.Types.ObjectId(targetId)
},
{
"following.following": true
}
]
},
{
$or: [{ firstName: firstNameRegex }, { lastName: lastNameRegex }]
},
{ blockedUsers: { $nin: mongoose.Types.ObjectId(req.userId) } }
]
};
If a user stops following someone, "following.following" property becomes false.
When run this query, I get every person who has followed that specific person in some time without looking "following.following": true property at all.
"following.following" doesn't evaluate the times when "following.userId" matches, rather it looks for whole array and matches if some of them has "following.following" true.
Here is the file structure

You are querying embedded array documents, simple $and query will not be helpful here the way you are using it.
Basically we want to match multiple fields from single embedded documents in an array.
so let's consider this example:
For simplicity, I have added a few fields from my understanding, and
considering you are facing an issue with $and query, I will and
accordingly assuming rest query does not change and works.
db.followers.find().pretty()
{
"_id" : ObjectId("5d984403d933b7b079038ca9"),
"userId" : "1",
"followers" : [
{
"fId" : "4",
"following" : true
},
{
"fId" : "2",
"following" : true
},
{
"fId" : "3",
"following" : false
}
]
}
{
"_id" : ObjectId("5d984422d933b7b079038caa"),
"userId" : "2",
"followers" : [
{
"fId" : "1",
"following" : true
},
{
"fId" : "3",
"following" : false
},
{
"fId" : "4",
"following" : false
}
]
}
{
"_id" : ObjectId("5d984432d933b7b079038cab"),
"userId" : "3",
"followers" : [
{
"fId" : "1",
"following" : true
},
{
"fId" : "2",
"following" : true
},
{
"fId" : "4",
"following" : true
}
]
}
{
"_id" : ObjectId("5d984446d933b7b079038cac"),
"userId" : "4",
"followers" : [
{
"fId" : "1",
"following" : false
},
{
"fId" : "2",
"following" : true
},
{
"fId" : "3",
"following" : true
}
]
}
ANS 1:
db.followers.find({followers:{ "fId": "1", "following": true }}).pretty()
{
"_id" : ObjectId("5d984422d933b7b079038caa"),
"userId" : "2",
"followers" : [
{
"fId" : "1",
"following" : true
},
{
"fId" : "3",
"following" : false
},
{
"fId" : "4",
"following" : false
}
]
}
{
"_id" : ObjectId("5d984432d933b7b079038cab"),
"userId" : "3",
"followers" : [
{
"fId" : "1",
"following" : true
},
{
"fId" : "2",
"following" : true
},
{
"fId" : "4",
"following" : true
}
]
}
Notice how the followers array is used in the query. ref enter link description here
In your case, we can modify your query like this:
{
$and: [ // by default it's $and only, you don't have to mention explicitly
{
$and: [ // you can even remove this $and
"following":
{
"userId": mongoose.Types.ObjectId(targetId),
"following": true
}
]
},
{
$or: [{ firstName: firstNameRegex }, { lastName: lastNameRegex }]
},
{ blockedUsers: { $nin: mongoose.Types.ObjectId(req.userId) } }
]
}
ANS 2:
You can use $elemMatch
$elemMatch is used to query multiple fields from a single document in an array.
db.followers.find({followers: {$elemMatch: { "fId": "1", "following": true }}}).pretty()
{
"_id" : ObjectId("5d984422d933b7b079038caa"),
"userId" : "2",
"followers" : [
{
"fId" : "1",
"following" : true
},
{
"fId" : "3",
"following" : false
},
{
"fId" : "4",
"following" : false
}
]
}
{
"_id" : ObjectId("5d984432d933b7b079038cab"),
"userId" : "3",
"followers" : [
{
"fId" : "1",
"following" : true
},
{
"fId" : "2",
"following" : true
},
{
"fId" : "4",
"following" : true
}
]
}
Query for you will be:
{
$and: [
{
"following":
{$elemMatch: {
"userId": mongoose.Types.ObjectId(targetId),
"following": true
}
}
},
{
$or: [{ firstName: firstNameRegex }, { lastName: lastNameRegex }]
},
{ blockedUsers: { $nin: mongoose.Types.ObjectId(req.userId) } }
]
}
BUT THIS WILL BE WRONG (See Query Below):
db.followers.find({"followers.fId": "1", "followers.following": true }).pretty()
{
"_id" : ObjectId("5d984422d933b7b079038caa"),
"userId" : "2",
"followers" : [
{
"fId" : "1",
"following" : true
},
{
"fId" : "3",
"following" : false
},
{
"fId" : "4",
"following" : false
}
]
}
{
"_id" : ObjectId("5d984432d933b7b079038cab"),
"userId" : "3",
"followers" : [
{
"fId" : "1",
"following" : true
},
{
"fId" : "2",
"following" : true
},
{
"fId" : "4",
"following" : true
}
]
}
{
"_id" : ObjectId("5d984446d933b7b079038cac"),
"userId" : "4",
"followers" : [
{
"fId" : "1",
"following" : false
},
{
"fId" : "2",
"following" : true
},
{
"fId" : "3",
"following" : true
}
]
}
Note
To see only match documents, you can use
db.followers.find({followers: {$elemMatch: { "fId": "1", "following": true }}},{"followers.$": 1}).pretty()
db.followers.find({followers: {$elemMatch: { "fId": "1", "following": true }}},{"followers.$": 1}).pretty()

I've managed to solve this problem using $elemMatch operator like this:
{
$and: [
{
following: {
$elemMatch: {
userId: mongoose.Types.ObjectId(targetId),
following: true
}
}
},
{
$or: [{ firstName: firstNameRegex }, { lastName: lastNameRegex }]
},
{ blockedUsers: { $nin: mongoose.Types.ObjectId(req.userId) } }
]
};

Related

How to find documents with child object that has matching value?

Suppose that I have a collection with documents like below
{
"location" : "Tokyo",
"region" : "Asia",
"attraction": {
"transportation" : "Subway",
"food" : {
"food_0" : {
"name" : "Sushi",
"price" : 100,
"restaurant" : "Ookinza"
},
"food_1" : {
"name" : "Sashimi",
"price" : 200,
"restaurant" : "Hibiki"
},
"food_2" : {
"name" : "N/A",
"price" : "N/A",
"restaurant" : "N/A"
}
}
}
},
{
"location" : "Taipei",
"region" : "Asia",
"attraction": {
"transportation" : "Subway",
"food" : {
"food_0" : {
"name" : "Bubble tea",
"price" : 50,
"restaurant" : "The Alley"
},
"food_1" : {
"name" : "Oyster cake",
"price" : 100,
"restaurant" : "Night market"
},
"food_2" : {
"name" : "N/A",
"price" : "N/A",
"restaurant" : "N/A"
}
}
}
},
{
"location" : "Toronto",
"region" : "North America",
"attraction": {
"transportation" : "Uber",
"food" : {
"food_0" : {
"name" : "Raman",
"price" : 300,
"restaurant" : "Kinto"
},
"food_1" : {
"name" : "Bubble tea",
"price" : 200,
"restaurant" : "Fresh Fruit"
},
"food_2" : {
"name" : "N/A",
"price" : "N/A",
"restaurant" : "N/A"
}
}
}
},
How do I find documents that have matching field in the child object of Food?
i.e. If I want to find document that has restaurant:"Fresh Tea"?
Currently what I have:
app.get(route, (req, res) => {
var detail = {};
if(req.query.location){
detail['location'] = req.query.location.toUpperCase();
}
if(req.query.region){
detail['region'] = req.query.region.toUpperCase();
}
if(req.query.transportation){
detail['attraction.transportation'] = new RegExp(req.query.transportation.split(","), "i"),
}
if(req.query.restaurant){
detail['attraction.food.food_0'] = req.query.restaurant;
}
db.collection(config.dbCollections.foodDB)
.aggregate([
$match: detail,
},
{
$lookup: {
... // code continues
Right now detail['attraction.food.food_0'] = req.query.restaurant is only able to find document that has matching food_0.restaurant, but I still can't find a way to make it check all child objects within "food".
Updated with more info:
User has the option to enter multiple search categories, and I want to combine all the search requests into "detail" and find all matching results. I.e. If user looks for transportation="Subway" and food="Bubble tea", then both Taipei and Toronto should come up as result.
Using dynamic value as field name is generally considered as anti-pattern and should be avoided. Nevertheless, you can convert the object attraction.food to an array of k-v tuple and perform the search with your criteria. For your case, $anyElementTrue with $map will help with processing the array.
db.collection.aggregate([
{
"$addFields": {
"test": {
"$anyElementTrue": {
"$map": {
"input": {
"$objectToArray": "$attraction.food"
},
"as": "t",
"in": {
$or: [
{
$eq: [
"$$t.v.transportation",
"Subway"
]
},
{
$eq: [
"$$t.v.name",
"Bubble tea"
]
}
]
}
}
}
}
}
},
{
$match: {
test: true
}
},
{
"$unset": "test"
}
])
Here is the Mongo Playground for your reference.
A possible aggregation pipeline
Add a temporary field using $addFields and $objectToArray which does something similar to javascript Object.entries()
Do the matching
Remove the added temporary field using $project 0
playground
db.collection.aggregate([
{
"$addFields": {
"foodArray": {
"$objectToArray": "$attraction.food"
},
},
},
{
"$match": {
"foodArray.v.restaurant": "Fresh Fruit"
}
},
{
"$project": {
"foodArray": 0
},
},
])

how to update document using aggregation

I have complex document I try to update using aggregate but it's only making copy when I use $out it's remove all other document I want to concate all other file to this and update........................................................................................................................................................................................................................................................................................................................................................................................................
db.getDb().collection(coll.seat).aggregate( [
{
'$unwind': {
'path': '$show_seats'
}
}, {
'$unwind': {
'path': '$show_seats.showByDate.shows'
}
}, {
'$unwind': {
'path': '$show_seats.showByDate.shows.showSeats'
}
}, {
'$unwind': {
'path': '$show_seats.showByDate.shows.showSeats'
}
}, {
'$unwind': {
'path': '$show_seats.showByDate.shows.showSeats.seat_details'
}
}, {
'$unwind': {
'path': '$show_seats.showByDate.shows.showSeats.seat_details.values'
}
}, {
'$match': {
'show_seats.showByDate.shows.showSeats.seat_details.values._id': ObjectId('62af61b72609bb5c0b664e7e')
}
}, {
'$addFields': {
'show_seats.showByDate.shows.showSeats.seat_details.values.seat_status': true
}
},
{
$out: 'shows'
}
])
this is my mongo db data look like
{
"_id" : ObjectId("62a43ac2d7213c7233cd1dee"),
"totalShowByDay" : "2",
"totalShowDays" : 4,
"movieId" : ObjectId("62953ba3cb6ae625ec9433e6"),
"screenId" : ObjectId("6293b9638dde2d92658d5513"),
"createdAt" : 1654930114438,
"showId" : ObjectId("62a43ac2d7213c7233cd14ed"),
"show_seats" : [
{
"showByDate" : {
"ShowDate" : "2022-06-11",
"shows" : [
{
"showTime" : "2022-06-11T10:00",
"showSeats" : [
[
{
"category" : "CLASSIC",
"seat_details" : [
{
"key" : "A",
"values" : [
{
"_id" : ObjectId("62a43ac2d7213c7233cd14ee"),
"seat_number" : "1",
"tag_name" : "A",
"seat_status" : false,
"user_id" : false,
"price" : "140",
"seats_category" : "CLASSIC",
"show_time" : "2022-06-11T10:00"
},
,
{
"_id" : ObjectId("62a43ac2d7213c7233cd14ef"),
"seat_number" : "2",
"tag_name" : "A",
"seat_status" : false,
"user_id" : false,
"price" : "140",
"seats_category" : "CLASSIC",
"show_time" : "2022-06-11T10:00"
},
{
"_id" : ObjectId("62a43ac2d7213c7233cd14f0"),
"seat_number" : "3",
"tag_name" : "A",
"seat_status" : false,
"user_id" : false,
"price" : "140",
"seats_category" : "CLASSIC",
"show_time" : "2022-06-11T10:00",,
{
"_id" : ObjectId("62a43ac2d7213c7233cd14ef"),
"seat_number" : "2",
"tag_name" : "A",
"seat_status" : false,
"user_id" : false,
"price" : "140",
"seats_category" : "CLASSIC",
"show_time" : "2022-06-11T10:00"
},
{
"_id" : ObjectId("62a43ac2d7213c7233cd14f0"),
"seat_number" : "3",
"tag_name" : "A",
"seat_status" : false,
"user_id" : false,
"price" : "140",
"seats_category" : "CLASSIC",
"show_time" : "2022-06-11T10:00"
}

How to group by sub documents and get unique value of value field?

This is my database collection:
{"productId" : 1,
"isVariant": 1,
"isComplete" : 1,
"variantId" : 1,
"attributeSet" : [
{
"name" : "Capacity",
"value" : "500 GB",
"id" : 3
},
{
"name" : "Form Factor",
"value" : "5 inch",
"id" : 4
},
{
"id" : 5,
"name" : "Memory Components",
"value" : "3D NAND",
"isVariation" : 0
}
]
},
{"productId" : 2,
"isVariant": 1,
"isComplete" : 1,
"variantId" : 1,
"attributeSet" : [
{
"name" : "Capacity",
"value" : "1 TB",
"id" : 3
},
{
"name" : "Form Factor",
"value" : "5 inch",
"id" : 4
},
{
"id" : 5,
"name" : "Memory Components",
"value" : "3D NAND",
"isVariation" : 0
}
]
},
{"productId" : 3,
"isVariant": 1,
"isComplete" : 0,
"variantId" : 1,
"attributeSet" : [
{
"name" : "Capacity",
"value" : "500 GB",
"id" : 3
},
{
"name" : "Form Factor",
"value" : "2.5 inch",
"id" : 4
},
{
"id" : 5,
"name" : "Memory Components",
"value" : "3D NAND",
"isVariation" : 0
}
]
},
{"productId" : 4,
"isVariant": 1,
"isComplete" : 0,
"variantId" : 1,
"attributeSet" : [
{
"name" : "Capacity",
"value" : "1 TB",
"id" : 3
},
{
"name" : "Form Factor",
"value" : "2.5 inch",
"id" : 4
},
{
"id" : 5,
"name" : "Memory Components",
"value" : "3D NAND",
"isVariation" : 0
}
]
}
Now I want to send the data of only the attribute where isVariation is not 0. Also I want to send the variant values of each attribute where isComplete =1. Hence the result should look like this
result : [{
"id": 3,
"name": "Capacity",
"value": [
"500 GB",
"1 TB"
]
}, {
"id": 4,
"name": "Form Factor",
"value": [
"5 inch"
]
}]
The above result does not have value of 2.5 inch as the isComplete is 0 for this document. Can anyone help me with the query
$match isComplete is 1
$project to show required fields
$unwind deconstruct attributeSet array
$match attributeSet.isVariation is not 0
$group by attributeSet.id and get first name and get unique value using $addToSet
db.collection.aggregate([
{ $match: { isComplete: 1 } },
{
$project: {
_id: 0,
attributeSet: 1
}
},
{ $unwind: "$attributeSet" },
{ $match: { "attributeSet.isVariation": { $ne: 0 } } },
{
$group: {
_id: "$attributeSet.id",
name: { $first: "$attributeSet.name" },
value: { $addToSet: "$attributeSet.value" }
}
}
])
Playground
The $project stage is not required in your query, i have added because this will optimize your query performance.

Mongodb $graphLookup build hierarchy [duplicate]

This question already has answers here:
MongoDB $graphLookup get children all levels deep - nested result
(2 answers)
Closed 3 years ago.
I have an output from mongodb $graphLookup aggregation:
db.getCollection('projects').aggregate([
{
$lookup: {
from: "projects",
localField: "_id",
foreignField: "parent",
as: "childrens"
}
}
])
{
"_id" : "1",
"name" : "Project1",
"parent" : null,
"childrens" : [
{
"_id" : "3",
"name" : "ProjectForId1",
"parent" : "1"
}
]
},
{
"_id" : "3",
"name" : "ProjectForId1",
"parent" : "1",
"childrens" : [
{
"_id" : "6",
"name" : "ProjectForId3",
"parent" : "3"
},
{
"_id" : "7",
"name" : "ProjectForId3",
"parent" : "3"
}
]
}
I need to build hierarchy from this output in javascript or if is possible directly from query so the final output should look like:
{
"_id" : "1",
"name" : "Project1",
"parent" : null,
"childrens" : [
{
"_id" : "3",
"name" : "ProjectForId1",
"parent" : "1",
"childrens" : [
{
"_id" : "6",
"name" : "ProjectForId3",
"parent" : "3"
},
{
"_id" : "7",
"name" : "ProjectForId3",
"parent" : "3"
}
]
}
]
}
Also if someone have a brave heart to help in one more case where the hierarchy will be created by filtering _id:
ex: For _id = "1" the output will be same as above but if _id is 3 the final output should look like:
{
"_id" : "3",
"name" : "ProjectForId1",
"parent" : "1",
"childrens" : [
{
"_id" : "6",
"name" : "ProjectForId3",
"parent" : "3"
},
{
"_id" : "7",
"name" : "ProjectForId3",
"parent" : "3"
}
]
}
Below solution is more or less the same as one of my past answers so you can get thorough explanation here
db.projects.aggregate([
{
$graphLookup: {
from: "projects",
startWith: "$_id",
connectFromField: "_id",
connectToField: "parent",
as: "children",
maxDepth: 4,
depthField: "level"
}
},
{
$unwind: "$children"
},
{
$sort: { "children.level": -1 }
},
{
$group: {
_id: "$_id",
children: { $push: "$children" }
}
},
{
$addFields: {
children: {
$reduce: {
input: "$children",
initialValue: {
currentLevel: -1,
currentLevelProjects: [],
previousLevelProjects: []
},
in: {
$let: {
vars: {
prev: {
$cond: [
{ $eq: [ "$$value.currentLevel", "$$this.level" ] },
"$$value.previousLevelProjects",
"$$value.currentLevelProjects"
]
},
current: {
$cond: [
{ $eq: [ "$$value.currentLevel", "$$this.level" ] },
"$$value.currentLevelProjects",
[]
]
}
},
in: {
currentLevel: "$$this.level",
previousLevelProjects: "$$prev",
currentLevelProjects: {
$concatArrays: [
"$$current",
[
{ $mergeObjects: [
"$$this",
{ children: { $filter: { input: "$$prev", as: "e", cond: { $eq: [ "$$e.parent", "$$this._id" ] } } } }
] }
]
]
}
}
}
}
}
}
}
},
{
$addFields: { children: "$children.currentLevelProjects" }
},
{
$match: {
_id: "1"
}
}
])
Last stage is supposed to be the filtering so you can get the data for any level of depth here.

Concatenate object's properties of variable length of a JavaScript object into a new property of the object

I have this complicated object structure:
myObject = {
"myObject" : [
{
"id" : 1,
"parameters" : [
{
"name" : "name1",
"special" : "xxx"
},
{
"name" : "name2",
"special" : "yyy"
}
]
},
{
"id" : 2,
"parameters" : [
{
"name" : "name3",
"special" : "zzz"
}
]
},
{
"id" : 2,
"parameters" : [
{
"name" : "name4",
"special" : "ttt"
},
{
"name" : "name5",
"special" : "aaa"
},
{
"name" : "name6",
"special" : "zzz"
}
]
},
...
]
};
It consists of a array of other objects, each of them having a variable number of parameters.
My goal is to concatenate the special of parameters of each object into a new string which must be stored as new property of it.
In this case, the result should look like:
myObject = {
"myObject" : [
{
"id" : 1,
"parameters" : [
{
"name" : "name1",
"special" : "xxx"
},
{
"name" : "name2",
"special" : "yyy"
}
],
"newProp" : "xxxyyy"
},
{
"id" : 2,
"parameters" : [
{
"name" : "name3",
"special" : "zzz"
}
],
"newProp" : "zzz"
},
{
"id" : 2,
"parameters" : [
{
"name" : "name4",
"special" : "ttt"
},
{
"name" : "name5",
"special" : "aaa"
},
{
"name" : "name6",
"special" : "zzz"
}
],
"newProp" : "tttaaazzz"
},
...
]
};
I tried something like this:
forEach(arr in myObject.myObject){
arr.parameters(forEach (i in arr.parameters.special) {
myObject.myObject = i.concat(myObject.myObject);
})
}
obviously, it does not work. But I guess that this could be the right approach.
Any suggestions?
You can loop through the object using Array#forEach and then construct the string based on parameter values using Array#map and Array#join, like this:
const myObject = {"myObject":[{"id":1,"parameters":[{"name":"name1","special":"xxx"},{"name":"name2","special":"yyy"}]},{"id":2,"parameters":[{"name":"name3","special":"zzz"}]},{"id":2,"parameters":[{"name":"name4","special":"ttt"},{"name":"name5","special":"aaa"},{"name":"name6","special":"zzz"}]}]};
myObject.myObject.forEach(item => {
item.newProp = item.parameters.map(p => p.special).join('');
});
console.log(myObject);
Use reduce and for Each
var myObject = {
"myObject" : [
{
"id" : 1,
"parameters" : [
{
"name" : "name1",
"special" : "xxx"
},
{
"name" : "name2",
"special" : "yyy"
}
]
},
{
"id" : 2,
"parameters" : [
{
"name" : "name3",
"special" : "zzz"
}
]
},
{
"id" : 2,
"parameters" : [
{
"name" : "name4",
"special" : "ttt"
},
{
"name" : "name5",
"special" : "aaa"
},
{
"name" : "name6",
"special" : "zzz"
}
]
}
]
};
myObject.myObject.forEach(arr => {
arr.prop = arr.parameters.reduce((res,obj)=> res+obj.special, '')
})
console.log(myObject)
You can use .map() and .reduce() like this:
let myObject = [{"id" : 1, "parameters" : [{ "name" : "name1", "special" : "xxx"}, { "name" : "name2", "special" : "yyy" }]}, { "id" : 2, "parameters" : [{ "name" : "name3", "special" : "zzz"}]}, {"id" : 2, "parameters" : [{ "name" : "name4", "special" : "ttt"}, { "name" : "name5", "special" : "aaa"},{ "name" : "name6", "special" : "zzz"}]}];
let result = myObject.map(
o => (o.newProp = o['parameters'].reduce((a, o) => a + o['special'], ""), o)
);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
One more way is to use nested map functions:
myObject = {"myObject":[{"id":1,"parameters":[{"name":"name1","special":"xxx"},{"name":"name2","special":"yyy"}]},{"id":2,"parameters":[{"name":"name3","special":"zzz"}]},{"id":2,"parameters":[{"name":"name4","special":"ttt"},{"name":"name5","special":"aaa"},{"name":"name6","special":"zzz"}]}]};
myObject.myObject.map(x => {
x.newProp = x.parameters.map(p => p.special).join('');
return x;
})
console.log(myObject);

Categories