Why doesn't mongoose aggregate method return all fields of a document? - mongodb

I have the following document
[
{
"_id": "624713340a3d2901f2f5a9c0",
"username": "fotis",
"exercises": [
{
"_id": "624713530a3d2901f2f5a9c3",
"description": "Sitting",
"duration": 60,
"date": "2022-03-24T00:00:00.000Z"
},
{
"_id": "6247136a0a3d2901f2f5a9c6",
"description": "Coding",
"duration": 999,
"date": "2022-03-31T00:00:00.000Z"
},
{
"_id": "624713a00a3d2901f2f5a9ca",
"description": "Sitting",
"duration": 999,
"date": "2022-03-30T00:00:00.000Z"
}
],
"__v": 3
}
]
And I am executing the following aggregation (on mongoplayground.net)
db.collection.aggregate([
{
$match: {
"_id": "624713340a3d2901f2f5a9c0"
}
},
{
$project: {
exercises: {
$filter: {
input: "$exercises",
as: "exercise",
cond: {
$eq: [
"$$exercise.description",
"Sitting"
]
}
}
},
limit: 1
}
}
])
And the result is the following
[
{
"_id": "624713340a3d2901f2f5a9c0",
"exercises": [
{
"_id": "624713530a3d2901f2f5a9c3",
"date": "2022-03-24T00:00:00.000Z",
"description": "Sitting",
"duration": 60
},
{
"_id": "624713a00a3d2901f2f5a9ca",
"date": "2022-03-30T00:00:00.000Z",
"description": "Sitting",
"duration": 999
}
]
}
]
So my first question is why is the username field not included in the result?
And the second one is why aren't the exercises limited to 1? Is the limit currently applied to the whole user document? If so, is it possible to apply it only on exercises subdocument?
Thank you!

First Question
When you use $project stage, then only the properties that you specified in the stage will be returned. You only specified exercises property, so only that one is returned. NOTE that _id property is returned by default, even you didn't specify it.
Second Question
$limit is also a stage as $project. You can apply $limit to the whole resulting documents array, not to nested array property of one document.
Solution
In $project stage, you can specify username filed as well, so it will also be returned. Instead of $limit, you can use $slice to specify the number of documents that you want to be returned from an array property.
db.collection.aggregate([
{
"$match": {
"_id": "624713340a3d2901f2f5a9c0"
}
},
{
"$project": {
"username": 1,
"exercises": {
"$slice": [
{
"$filter": {
"input": "$exercises",
"as": "exercise",
"cond": {
"$eq": [
"$$exercise.description",
"Sitting"
]
}
}
},
1
]
}
}
}
])
Working example

Related

Find specific field in MongoDB document based on condition

I have the following MongoDB documents like this one:
{
"_id": "ABC",
"properties":
[
{
"_id": "123",
"weight":
{
"$numberInt": "0"
},
"name": "Alice"
},
{
"_id": "456",
"weight":
{
"$numberInt": "1"
},
"name": "Bob"
},
{
"_id": "789",
"weight":
{
"$numberInt": "1"
},
"name": "Charlie"
}
]
}
And I would like to find the _id of the property with name "Alice", or the _id of the property with "$numberInt": "0".
I'm using pymongo.
The following approach:
from pymongo import MongoClient
mongo_client = MongoClient("mymongourl")
mongo_collection = mongo_client.mongo_database.mongo_collection
mongo_collection.find({'properties.name': 'Alice'}, {'properties': 1})[0]['_id']
Gives the very first _id ("123")
But since I filtered for the document, if Alice was in the second element of the properties array (_id: "456") I would have missed her.
Which is the best method to find for the specific _id associated with the element with the specified name?
You can simply use $reduce to iterate through the properties array. Conditionally store the _id field if it matches your conditions.
db.collection.aggregate([
{
"$addFields": {
"answer": {
"$reduce": {
"input": "$properties",
"initialValue": null,
"in": {
"$cond": {
"if": {
$or: [
{
$eq: [
"$$this.name",
"Alice"
]
},
{
$eq: [
"$$this.weight",
0
]
}
]
},
"then": "$$this._id",
"else": "$$value"
}
}
}
}
}
}
])
Mongo Playground

Can I get the count of subdocuments that match a filter?

I have the following document
[
{
"_id": "624713340a3d2901f2f5a9c0",
"username": "fotis",
"exercises": [
{
"_id": "624713530a3d2901f2f5a9c3",
"description": "Sitting",
"duration": 60,
"date": "2022-03-24T00:00:00.000Z"
},
{
"_id": "6247136a0a3d2901f2f5a9c6",
"description": "Coding",
"duration": 999,
"date": "2022-03-31T00:00:00.000Z"
},
{
"_id": "624713a00a3d2901f2f5a9ca",
"description": "Sitting",
"duration": 999,
"date": "2022-03-30T00:00:00.000Z"
}
],
"__v": 3
}
]
And I am trying to get the count of exercises returned with the following aggregation (I know it is way easier to do it in my code, but I am trying to understand how to use mongodb queries)
db.collection.aggregate([
{
"$match": {
"_id": "624713340a3d2901f2f5a9c0"
}
},
{
"$project": {
"username": 1,
"exercises": {
"$slice": [
{
"$filter": {
"input": "$exercises",
"as": "exercise",
"cond": {
"$eq": [
"$$exercise.description",
"Sitting"
]
}
}
},
1
]
},
"count": {
"$size": "exercises"
}
}
}
])
When I try to access the exercises field using "$size": "exercises", I get an error query failed: (Location17124) Failed to optimize pipeline :: caused by :: The argument to $size must be an array, but was of type: string.
But when I access the subdocument exercises using "$size": "$exercises" I get the count of all the subdocuments contained in the document.
Note: I know that in this example I use $slice and I set the limit to 1, but in my code it is a variable.
You are actually on the right track. You don't really need the $slice. You can just use $reduce to perform the filtering. The reason that your count is not working is that the filtering and the $size are in the same stage. In such case, it will take the pre-filtered array to do the count. You can resolve this by adding a $addFields stage.
db.collection.aggregate([
{
"$match": {
"_id": "624713340a3d2901f2f5a9c0"
}
},
{
"$project": {
"username": 1,
"exercises": {
"$filter": {
"input": "$exercises",
"as": "exercise",
"cond": {
"$eq": [
"$$exercise.description",
"Sitting"
]
}
}
}
}
},
{
"$addFields": {
"count": {
$size: "$exercises"
}
}
}
])
Here is the Mongo playground for your reference.

How can I get multiple elements from an array in MongoDB?

How can I get multiple elements from an array at once that satisfy a specific condition, for example: Date <= 2020-12-31. I read about $elemMatch, but I can only get one specific element with it.
"someArray": [
{
"Date": "2021-09-30",
"value": "6.62"
},
{
"Date": "2020-12-31",
"value": "8.67"
},
{
"Date": "2019-12-31",
"value": "12.81"
},
{
"Date": "2018-12-31",
"value": "13.82"
},
{
"Date": "2017-12-31",
"value": "13.83"
},
...
]
You can use $filter in an aggregation query like this:
db.collection.aggregate([
{
"$project": {
"someArray": {
"$filter": {
"input": "$someArray",
"as": "a",
"cond": {
"$lte": [
"$$a.Date",
ISODate("2020-12-31")
]
}
}
}
}
}
])
Example here
Note that you can use $project or $set (available since version 4.2): example or $addFields: example

How to get count by order in mongodb aggregate?

I have two collections name listings and moods.
listings sample:
{
"_id": ObjectId("5349b4ddd2781d08c09890f3"),
"name": "Hotel Radisson Blu",
"moods": [
ObjectId("507f1f77bcf86cd799439010"),
ObjectId("507f1f77bcf86cd799439011")
]
}
moods sample:
{
"_id": ObjectId("507f1f77bcf86cd799439011"),
"name": "Sports"
},
{
"_id": ObjectId("507f1f77bcf86cd799439010"),
"name": "Spanish Food"
},
{
"_id": ObjectId("507f1f77bcf86cd799439009"),
"name": "Action"
}
I need this record.
{
"_id": ObjectId("507f1f77bcf86cd799439011"),
"name": "Sports",
"count": 1
},
{
"_id": ObjectId("507f1f77bcf86cd799439010"),
"name": "Spanish Food",
"count": 1
},
{
"_id": ObjectId("507f1f77bcf86cd799439009"),
"name": "Action",
"count": 0
}
I need this type of record. I have no idea about aggregate.
You can do it using aggregate(),
$lookup to join collection listings
$match pipeline to check moods _id in listings field moods array
db.moods.aggregate([
{
"$lookup": {
"from": "listings",
"as": "count",
let: { id: "$_id" },
pipeline: [
{
"$match": {
"$expr": { "$in": ["$$id", "$moods"] }
}
}
]
}
},
$addFields to add count on the base of $size of array count that we got from above lookup
{
$addFields: {
count: { $size: "$count" }
}
}
])
Playground
did this work:
db.collection.aggrate().count()
Try to combine the functions, it might work.

MongoDB condition for all fields of array

Is it possible in mongodb do a query for find documents where the field is an array and all its elements satisfy a condition?
For example:
[
{
"_id": ObjectId("53d63d0f5c7ff000227cd372"),
"works": [
{
"code": "A001",
"items": [
{
"_id": "534664b081362062015d1b77",
"qty": 6
},
{
"_id": "534ba71f394835a7e51dd938",
"qty": 5
}
],
"name": "Cambiar bombilla",
"price": 100,
"Date": "2014-07-30T09:43:17.593Z",
"TechnicianId": "538efd918163b19307c59e8e",
"percent": 2,
"_id": ObjectId("53d63d0f5c7ff002207cd372")
},
{
"code": "A001",
"name": "Cambiar bombilla",
"price": 100,
"type": "Bombillas",
"TechnicianId": "538efd918163b19307c59e8e",
"date": "2014-07-31T13:36:34.019Z",
"orderId": "53d63d0f5c7ff000007cd372",
"_id": ObjectId("53da466568c26f8a72b50fcb"),
"percent": 66
}
]
},
{
"_id": ObjectId("53d63d0f5c7ff000007cd372"),
"works": [
{
"code": "A001",
"items": [
{
"_id": "534664b081362062015d1b77",
"qty": 6
},
{
"_id": "534ba71f394835a7e51dd938",
"qty": 5
}
],
"name": "Cambiar bombilla",
"price": 100,
"Date": "2014-07-30T09:43:17.593Z",
"TechnicianId": "538efd918163b19307c59e8e",
"percent": 2,
"_id": ObjectId("53d63d0f5c7ff002207cd372")
},
{
"code": "A001",
"name": "Cambiar bombilla",
"price": 100,
"type": "Bombillas",
"TechnicianId": "538efd918163b19307c59e8e",
"date": "2014-07-31T13:36:34.019Z",
"orderId": "53d63d0f5c7ff000007cd372",
"_id": ObjectId("53da466568c26f8a72b50fcb"),
}
]
}
]
I need to find the documents like first document because in works field his subdocuments have percent, but in the second document, works array at second position doesnt have percent.
It's not easy to test for the existence of a field in every element of an array in a simple way. Values can be tested for buy keys take a bit more work.
The approach is done with the aggregation framework in order to process conditions for the array elements and then match the result.
Firstly with MongoDB 2.6 and greater you get some helpers:
db.collection.aggregate([
// Filter out to match only possible documents
{ "$match": {
"works.percent": { "$exists": true }
}},
// Find matching through projection
{ "$project": {
"works": 1,
"matched": {
"$allElementsTrue": {
"$map": {
"input": "$works",
"as": "el",
"in": { "$ifNull": [ "$$el.percent", false ] }
}
}
}
}},
// Filter to return only the true matches
{ "$match": { "matched": true } }
])
Or a bit longer and possibly slower with $unwind in prior versions:
db.collection.aggregate([
// Filter out to match only possible documents
{ "$match": {
"works.percent": { "$exists": true }
}},
// Unwind the array
{ "$unwind": "$works" },
// Find matching by conditionally evaluating and grouping
{ "$group": {
"_id": "$_id",
"works": { "$push": "$works" },
"matched": {
"$min": {
"$cond": [
{ "$ifNull": [ "$works.percent", false ] },
true,
false
]
}
}
}}
// Filter to return only the true matches
{ "$match": { "matched": true } }
])
In either case, it is the $ifNull operator that works as the equivalent of the $exists operator in a aggregation operator sense. Where the "query" form for $exists tests whether a field is present, $ifNull evaluates that where the field is present then the value is returned, otherwise the alternate argument is returned instead.
But the arrays basically need to be processed to see if all elements have the required field present as there is no standard query equivalent for this.