Find in 2 levels of nested arrays in MongoDB - mongodb

After googling a lot, I can't locate something that can handle this. And it's supposed to be simple, I guess. I've this simple json...
{
"_id" : ObjectId("555bd34329de3cf232434ef2"),
"clients" : [
{
"Client1" : {
"positions" : [
{
"systemId" : "xxx1",
"type" : "alfa"
},
{
"systemId" : "xxx2",
"type" : "bravo"
},
{
"systemId" : "xxx3",
"type" : "charlie"
}
]
},
"Client2" : {
"positions" : [
{
"systemId" : "xxx4",
"type" : "alfa"
},
{
"systemId" : "xxx5",
"type" : "bravo"
},
{
"systemId" : "xxx6",
"type" : "charlie"
}
]
}
}
]
}
And I'm trying to perform a query based on the {systemId} into positions array that's inside an array that's inside another array. I can easily use find() in a single level array. But at this time I need an additional depth and I really facing difficulty. Can someone give me a help ?
tyvm !

If you want to find out Client1.positions with systemId and Client2.positions with systemId use following aggregation :
db.collectionName.aggregate([
{
"$unwind": "$clients"
},
{
"$unwind": "$clients.Client1.positions"
},
{
"$unwind": "$clients.Client2.positions"
},
{
"$match": {
"clients.Client1.positions.systemId": "xxx1",
"clients.Client2.positions.systemId": "xxx4"
}
}
]).pretty()
If you want to find out only Client1 then remove "$unwind": "$clients.Client2.positions" and in match "clients.Client2.positions.systemId": "xxx4"

As per your sample data, clients contains different objects like Client1 Client2 etc which further contains positions array of objects. In this case to find systemId, You need to use $elemMatch as following:
db.collection.find({
"clients": {
$elemMatch: {
"Client2.positions": {
$elemMatch: {
"systemId": "xxx4"
}
}
}
}
})

Related

How to convert multiple documents from a single collection to a single document containing one array

I have an aggregation pipeline that nearly does what I want. I've used match / unwind / project / sort to get 99% of the way. It is returning multiple documents:
[
{
"_id" : 254.8
},
{
"_id" : 93.7
},
{
"_id" : 89.9
},
{
"_id" : 94.15
},
{
"_id" : 102.1
},
{
"_id" : 93.9
},
{
"_id" : 102.7
}
]
Note: I've added the array brackets and commas to make it more readable, but you can also read it as:
{
"_id" : 254.8
}
{
"_id" : 93.7
}
{
"_id" : 89.9
}
{
"_id" : 94.15
}
{
"_id" : 102.1
}
I need the contents of the ID fields from all 7 documents in an array of values in one document:
{values: [254.8, 93.7, 89.9, 94.15, 102.1, 93.9, 102.7]}
It would be easy to sort this with JS once I have the results but I'd rather do it in the pipeline if possible so my JS stays 100% generic and only returns pure pipeline data.
Here is what you need to complete the job:
db.collection.aggregate([
{
"$group": {
"_id": null,
"values": {
$push: "$_id"
}
}
},
{
"$project": {
_id: false
}
}
])
The result will be:
[
{
"values": [
254.8,
93.7,
89.9,
94.15,
102.1,
93.9,
102.7
]
}
]
https://mongoplayground.net/p/pTmR_rni0J1

MongoDB: projection $ when find document into nested arrays

I have the following document of collection "user" than contains two nested arrays:
{
"person" : {
"personId" : 78,
"firstName" : "Mario",
"surname1" : "LOPEZ",
"surname2" : "SEGOVIA"
},
"accounts" : [
{
"accountId" : 42,
"accountRegisterDate" : "2018-01-04",
"banks" : [
{
"bankId" : 1,
"name" : "Bank LTD",
},
{
"bankId" : 2,
"name" : "Bank 2 Corp",
}
]
},
{
"accountId" : 43,
"accountRegisterDate" : "2018-01-04",
"banks" : [
{
"bankId" : 3,
"name" : "Another Bank",
},
{
"bankId" : 4,
"name" : "BCT bank",
}
]
}
]
}
I'm trying to get a query that will find this document and get only this subdocument at output:
{
"bankId" : 3,
"name" : "Another Bank",
}
I'm getting really stucked. If I run this query:
{ "accounts.banks.bankId": "3" }
Gets the whole document. And I've trying combinations of projection with no success:
{"accounts.banks.0.$": 1} //returns two elements of array "banks"
{"accounts.banks.0": 1} //empty bank array
Maybe that's not the way to query for this and I'm going in bad direction.
Can you please help me?
You can try following solution:
db.user.aggregate([
{ $unwind: "$accounts" },
{ $match: { "accounts.banks.bankId": 3 } },
{
$project: {
items: {
$filter: {
input: "$accounts.banks",
as: "bank",
cond: { $eq: [ "$$bank.bankId", 3 ] }
}
}
}
},
{
$replaceRoot : {
newRoot: { $arrayElemAt: [ "$items", 0 ] }
}
}
])
To be able to filter accounts by bankId you need to $unwind them. Then you can match accounts to the one having bankId equal to 3. Since banks is another nested array, you can filter it using $filter operator. This will give you one element nested in items array. To get rid of the nesting you can use $replaceRoot with $arrayElemAt.

Compare two fields in a MongoDB aggregation's match on a looked up value

I want to compare two dynamic fields like I'd do with:
$where: [ "$foo_update > $bar_update" ]
I need this to get a bunch of objects that must be updated. It depends on several conditions if they must get updated, so that's why I want to make it with an aggregation.
The current query for the related part looks like:
[
{ $sort: { "updated_at": 1 }
{ $group: {
"_id" : "$bar",
"foo" : { "$first" : "$foo" },
"bar" : { "$first" : "$bar" },
"last_update" : { "$last " : "$updated_at" }
} },
{ $lookup: {
"from" : "table_foo",
"localField" : "foo",
"foreignField" : "_id",
"as" : "foo"
} },
{ $lookup: {
"from" : "table_bar",
"localField" : "bar",
"foreignField" : "_id",
"as" : "bar"
} }
]
Here I could follow with another $group operator to get the values I need out to the top-level. But I cannot do that with the lookup values as it is mostly an array of items.
Here, one item is expected (and I make a query for that too as we need update if the other item is removed).
So now I want to compare the $last_update and the foo.update_at field. It would look something like this in my head.
$match: {
"foo": {
$elemMatch: {
"updated_at": { "$gte": "$last_update" }
}
}
}
Is this even possible?
If yes, how would you do it?
And yes, it is possible. It turned out that you can move the value of the array to the top with operators like $max.
So my solution looks like this now:
[
/** the beginning of the query above **/
{ $group: {
"_id" : "$_id",
"foo" : { "$first" : "$foo" },
"foo_updated" : { "$max" : "$foo_updated_at" },
"bar" : { "$first" : "$bar" },
"bar_updated" : { "$max" : "$bar.updated_at" },
"last_update" : { "$last " : "$updated_at" }
}, $project: {
"_id" : "$_id",
"foo" : "$foo",
"foo_updated" : { "$gt": [ "$foo_updated", "$last_create" ] },
"bar" : "$bar",
"bar_updated" : { "$gt": [ "$bar_updated", "$last_create" ] },
"last_create" : "$last_create"
}, $match: {
"$or": [
/** other conditions **/
{ "foo_updated": true },
{ "bar_updated": true }
]
} }
]

In MongoDB how to find documents where property is object but not Array

I have collection users where each contains property gyms e.g.:
{
"_id" : ObjectId("aaaaa"),
"firstName" : "first",
"lastName" : "second",
"email" : "aaa#aaaa",
"password" : "aaaaa",
"gyms" : [
{
"name" : "aaaaaa",
"address" : "aaaaaaaaaaaaa"
}
]
}
However when I run db.users.aggregate(..) I get:
exception: Value at end of $unwind field path '$gyms' must be an Array, but is a Object
It seems that some of the users documents contain gym:{} or no gym at all rather than array. I need to find these documents, how would I do that?
EDIT:
Aggregate command I run:
db.users.aggregate({ $unwind: "$gyms" }, { $match: { "gyms.address": { $exists: true } } } )
Using {$type: "object"} gives you results containing documents with arrays too. So, to check for pure objects, just add an extra check to ensure that arrays are discarded, viz., obj.0.key: {$exists: false}.
In your case,
db.users.find({"gyms": {$type: "object"}, "gyms.0.name": {$exists: false}}).map(function(u) {
// modify u.gyms to your satisfaction
db.conversations.save(c);
})
Try this may be it also help you
db.users.aggregate([{
"$match": {
"$nor": [{
"gyms": {
"$exists": false
}
}, {
"gyms": {
"$size": 0
}
}, {
"gyms": {
"$size": 1
}
}]
}
}, {
"$unwind": "$gyms"
}]).pretty()
Try something like this:
> db.test.drop()
> db.test.insert({ "x" : ["an array"] })
> db.test.insert({ "x" : { "type" : "object" } })
> db.test.find({ "x" : { "$type" : 3 } })
{ "x" : { "type" : "object" } }

How to get mongodb deeply embeded document id

I have the following mongo document, which is part of a bigger document called attributes, which also has Colour and Size
> db.attributes.find({'name': {'en-UK': 'Fabric'}}).pretty()
{
"_id" : ObjectId("543261cda14c971132fa2b91"),
"values" : [
{
"source" : [
{
"_id" : ObjectId("543261cda14c971132fa2b79"),
"name" : {
"en-UK" : "Combed Cotton"
}
},
],
"name" : [
{
"_id" : ObjectId("543261cda14c971132fa2b85"),
"name" : {
"en-UK" : "Brushed 3-ply"
}
},
{
"_id" : ObjectId("543261cda14c971132fa2b8f"),
"name" : {
"en-UK" : "Plain Weave"
}
},
{
"_id" : ObjectId("543261cda14c971132fa2b90"),
"name" : {
"en-UK" : "1x1 Rib"
}
}
]
}
],
"name" : {
"en-UK" : "Fabric"
}
}
I am trying to return the _id for a sub document and have the following:
db.attributes.aggregate([
{ '$match': {'name.en-UK': 'Fabric'} },
{ '$unwind' : '$values' },
{ '$project': { 'name' : '$values.name'} },
{ '$match': { '$and': [{"name.name.en-UK" : "1x1 Rib"} ] }}
])
What is the correct way to do this?
Also, the values of Fabric is an array with two items, source and name, but if I populate it like:
> db.attributes.find({'name': {'en-UK': 'Fabric'}}).pretty()
{
"_id" : ObjectId("543261cda14c971132fa2b91"),
"values" : {
"source" : [{ ... }]
"name": [{ ... }]
}
}
I get the following error
"errmsg" : "exception: $unwind: value at end of field path must be an array"
But if I wrap it inside a square brackets this then works, so that
> db.attributes.find({'name': {'en-UK': 'Fabric'}}).pretty()
{
"_id" : ObjectId("543261cda14c971132fa2b91"),
"values" : [{
"source" : [{ ... }],
"name": [{ ... }]
}]
}
what am I missing as values is an array of two objects, source and name each containing a list of arrays
Any advice much appreciated
What you seem to be "missing" here is that "some" of your documents do either not contain a "value" property at all or at the very least it is "not an array". This is the basic context of the error you have been given.
Fortunately there are a couple of ways to get around this. Namely, either "testing" for the presence of an array when submitting you original query. Or actually "substituting" the missing element for some kind of array when processing the pipeline.
Here are both approaches in what is effectively an redundant form since the first $match condition really sorts this out:
db.attributes.aggregate([
{ "$match": {
"name.en-UK": "Fabric",
"values.0": { "$exists": true }
}},
{ "$project": {
"name": 1,
"values": { "$ifNull": [ "$values", [] ] }
}},
{ "$unwind": "$values" },
{ "$unwind": "$values.name" },
{ "$match": { "values.name.name.en-UK" : "1x1 Rib" }}
])
So as I said. Really redundant in that the initial $match actually asks if an "initial array element" actually exists. Which kind of means that there is an array there.
The second $project phase actually uses the $ifNull operator to "fill in" a value ( or basically an empty array ) where the tested element does not exist. We tested for that anyway before, but this demonstrates the different approaches.
But the basic idea id either "avoiding" or "filling-in" where your document does not have the expected data that you want to process. Which is the cause of your error.