Select sub-documents where field's value is in an some array - mongodb

I want to filter according the sub documents, but actually I am repeating the document for each sub document. I want one document and a list of sub documents if that is the case.
My data looks like:
{
"_id" : ObjectId("582eeb5f75f58055246bd22d"),
"filename" : "file1",
"cod" : NumberLong(90),
"subdocs" : [
{
"length" : NumberLong(10),
"desc" : "000"
},
{
"length" : NumberLong(15),
"desc" : "011"
},
{
"length" : NumberLong(30),
"desc" : "038"
}
]
}
{
"_id" : ObjectId("582eeb5f75f58055246bd22e"),
"filename" : "file2",
"cod" : NumberLong(95),
"subdocs" : [
{
"length" : NumberLong(11),
"desc" : "000"
},
{
"length" : NumberLong(21),
"desc" : "018"
},
{
"length" : NumberLong(41),
"desc" : "008"
}
]
}
I am using this query to filter for the desc (000, 011) on the subdocs
db.ftmp.aggregate(
{ $match:
{ "subdocs.desc":
{ $in: ["000", "011"] }
}
},
{ $unwind : "$subdocs" },
{ $match :
{ "subdocs.desc" :
{ $in:["000", "011"] }
}
}
)
But the result shows 3 documents, 1 document for each sub-document that matches with that query.
{
"_id" : ObjectId("582eeb5f75f58055246bd22d"),
"filename" : "file1",
"cod" : NumberLong(90),
"subdocs" : {
"length" : NumberLong(10),
"desc" : "000"
}
}
{
"_id" : ObjectId("582eeb5f75f58055246bd22d"),
"filename" : "file1",
"cod" : NumberLong(90),
"subdocs" : {
"length" : NumberLong(15),
"desc" : "011"
}
}
{
"_id" : ObjectId("582eeb5f75f58055246bd22e"),
"filename" : "file2",
"cod" : NumberLong(95),
"subdocs" : {
"length" : NumberLong(11),
"desc" : "000"
}
}
However I want to get: file1 with the subdocuments with desc 000 and 011, and file2 with the subdocumnt 000
{
"_id" : ObjectId("582eeb5f75f58055246bd22d"),
"filename" : "file1",
"cod" : NumberLong(90),
"subdocs" : [
{
"length" : NumberLong(10),
"desc" : "000"
},
{
"length" : NumberLong(15),
"desc" : "011"
}
]
}
{
"_id" : ObjectId("582eeb5f75f58055246bd22e"),
"filename" : "file2",
"cod" : NumberLong(95),
"subdocs" : {
"length" : NumberLong(11),
"desc" : "000"
}
}
What is the correct way to do that? Any idea?

First of all using the $unwind operator as mentioned in this answer will cause a drop of performance in your application because unwinding your array result in more documents to process down in the pipeline. There is a better way to achieve this since MongoDB 2.6.
That being said, this is a perfect job for the $filter operator new in MongoDB 3.2.
The most efficient way to do this is in MongoDB 3.4. MongoDB 3.4 introduced the $in array operator for the aggregation framework which can be used in the $filter conditional expression which, when evaluates to true include the sub-document in the resulting array.
let values = [ '000', '011' ];
db.collection.aggregate([
{ "$project": {
"filename": 1,
"cod": 1,
"subdocs": {
"$filter": {
"input": "$subdocs",
"as": "s",
"cond": { "$in": [ "$$s.desc", values ] }
}
}
}}
])
In MongoDB 3.2 we need a slightly different approach because we can use the $in operator there. But luckily we have the $setIsSubset operator and as you might have guess performs a set operation on two array and return true if the first array is a subset of the second array. Because $setIsSubset first expression must be an array, need to make the desc field an array in our pipeline. To do this, we simply use the [] bracket the create that array field which is new MongoDB 3.2
db.collection.aggregate([
{ "$project": {
"filename": 1,
"cod": 1,
"subdocs": {
"$filter": {
"input": "$subdocs",
"as": "s",
"cond": { "$setIsSubset": [ [ "$$s.desc" ], values ] }
}
}
}}
])
MongoDB 3.0 is dead to me but if for some reasons you are running that version, you can use the $literal operator to return the one element array you need for the set operation and the $setDifference operator. This is left as exercise to the reader.

You just need to add $group & $push. First you $unwind the subdocs to apply the $match followed by $group on id and $push the grouped subdocs.
db.ftmp.aggregate({
$unwind: "$subdocs"
}, {
$match: {
"subdocs.desc": {
$in: ["000", "011"]
}
}
}, {
$group: {
_id: "$_id",
subdocs: {
$push: "$subdocs"
},
filename: {
$first: "$filename"
},
cod: {
$first: "$cod"
}
}
})

Related

MongoDB - find document whose array length is less than or equal to 5

Can't we pass an object to $size operator in mongoose? Is there any ways to query on array for length so we can fetch document which contains an array of a particular length.
Hers is Sample Document
"_id" : ObjectId("5e8c9becd1257f66c4b8cd63"),
"index" : 0,
"name" : "Aurelia Gonzales",
"isActive" : false,
"registered" : ISODate("2015-02-11T09:52:39.000+05:30"),
"age" : 20,
"gender" : "female",
"eyeColor" : "green",
"favoriteFruit" : "banana",
"company" : {
"title" : "YURTURE",
"email" : "aureliagonzales#yurture.com",
"phone" : "+1 (940) 501-3963",
"location" : {
"country" : "USA",
"address" : "694 Hewes Street"
}
},
"tags" : [
"enim",
"id",
"velit",
"ad",
"consequat"
]
}
Here is query
db.admin.aggregate([
{
$match : {tags : {$size : {$lte : 5}}}
}
])
Here is Output
{
"message" : "$size needs a number",
"ok" : 0,
"code" : 2,
"codeName" : "BadValue",
"name" : "MongoError"
}
You can't use $size like that & needed to use aggregation $size operator to do this.
Query :
db.collection.find({
$expr: { /** Allows the use of aggregation expressions within the query language */
$lte: [
{
$size: "$tags"
},
5
]
}
})
Test : MongoDB-Playground
Although if the size of the array is important enough, it could be stored in the documents and indexed to fetch much faster results.
Following a similar logic a solution could be, two stage aggregation using $addFields and $size, $lte.
db.collection.aggregate([
{
$addFields: {
sizeOfTags: {
$size: "$tags"
}
}
},
{
$match: {
sizeOfTags: {
$lte: 5
}
}
}
])

Mongodb use slice and elementmatch

I am trying to retrieve elements in an array in mongo db. I would like to retrieve the 15 first elements which do not match a pattern
So let's imagine I have
{
"_id" : ObjectId("s4dcsd5s4d6c54s6d"),
"items" : [
{
type : "TYPE_1",
text : "blablabla"
},
{
type : "TYPE_2",
text : "blablabla"
},
{
type : "TYPE_3",
text : "blablabla"
},
{
type : "TYPE_1",
text : "blablabla"
},
{
type : "TYPE_2",
text : "blablabla"
},
{
type : "TYPE_1",
text : "blablabla"
}
]
}
So currently I have more element to match compared to the element to not match that's why I use nin. but it is to simplifiy
If I use
db.history.find({ "_id" : ObjectId("s4dcsd5s4d6c54s6d")}, { "items" : { "$elemMatch" : { "type" : { "$nin" : [ "TYPE_2" , "TYPE_3"]}}}, "items" : { $slice : [0, 2]}}).pretty()
It seems that the element match is not taken into account (inverse as well if i put element match after slice)
Then if I do:
db.history.find({ "_id" : ObjectId("s4dcsd5s4d6c54s6d")}, { "items" : { "$elemMatch" : { "type" : { "$nin" : [ "TYPE_2" , "TYPE_3"]}}, $slice : [0, 2]}}).pretty()
An error is thrown by mongo
Do you know how I can do?
Thanks a lot
You can't use $elemMatch for your case since it will only return the first element. From documentation :
$elemMatch The $elemMatch operator limits the contents of an
field from the query results to contain only the first element
matching the $elemMatch condition.
You can do an aggregation query which will do the following:
match your _id
unwind your items array to have one record per items in the array
match the types $nin your array [ "TYPE_2" , "TYPE_3"]
limit the number of result
The query is :
db.history.aggregate([{
$match: {
_id: ObjectId("s4dcsd5s4d6c54s6d")
}
}, {
$unwind: '$items'
}, {
$match: {
'items.type': { '$nin': ["TYPE_2", "TYPE_3"] }
}
},
{ $limit: 2 }
])
It gives :
{ "_id" : "s4dcsd5s4d6c54s6d", "items" : { "type" : "TYPE_1", "text" : "blablabla" } }
{ "_id" : "s4dcsd5s4d6c54s6d", "items" : { "type" : "TYPE_1", "text" : "blablabla" } }
You will need to use aggregation for restricting the array in the form you have. Use $filter to apply the condition and $slice to limit the array elements.
db.history.aggregate([{
$match: {
_id: ObjectId("586309d6772c68234445f2a5")
}
}, {
"$project": {
"items": {
"$slice": [{
"$filter": {
"input": "$items",
"as": "item",
"cond": {
"$and": [{
$ne: ["$$item.type", "TYPE_2"]
}, {
$ne: ["$$item.type", "TYPE_3"]
}]
}
}
},
2
]
}
}
}])
Sample Output:
{ "_id" : ObjectId("586309d6772c68234445f2a5"), "items" : [ { "type" : "TYPE_1", "text" : "blablabla" }, { "type" : "TYPE_1", "text" : "blablabla" } ] }

How to implement $in operation in $filter aggregation in MongoDB [duplicate]

I want to filter according the sub documents, but actually I am repeating the document for each sub document. I want one document and a list of sub documents if that is the case.
My data looks like:
{
"_id" : ObjectId("582eeb5f75f58055246bd22d"),
"filename" : "file1",
"cod" : NumberLong(90),
"subdocs" : [
{
"length" : NumberLong(10),
"desc" : "000"
},
{
"length" : NumberLong(15),
"desc" : "011"
},
{
"length" : NumberLong(30),
"desc" : "038"
}
]
}
{
"_id" : ObjectId("582eeb5f75f58055246bd22e"),
"filename" : "file2",
"cod" : NumberLong(95),
"subdocs" : [
{
"length" : NumberLong(11),
"desc" : "000"
},
{
"length" : NumberLong(21),
"desc" : "018"
},
{
"length" : NumberLong(41),
"desc" : "008"
}
]
}
I am using this query to filter for the desc (000, 011) on the subdocs
db.ftmp.aggregate(
{ $match:
{ "subdocs.desc":
{ $in: ["000", "011"] }
}
},
{ $unwind : "$subdocs" },
{ $match :
{ "subdocs.desc" :
{ $in:["000", "011"] }
}
}
)
But the result shows 3 documents, 1 document for each sub-document that matches with that query.
{
"_id" : ObjectId("582eeb5f75f58055246bd22d"),
"filename" : "file1",
"cod" : NumberLong(90),
"subdocs" : {
"length" : NumberLong(10),
"desc" : "000"
}
}
{
"_id" : ObjectId("582eeb5f75f58055246bd22d"),
"filename" : "file1",
"cod" : NumberLong(90),
"subdocs" : {
"length" : NumberLong(15),
"desc" : "011"
}
}
{
"_id" : ObjectId("582eeb5f75f58055246bd22e"),
"filename" : "file2",
"cod" : NumberLong(95),
"subdocs" : {
"length" : NumberLong(11),
"desc" : "000"
}
}
However I want to get: file1 with the subdocuments with desc 000 and 011, and file2 with the subdocumnt 000
{
"_id" : ObjectId("582eeb5f75f58055246bd22d"),
"filename" : "file1",
"cod" : NumberLong(90),
"subdocs" : [
{
"length" : NumberLong(10),
"desc" : "000"
},
{
"length" : NumberLong(15),
"desc" : "011"
}
]
}
{
"_id" : ObjectId("582eeb5f75f58055246bd22e"),
"filename" : "file2",
"cod" : NumberLong(95),
"subdocs" : {
"length" : NumberLong(11),
"desc" : "000"
}
}
What is the correct way to do that? Any idea?
First of all using the $unwind operator as mentioned in this answer will cause a drop of performance in your application because unwinding your array result in more documents to process down in the pipeline. There is a better way to achieve this since MongoDB 2.6.
That being said, this is a perfect job for the $filter operator new in MongoDB 3.2.
The most efficient way to do this is in MongoDB 3.4. MongoDB 3.4 introduced the $in array operator for the aggregation framework which can be used in the $filter conditional expression which, when evaluates to true include the sub-document in the resulting array.
let values = [ '000', '011' ];
db.collection.aggregate([
{ "$project": {
"filename": 1,
"cod": 1,
"subdocs": {
"$filter": {
"input": "$subdocs",
"as": "s",
"cond": { "$in": [ "$$s.desc", values ] }
}
}
}}
])
In MongoDB 3.2 we need a slightly different approach because we can use the $in operator there. But luckily we have the $setIsSubset operator and as you might have guess performs a set operation on two array and return true if the first array is a subset of the second array. Because $setIsSubset first expression must be an array, need to make the desc field an array in our pipeline. To do this, we simply use the [] bracket the create that array field which is new MongoDB 3.2
db.collection.aggregate([
{ "$project": {
"filename": 1,
"cod": 1,
"subdocs": {
"$filter": {
"input": "$subdocs",
"as": "s",
"cond": { "$setIsSubset": [ [ "$$s.desc" ], values ] }
}
}
}}
])
MongoDB 3.0 is dead to me but if for some reasons you are running that version, you can use the $literal operator to return the one element array you need for the set operation and the $setDifference operator. This is left as exercise to the reader.
You just need to add $group & $push. First you $unwind the subdocs to apply the $match followed by $group on id and $push the grouped subdocs.
db.ftmp.aggregate({
$unwind: "$subdocs"
}, {
$match: {
"subdocs.desc": {
$in: ["000", "011"]
}
}
}, {
$group: {
_id: "$_id",
subdocs: {
$push: "$subdocs"
},
filename: {
$first: "$filename"
},
cod: {
$first: "$cod"
}
}
})

Retrieve the second element of array for each document - MongoDB

I have a MongoDB collection, called bios, that contains documents similar to these:
{
"_id" : ObjectId("51df07b094c6acd67e492f41"),
"name" : {
"first" : "John",
"last" : "McCarthy"
},
"birth" : ISODate("1927-09-04T04:00:00Z"),
"death" : ISODate("2011-12-24T05:00:00Z"),
"contribs" : [
"Lisp",
"Artificial Intelligence",
"ALGOL"
]
},
{
"_id" : 3,
"name" : {
"first" : "Grace",
"last" : "Hopper"
},
"title" : "Rear Admiral",
"birth" : ISODate("1906-12-09T05:00:00Z"),
"death" : ISODate("1992-01-01T05:00:00Z"),
"contribs" : [
"UNIVAC",
"compiler",
"FLOW-MATIC",
"COBOL"
]
}
My target is to retrieve the second element of the array contribs for each document in bios collection.
Using the new aggregation pipeline operator $filter I run the following query:
> db.bios.aggregate([
{
$match: {"contribs.2":{"$exists":1}}},
{
$project:{contribs:
{
$filter:{input:"$contribs", as: "contribs", cond:{}}},_id:0}}])
With my query, the output is:
{ "contribs" : [ "Lisp", "Artificial Intelligence", "ALGOL" ] }
{ "contribs" : [ "UNIVAC", "compiler", "FLOW-MATIC", "COBOL" ] }
that is not just the second element of the array contribs but a projection on contribs array when its second element exists.
did you try $elementAt ?
db.bios.aggregate([
{ $match: {"contribs.1": { "$exists": 1 } }},
{ $project: { contribs: { $arrayElemAt: [ "$contribs", 1 ] } } }
]);

MongoDB projection into array

The below document has the dob of student and its parent's dob.
{
"_id" : ObjectId("56a31573a3b1f89cb895abd3"),
"dob" : {
"isodate" : ISODate("1996-01-21T18:30:00.000+0000")
},
"parent" : [
{
"dob" : {
"isodate" : ISODate("1956-07-21T18:30:00.000+0000")
},
"type" : "father"
},
{
"dob" : {
"isodate" : ISODate("1958-11-01T18:30:00.000+0000")
},
"type" : "mother"
}
]
}
In one of the application use case, it is better to receive output in the below format
{
"_id" : ObjectId("56a31573a3b1f89cb895abd3"),
"dob" : {
"isodate" : ISODate("1996-01-21T18:30:00.000+0000")
},
"type" : "student"
},
{
"_id" : ObjectId("56a31573a3b1f89cb895abd3"),
"dob" : {
"isodate" : ISODate("1956-07-21T18:30:00.000+0000")
},
"type" : "father"
},
{
"_id" : ObjectId("56a31573a3b1f89cb895abd3"),
"dob" : {
"isodate" : ISODate("1958-11-01T18:30:00.000+0000")
},
"type" : "mother"
}
The approach is to $project the fields into array and then $unwind that array. However, projection doesn't allow me to create array.
I believe $group and its associated aggregation cannot be used as my operations are on the same document in the pipeline.
Is this possible?
Note - i have the flexibility to change the document design as well.
For Mongo 3.0
Here I have included a [null] array which gives me the option to insert array in projection using a combination of $setDiffernce and $cond. The output of this is given to $setUnion with $parent array.
db.p1.aggregate(
{ "$project": {
"allVal": {
'$setUnion': [
{"$setDifference": [
{ "$map": {
"input": [null],
"as": "type",
"in": { "$cond": [
{"$eq": ["$$type", null]},
{dob:"$dob", type:{$literal:'student'}},
null
]}
}},
[null]
]}
,
'$parent'
]
}
}},
{$unwind : '$allVal'}
)
For mongo 3.2
Feels heaven as I have avoided $setDifference and $literal hack adjustments.
db.p1.aggregate([
{
$project:{
parent : 1,
type: {$literal : 'student'},
'dob.isodate' : 1
}
},
{
$project:{
allValues: { $setUnion: [ [{dob:"$dob", type:'$type'}], "$parent" ] }
}
},
{
$unwind : '$allValues'
}
])
In the first projection, I am adding a new field called type
In the 2nd projection, I am creating a new array with 2 different nodes of the same document.
Currently this solution works for Mongo 3.2