Mongodb use slice and elementmatch - mongodb

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" } ] }

Related

mongodb $elemMatch with regex return only one element from array of a single document

I want a string search in a nested array. For every document, it only returns the first matched array element though there are a more matched array element in a document.
Mongo structure:
{
"bookmarkId" : "5b9ce0be489cdc34ffa9d650",
"notes" : [
{
"noteId" : "5b9ce0be489cdc34ffa9d650",
"note" : "number"
},
{
"noteId" : "5b9ce4ba489cdc34ffa9d653",
"note" : "hhgjjkg"
},
{
"noteId" : "5b9ce4cc489cdc34ffa9d654",
"note" : "test"
},
{
"noteId" : "5b9ce8a2a3219b3f166cc5de",
"note" : "hhgjjkg"
},
{
"noteId" : "5b9cf703a3219b3f166cc5ea",
"note" : "number"
}
],
"userId" : "5aeae1da9072420ff68bd48e"
}
The mongodb query is :
db.bookmark.find({"userId" : "5aeae1da9072420ff68bd48e","notes.note":new RegExp('hgjj', 'i')},{_id:0,bookmarkId:1,notes:{$elemMatch:{note:new RegExp('hgjj', 'i')}}})
Though the 2nd and 4th element match the query, it only returns the first element.
Output:
{ "bookmarkId" : "5b9ce0be489cdc34ffa9d650", "notes" : [ { "noteId" : "5b9ce4ba489cdc34ffa9d653", "note" : "hhgjjkg" } ] }
How to get all the matched element from mongodb?
Based on the mongoDB documentation of the find projection you can't use $elemMatch to filter elements.
Instead you can concider to use an aggregation:
db.bookmark.aggregate([
{
$unwind: '$notes'
},
{
$match: {"userId" : "5aeae1da9072420ff68bd48e","notes.note":new RegExp('hgjj', 'i')}
},
{
$group: {
_id: '$bookmarkId',
notes: {$push : '$notes'}
}
},
{
$project: {
_id: 0,
bookmarkId: '$_id',
notes: '$notes'
}
}])
See here: https://docs.mongodb.com/manual/reference/operator/projection/elemMatch/
Definition $elemMatch:
The $elemMatch operator limits the contents of
an field from the query results to contain only the first
element matching the $elemMatch condition.
So if you use the elemMatch operator to compute a projection, then only the first element in the nested array that matches will be projected.
To me it looks like you wanted to create a new "notes" array containing only the matching nested documents. One way to do this is with grouping via the aggregation framework:
db.bookmark.aggregate([
{
"$match": {
"userId" : "5aeae1da9072420ff68bd48e",
"notes.note":new RegExp('hgjj', 'i')
}
},
{
"$unwind": {
"path": "$notes"
}
},
{
"$match": {
"notes.note": new RegExp('hgjj', 'i')
}
},
{
"$group": {
"_id": "$_id",
"bookmark_id": {
"$first": "$bookmarkId"
},
"notes": {
"$push": "$notes"
}
}
},
{
"$project": {
"_id": 0,
"bookmark_id": 1,
"notes": 1
}
}
])
yields:
{ "bookmark_id" : "5b9ce0be489cdc34ffa9d650", "notes" : [ { "noteId" : "5b9ce4ba489cdc34ffa9d653", "note" : "hhgjjkg" } ] }
{ "bookmark_id" : "5b9ce0be489cdc34ffa9d650", "notes" : [ { "noteId" : "5b9ce8a2a3219b3f166cc5de", "note" : "hhgjjkg" } ] }
{ "bookmark_id" : "5b9ce0be489cdc34ffa9d650", "notes" : [ { "noteId" : "5b9ce4ba489cdc34ffa9d653", "note" : "hhgjjkg" }, { "noteId" : "5b9ce8a2a3219b3f166cc5de", "note" : "hhgjjkg" } ] }

How can I do match after second level unwind in mongodb?

I am working on a software that uses MongoDB as a database. I have a collection like this (this is just one document)
{
"_id" : ObjectId("5aef51e0af42ea1b70d0c4dc"),
"EndpointId" : "89799bcc-e86f-4c8a-b340-8b5ed53caf83",
"DateTime" : ISODate("2018-05-06T19:05:04.574Z"),
"Url" : "test",
"Tags" : [
{
"Uid" : "E2:02:00:18:DA:40",
"Type" : 1,
"DateTime" : ISODate("2018-05-06T19:05:04.574Z"),
"Sensors" : [
{
"Type" : 1,
"Value" : NumberDecimal("-98")
},
{
"Type" : 2,
"Value" : NumberDecimal("-65")
}
]
},
{
"Uid" : "12:3B:6A:1A:B7:F9",
"Type" : 1,
"DateTime" : ISODate("2018-05-06T19:05:04.574Z"),
"Sensors" : [
{
"Type" : 1,
"Value" : NumberDecimal("-95")
},
{
"Type" : 2,
"Value" : NumberDecimal("-59")
},
{
"Type" : 3,
"Value" : NumberDecimal("12.939770381907275")
}
]
}
]
}
and I want to run this query on it.
db.myCollection.aggregate([
{ $unwind: "$Tags" },
{
$match: {
$and: [
{
"Tags.DateTime": {
$gte: ISODate("2018-05-06T19:05:02Z"),
$lte: ISODate("2018-05-06T19:05:09Z"),
},
},
{ "Tags.Uid": { $in: ["C1:3D:CA:D4:45:11"] } },
],
},
},
{ $unwind: "$Tags.Sensors" },
{ $match: { "$Tags.Sensors.Type": { $in: [1, 2] } } },
{
$project: {
_id: 0,
EndpointId: "$EndpointId",
TagId: "$Tags.Uid",
Url: "$Url",
TagType: "$Tags.Type",
Date: "$Tags.DateTime",
SensorType: "$Tags.Sensors.Type",
Value: "$Tags.Sensors.Value",
},
},
])
the problem is, the second match (that checks $Tags.Sensors.Type) doesn't work and doesn't affect the result of the query.
How can I solve that?
If this is not the right way, what is the right way to run these conditions?
The $match stage accepts field names without a leading $ sign. You've done that correctly in your first $match stage but in the second one you write $Tags.Sensors.Type. Simply removing the leading $ sign should make your query work.
Mind you, the whole thing can be a bit simplified (and some beautification doesn't hurt, either):
You don't need to use $and in your example since it's assumed by default if you specify more than one criterion in a filter.
The $in that you use for the Tags.Sensors.Type filter can be a simple : kind of equality operator unless you have more than one element in the list of acceptable values.
In the $project stage, instead of (kind of) duplicating identical field names you can use the <field>: 1 syntax unless the order of the fields matters.
So the final query would be something like this.
db.myCollection.aggregate([
{
"$unwind" : "$Tags"
},
{
"$match" : {
"Tags.DateTime" : { "$gte" : ISODate("2018-05-06T19:05:02Z"), "$lte" : ISODate("2018-05-06T19:05:09Z") },
"Tags.Uid" : { "$in" : ["C1:3D:CA:D4:45:11"] }
}
}, {
"$unwind" : "$Tags.Sensors"
}, {
"$match" : {
"Tags.Sensors.Type" : { "$in" : [1,2] }
}
},
{
"$project" : {
"_id" : 0,
"EndpointId" : 1,
"TagId" : "$Tags.Uid",
"Url" : 1,
"TagType" : "$Tags.Type",
"Date" : "$Tags.DateTime",
"SensorType" : "$Tags.Sensors.Type",
"Value" : "$Tags.Sensors.Value"
}
}])

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

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"
}
}
})

Count same types element in array mongodb

{
_id:1, members: [
{
name:"John",
status:"A"
},
{
name:"Alex",
status:"D"
},
{
name:"Jack",
status:"A"
},
{
name:"Robin",
status:"D"
}
]}
That is Channel document.
Now I need to count all elements in members array where status equal to 'A'.
For example the above doc has 2 members with status 'A'.
How can I achieve this?
You can use mongodb-count to achieve the desired result.
Returns the count of documents that would match a find() query. The db.collection.count() method does not perform the find() operation but instead counts and returns the number of results that match a query.
So your query will be
var recordcount = db.collName.count({"members.status":"A"});
Now recordCount will be number of records that matches {"members.status":"A"} query.
Here Is your Json file
{
"_id" : ObjectId("575915653b3cc43fca1fca4c"),
"members" : [
{
"name" : "John",
"status" : "A"
},
{
"name" : "Alex",
"status" : "D"
},
{
"name" : "Jack",
"status" : "A"
},
{
"name" : "Robin",
"status" : "D"
}
]
}
And you want to the count of all elements in members array where
status equal to 'A'.
you have to try this one to find out your count
db.CollectionName.aggregate([{
"$project": {
"members": {
"$filter": {
"input": "$members",
"as": "mem",
"cond": {
"$eq": ["$$mem.status", "A"]
}
}
}
}
}, {
"$project": {
"membersize": {
"$size": "$members"
}
}
}]).pretty()
And you found your answer is like that { "_id" :
ObjectId("575915653b3cc43fca1fca4c"), "membersize" : 2 }
try this one for old version......
db.CollectionName.aggregate([{"$unwind":"$members"},{"$match":{"members.status":"A"}},{"$group":{_id:"$_id","memberscount":{"$sum":1}}}]).pretty()
{ "_id" : ObjectId("575915653b3cc43fca1fca4c"), "memberscount" : 2 }
Here Is your Json file
{
"_id" : ObjectId("575915653b3cc43fca1fca4c"),
"members" : [
{
"name" : "John",
"status" : "A"
},
{
"name" : "Alex",
"status" : "D"
},
{
"name" : "Jack",
"status" : "A"
},
{
"name" : "Robin",
"status" : "D"
}
]
}
And you want to the count of all elements in members array where
status equal to 'A'.
you have to try this one to find out your count
db.CollectionName.aggregate([{
"$project": {
"members": {
"$filter": {
"input": "$members",
"as": "mem",
"cond": {
"$eq": ["$$mem.status", "A"]
}
}
}
}
}, {
"$project": {
"membersize": {
"$size": "$members"
}
}
}]).pretty()
And you found your answer is like that { "_id" :
ObjectId("575915653b3cc43fca1fca4c"), "membersize" : 2 }

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