MongoDB - Find in nested arrays - javascript

My goal is to:
Get all the documents with status=true.
And return only objects with active=true in the life array.
Below is what my MongoDB documents look like:
{
"name": "justine",
"life" : [
{
"status" : true,
"service" : [
{
"partner" : "pat 1",
"active" : true,
},
{
"partner" : "pat 2",
"active" : false
}
}
]
},
{
"name": "okumu",
"life" : [
{
"status" : true,
"service" : [
{
"partner" : "pat 1",
"active" : true,
},
{
"partner" : "pat 2",
"active" : true
}
}
]
}
Expected output:
{
"name": "justine",
"life" : [
{
"status" : true,
"service" : [
{
"partner" : "pat 1",
"active" : true,
}
}
]
},
{
"name": "okumu",
"life" : [
{
"status" : true,
"service" : [
{
"partner" : "pat 1",
"active" : true,
},
{
"partner" : "pat 2",
"active" : true
}
}
]
}
This is what I did:
await Users.find({ life: { $elemMatch: { status: true, life: { $elemMatch: { active: false } } } }});
This is working well for the first condition, in case the second condition is not met, the entire object is not returned, however, if it's met, even the active=false objects are returned.
I'll be grateful if you can help me out, am not a MongoDB expert.

I think it is complex (possible not doable) with the .find() query.
You should use .aggregate() query.
$match - Filter document with life.status is true.
$project -
2.1 $filter - Filter the document with status is true in life array document.
2.1.1 $map - Merge the life document with the Result 2.1.1.1.
2.1.1.1 $filter - The inner $filter operator to filter the document with active is true for service document in service array.
db.collection.aggregate([
{
$match: {
"life.status": true
}
},
{
$project: {
name: 1,
life: {
"$filter": {
"input": {
"$map": {
"input": "$life",
"in": {
"$mergeObjects": [
"$$this",
{
"service": {
"$filter": {
"input": "$$this.service",
"as": "service",
"cond": {
$eq: [
"$$service.active",
true
]
}
}
}
}
]
}
}
},
as: "life",
"cond": {
$eq: [
"$$life.status",
true
]
}
}
}
}
}
])
Sample Mongo Playground

Related

Pull an item from all arrays MongoDB

idk if this is possible but need some help with mongo, I have the following document, and I want to make it so I can use $addToSet to add a value to one of the items in votes, but remove that item from all the other items in votes but have no idea how
{
_id: '872952643117518909',
questions: [
{ question: 'a', number: 1, dropDownInfo: [Object] },
{ question: 'b', number: 2, dropDownInfo: [Object] },
{ question: 'c', number: 3, dropDownInfo: [Object] }
],
votes: {
'1': [ '619284841187246090', '662697094104219678' ],
'2': [ '619284841187246090', '662697094104219678' ],
'3': [ '662697094104219678', '619284841187246090' ]
},
question: 'abc',
timestamp: 1628198528903,
finished: false,
channel: '812038854302892064'
}
The bellow pipeline adds a vote('619284841187246090') to a specific field,here randomly "2" was chosen,and removes that vote from "1" and "3" array.
Solution is general,can work with any vote fields not just "1" "2" "3".
You can use this pipeline in aggregation or update with pipeline (Mongodb>=4.2)
$addToSet doesn't work in arrays, it works when grouping and
in some other places in MongoDB 5.
I think your schema has a problem, because you are saving data in the schema, and that makes querying harder and creating indexing harder etc.
But we can still do it converting the object to array and back to object.
I think its best to keep data in arrays,and fields to be the known schema.
You can run the bellow code here
Query
db.collection.aggregate( [ {
"$addFields" : {
"votes" : {
"$arrayToObject" : {
"$map" : {
"input" : {
"$map" : {
"input" : {
"$objectToArray" : "$votes"
},
"as" : "m",
"in" : [ "$$m.k", "$$m.v" ]
}
},
"as" : "vote",
"in" : {
"$cond" : [ {
"$eq" : [ {
"$arrayElemAt" : [ "$$vote", 0 ]
}, "2" ]
}, [ {
"$arrayElemAt" : [ "$$vote", 0 ]
}, {
"$cond" : [ {
"$in" : [ "619284841187246090", {
"$arrayElemAt" : [ "$$vote", 1 ]
} ]
}, {
"$arrayElemAt" : [ "$$vote", 1 ]
}, {
"$concatArrays" : [ {
"$arrayElemAt" : [ "$$vote", 1 ]
}, [ "619284841187246090" ] ]
} ]
} ], [ {
"$arrayElemAt" : [ "$$vote", 0 ]
}, {
"$filter" : {
"input" : {
"$arrayElemAt" : [ "$$vote", 1 ]
},
"as" : "v",
"cond" : {
"$not" : [ {
"$eq" : [ "$$v", "619284841187246090" ]
} ]
}
}
} ] ]
}
}
}
}
}
} ])
Results
[
{
"_id": "872952643117518909",
"channel": "812038854302892064",
"finished": false,
"question": "abc",
"questions": [
{
"dropDownInfo": "",
"number": 1,
"question": "a"
},
{
"dropDownInfo": "",
"number": 2,
"question": "b"
},
{
"dropDownInfo": "",
"number": 3,
"question": "c"
}
],
"timestamp": 1.628198528903e+12,
"votes": {
"1": [
"662697094104219678"
],
"2": [
"619284841187246090",
"662697094104219678"
],
"3": [
"662697094104219678"
]
}
}
]

mongodb - aggregating and unwinding foreign ref documents

So for my example database set up:
db.lists.insertMany([
{ _id: "1", name: "list1", included_lists: ["2"], items: ["i1"] },
{ _id: "2", name: "list2", included_lists: [], items: ["i2", "i3"] }
])
db.items.insertMany([
{ _id: "i1", name: "item1", details: [{}, {}, {}] },
{ _id: "i2", name: "item2", details: [{}, {}, {}] },
{ _id: "i3", name: "item3", details: [{}, {}, {}] }
])
I'm currently getting my items data via:
db.lists.aggregate([
{ "$match": { "_id": { "$in": ["1", "2"] } } },
{
"$lookup": {
"from": "items",
"localField": "items",
"foreignField": "_id",
"as": "item"
}
},
{ "$unwind": "$item" },
{
"$facet": {
"results": [
{ "$skip": 0 },
{ "$limit": 10 },
{
"$project": {
name: 1,
item: 1
}
}
],
"total": [
{ "$count": "total" },
]
}
}
]).pretty()
which returns:
{
"results" : [
{
"_id" : "1",
"name" : "list1",
"item" : {
"_id" : "i1",
"name" : "item1",
"details" : [
{
},
{
},
{
}
]
}
},
{
"_id" : "2",
"name" : "list2",
"item" : {
"_id" : "i2",
"name" : "item2",
"details" : [
{
},
{
},
{
}
]
}
},
{
"_id" : "2",
"name" : "list2",
"item" : {
"_id" : "i3",
"name" : "item3",
"details" : [
{
},
{
},
{
}
]
}
}
],
"total" : [
{
"total" : 3
}
]
}
What I'm trying to do, is remove the { "$match": { "_id": { "$in": ["1", "2"] } } }, as I want to remove the query needed to get the array of ids, and instead just get all the ids from list _id and its included_lists ids. Then have return all the items return like my result.
This question is similar to: mongodb - unwinding nested subdocuments but I've reasked due to ambiguity and lack of db documents.
you can do it with graph lookup and then group
db.lists.aggregate([
{ "$match": { "_id": { "$in": ["1"] } } },
{
$graphLookup: {
from: "lists",
startWith: "$_id" ,
connectFromField: "included_lists",
connectToField: "_id",
as: "connected",
}
},
{$unwind:"$connected"},
{ $group:{_id:"$connected._id",items:{$first:'$connected.items'},name:{$first:'$connected.name'}}},
{
"$lookup": {
"from": "items",
"localField": "items",
"foreignField": "_id",
"as": "item"
}
},
{ "$unwind": "$item" },
{
"$facet": {
"results": [
{ "$skip": 0 },
{ "$limit": 10 },
{
"$project": {
name: 1,
item: 1
}
}
],
"total": [
{ "$count": "total" },
]
}
}
]).pretty()

Join two collection in mongoDB and extract out data in node js

I am using MongoDB 3.6 for my project.
I have 2 collections "users" and "follow". I want to extract out details of user's followers and following (like an Instagram app).
users collection
{
"id" : "1",
"name" : "abc",
"age" : "26"
},
{
"id" : "2",
"name" : "xyz",
"age" : "22"
},
{
"id" : "3",
"name" : "qwe",
"age" : "23"
}
follow collection
{
"id" : "2",
"follow id" : "1"
},
{
"id" : "3",
"follow id" : "1"
},
{
"id" : "1",
"follow id" : "2"
},
{
"id" : "2",
"follow id" : "3"
},
{
"id" : "1",
"follow id" : "3"
}
Now i want following list of id 2 So id 2 is following id 1 and id 3
So, Output should be like this
{
"id" : "1",
"name" : "abc",
"age" : "26"
},
{
"id" : "3",
"name" : "qwe",
"age" : "23"
}
For that, I am using $lookup aggregation. But this is not giving the desired output which I want.
Here is my code -
Follow.aggregate([
{
$lookup:{
from:"users",
localField:"id",
foreignField:"id",
as:"fromItems"
}
},
{
$replaceRoot:{newRoot: {$mergeObjects: [ { $arrayElemAt: ["$fromItems", 0 ] }, "$$ROOT" ] } }
},
{ $project :
{
fromItems : 0
}
}
], callback)
For more understanding please refer the image
To get following list of id 2 you can use following query:
Follow.aggregate([
{
$match: { "id": "2" }
},
{
$lookup:{
from:"users",
localField:"follow id",
foreignField:"id",
as:"fromItems"
}
},
{
$replaceRoot:{newRoot: {$mergeObjects: [ { $arrayElemAt: ["$fromItems", 0 ] }, "$$ROOT" ] } }
},
{ $project :
{
id : "$follow id",
name: 1,
age: 1
}
}
])
So the point here is that you have a relation between id and follow id and after $lookup phase follow id becomes the new id since it's parent-child relation.
EDIT:
3.4 solution below
Follow.aggregate([
{
$match: { "id": "2" }
},
{
$lookup:{
from:"users",
localField:"follow id",
foreignField:"id",
as:"fromItems"
}
},
{
$project: {
id: "$follow id",
from: { $arrayElemAt: ["$fromItems", 0 ] }
}
},
{ $project :
{
id : 1,
name: "$from.name",
age: "$from.age"
}
}
])

How can I put null values to separate field and others to different to field in MongoDB aggregation?

I have the following document in my collection.
{
"_id" : ObjectId("55961a28bffebcb8058b4570"),
"title" : "BackOffice 2",
"cts" : NumberLong(1435900456),
"todo_items" : [
{
"id" : "55961a42bffebcb7058b4570",
"task_desc" : "test 1",
"completed_by" : "557fccb5bffebcf7048b457c",
"completed_date" : NumberLong(1436161096)
},
{
"id" : "559639afbffebcc7098b45a6",
"task_desc" : "test 2",
"completed_by" : "557fccb5bffebcf7048b457c",
"completed_date" : NumberLong(1435911809)
},
{
"id" : "559a22f5bffebcb0048b476c",
"task_desc" : "test 3",
}
],
"uts" : NumberLong(1436164853)
}
I need an aggregation query to perform following, if there is field "completed_by" and "completed_date" and if there is a value which is not null push in to the "completed" array field, otherwise push them into the "incomplete" field.
Following is a sample result I want.
{
"_id" : ObjectId("55961a28bffebcb8058b4570"),
"completed" : [
{
"id":"557fccb5bffebcf7048b457c",
"title":"test 1",
"completed_by" : "557fccb5bffebcf7048b457c",
"completed_date" : NumberLong(1436161096)
},
{
"id":"557fccb5bffebcf7048b457c",
"title":"test 1",
"completed_by" : "557fccb5bffebcf7048b457c",
"completed_date" : NumberLong(1436161096)
}
],
"incomplete":[
{
"id" : "559a22f5bffebcb0048b476c",
"title" : "test 3"
}
]
}
As long as your "array" items have "distinct" identifiers ( which they have ) there are a couple of approaches to this;
Firstly, without actually "aggregating accross documents":
db.collection.aggregate([
{ "$project": {
"title": 1,
"cts": 1,
"completed": { "$setDifference": [
{ "$map": {
"input": "$todo_items",
"as": "i",
"in": {
"$cond": [
"$$i.completed_date",
"$$i",
false
]
}
}},
[false]
]},
"incomplete": { "$setDifference": [
{ "$map": {
"input": "$todo_items",
"as": "i",
"in": {
"$cond": [
"$$i.completed_date",
false,
"$$i"
]
}
}},
[false]
]}
}}
])
That requires that you at least have MongoDB 2.6 available on the server in order to use the required $map and $setDifference operators. It's pretty fast considering that all the work is done in a single $project stage.
The alternative, which you should only use when "aggregating across documents", is available to all versions supporting the aggregation framework post MongoDB 2.2:
db.collection.aggregate([
{ "$unwind": "$todo_items" },
{ "$group": {
"_id": "$_id",
"title": { "$first": "$title" },
"cts": { "$first": "$cts" },
"completed": {
"$addToSet": {
"$cond": [
"$todo_items.completed_date",
"$todo_items",
null
]
}
},
"incomplete": {
"$addToSet": {
"$cond": [
"$todo_items.completed_date",
null,
"$todo_items",
]
}
}
}},
{ "$unwind": "$completed" },
{ "$match": { "completed": { "$ne": null } } },
{ "$group": {
"_id": "$_id",
"title": { "$first": "$title" },
"cts": { "$first": "$cts" },
"completed": { "$push": "$completed" },
"incomplete": { "$first": "$incomplete" }
}}
{ "$unwind": "$incomplete" },
{ "$match": { "incomplete": { "$ne": null } } },
{ "$group": {
"_id": "$_id",
"title": { "$first": "$title" },
"cts": { "$first": "$cts" },
"completed": { "$first": "$completed" },
"incomplete": { "$push": "$incomplete" }
}}
])
Which isn't entirely all there since you need to cater for conditions where an array may end up empty. But that is not the real lesson here since MongoDB 2.6 is already a couple of years in circulation.
In aggregation, you cannot really exclude the "null/false" results, but you can "filter" them.
Also, unless you are actually "aggregating accross documents" as mentioned already, then the second form with $unwind to process the arrays comes with a "lot" of overhead. So you really should be altering the array contents in your client code as each document is read.
Can you please check the below :
db.collection.aggregate([
{$unwind : "$todo_items"},
{$group: {_id : "$_id" , completed : {{$cond :
{
if : { $and : [ {"todo_items.completed_by" : {$exists: true, $ne : null }},
{"todo_items.completed_date" : {$exists : true, $ne : null}} ] } },
then : {$push : {"old_completed" : "$todo_items"}},
else: {$push : {"old_incompleted" : "$todo_items"}}
} } } },
{$project: {_id : "$_id", completed : "$completed.old_completed" ,
incompleted : "$completed.old_incompleted"}}
]);

Correct Mongoose Syntax to Query messages that user1 archived?

Is this the correct query for finding all docs that user1 received where archived = true for user1?
var query = {
"to.username": user1,
"to.section.archive": true
};
Models.Message.find( query ).sort([['to.updated','descending']]).exec(function (err, messages) {
A sample embedded 'To' array of a messages Schema looks like this:
"to" : [
{
"user" : ObjectId("53b96c735f4a3902008aa019"),
"username" : "user1",
"updated" : ISODate("2014-07-08T06:23:43.000Z"),
"_id" : ObjectId("53bb8e6f1e2e72fd04009dad"),
"section" : {
"in" : true,
"out" : false,
"archive" : true
}
}
]
The query should only return the doc above (user1 and archive is true)..not this next doc (archive is true, but not user1):
"to" : [
{
"user" : ObjectId("53b96c735f4a3902008aa019"),
"username" : "user2",
"updated" : ISODate("2014-07-08T06:24:42.000Z"),
"_id" : ObjectId("53bb8e6f1e2e72fd04009dad"),
"section" : {
"in" : true,
"out" : false,
"archive" : true
}
}
]
You want the $elemMatch operator to select the element that has both conditions and the positional $ operator for projection:
Models.Message.find(
{
"to": {
"$elemMatch": {
"username": "user2",
"section.archive": true
}
}
},
{ "created": 1, "message": 1, "to.$": 1 }
).sort([['to.updated','descending']]).exec(function (err, messages) {
});
Please note that this only works in matching the "first" element for projection. Also you want to "sort" on the value of the matching array element, and you cannot do that with .find() and the .sort() modifier.
If you want more than one match in the array then you need to use the aggregate method. This does more complex "filtering" and "projection" than is possible otherwise:
Models.Message.aggregate([
// Match documents
{ "$match": {
"to": {
"$elemMatch": {
"username": "user2",
"section.archive": true
}
}
}},
// Unwind to de-normalize
{ "$unwind": "$to" },
// Match the array elements
{ "$match": {
"to.username": "user2",
"to.section.archive": true
}},
// Group back to the original document
{ "$group": {
"_id": "$_id",
"created": { "$first": "$created" },
"message": { "$first": "$message" },
"to": { "$push": "$to" }
}}
// Sort the results "correctly"
{ "$sort": { "to.updated": -1 } }
],function(err,messages) {
});
Or you can avoid using $unwind and $group by applying some logic with the $map operator in MongoDB 2.6 or greater. Just watching that your array contents are "truly" unique as $setDifference is applied to the resulting "filtered" array:
Models.Message.aggregate([
{ "$match": {
"to": {
"$elemMatch": {
"username": "user2",
"section.archive": true
}
}
}},
{ "$project": {
"created": 1,
"message": 1,
"_id": 1,
"to": {
"$setDifference": [
{
"$map": {
"input": "$to",
"as": "el",
"in": {
"$cond": [
{
"$and": [
{ "$eq": [ "$$el.username", "user2" ] },
"$$el.section.archive"
]
},
"$$el",
false
]
}
}
},
[false]
]
}
}},
{ "$sort": { "to.updated": -1 } }
],function(err,messages) {
});
Or even using $redact:
Models.Messages.aggregate([
{ "$match": {
"to": {
"$elemMatch": {
"username": "user2",
"section.archive": true
}
}
}},
{ "$redact": {
"$cond": {
"if": {
"$and": [
{ "$eq": [
{ "$ifNull": [ "$username", "user2" ] },
"user2"
] },
{ "$ifNull": [ "$section.archive", true ] }
]
},
"then": "$$DESCEND",
"else": "$$PRUNE"
}
}},
{ "$sort": { "to.updated": -1 } }
],function(err,messages) {
});
But be careful as $redact operates over all levels of the document, so your result might be unexpected.
Likely your "to" array actually only has single entries that will match though, so generally the standard projection should be fine. But here is how you do "multiple" matches in an array element with MongoDB.

Categories

Resources