How do I dynamically build a Mongodb aggregation statement? - javascript

In the following function I am trying to dynamically build a mongo $or query condition. The priceRanges is received as a parameter to the function. I iterate over priceRanges as follows to build the $or statement for my projection:
let $or = [];
for(let filter of priceRanges) {
$or.push( { $gte: [ "$price", +filter.low ] }, { $lte: [ "$price", +filter.high ] })
}
The $or array now contains the following values:
console.log('$or', $or)
$or [
{ '$gte': [ '$price', 100 ] }, { '$lte': [ '$price', 200 ] },
{ '$gte': [ '$price', 200 ] }, { '$lte': [ '$price', 300 ] },
{ '$gte': [ '$price', 300 ] }, { '$lte': [ '$price', 400 ] }
]
Here I build the project statement:
let $project = {
name:1,
producttype:1,
brand:1,
model:1,
price:1,
list_price:1,
description:1,
rating:1,
sku:1,
feature:1,
image:1,
images: 1,
specifications:1,
};
I append the $or condition to the projection:
$project.priceRange = {$or: $or};
The $or statement looks like this:
{ '$or':
[ { '$gte': [Array] },{ '$lte': [Array] },
{ '$gte': [Array] }, { '$lte': [Array] },
{ '$gte': [Array] }, { '$lte': [Array] } ] }
I create an array of my projection statement:
aggregateArray.push({$project: $project});
console.log(aggregateArray) looks like this:
aggregateArray [ { '$project':
{ name: 1,
producttype: 1,
brand: 1,
model: 1,
price: 1,
list_price: 1,
description: 1,
rating: 1,
sku: 1,
feature: 1,
image: 1,
images: 1,
specifications: 1,
priceRange: [Object] } },
{ '$skip': 1 },
{ '$limit': 4 } ]
I execute the projection as follows:
let products = await Product.aggregate(aggregateArray);
When executed, the $or statement doesn't seem to have any effect. The result contains random prices and not the ranges specified.

The problem here is that javascript array's push methods takes an array of values as a parameter so $or.push( { $gte: [ "$price", +filter.low ] }, { $lte: [ "$price", +filter.high ] }) pushes two separate filtering conditions. Therefore price equal to 50 will also be included in your result since it matches second condition (lower than 200). To fix that you need to combine those pairs using $and so your final filtering condition should look like this:
var $or = [
{ $and: [ { '$gte': [ '$price', 100 ] }, { '$lte': [ '$price', 200 ] } ] },
{ $and: [ { '$gte': [ '$price', 200 ] }, { '$lte': [ '$price', 300 ] } ] },
{ $and: [ { '$gte': [ '$price', 300 ] }, { '$lte': [ '$price', 400 ] } ] }
]
Then if you need it for filtering it should be using with $expr inside of $match stage.
db.col.aggregate([
{
$match: { $expr: { $or: $or } }
},
// other aggregation stages
])

Related

MongoDB fill missing dates in aggregation pipeline

I have this pipeline :
let pipeline = [
{
$match: {
date: { $gte: new Date("2022-10-19"), $lte: new Date("2022-10-26") },
},
},
{
$group: {
_id: "$date",
tasks: { $push: "$$ROOT" },
},
},
{
$sort: { _id: -1 },
},
];
const aggregationData = await ScheduleTaskModel.aggregate(pipeline);
where i group all "tasks" between a date range by date and i get that result :
[
{
"date": "2022-10-21T00:00:00.000Z",
"tasks": [...tasks with this date]
},
{
"date": "2022-10-20T00:00:00.000Z",
"tasks": [...tasks with this date]
}
]
as you see i have "tasks" only for 2 dates in that range,what if i want all dates to appear even the ones with no tasks so it would be like this with empty arrays ?
[
{
"date": "2022-10-26T00:00:00.000Z",
"tasks": []
},
{
"date": "2022-10-25T00:00:00.000Z",
"tasks": []
},
{
"date": "2022-10-24T00:00:00.000Z",
"tasks": []
},
{
"date": "2022-10-23T00:00:00.000Z",
"tasks": []
},
{
"date": "2022-10-22T00:00:00.000Z",
"tasks": []
},
{
"date": "2022-10-21T00:00:00.000Z",
"tasks": [...tasks with this date]
},
{
"date": "2022-10-20T00:00:00.000Z",
"tasks": [...tasks with this date]
},
{
"date": "2022-10-19T00:00:00.000Z",
"tasks": []
},
]
i tried to use $densify but unfortunately it requires upgrading my mongoDb atlas cluster which is not possible..
The answer of #WernfriedDomscheitAnother has a downside of grouping together all the documents in the collection, creating one large document, while a document has a size limit. A variation on it, without this downside, can be:
$match only the relevant document, same as in your current query
Use $facet to handle the case of no relevant documents at all. This will allow you to group all the relevant documents as you did in your query, but to keep a working-document even if there are any.
Add the relevant dates inside an array (since we use $facet this will happen even if the first match is empty)
Concatenate the array of matched data with the array of empty entries, use the real-data first.
$unwind the separate the documents by date, and $group again by date to remove the duplicates.
Format the result
db.collection.aggregate([
{$match: {date: {$gte: new Date("2022-10-19"), $lte: new Date("2022-10-26")}}},
{$facet: {
data: [
{$group: {_id: "$date", tasks: {$push: "$$ROOT"}}},
{$project: {date: "$_id", tasks: 1}}
]
}},
{$addFields: {
dates: {$map: {
input: {$range: [0, 8]},
// maybe more dynamic with $dateDiff -> { $dateDiff: { startDate: new Date("2022-10-19"), endDate: new Date("2022-10-26") }, unit: "day" } }
in: {
date: {$dateAdd: {
startDate: ISODate("2022-10-19T00:00:00.000Z"),
unit: "day",
amount: "$$this"
}},
tasks: []
}
}}
}},
{$project: {data: {$concatArrays: ["$data", "$dates"]}}},
{$unwind: "$data"},
{$group: {_id: "$data.date", "tasks": {$first: "$data.tasks"}}},
{$project: { _id: 0, date: "$_id", tasks: 1 }},
{$sort: { date: -1 }},
])
See how it works on the playground example
New function $densify would be the simplest, of course. The manual way of doing it would be this one:
db.collection.aggregate([
{
$group: {
_id: null,
data: { $push: "$$ROOT" }
}
},
{
$set: {
dates: {
$map: {
input: { $range: [ 0, 8 ] }, // maybe more dynamic with $dateDiff -> { $dateDiff: { startDate: new Date("2022-10-19"), endDate: new Date("2022-10-26") }, unit: "day" } }
in: {
date: {
$dateAdd: {
startDate: ISODate("2022-10-19T00:00:00.000Z"),
unit: "day",
amount: "$$this"
}
}
}
}
}
}
},
{
$set: {
dates: {
$map: {
input: "$dates",
as: "d",
in: {
$mergeObjects: [
"$$d",
{
tasks: {
$filter: {
input: "$data",
cond: { $eq: [ "$$d.date", "$$this.date" ] }
}
}
}
]
}
}
}
}
},
{
$project: {
data: {
$map: {
input: "$dates",
in: {
$cond: {
if: { $eq: [ "$$this.tasks", [] ] },
then: "$$this",
else: { $first: "$$this.tasks" }
}
}
}
}
}
},
{ $unwind: "$data" },
{ $replaceWith: "$data" }
])
Mongo Playground

Add field to each object of an array based on other field

I have the following Array of data:
{
_id: 5f5726ef7d475a61a95c5e0c,
attributes: [
{
values: [
{ name: '1' }
],
},
{
values: [
{ name: '2' }
]
}
],
attr1: [
{ name: "Study Code" },
{ name: "Patient Study" }
]
}
What I need is to add the correspondent value to each on of attr1 objects based on index. So the result would be:
{
_id: 5f5726ef7d475a61a95c5e0c,
attributes: [
{
values: [
{ name: '1' }
],
},
{
values: [
{ name: '2' }
]
},
],
attr1: [
{
name: "Study Code",
values: [{ name: "1" }]
},
{
name: "Patient Study",
values: [{ name: "2" }]
}
],
}
I wonder if that possible using aggregation $addFields in MongoDB
Query
query works if arrays same size
ziparray to make [[member1_1 member2_1], ....]
map to merge member1_1,member2_1 to a document
Playmongo
aggregate(
[{"$set": {"attr1": {"$zip": {"inputs": ["$attributes", "$attr1"]}}}},
{"$set":
{"attr1":
{"$map":
{"input": "$attr1",
"in":
{"$mergeObjects":
[{"$arrayElemAt": ["$$this", 1]},
{"$arrayElemAt": ["$$this", 0]}]}}}}}])
You can use $zip
db.collection.aggregate([
{
"$project": {
attributes: {
"$zip": {
"inputs": [
"$attributes",
"$attr1"
]
}
}
}
}
])
Here is the Mongo playground for your reference.

Mongodb, Verify if my query range is not overlapping a list of Date Ranges in mongodb

I have the following data:
const Availabilities = new Schema({
id: Number,
reservations: [{ from: Date, to: Date }]
});
const Availability = mongoose.model('Availability', Availabilities);
Basically i want to get a list of products in which the dates do not overlap with the query
[
{
id: 1,
reservations: [
{
from: '2022-01-01',
to: '2022-03-31',
},
{
from: '2022-04-01',
to: '2022-06-01',
},
],
},
{
id: 2,
reservations: [
{
from: '2022-01-01',
to: '2022-12-31',
},
],
},
{
id: 3,
reservations: [
{
from: '2022-02-01',
to: '2022-06-30',
},
],
},
]
and I want to filter all those who are not in a specific range.
Example:
query = { from: "2022-07-01", to: "2022-08-31" }
should return
[
{
id: 1,
reservations: [
{
from: '2022-01-01',
to: '2022-03-31',
},
{
from: '2022-04-01',
to: '2022-06-01',
},
],
},
{
id: 3,
reservations: [
{
from: '2022-02-01',
to: '2022-06-30',
},
],
},
]
You can just check if one of two conditions are met, either:
to is smaller than the start of your input ( meaning the session ended before )
from is bigger than the end of your input ( meaning the session started after )
This is how it'll look in code:
db.collection.aggregate([
{
$match: {
reservations: {
$elemMatch: {
$or: [
{
to: {
$lt: "2022-07-01"
},
},
{
from: {
$gt: "2022-08-31"
}
}
]
}
}
}
},
{
$addFields: {
reservations: {
$filter: {
input: "$reservations",
cond: {
$or: [
{
$lt: [
"$$this.to",
"2022-07-01"
]
},
{
$gt: [
"$$this.to",
"2022-08-31"
]
}
]
}
}
}
}
}
])
Mongo Playground

MongoDB Aggregation - Query array of objects

I have a collection with this data:
[
{
_id: "123",
number: 10,
users: [
{
amount: 20
}
]
},
{
_id: "456",
number: 20,
users: [
{
amount: 10
}
]
}
]
I need to find documents where users[0].amount is greater than or equal to number. I have the following Mongoose query:
Model.aggregate([
{
$match: {
$expr: {
$and: [
{ $gte: ["$users.0.amount", "$number"] },
...
]
}
}
}
]);
However, this doesn't seem to be filtering the documents properly. I'm assuming $users.0.amount isn't the correct syntax. How can I fix this?
$expr requires Aggregation Framework's syntax so you have to use $arrayElemAt instead:
{ $gte: [ { $arrayElemAt: [ "$users.amount", 0] }, "$number" ] }
where users.amount is an array of numbers
MongoDB Playground

MongoDB Aggregation $size on Nested Field

I'm trying to perform a tricky aggregation to return the size of a nested array within a document in the collection.
Here is how to re-create my sample data:
db.test.insert({
projects: [
{
_id: 1,
comments: [
'a',
'b',
'c'
]
},
{
_id: 2,
comments: [
'a',
'b'
]
},
{
_id: 3,
comments: []
}
]
})
The aggregation I would perform goes here:
db.test.aggregate([
// enter aggregation here
])
Here is the desired output:
[{
projects: [
{
_id: 1,
comment_count: 3
},
{
_id: 2,
comment_count: 2
},
{
_id: 3,
comment_count: 0
}
]
}]
I'm struggling with how to write this. If I try the following:
"projects.comment_count": {"$size": }
The result returns the size of the resulting array:
[{
projects: [
{
_id: 1,
comment_count: 3
},
{
_id: 2,
comment_count: 3
},
{
_id: 3,
comment_count: 3
}
]
}]
If I try to use the $map method like this:
"projects.comment_count": {
"$map": {
"input": "$projects",
"as": "project",
"in": {
"$size": "$$project.comments"
}
}
}
It will return an array that looks like this for each object in the array:
[{
projects: [
{
_id: 1,
comment_count: [3, 2, 0]
},
{
_id: 2,
comment_count: [3, 2, 0]
},
{
_id: 3,
comment_count: [3, 2, 0]
}
]
}]
Thanks in advance!
Here is an idea using $unwind, $group and then $push with $size. Finally $project to get rid of that _id:
db.collection.aggregate([
{
"$unwind": "$projects"
},
{
$group: {
_id: null,
"projects": {
$push: {
_id: "$projects._id",
comment_count: {
$size: "$projects.comments"
}
}
}
}
},
{
"$project": {
"_id": 0
}
}
])
You can see the result here
You need to specify each field inside the in argument of $map aggregation and finally use $size with the comments array.
Something like this
db.collection.aggregate([
{ "$project": {
"projects": {
"$map": {
"input": "$projects",
"in": {
"_id": "$$this._id",
"comment_count": {
"$size": "$$this.comments"
}
}
}
}
}}
])
Output
[
{
"projects": [
{
"_id": 1,
"comment_count": 3
},
{
"_id": 2,
"comment_count": 2
},
{
"_id": 3,
"comment_count": 0
}
]
}
]

Categories

Resources