Need to generate a structured document out of MongoDB collection - mongodb

We have a MongoDB collection like this:
{
a : string, nullable
b : string, nullable
c : boolean, not nullable
d : string, nullable
e : number, nullable
}
out of which we need the result like this:
{
ab : {
a: <count of a where a is not blank or null>,
b: <count of b where b is not blank or null>
},
c : {
true: <count of c where c true>,
false: <count of c where c false>
},
d : [<all distinct/unique values of d],
e : {
average : <average value of e>,
min : <minimum value of e>,
max : <maximum value of e>
}
}
We do not wish to fire multiple find queries and manipulate the result in-memory to bring up the results.
How do we achieve this using MongoDB's queries only? Any suggestions will be appreciated

You need to run multiple pipelines and then combine results. That's possible with $facet operator. Try below query:
db.col.aggregate([
{
$facet: {
q1: [
{ $match: { a: { $exists: true, $ne: null } } },
{ $count: "total" }
],
q2: [
{ $match: { b: { $exists: true, $ne: null } } },
{ $count: "total" }
],
q3: [
{ $match: { c: true } },
{ $count: "total" }
],
q4: [
{ $match: { c: false } },
{ $count: "total" }
],
q5: [
{
$group: {
_id: null,
unique: { $addToSet: "$d" }
}
}
],
q6: [
{
$group: {
_id: null,
average: { $avg: "$e" },
min: { $min: "$e" },
max: { $max: "$e" },
}
}
],
}
},
{
$project: {
q1: { $arrayElemAt: [ "$q1", 0 ] },
q2: { $arrayElemAt: [ "$q2", 0 ] },
q3: { $arrayElemAt: [ "$q3", 0 ] },
q4: { $arrayElemAt: [ "$q4", 0 ] },
q5: { $arrayElemAt: [ "$q5", 0 ] },
q6: { $arrayElemAt: [ "$q6", 0 ] }
}
},
{
$project: {
"ab.a": { $ifNull: [ "$q1.total", 0 ] },
"ab.b": { $ifNull: [ "$q2.total", 0 ] },
"c.true": { $ifNull: [ "$q3.total", 0 ] },
"c.false": { $ifNull: [ "$q4.total", 0 ] },
d: "$q5.unique",
"e.average": "$q6.average",
"e.min": "$q6.min",
"e.max": "$q6.max",
}
}
])
So q1-q6 are just separate aggregation pipelines. Each of them returns an array of results which can be converted to separate subdocuments using $arrayElemAt. Then you can use simple $project to reshape that into final result. Using $addToSet to get unique values for d and $ifNull to replace those counts when there's no value with default 0.

Related

How to remove field conditionally mongoodb

I have a collection and its documents look like:
{
_id: ObjectId('111111111122222222223333'),
my_array: [
{
id: ObjectId('777777777788888888889999')
name: 'foo'
},
{
id: ObjectId('77777777778888888888555')
name: 'foo2'
}
//...
]
//more attributes
}
However, some documents have my_array: [{}] (with one element which is an empty array).
How can I add conditionally a projection or remove it?
I have to add it to a mongo pipeline at the end of the query, and I want to get my_array only when it has at least one element which is not an empty object. If there's an empty object remove it.
I tried with $cond and $eq in a projection stage but it is not supported. Any suggestion to solve this?
Suppose you have documents like this with my_array field:
{ "my_array" : [ ] }
{ "my_array" : [ { "a" : 1 } ] } // #(1)
{ "my_array" : null }
{ "some_fld" : "some value" }
{ "my_array" : [ { } ] }
{ "my_array" : [ { "a" : 2 }, { "a" : 3 } ] } // #(2)
And, the following aggregation will filter and the result will have the two documents (1) and (2):
db.collection.aggregate([
{
$match: {
$expr: {
$and: [
{ $eq: [ { $type: "$my_array" }, "array" ] },
{ $gt: [ { $size: "$my_array" }, 0 ] },
{ $ne: [ [{}], "$my_array" ] }
]
}
}
}
])
This also works with a find method:
db.collection.find({
$expr: {
$and: [
{ $eq: [ { $type: "$my_array" }, "array" ] },
{ $gt: [ { $size: "$my_array" }, 0 ] },
{ $ne: [ [{}], "$my_array" ] }
]
}
})
To remove the my_array field, from a document when its empty, then you try this aggregation:
db.collection.aggregate([
{
$addFields: {
my_array: {
$cond: [
{$and: [
{ $eq: [ { $type: "$my_array" }, "array" ] },
{ $gt: [ { $size: "$my_array" }, 0 ] },
{ $ne: [ [{}], "$my_array" ] }
]},
"$my_array",
"$$REMOVE"
]
}
}
}
])
The result:
{ }
{ "my_array" : [ { "a" : 1 } ] }
{ }
{ "a" : 1 }
{ }
{ "my_array" : [ { "a" : 2 }, { "a" : 3 } ] }
You can't do that in a query, however in an aggregations you can add $filter to you pipeline, like so:
db.collection.aggregate([
{
$project: {
my_array: {
$filter: {
input: "$my_array",
as: "elem",
cond: {
$ne: [
{},
"$$elem"
]
}
}
}
}
}
])
Mongo Playground
However unless this is "correct" behavior I suggest you clean up your database, it's much simpler to maintain "proper" structure than to update all your queries everywhere.
You can use this update to remove these objects:
db.collection.update({
"myarray": {}
},
[
{
"$set": {
"my_array": {
$filter: {
input: "$my_array",
as: "elem",
cond: {
$ne: [
{},
"$$elem"
]
}
}
}
}
},
],
{
"multi": false,
"upsert": false
})
Mongo Playground

how to project field in array with mongodb

my collection in mongo db like this:
{
name:"mehdi",
grades:
[
{
a:1,
b:[2,3,4],
c:3,
d:4,
e:5
},
{
a:11,
b:[22,33,44],
c:33,
d:44,
e:55
}
]
}
I want to get a result with project op to give me a specific field in an array like this:
{
name:"mehdi",
grades:
[
{
a:1,
b:2
},
{
a:11,
b:22
}
]
}
how can I do this?
You can use $map to select a,b fields using $type to determine whether it's an array or number:
db.collection.aggregate([
{
$project: {
grades: {
$map: {
input: "$grades",
in: {
a: { $cond: [ { $eq: [ { $type: "$$this.a" }, "array" ] }, { $arrayElemAt: [ "$$this.a", 0 ] }, "$$this.a" ] },
b: { $cond: [ { $eq: [ { $type: "$$this.b" }, "array" ] }, { $arrayElemAt: [ "$$this.b", 0 ] }, "$$this.b" ] },
}
}
}
}
}
])
Mongo Playground

Aggregate and project with multiples conditions

I have a collection myCollection with array of members :
{
name : String,
members: [{status : Number, memberId : {type: Schema.Types.ObjectId, ref: 'members'}]
}
and i have this data
"_id" : ObjectId("5e83791eb49ab07a48e0282b")
"members" : [
{
"status" : 1,
"_id" : ObjectId("5e83791eb49ab07a48e0282c"),
"memberId" : ObjectId("5e7dbf5b257e6b18a62f2da9")
},
{
"status" : 2,
"_id" : ObjectId("5e837944b49ab07a48e0282d"),
"memberId" : ObjectId("5e7de2dbe027f43adf678db8")
}
],
I want to check by aggregate query if member 5e7dbf5b257e6b18a62f2da9 exists with status 1 but it didn't return true
db.getCollection('myCollection').aggregate([
{$match: {_id: ObjectId("5e83791eb49ab07a48e0282b")}},
{
$project: {
isMember: {
$cond: [
{ $and: [ {$in: [ObjectId("5e7dbf5b257e6b18a62f2da9"), '$members.memberId']}, {$eq: ['$members.status', 1]} ] },
// if
true, // then
false // else
]
}
}
}
])
Thank you for your responses.
If you want to get just true/false you can shortcut like this:
db.collection.aggregate([
{ $match: { _id: ObjectId("5e83791eb49ab07a48e0282b") } },
{
$project: {
isMember: {
$map: {
input: "$members",
in: {
$and: [
{ $eq: [ObjectId("5e7dbf5b257e6b18a62f2da9"), '$$this.memberId'] },
{ $eq: [1, '$$this.status'] }
]
}
}
}
}
},
{ $set: { isMember: { $anyElementTrue: "$isMember" } } }
])
A different style would be this:
db.collection.aggregate([
{ $match: { _id: ObjectId("5e83791eb49ab07a48e0282b") } },
{
$project: {
isMember: {
$map: {
input: "$members",
in: {
$eq: [
{ memberId: ("5e7dbf5b257e6b18a62f2da9"), status: 1 },
{ memberId: "$$this.memberId", status: "$$this.status" }
]
}
}
}
}
},
{ $set: { isMember: { $anyElementTrue: "$isMember" } } }
])

How to get several combinations of documents where sums of properties reach a certain value in Mongodb?

If we imagine this kind of document structure :
[
{
id: 1,
name: "",
values : {
a: 24,
b: 42
}
},
{
id: 2,
name: "",
values : {
a: 43,
b: 53
}
},
{
id: 3,
name: "",
values : {
a: 33,
b: 25
}
},
{
id: 4,
name: "",
values : {
a: 89,
b: 2
}
}
// ...
]
Is it possible to get one or more lists of documents where, for example, the sum of the $.values.a equals 100 and the sum of the $.values.b equals 120? Or if not is it possible to sort the bests fits with a kind of threshold?
For example, the best output can be something like that :
[
{
id: 1,
name: "",
values : {
a: 24,
b: 42
}
},
{
id: 2,
name: "",
values : {
a: 43,
b: 53
}
},
{
id: 3,
name: "",
values : {
a: 33,
b: 25
}
}
]
There is no any native implementation...
But, You can have desired results if your data meets some requirements:
You collection has no too much data (this solution scales badly)
Your id field is unique
Your collection has index for id field
Explanation
We sort by id
With $lookup with the same collection (it's important ´id´ to be indexed) and pick next 10 documents for the current document L i=(Doc i+1 ... Doc i+11)
With $reduce, we count from i ... i+n untill a > 100 and b > 120
With $facet, we separate lists which meets exactly a=100, b=120 results (equals) and threshold (+- 10 for values.a and values.b)
Last steps, if any equals exists, we ignore threshold. Otherwise, we take threshold.
db.collection.aggregate([
{
$sort: {
id: 1
}
},
{
$lookup: {
from: "collection",
let: {
id: "$id"
},
pipeline: [
{
$sort: {
id: 1
}
},
{
$match: {
$expr: {
$gt: [
"$id",
"$$id"
]
}
}
},
{
$limit: 10
}
],
as: "bucket"
}
},
{
$replaceRoot: {
newRoot: {
$reduce: {
input: "$bucket",
initialValue: {
a: "$values.a",
b: "$values.b",
data: [
{
_id: "$_id",
id: "$id",
name: "$name",
values: "$values"
}
]
},
in: {
a: {
$add: [
"$$value.a",
{
$cond: [
{
$and: [
{
$lt: [
"$$value.a",
100
]
},
{
$lt: [
"$$value.b",
120
]
}
]
},
"$$this.values.a",
0
]
}
]
},
b: {
$add: [
"$$value.b",
{
$cond: [
{
$and: [
{
$lt: [
"$$value.a",
100
]
},
{
$lt: [
"$$value.b",
120
]
}
]
},
"$$this.values.b",
0
]
}
]
},
data: {
$concatArrays: [
"$$value.data",
{
$cond: [
{
$and: [
{
$lt: [
"$$value.a",
100
]
},
{
$lt: [
"$$value.b",
120
]
}
]
},
[
"$$this"
],
[]
]
}
]
}
}
}
}
}
},
{
$facet: {
equals: [
{
$match: {
a: 100,
b: 120
}
}
],
threshold: [
{
$match: {
a: {
$gte: 90,
$lt: 110
},
b: {
$gte: 110,
$lt: 130
}
}
}
]
}
},
{
$project: {
result: {
$cond: [
{
$gt: [
{
$size: "$equals"
},
0
]
},
"$equals",
"$threshold"
]
}
}
},
{
$unwind: "$result"
}
])
MongoPlayground

aggregate operation coming as null in mongodb

Inventors
.aggregate([{
$match: filter
},
{
$group: {
"_id": {
"store_id": "$store_id"
},
stockAmount: {
$sum: {
$multiply: ["$intProductQty", "$dblMRP"]
}
},
storeValue: {
$sum: "$intProductQty"
},
}
},
])
.exec(function(err, stock) {
return res.send(stock);
});
schema
{
"store_id" : "BST000433",
"strProductCode" : "9000000064775",
"dblMRP" : 25,
"intProductQty" : 1,
}
I initailized these fields(intProductQty, dblMRP, strPurchasePrice) as integer. But when I execute above command, I'm getting that three values(stockAmount, purchaseAmount, storeValue) as null.
If it is still possible that some of those values are not set, you could check if they are null with $ifNull and set them to 0 for those calculations in a $project step after the $match:
$project: {
intProductQty: { $ifNull: [ "$intProductQty", 0 ] },
dblMRP: { $ifNull: [ "$dblMRP", 0 ] },
strPurchasePrice: { $ifNull: [ "$strPurchasePrice", 0 ] }
},
Also, I guess it's not your case, but you could filter out those that are not numeric with $type:
$match: {
intProductQty: { $type: "number" },
dblMRP: { $type: "number" },
strPurchasePrice: { $type: "number" }
},