How do I compute the difference of two queries? - mongodb

I have a MongoDB collection that contains a set of documents. Each document has an ISODate date and an integer id (not _id). id: X is said to exist for date: D if there is a document in the collection with field values { id: X, date: D }. So, for example:
{ id: 1, date: 1/1/2000 }
{ id: 1, date: 1/2/2000 }
{ id: 1, date: 1/3/2000 }
{ id: 1, date: 1/4/2000 }
{ id: 2, date: 1/2/2000 }
{ id: 2, date: 1/3/2000 }
{ id: 3, date: 1/3/2000 }
I would like to track ids over time as they are created and destroyed day-to-day. Using the above data, over the date range 1/1/2000 to 1/4/2000:
1/1/2000: id 1 is created
1/2/2000: id 2 is created
1/3/2000: id 3 is created
1/4/2000: id 2 is destroyed
1/4/2000: id 3 is destroyed
I think the best way to solve this would be to loop day by day, see what ids exist between today and the next day, and perform a set difference. For example, to get the set of ids created and destroyed on 1/2/2000, I need to perform two set differences between arrays for either day:
var A = [ <ids that exist on 1/1/2000> ];
var B = [ <ids that exist on 1/2/2000> ];
var created_set = set_difference(B, A); // Those in B and not in A
var destroyed_set = set_difference(A, B); // Those in A and not in B
I can use a find() command to get cursors for A and B, but I do not know how to perform the set_difference between two cursors.
My other option was to use an aggregation pipeline, but I cannot think about how to formulate the pipeline in such a way that I can use the $setDifference operator.
As a MongoDB novice, I am sure I'm thinking about the problem the wrong way. Surely this is something that can be done? What am I missing?

db.mystuff.aggregate([
{$group: {_id: '$id', created: {$first: '$date'}, destroyed: {$last: '$date'}}}
])

Suppose you have the following sample collection:
db.collection.insert([
{ id: 1, date: ISODate("2000-01-01") },
{ id: 1, date: ISODate("2000-01-02") },
{ id: 1, date: ISODate("2000-01-03") },
{ id: 1, date: ISODate("2000-01-04") },
{ id: 2, date: ISODate("2000-01-02") },
{ id: 2, date: ISODate("2000-01-03") },
{ id: 3, date: ISODate("2000-01-03") }
]);
The following aggregation will give you some direction towards what you are trying to achieve using the $setDifference operator:
var start = new Date(2000, 0, 1);
var end = new Date(2000, 0, 2)
db.collection.aggregate([
{
"$match":{
"date": {
"$gte": start,
"$lte": end
}
}
},
{
$group: {
_id: "$date",
"A": {
"$addToSet": {
"$cond": [
{ "$eq": [ "$date", start ] },
"$id",
false
]
}
},
"B": {
"$addToSet": {
"$cond": [
{ "$eq": [ "$date", end ] },
"$id",
false
]
}
}
}
},
{
"$project": {
"A": {
"$setDifference": [ "$A", [false] ]
},
"B": {
"$setDifference": [ "$B", [false] ]
}
}
},
{
"$project": {
"_id": 0,
"date": "$_id",
"created_set": {
"$setDifference": [ "$B", "$A" ]
},
"destroyed_set": {
"$setDifference": [ "$A", "$B" ]
}
}
}
]);
Output:
{
"result" : [
{
"date" : ISODate("2000-01-02T00:00:00.000Z"),
"created_set" : [2, 1],
"destroyed_set" : []
},
{
"date" : ISODate("2000-01-01T00:00:00.000Z"),
"created_set" : [],
"destroyed_set" : [1]
}
],
"ok" : 1
}

Related

MongoDB get count of field per season from MM/DD/YYYY date field

I am facing a problem in MongoDB. Suppose, I have the following collection.
{ id: 1, issueDate: "07/05/2021", code: "31" },
{ id: 2, issueDate: "12/11/2020", code: "14" },
{ id: 3, issueDate: "02/11/2021", code: "98" },
{ id: 4, issueDate: "01/02/2021", code: "14" },
{ id: 5, issueDate: "06/23/2020", code: "14" },
{ id: 6, issueDate: "07/01/2020", code: "31" },
{ id: 7, issueDate: "07/05/2022", code: "14" },
{ id: 8, issueDate: "07/02/2022", code: "20" },
{ id: 9, issueDate: "07/02/2022", code: "14" }
The date field is in the format MM/DD/YYYY. My goal is to get the count of items with each season (spring (March-May), summer (June-August), autumn (September-November) and winter (December-February).
The result I'm expecting is:
count of fields for each season:
{ "_id" : "Summer", "count" : 6 }
{ "_id" : "Winter", "count" : 3 }
top 2 codes (first and second most recurring) per season:
{ "_id" : "Summer", "codes" : {14, 31} }
{ "_id" : "Winter", "codes" : {14, 98} }
How can this be done?
You should never store date/time values as string, store always proper Date objects.
You can use $setWindowFields opedrator for that:
db.collection.aggregate([
// Convert string into Date
{ $set: { issueDate: { $dateFromString: { dateString: "$issueDate", format: "%m/%d/%Y" } } } },
// Determine the season (0..3)
{
$set: {
season: { $mod: [{ $toInt: { $divide: [{ $add: [{ $subtract: [{ $month: "$issueDate" }, 1] }, 1] }, 3] } }, 4] }
}
},
// Count codes per season
{
$group: {
_id: { season: "$season", code: "$code" },
count: { $count: {} },
}
},
// Rank occurrence of codes per season
{
$setWindowFields: {
partitionBy: "$_id.season",
sortBy: { count: -1 },
output: {
rank: { $denseRank: {} },
count: { $sum: "$count" }
}
}
},
// Get only top 2 ranks
{ $match: { rank: { $lte: 2 } } },
// Final grouping
{
$group: {
_id: "$_id.season",
count: { $first: "$count" },
codes: { $push: "$_id.code" }
}
},
// Some cosmetic for output
{
$set: {
season: {
$switch: {
branches: [
{ case: { $eq: ["$_id", 0] }, then: 'Winter' },
{ case: { $eq: ["$_id", 1] }, then: 'Spring' },
{ case: { $eq: ["$_id", 2] }, then: 'Summer' },
{ case: { $eq: ["$_id", 3] }, then: 'Autumn' },
]
}
}
}
}
])
Mongo Playground
I will give you clues,
You need to use $group with _id as $month on issueDate, use accumulator $sum to get month wise count.
You can divide month by 3, to get modulo, using $toInt, $divide, then put them into category using $cond.
Another option:
db.collection.aggregate([
{
$addFields: {
"season": {
$switch: {
branches: [
{
case: {
$in: [
{
$substr: [
"$issueDate",
0,
2
]
},
[
"06",
"07",
"08"
]
]
},
then: "Summer"
},
{
case: {
$in: [
{
$substr: [
"$issueDate",
0,
2
]
},
[
"03",
"04",
"05"
]
]
},
then: "Spring"
},
{
case: {
$in: [
{
$substr: [
"$issueDate",
0,
2
]
},
[
"12",
"01",
"02"
]
]
},
then: "Winter"
}
],
default: "No date found."
}
}
}
},
{
$group: {
_id: {
s: "$season",
c: "$code"
},
cnt1: {
$sum: 1
}
}
},
{
$sort: {
cnt1: -1
}
},
{
$group: {
_id: "$_id.s",
codes: {
$push: "$_id.c"
},
cnt: {
$sum: "$cnt1"
}
}
},
{
$project: {
_id: 0,
season: "$_id",
count: "$cnt",
codes: {
"$slice": [
"$codes",
2
]
}
}
}
])
Explained:
Add one more field for season based on $switch per month(extracted from issueDate string)
Group to collect per season/code.
$sort per code DESCENDING
group per season to form an array with most recurring codes in descending order.
Project the fields to the desired output and $slice the codes to limit only to the fist two most recurring.
Comment:
Indeed keeping dates in string is not a good idea in general ...
Playground

MongoDB - Calculate time difference between documents based on the existence of a value inside an array?

I'm trying to calculate the time difference between the two documents but I'm not sure how to do this based on the existence of a value inside an array.
Let me explain in a little more detail. Say I have five documents: A, B, C, D, E in a collection.
Each document has referenceKey, timestamp and persons fields.
And each element inside a persons array have personType field along with other fields:
A: { referenceKey: 1, timestamp: ISODate, persons: [ { personType: "ALICE", ... }, { personType: "BOB", ... } ] }
B: { referenceKey: 1, timestamp: ISODate, persons: [ { personType: "ALICE", ... }, { personType: "BOB", ... } ] }
C: { referenceKey: 1, timestamp: ISODate, persons: [ { personType: "BOB", ... } ] }
D: { referenceKey: 1, timestamp: ISODate, persons: [ { personType: "ALICE", ... }, { personType: "BOB", ... } ] }
E: { referenceKey: 1, timestamp: ISODate, persons: [ { personType: "BOB", ... } ] }
What I want to achieve is to calculate how much time the person with type ALICE has spent for each visit.
In other words, this should calculate and return an array of time differences:
[{ timeSpent: C.timestamp - A.timestamp }, { timeSpent: E.timestamp - D.timestamp }]
Here is an example collection to test:
[
{
timestamp: ISODate("2019-04-12T20:00:00.000Z"),
referenceKey: 1,
persons: [
{
personType: "BOB"
},
{
personType: "ALICE"
}
]
},
{
timestamp: ISODate("2019-04-12T20:10:00.000Z"),
referenceKey: 1,
persons: [
{
personType: "BOB"
}
]
},
{
timestamp: ISODate("2019-04-12T21:00:00.000Z"),
referenceKey: 1,
persons: [
{
personType: "BOB"
},
{
personType: "ALICE"
}
]
},
{
timestamp: ISODate("2019-04-12T21:15:00.000Z"),
referenceKey: 1,
persons: [
{
personType: "BOB"
}
]
},
{
timestamp: ISODate("2019-04-12T21:20:00.000Z"),
referenceKey: 1,
persons: [
{
personType: "BOB"
}
]
},
{
timestamp: ISODate("2019-04-12T21:45:00.000Z"),
referenceKey: 1,
persons: [
{
personType: "BOB"
},
{
personType: "ALICE"
}
]
},
{
timestamp: ISODate("2019-04-12T22:05:00.000Z"),
referenceKey: 1,
persons: [
{
personType: "BOB"
},
{
personType: "ALICE"
}
]
},
{
timestamp: ISODate("2019-04-12T23:00:00.000Z"),
referenceKey: 1,
persons: [
{
personType: "BOB"
}
]
},
{
timestamp: ISODate("2019-04-12T18:30:00.000Z"),
referenceKey: 2,
persons: [
{
personType: "BOB"
},
{
personType: "JOHN"
}
]
}
]
I thought I can add a new boolean field hasAlice using $in based on the existence of person type ALICE. But the problem is the time spent calculation should be done for each visit so I cannot just use $reduce to calculate the total time. Can I somehow use $group by hasAlice field change and then use $reduce?
What I've tried (and failed) so far:
db.collection.aggregate([
{
"$match": { // I also filter by timestamp and referenceKey but it is not relevant to the problem
timestamp: {
"$gte": ISODate("2019-04-12T00:00:00.000Z"),
"$lt": ISODate("2019-04-12T23:59:00.000Z")
},
referenceKey: 1
}
},
{
"$project": {
_id: 0,
timestamp: 1,
hasAlice: {
"$in": [
"ALICE",
"$persons.personType"
]
}
}
},
{
"$sort": {
timestamp: 1
}
}
])
What I want to get:
[
{ timeSpent: 10 }, // in minutes
{ timeSpent: 15 },
{ timeSpent: 75 },
]
What I actually get when I run the aggregation:
[
{
"hasAlice": true, // 1. visit starts
"timestamp": ISODate("2019-04-12T20:00:00Z")
},
{
"hasAlice": false, // 1. visit ends
"timestamp": ISODate("2019-04-12T20:10:00Z")
},
{
"hasAlice": true, // 2. visit starts
"timestamp": ISODate("2019-04-12T21:00:00Z")
},
{
"hasAlice": false, // 2. visit ends
"timestamp": ISODate("2019-04-12T21:15:00Z")
},
{
"hasAlice": false,
"timestamp": ISODate("2019-04-12T21:20:00Z")
},
{
"hasAlice": true, // 3. visit starts
"timestamp": ISODate("2019-04-12T21:45:00Z")
},
{
"hasAlice": true, // NOTE: there are some misleading documents such as these (e.g. document B)
"timestamp": ISODate("2019-04-12T22:05:00Z")
},
{
"hasAlice": false, // 3. visit ends
"timestamp": ISODate("2019-04-12T23:00:00Z")
}
]
I don't know if my logic is correct or can I somehow reduce these documents to calculate the time spent for each visit. But any help is appreciated.
Thanks in advance.
I finally figured it out, thanks to this somewhat similar post.
The trick is to use $lookup to left-join the collection with itself and then getting the first element that does not contain the person type ALICE from the joined collection. This element from the joined collection gives us the ending of each visit (i.e leaveTimestamp).
From there, we can further $group by end of each visit and select only the first timestamp of matching documents so that we can eliminate any misleading documents (e.g. document B).
Here is the full aggregate pipeline:
db.collection.aggregate([
{
"$match": {
timestamp: {
"$gte": ISODate("2019-04-12T00:00:00.000Z"),
"$lt": ISODate("2019-04-12T23:59:00.000Z")
},
referenceKey: 1,
persons: {
"$elemMatch": {
"personType": "ALICE"
}
}
}
},
{
"$project": {
timestamp: 1,
hasAlice: {
"$in": [
"ALICE",
"$persons.personType"
]
}
}
},
{
$lookup: {
from: "collection",
let: {
root_id: "$_id"
},
pipeline: [
{
"$match": {
timestamp: {
"$gte": ISODate("2019-04-12T00:00:00.000Z"),
"$lt": ISODate("2019-04-12T23:59:00.000Z")
},
referenceKey: 1
}
},
{
"$project": {
timestamp: 1,
hasAlice: {
"$in": [
"ALICE",
"$persons.personType"
]
}
}
},
{
$match: {
$expr: {
$gt: [
"$_id",
"$$root_id"
]
}
}
}
],
as: "tmp"
}
},
{
"$project": {
_id: 1,
timestamp: 1,
tmp: {
$filter: {
input: "$tmp",
as: "item",
cond: {
$eq: [
"$$item.hasAlice",
false
]
}
}
}
}
},
{
"$project": {
timestamp: 1,
leaveTimestamp: {
$first: "$tmp.timestamp"
}
}
},
{
"$group": {
"_id": "$leaveTimestamp",
"timestamp": {
"$min": "$timestamp"
},
leaveTimestamp: {
$first: "$leaveTimestamp"
}
}
},
{
$addFields: {
"visitingTime": {
$dateToString: {
date: {
$toDate: {
$subtract: [
"$leaveTimestamp",
"$timestamp"
]
}
},
format: "%H-%M-%S"
}
}
}
},
{
"$sort": {
"timestamp": 1
}
}
])
Mongoplayground

MongoDB Conditional Sum of Array Elements for a collection of documents

I have the following MongoDB collection of documents, each containing a field called "history", which contains an array of sub-documents with fields "date" and "points".
[{
history: [{
date: "2019-20-20",
points: 1,
}, {
date: "2019-20-21",
points: 1,
}, {
date: "2019-20-22",
points: 1,
}, {
date: "2019-20-23",
points: 1,
}],
}, {
history: [{
date: "2019-20-20",
points: 1,
}, {
date: "2019-20-21",
points: 2,
}, {
date: "2019-20-22",
points: 3,
}, {
date: "2019-20-23",
points: 4,
}],
}]
I'm not sure what is the best way to construct a query that produces the below output. For the following example, the date range (inclusive) is "2019-20-21" to "2019-20-22". "totalPoints" is a new field, which contains the sum of all the points in the "history" field across that date range.
[{
history: [{
date: "2019-20-20",
points: 1,
}, {
date: "2019-20-21",
points: 1,
}, {
date: "2019-20-22",
points: 1,
}, {
date: "2019-20-23",
points: 1,
}],
totalPoints: 2,
}, {
history: [{
date: "2019-20-20",
points: 1,
}, {
date: "2019-20-21",
points: 2,
}, {
date: "2019-20-22",
points: 3,
}, {
date: "2019-20-23",
points: 4,
}],
totalPoints: 5,
}]
Below is a general idea of what I'm trying to do:
User.aggregate([{
$addFields: {
totalPoints: { $sum: points in "history" field if date range between "2019-20-21" and "2019-20-22" } ,
}
}]);
The reason I want to create a new "totalPoints" field is because eventually I want to sort via the "totalPoints" field.
For a single pipeline, you can combine $reduce with $filter to get the sum as follows:
var startDate = "2019-20-21";
var endDate = "2019-20-22";
User.aggregate([
{ "$addFields": {
"totalPoints": {
"$reduce": {
"input": {
"$filter": {
"input": "$history",
"as": "el",
"cond": {
"$and": [
{ "$gte": ["$$el.date", startDate] },
{ "$lte": ["$$el.date", endDate ] },
]
}
}
},
"initialValue": 0,
"in": { "$add": [ "$$value", "$$this.points" ] }
}
}
} }
]);
Another alternative is having two pipeline stages where you start your aggregation with a filtered array which contains only the elements that match the date range query. Combine $addFields with $filter for this and your filter condition uses the conditional operator $and with the comparison operators $gte and $lte. The following pipeline shows this:
{ "$addFields": {
"totalPoints": {
"$filter": {
"input": "$history",
"cond": {
"$and": [
{ "$gte": ["$$this.date", "2019-20-21"] },
{ "$lte": ["$$this.date", "2019-20-22"] },
]
}
}
}
} },
On getting the filtered array you can then get the sum easily in the next pipeline with $sum, so your complete pipeline becomes
var startDate = "2019-20-21";
var endDate = "2019-20-22";
User.aggregate([
{ "$addFields": {
"totalPoints": {
"$filter": {
"input": "$history",
"cond": {
"$and": [
{ "$gte": ["$$this.date", startDate] },
{ "$lte": ["$$this.date", endDate ] },
]
}
}
}
} },
{ "$addFields": {
"totalPoints": { "$sum": "$totalPoints.points" }
} }
])

How to group date quarterly wise?

I have documents which contain a date and I'm wondering how to group them according to quarterly basis?
My schema is:
var ekgsanswermodel = new mongoose.Schema({
userId: {type: Schema.Types.ObjectId},
topicId : {type: Schema.Types.ObjectId},
ekgId : {type: Schema.Types.ObjectId},
answerSubmitted :{type: Number},
dateAttempted : { type: Date},
title : {type: String},
submissionSessionId : {type: String}
});
1st quarter contains months 1, 2, 3. 2nd quarter contains months 4, 5, 6 and so on up-to 4th quarter.
My final result should be:
"result" : [
{
_id: {
quater:
},
_id: {
quater:
},
_id: {
quater:
},
_id: {
quater:
}
}
You could make use of the $cond operator to check if:
The $month is <= 3, project a field named quarter with
value as "one".
The $month is <= 6, project a field named quarter with
value as "two".
The $month is <= 9, project a field named quarter with
value as "three".
else the value of the field quarter would be "fourth".
Then $group by the quarter field.
Code:
db.collection.aggregate([
{
$project: {
date: 1,
quarter: {
$cond: [
{ $lte: [{ $month: "$date" }, 3] },
"first",
{
$cond: [
{ $lte: [{ $month: "$date" }, 6] },
"second",
{
$cond: [{ $lte: [{ $month: "$date" }, 9] }, "third", "fourth"],
},
],
},
],
},
},
},
{ $group: { _id: { quarter: "$quarter" }, results: { $push: "$date" } } },
]);
Specific to your schema:
db.collection.aggregate([
{
$project: {
dateAttempted: 1,
userId: 1,
topicId: 1,
ekgId: 1,
title: 1,
quarter: {
$cond: [
{ $lte: [{ $month: "$dateAttempted" }, 3] },
"first",
{
$cond: [
{ $lte: [{ $month: "$dateAttempted" }, 6] },
"second",
{
$cond: [
{ $lte: [{ $month: "$dateAttempted" }, 9] },
"third",
"fourth",
],
},
],
},
],
},
},
},
{ $group: { _id: { quarter: "$quarter" }, results: { $push: "$$ROOT" } } },
]);
You could use following to group documents quarterly.
{
$project : {
dateAttempted : 1,
dateQuarter: {
$trunc : {$add: [{$divide: [{$subtract: [{$month:
"$dateAttempted"}, 1]}, 3]}, 1]}
}
}
}
Starting in Mongo 5, it's a perfect use case for the new $dateTrunc aggregation operator:
// { date: ISODate("2012-10-11") }
// { date: ISODate("2013-02-27") }
// { date: ISODate("2013-01-12") }
// { date: ISODate("2013-03-11") }
// { date: ISODate("2013-07-14") }
db.collection.aggregate([
{ $group: {
_id: { $dateTrunc: { date: "$date", unit: "quarter" } },
total: { $count: {} }
}}
])
// { _id: ISODate("2012-10-01"), total: 1 }
// { _id: ISODate("2013-01-01"), total: 3 }
// { _id: ISODate("2013-07-01"), total: 1 }
$dateTrunc truncates your dates at the beginning of their quarter (the truncation unit). It's kind of a modulo on dates per quarter.
Quarters in the output will be defined by their first day (Q3 2013 will be 2013-07-01). And you can always adapt it using $dateToString projection for instance.

Unable to filter by date the array field of a Collection in MongoDB

I am struggling with MongoDb in order to achieve a desirable result.
My Collection looks like:
{
_id: ...
place: 1
city: 6
user: 306
createDate: 2014-08-10 12:20:21,
lastUpdate: 2014-08-14 10:11:01,
data: [
{
customId4: 4,
entryDate: 2014-07-12 12:01:11,
exitDate: 2014-07-12 13:12:12
},
{
customId4: 4,
entryDate: 2014-07-14 00:00:01,
},
{
customId4: 5,
entryDate: 2014-07-15 11:01:11,
exitDate: 2014-07-15 11:05:15
},
{
customId4: 5,
entryDate: 2014-07-22 21:01:11,
exitDate: 2014-07-22 21:23:22
},
{
customId4: 4,
entryDate: 2014-07-23 14:00:11,
},
{
customId4: 4,
entryDate: 2014-07-29 22:00:11,
exitDate: 2014-07-29 23:00:12
},
{
customId4: 5,
entryDate: 2014-08-12 12:01:11,
exitDate: 2014-08-12 13:12:12
},
]
}
So what I would like to achieve is the array data that meets the requirements of a certain interval and that has both, entryDate and exitDate values set.
For example, if I filter by the interval "2014-07-23 00:00:00 to 2014-08-31 00:00:00" I would like the result like:
{
result: [
{
_id: {
place: 1,
user: 306
},
city: 6,
place: 1,
user: 306,
data: [
{
customMap: 4,
entryDate: 2014-07-22 21:01:11,
exitDate: 2014-07-22 21:23:22
},
{
customId4: ,
entryDate: 2014-07-29 22:00:11,
exitDate: 2014-07-29 23:00:12
},
]
}
],
ok: 1
}
My custom mongodb query looks like (from, to and placeIds are variables properly configured)
db.myColl.aggregate(
{ $match: {
'user': 1,
'data.entryDate': { $gte: from, $lte: to },
'place': { $in: placeIds },
}},
{ $unwind : "$data" },
{ $project: {
'city': 1,
'place': 1,
'user': 1,
'lastUpdate': 1,
'data.entryDate': 1,
'data.exitDate': 1,
'data.custom': 1,
fromValid: { $gte: ["$'data.entryDate'", from]},
toValid: { $lte: ["$'data.entryDate'", to]}}
},
{ $group: {
'_id': {'place': '$place', 'user': '$user'},
'city': {'$first': '$city'},
'place': {'$first': '$place'},
'user': {'$first': '$user'},
'data': { '$push': '$data'}
}}
)
But this doesn't filter the way I want because it outputs every document that meets the $match operand conditions, inside the $project operand I am unable to define the condition (I don't know if this is how it has to be done in mongoDB)
Thanks in advance!
You were on the right track, but what you might be missing with the aggregation "pipeline" is that just like the "|" pipe operator in the unix shell you "chain" the pipeline stages together just as you would chain commands.
So in fact to can have a second $match pipeline stage that does the filtering for you:
db.myColl.aggregate([
{ "$match": {
"user": 1,
"data.entryDate": { "$gte": from, "$lte": to },
"place": { "$in": "placeIds" },
}},
{ "$unwind": "$data" },
{ "$match": {
"data.entryDate": { "$gte": from, "$lte": to },
}},
{ "$group": {
"_id": "$_id",
"place": { "$first": "$place" },
"city": { "$first": "$city" },
"user": { "$first": "$user" },
"data": { "$push": "$data" }
}}
])
Using the actual _id of the document as a grouping key presuming that you want the document back but just with a filtered array.
From MongoDB 2.6, as long as your matching array elements are unique, you could just do the same thing within $project using the $map and $setDifference** operators:
db.myColl.aggregate([
{ "$match": {
"user": 1,
"data.entryDate": { "$gte": from, "$lte": to },
"place": { "$in": "placeIds" },
}},
{ "$project": {
"place": 1,
"city": 1,
"user": 1,
"data": {
"$setDifference": [
{ "$map": {
"input": "$data",
"as": "el",
"in": {"$cond": [
{ "$and": [
{ "$gte": [ "$$el.entryDate", from ] },
{ "$lte": [ "$$el.entryDate", to ] }
]},
"$$el",
false
]}
}},
[false]
]
}
}}
])
That does the same logical thing by processing each array element and evaluating whether it meets the conditions. If so then the element content is returned, if not the false is returned. The $setDifference filters out all the false values so that only those that match remain.