How to project only matched array item in mongodb aggregation? - arrays

I have a collection called shows, with documents as:
{
"url": "http://www.tvmaze.com/shows/167/24",
"name": "24",
"genres": [
"Drama",
"Action"
],
"runtime": 60
},
{
"url": "http://www.tvmaze.com/shows/4/arrow",
"name": "Arrow",
"genres": [
"Drama",
"Action",
"Science-Fiction"
],
"runtime": 60
}
I wanted to search shows with genre 'Action' and project the result array as
{
"url": "http://www.tvmaze.com/shows/167/24",
"name": "24",
"genres": [
"Action" // I want only the matched item in
//my result array
],
"runtime": 60
} , //same for the second doc as well
If I use
db.shows.find({genres:'Action'}, {'genres.$': 1});
It works but the same does not work in aggregate method with $project
Shows.aggregate([
{
$match: { 'genres': 'Action'}
},
{
$project: {
_id: 0,
url: 1,
name: 1,
runtime: 1,
'genres.$': 1
}
}
]);
this is the error I get on this aggregate query
Invalid $project :: caused by :: FieldPath field names may not start with '$'."

db.collection.aggregate([
{
$match: {
"genres": {
$regex: "/^action/",
$options: "im"
}
}
},
{
$project: {
_id: 0,
url: 1,
name: 1,
runtime: 1,
genres: {
$filter: {
input: "$genres",
as: "genre",
cond: {
$regexMatch: {
input: "$$genre",
regex: "/^action/",
options: "im"
}
}
}
}
}
}
])
Here is how I solved it, Thanks #turivishal for the help

Related

(Mongodb) Getting an error (BadValue) $in needs an array , when using $map aggregate function

I am trying to convert array of strings to array of ObjectId using map function, map will return me array of ObjectIds that I am using in $match to check if _id matches with any of objectids present in array using $in operator.
Database configuration is
db={
"students": [
{
"_id": ObjectId("5c1a79f7c98da061141475b7"),
"firstName": "Ibrahim",
"kelasID": ObjectId("5c429f9906f2a805bc6cd494"),
"lastName": "Ali",
"schoolID": ObjectId("5c1a735fc98da061141475a1"),
"year": 2018,
"__v": 0,
"addedOn": ISODate("2018-12-25T04:27:47.909Z"),
"checkIn": false,
"checkInStatus": 1,
"contactNo1": "012225656",
"father": "Ali",
"fatherID": "8852245",
"idType": 0,
"lastModified": ISODate("2018-12-25T04:27:47.909Z"),
"mother": "",
"motherID": ""
},
{
"_id": ObjectId("5c3bfea37774fb0b55000cb5"),
"idType": 0,
"checkIn": false,
"checkInStatus": 1,
"year": 2019,
"schoolID": ObjectId("5c1a735fc98da061141475a1"),
"kelasID": ObjectId("5c1a7534c98da061141475a3"),
"firstName": "Umar",
"lastName": "Bin Al-Khattab",
"contactNo1": "601222",
"status": 1,
"addedOn": ISODate("2019-01-14T03:14:43.597Z"),
"lastModified": ISODate("2019-01-14T03:14:43.597Z"),
"__v": 0
},
{
"_id": ObjectId("5c1a7c69c98da061141475bb"),
"idType": 0,
"checkIn": false,
"checkInStatus": 1,
"year": 2018,
"schoolID": ObjectId("5c1a735fc98da061141475a1"),
"kelasID": ObjectId("5c1a7540c98da061141475a5"),
"firstName": "Abdul Rahman",
"lastName": "Affan",
"father": "Affan",
"fatherID": "54321",
"contactNo1": "602288",
"status": 1,
"addedOn": ISODate("2018-12-25T04:30:16.130Z"),
"lastModified": ISODate("2018-12-25T04:30:16.130Z"),
"__v": 0
}
]
}
and the query is
db.students.aggregate([
{
"$match": {
"_id": {
$in: {
"stud": {
"$map": {
"input": [
"5c1a79f7c98da061141475b7",
"5c3bfea37774fb0b55000cb5",
"5c1a7c69c98da061141475bb",
"5c3bfea37774fb0b55000cb4",
"5c1a7d32c98da061141475be",
"5c3bfea37774fb0b55000cb7"
],
"in": {
"$toObjectId": "$$this"
}
}
}
}
}
}
}
])
You can also check this out here- https://mongoplayground.net/p/8tQFHxtrttW
The above doesn't work but if I change the query like below it works.
db.students.aggregate([
{
"$match": {
"_id": {
$in: [
ObjectId("5c1a79f7c98da061141475b7"),
ObjectId("5c3bfea37774fb0b55000cb5")
]
}
}
}
])
Link to above query- https://mongoplayground.net/p/cjq77KIAcPE
Error i'm getting - query failed: (BadValue) $in needs an array
Can anybody help with the reason to the error , as according to docs map does return an array , so what am I doing wrong?
Not sure why it doesn't work it might have to do with $in operator and $in aggregation. When you use an array that is defined by a variable you can use $in operator but when you are using aggregation result ( in this case $map ) you have to use $in aggregation. But to use $in aggregation you need to work with $expr for $match.
You can try the below code which works fine
db.students.aggregate([
{
$addFields: {
studentIds: {
$map: {
input: [
"5c1a79f7c98da061141475b7",
"5c3bfea37774fb0b55000cb5",
"5c1a7c69c98da061141475bb",
"5c3bfea37774fb0b55000cb4",
"5c1a7d32c98da061141475be",
"5c3bfea37774fb0b55000cb7",
],
in: {
$toObjectId: "$$this",
},
},
},
},
},
{
$match: {
$expr: {
$in: ["$_id", "$studentIds"],
},
},
},
]);
or if you want to combine the two of the stages you can do it like this
db.students.aggregate([
{
$match: {
$expr: {
$in: [
"$_id",
{
$map: {
input: [
"5c1a79f7c98da061141475b7",
"5c3bfea37774fb0b55000cb5",
"5c1a7c69c98da061141475bb",
"5c3bfea37774fb0b55000cb4",
"5c1a7d32c98da061141475be",
"5c3bfea37774fb0b55000cb7",
],
in: {
$toObjectId: "$$this",
},
},
},
],
},
},
},
]);

Mongodb: how to "flatten" some query results

Help to "flatten" (to pull nested fields at same level as document's fields) a mongodb document in a query
//this is "anagrafiche" collection
[{
"name": "tizio"
,"surname": "semproni"
,"birthday": "01/02/1923"
,"home": {
"road": "via"
,"roadname": "bianca"
,"roadN": 12
,"city": "rome"
,"country": "italy"
}
},
{
"name": "caio"
,"surname": "giulio"
,"birthday": "02/03/1932"
,"home": {
"road": "via"
,"roadname": "rossa"
,"roadN": 21
,"city": "milan"
,"country": "italy"
}
},
{
"name": "mario"
,"surname": "rossi"
// birthday is not present for this document
,"home": {
"road": "via"
,"roadname": "della pace"
,"roadN": 120
,"city": "rome"
,"country": "italy"
}
}
]
my query:
db.anagrafiche.aggregate([ {$match {"home.city": "rome"}}
{$project:{"name": 1, "surname":1, <an expression to flatten the address>, "birthday": 1, "_id":0}}
]
);
expected result:
{
,"name": "tizio"
,"surname": "semproni"
,"address": "via bianca 12 rome"
,"birthday": 01/02/1923
},{
,"name": "mario"
,"surname": "rossi"
,"address": "via della pace 120 rome"
,"birthday": NULL
}
You can use $objectToArray to get nested document keys and values and then use $reduce along with $concat to concatenate values dynamically:
db.collection.aggregate([
{
$project: {
_id: 0,
name: 1,
surname: 1,
birthday: 1,
address: {
$reduce: {
input: { $objectToArray: "$home" },
initialValue: "",
in: {
$concat: [
"$$value",
{ $cond: [ { $eq: [ "$$value", "" ] }, "", " " ] },
{ $toString: "$$this.v" }
]
}
}
}
}
}
])
Mongo Playground

How to convert ISO Date to 'yyyy-mm-dd hh:mm:ss' in mongodb from embedded array of documents?

There's a 'Created' field in my collection, the date format is ISO date. How can I convert it to 'yyyy-mm-dd hh:mm:ss'?
Document sample:
{
"_id" : ObjectId("432babb4d3281999g902a378"),
"ID" : "290283667",
"Data" : {
"Product-2713890" : {
"Created" : ISODate("2016-08-23T20:55:39.437Z"),
"Product" : "Product-2713890"
}
}
}
Expected Result:
{
"_id" : ObjectId("432babb4d3281999g902a378"),
"ID" : "290283667",
"Product" : "Product-2713890",
"Created" : "2016-08-23 20:55:39"
}
Here is my code:
db.getCollection('BasicInfo').aggregate([
{$match:{Type:'subscriptions'}},
{$project: {hashmaps: { $objectToArray: '$$ROOT.Data'},ID:'$$ROOT'}},
{$project: {ID:'$ID.ID',
Product: '$hashmaps.v.Product',
Created: '$hashmaps.v.Created'} },
{$unwind:'$Product'},
{$unwind:'$Created'}
])
I tried to use $dateToString like below, but it gave the error message : "can't convert from BSON type array to Date"
db.getCollection('BasicInfo').aggregate([
{$match:{Type:'subscriptions'}},
{$project: {hashmaps: { $objectToArray: '$$ROOT.Data'},ID:'$$ROOT'}},
{$project: {ID:'$ID.ID',
Product: '$hashmaps.v.Product',
Created: {$dateToString:{format:'%Y-%m-%d',date:'$hashmaps.v.Created'}}} },
{$unwind:'$Product'},
{$unwind:'$Created'}
])
Following aggregation query should get the expected output.
With the help of $addFields and $arrayElemAt why? because Data value in sample document converted with $objectToArray will be at 0th index. Playground Link
Note: considering that Data field can contain more than one product information in which case before we pick the first element the hashmaps array should be filtered $filter (one way) based on product key to work this out correctly.
db.collection.aggregate([
{$match:{Type:'subscriptions'}},
{
$addFields: {
hashmaps: {
$objectToArray: "$$ROOT.Data"
}
}
},
{
$project: {
"ID": 1,
"hashmaps": {
$arrayElemAt: [
"$hashmaps",
0
]
}
}
},
{
$project: {
"ID": 1,
"Product": "$hashmaps.k",
"Created": {
$dateToString: {
format: "%Y-%m-%d %H:%M:%S",
date: "$hashmaps.v.Created"
}
}
}
}
])
++ Updated Query based on comments: Case where the data would indeed have multiple products. Using $map
db.collection.aggregate([
{
$addFields: {
productsMap: {
$objectToArray: "$$ROOT.Data"
}
}
},
{
$project: {
"Products": {
$map: {
input: "$productsMap",
as: "p",
in: {
Product: "$$p.k",
Created: {
$dateToString: {
format: "%Y-%m-%d %H:%M:%S",
date: "$$p.v.Created"
}
},
ID: "$ID",
_id: "$_id"
}
}
},
_id: 0
}
}
])
Sample output: playground with new query
[
{
"Products": [
{
"Created": "2016-08-23 20:55:39",
"ID": "290283667",
"Product": "Product-2713890",
"_id": ObjectId("5a934e000102030405000000")
},
{
"Created": "2017-07-23 20:55:39",
"ID": "290283667",
"Product": "Product-6943532",
"_id": ObjectId("5a934e000102030405000000")
}
]
}]

Avoid empty array elements in mongo db

How to avoid empty array while filtering results while querying a collection in MongoDb
[
{
"_id": ObjectId("5d429786bd7b5f4ae4a64790"),
"extensions": {
"outcome": "success",
"docType": "ABC",
"Roll No": "1"
},
"data": [
{
"Page1": [
{
"heading": "LIST",
"content": [
{
"text": "<b>12345</b>"
},
],
}
],
"highlights": [
{
"name": "ABCD",
"text": "EFGH",
}
],
"marks": [
{
"revision": "revision 1",
"Score": [
{
"maths": "100",
"science": "40",
"history": "90"
},
{
"lab1": "25",
"lab2": "25"
}
],
"Result": "Pass"
},
{
"revision": "revision 1",
"Score": [
{
"maths": "100",
"science": "40"
},
{
"lab1": "25",
"lab2": "25"
}
],
"Result": "Pass"
}
]
}
]
}
]
I am looking for results that has only "history" marks in the score array.
I tried the following query (in mongo 3.6.10) but it returns empty score array as well the array that has history as well
db.getCollection('student_scores').find({
"data.marks.score.history": {
$not: {
$type: 10
},
$exists: true
}
},
{
"extensions.rollNo": 1,
"data.marks.score.history": 1
})
Desired output is
{
"extensions": {
"rollNo": "1"
},
"data": [
{
"marks": [
{
"Score": [
{
"history": "90"
}
]
}
]
}
]
}
I used something like the following;
db.getCollection('student_scores').aggregate([
{
$unwind: "$data"
},
{
$unwind: "$data.marks"
},
{
$unwind: "$data.marks.Score"
},
{
$match: {
"data.marks.Score.history": {
$exists: true,
$not: {
$type: 10
}
}
}
},
{
$project: {
"extensions.Roll No": 1,
"data.marks.Score.history": 1
}
},
{
$group: {
_id: "$extensions.Roll No",
history_grades: {
$push: "$data.marks.Score.history"
}
}
}
])
where I got the following result with your input (I think more readable than your expected output);
[
{
"_id": "1",
"history_grades": [
"90"
]
}
]
where _id represents "extensions.Roll No" value for any given data set.
What do you think?
check with a bigger input on mongoplayground
OK, so I still think the data design here with the Score array is a little off but here is solution that will ensure that a Score array contains only 1 entry and that entry is for a key of history. We use dotpath array diving as a trick to get to the value of history.
c = db.foo.aggregate([
{$unwind: "$data"}
,{$unwind: "$data.marks"}
,{$project: {
result: {$cond: [
{$and: [ // if
{$eq: [1, {$size: "$data.marks.Score"}]}, // Only 1 item...
// A little trick! $data.marks.Score.history will resolve to an *array*
// of the values associated with each object in $data.marks.Score (the parent
// array) having a key of history. BUT: As it resolves, if there is no
// field for that key, nothing is added to resolution vector -- not even a null.
// This means the resolved array could
// be **shorter** than the input. FOr example:
// > db.foo.insert({"x":[ {b:2}, {a:3,b:4}, {b:7}, {a:99} ]});
// WriteResult({ "nInserted" : 1 })
// > db.foo.aggregate([ {$project: {z: "$x.b", n: {$size: "$x.b"}} } ]);
// { "z" : [ 2, 4, 7 ], "n" : 3 }
// > db.foo.aggregate([ {$project: {z: "$x.a", n: {$size: "$x.a"}} } ]);
// { "z" : [ 3, 99 ], "n" : 2 }
//
// You must be careful about this.
// But we also know this resolved vector is of size 1 (see above) so we can go ahead and grab
// the 0th item and that becomes our output.
// Note that if we did not have the requirement of ONLY history, then we would not
// need the fancy $cond thing.
{$arrayElemAt: ["$data.marks.Score.history",0]}
]},
{$arrayElemAt: ["$data.marks.Score.history",0]}, // then (use value of history)
null ] } // else set null
,extensions: "$extensions" // just carry over extensions
}}
,{$match: {"result": {$ne: null} }} // only take good ones.

Query only for numbers in nested array

I am trying to get an average number of an key in a nested array inside a document, but not sure how to accomplish this.
Here is how my document looks like:
{
"_id": {
"$oid": "XXXXXXXXXXXXXXXXX"
},
"data": {
"type": "PlayerRoundData",
"playerId": "XXXXXXXXXXXXX",
"groupId": "XXXXXXXXXXXXXX",
"holeScores": [
{
"type": "RoundHoleData",
"points": 2
},
{
"type": "RoundHoleData",
"points": 13
},
{
"type": "RoundHoleData",
"points": 3
},
{
"type": "RoundHoleData",
"points": 1
},
{
"type": "RoundHoleData",
"points": 21
}
]
}
}
Now, the tricky part of this is that I only want the average of points for holeScores[0] of all documents with this playerid and this groupid.
Actually, the best solution would be collecting all documents with playerid and groupid and create a new array with the average of holeScores[0], holeScores[1], holeScores[2]... But if I only can get one array key at the time, that would be OK to :-)
Here is what I am thinking but not quit sure how to put it together:
var allScores = dbCollection('scores').aggregate(
{$match: {"data.groupId": groupId, "playerId": playerId}},
{$group: {
_id: playerId,
rounds: { $sum: 1 }
result: { $sum: "$data.scoreTotals.points" }
}}
);
Really hoping for help with this issue and thanks in advance :-)
You can use $unwind with includeArrayIndex to get index and then use $group to group by that index
dbCollection('scores').aggregate(
{
$match: { "data.playerId": "XXXXXXXXXXXXX", "data.groupId": "XXXXXXXXXXXXXX" }
},
{
$unwind: {
path: "$data.holeScores",
includeArrayIndex: "index"
}
},
{
$group: {
_id: "$index",
playerId: { $first: "data.playerId" },
avg: { $avg: "$data.holeScores.points" }
}
}
)
You can try below aggregation
db.collection.aggregate(
{ "$match": { "data.groupId": groupId, "data.playerId": playerId }},
{ "$group": {
"_id": null,
"result": {
"$sum": {
"$arrayElemAt": [
"$data.holeScores.points",
0
]
}
}
}}
)

Resources