Not able to find right query in MongoDB - node.js

I have been trying to come up with a query for these (simplified) documents below. My database consists of several data similar as these.
Since there is no nested querying in Mongo shell, is there another possible way to get what I want?
I am trying to get a list of Medicines that are owned by more than 30% of the pharmacies in my DB (regardless of quantity).
[
{
"Pharmacy": "a",
"Medicine": [
{
"MedName": "MedA",
"Quantity": 55
},
{
"MedName": "MedB",
"Quantity": 34
},
{
"MedName": "MedD",
"Quantity": 25
}
]
},
{
"Pharmacy": "b",
"Medicine": [
{
"MedName": "MedB",
"Quantity": 60
},
{
"MedName": "MedC",
"Quantity" : 34
}
]
}
]
How can I do this (if possible)?

Please check the answer here: https://mongoplayground.net/p/KVZ4Ee9Qhu-
var PharmaCount = db.collection.count();
db.collection.aggregate([
{
"$unwind": "$Medicine"
},
{
"$project": {
"medName": "$Medicine.MedName",
"Pharmacy": "$Pharmacy"
}
},
{
"$group": {
"_id": {
"medName": "$medName"
},
"count": {
"$sum": 1
}
}
},
{
"$project": {
"count": 1,
"percentage": {
"$concat": [
{
"$substr": [
{
"$multiply": [
{
"$divide": [
"$count",
{
"$literal": 2 // Your total number of pharmacies i.e PharmaCount
}
]
},
100
]
},
0,
3
]
},
"",
"%"
]
}
}
}
])
You should get results like:
[
{
"_id": {
"medName": "MedC"
},
"count": 1,
"percentage": "50%"
},
{
"_id": {
"medName": "MedD"
},
"count": 1,
"percentage": "50%"
},
{
"_id": {
"medName": "MedB"
},
"count": 2,
"percentage": "100%"
},
{
"_id": {
"medName": "MedA"
},
"count": 1,
"percentage": "50%"
}
]
Hope this helps.

You can not do this in a single query, but here is a way :
size = (db['01'].distinct("Pharmacy")).length;
minPN = Math.ceil(size*0.3);
db['01'].aggregate(
// Pipeline
[
// Stage 1
{
$unwind: {
path : "$Medicine",
}
},
// Stage 2
{
$group: {
_id:"$Medicine.MedName",
pharmacies:{$addToSet:"$Pharmacy"}
}
},
// Stage 3
{
$project: {
pharmacies:1,
pharmacies_count:{$size:"$pharmacies"}
}
},
{
$match:{pharmacies_count:{$gte:minPN}}
}
]
);

Related

How to bulkWrite decrease quantity in products when order mongodb?

I have table "products" in mongodb example:
{
"_id": "62ab02ebd3e608133c947798",
"status": true,
"name": "Meat",
"type": "62918ab4cab3b0249cbd2de3",
"price": 34400,
"inventory": [
{
"_id": "62af007abb78a63a44e88561",
"locator": "62933b3fe744ac34445c4fc0",
"imports": [
{
"quantity": 150,
"_id": "62aefddcd5b52c1da07521f2",
"date_manufacture": "2022-03-01T10:43:11.842Z",
"date_expiration": "2023-05-20T10:43:20.431Z"
},
{
"quantity": 200,
"_id": "62af007abb78a63a44e88563",
"date_manufacture": "2022-04-01T10:45:01.711Z",
"date_expiration": "2023-05-11T10:45:06.882Z"
}
]
},
{
"_id": "62b3c2545a78fb4414dd718f",
"locator": "62933e07c224b41fc48a1182",
"imports": [
{
"quantity": 120,
"_id": "62b3c2545a78fb4414dd7190",
"date_manufacture": "2022-03-01T01:30:07.053Z",
"date_expiration": "2023-05-01T10:43:20.431Z"
}
]
}
],
}
I want to decrease quantity in one locator by id in imports of inventory with multiple product (bulkWrite). And can I decrease quantity sort by date_expiration?
Example: when customer order product with quantity 300 and locator 62933b3fe744ac34445c4fc0, I want to product update belike:
{
...
"name": "Meat",
"price": 34400,
"inventory": [
{
"_id": "62af007abb78a63a44e88561",
"locator": "62933b3fe744ac34445c4fc0",
"imports": [
{
"quantity": 50,
"_id": "62aefddcd5b52c1da07521f2",
"date_manufacture": "2022-03-01T10:43:11.842Z",
"date_expiration": "2023-05-20T10:43:20.431Z"
}
]
},
{
"_id": "62b3c2545a78fb4414dd718f",
"locator": "62933e07c224b41fc48a1182",
"imports": [
{
"quantity": 120,
"_id": "62b3c2545a78fb4414dd7190",
"date_manufacture": "2022-03-01T01:30:07.053Z",
"date_expiration": "2023-05-01T10:43:20.431Z"
}
]
}
],
}
Thank you so much!
You should refactor your schema as nesting array as it is considered an anti-pattern and introduces unnecessary complexity to query.
One of the options:
db={
"products": [
{
"_id": "62ab02ebd3e608133c947798",
"status": true,
"name": "Meat",
"type": "62918ab4cab3b0249cbd2de3",
"price": 34400,
"inventory": [
"62af007abb78a63a44e88561",
"62b3c2545a78fb4414dd718f"
]
}
],
"inventory": [
{
"_id": "62af007abb78a63a44e88561",
"locator": "62933b3fe744ac34445c4fc0",
"imports": [
{
"quantity": 150,
"_id": "62aefddcd5b52c1da07521f2",
"date_manufacture": ISODate("2022-03-01T10:43:11.842Z"),
"date_expiration": ISODate("2023-05-20T10:43:20.431Z")
},
{
"quantity": 200,
"_id": "62af007abb78a63a44e88563",
"date_manufacture": ISODate("2022-04-01T10:45:01.711Z"),
"date_expiration": ISODate("2023-05-11T10:45:06.882Z")
}
]
},
{
"_id": "62b3c2545a78fb4414dd718f",
"locator": "62933e07c224b41fc48a1182",
"imports": [
{
"quantity": 120,
"_id": "62b3c2545a78fb4414dd7190",
"date_manufacture": ISODate("2022-03-01T01:30:07.053Z"),
"date_expiration": ISODate("2023-05-01T10:43:20.431Z")
}
]
}
]
}
You can then do something relatively simple. Use $sortArray to sort the date_expiration and start to iterate through the arrays using $reduce.
db.inventory.aggregate([
{
$match: {
locator: "62933b3fe744ac34445c4fc0"
}
},
{
"$set": {
"imports": {
$sortArray: {
input: "$imports",
sortBy: {
date_expiration: 1
}
}
}
}
},
{
$set: {
result: {
"$reduce": {
"input": "$imports",
"initialValue": {
"qtyToDecrease": 300,
"arr": []
},
"in": {
"qtyToDecrease": {
$subtract: [
"$$value.qtyToDecrease",
{
$min: [
"$$value.qtyToDecrease",
"$$this.quantity"
]
}
]
},
"arr": {
"$concatArrays": [
"$$value.arr",
[
{
"$mergeObjects": [
"$$this",
{
"quantity": {
$subtract: [
"$$this.quantity",
{
$min: [
"$$value.qtyToDecrease",
"$$this.quantity"
]
}
]
}
}
]
}
]
]
}
}
}
}
}
},
{
$set: {
imports: "$result.arr",
result: "$$REMOVE"
}
},
{
"$merge": {
"into": "inventory",
"on": "_id"
}
}
])
Mongo Playground
Here is another version that keeps your original schema. You can see it is much more complex.

Is it a good idea writing multiple aggregate in mongodb?

My schema looks like this
{
_id: ObjectID,
gender: "MALE", // MALE or FEMALE
status: "ACTIVE", // ACTIVE or INACTIVE
method: "A" // A or B
}
The API needs to return a total document count, total count by gender, total count by status and total count by method. My current approach is making multiple aggregate calls and one count method.
As such,
const genderCursor = db.collection(Collection.Sample).aggregate([
{"$group": { _id: "$gender", count: { $sum: 1 }}}
]);
const statusCursor = db.collection(Collection.Sample).aggregate([
{"$group": { _id: "$status", count: { $sum: 1 }}}
]);
const methodCursor = db.collection(Collection.Sample).aggregate([
{"$group": { _id: "$method", count: { $sum: 1 }}}
]);
const total = await db.collection(Collection.Sample).count();
await genderCursor.forEach(x => gender.push({ name: x._id, count: x.count}))
await statusCursor.forEach(x => statuses.push({ name: x._id, count: x.count}))
await methodCursor.forEach(x => methods.push({ name: x._id, count: x.count}))
Results,
{
"total": 100,
"gender": [
{
"name": "MALE",
"count": 30
},
{
"name": "FEMALE",
"count": 70
},
],
"statuses": [
{
"name": "APPROVED",
"count": 81
},
{
"name": "CREATED",
"count": 19
},
],
"methods": [
{
"name": "A",
"count": 50
},
{
"name": "B",
"count": 50
},
],
}
Is there a better and cost effective method to achieve the same thing as above?
You should combine all the queries into a single Aggregation Query since it will reduce your network roundtrip times and load on MongoDB servers.
There are two methods in doing this.
Method-1: Using null Group
You can group with _id null and apply $cond Operator. This is much
faster than the second method, but you have to apply all the outcomes required in the $cond.
Choose whichever method works best for your use case.
db.collection.aggregate([
{
"$group": {
"_id": null,
"male": {
"$sum": {
"$cond": {
"if": {
"$eq": [
"$gender",
"MALE"
]
},
"then": 1,
"else": 0,
},
},
},
"female": {
"$sum": {
"$cond": {
"if": {
"$eq": [
"$gender",
"FEMALE"
]
},
"then": 1,
"else": 0,
},
}
},
"active": {
"$sum": {
"$cond": {
"if": {
"$eq": [
"$status",
"ACTIVE"
]
},
"then": 1,
"else": 0,
},
}
},
"inactive": {
"$sum": {
"$cond": {
"if": {
"$eq": [
"$status",
"INACTIVE"
]
},
"then": 1,
"else": 0,
}
},
},
"methodA": {
"$sum": {
"$cond": {
"if": {
"$eq": [
"$method",
"A"
]
},
"then": 1,
"else": 0,
},
}
},
"methodB": {
"$sum": {
"$cond": {
"if": {
"$eq": [
"$method",
"B"
]
},
"then": 1,
"else": 0,
},
},
}
}
},
])
Mongo Playground Sample Execution
Method-2: Using $facet
You can also use the $facet stage, but it requires more computation on MongoDB compared with $group, but you don't have to write all the outcomes manually.
db.collection.aggregate([
{
"$facet": {
"gender": [
{
"$group": {
"_id": "$gender",
"count": {
"$sum": 1
}
}
},
],
"status": [
{
"$group": {
"_id": "$status",
"count": {
"$sum": 1
}
}
},
],
"method": [
{
"$group": {
"_id": "$method",
"count": {
"$sum": 1
}
}
},
],
}
}
])
Mongo Playground Sample Execution

Use Mongoose aggregate to fetch object inside of an array

Here is my MongoDB schema:
{
"_id": "603f23ff6c1d862e5ced9e35",
"reviews": [
{
"like": 0,
"dislike": 0,
"_id": "603f23ff6c1d862e5ced9e34",
"userID": "5fd864abb53d452e0cbb5ef0",
"comment": "Not so good",
},
{
"like": 0,
"dislike": 0,
"_id": "603f242a6c1d862e5ced9e36",
"userID": "5fd864abb53d452e0cbb5ef0",
"comment": "Not so good",
}
]
productID:"hdy6nch99dndn"
}
I want to use aggregate to get the review object of a particular id. I tried but not with any success.
Here is my code:
ProductReview.aggregate([
{ $match: { productID: productID } }
])
$match
$unwind
db.collection.aggregate([
{
$match: {
productID: 1
}
},
{
$unwind: "$reviews"
},
{
$match: {
"reviews._id": 2
}
}
])
Output:
[
{
"_id": ObjectId("5a934e000102030405000000"),
"productID": 1,
"reviews": {
"_id": 2,
"comment": "second comment",
"dislikes": [
{
"userID": 3
},
{
"userID": 4
}
],
"likes": [
{
"userID": 1
},
{
"userID": 2
}
]
}
}
]
Mongo Playground: https://mongoplayground.net/p/qfWS1rCuMfc

Projecting flat values from nested objects

I am writing a small aggregation in mongoose to filter values from db and return them in order of frequency they occur.
For example:
Say multiple documents have partners array as a field, which is an array of objects, each having two values "partner_id" and "passed_tests". I want to return all the unique partners from all the documents sorted according to their frequency in the collection in decreasing order.
Here is a sample document:
{
"location": "eindhoven",
"partners": [
{
"partner_id": 3,
"passed_tests": true
},
{
"partner_id": 2,
"passed_tests": false
}
],
"_id": "3136323031333066306d4438",
"uid": "d95f2e446c052514c097e6c925408774",
"__v": 0,
"is_approved": true
}
My code is as follows:
function returnAll(callback) {
TestService.aggregate([
{
$match: { "is_approved": true }
},
{
$unwind: "$partners"
}, {
$group: {
"_id": {
partner: { $objectToArray: "$partners" },
partner_id: { $arrayElemAt: ["$partner", 0] }
},
"count": { "$sum": 1 }
}
},
{
$sort: { "count": -1 }
},
{
$project: {
"partner_values": {
$map: {
input: "$_id.partner",
as: "el",
in: {
$cond: {
if: {
$or: [{ $eq: ["$$el.v", true] },
{ $eq: ["$$el.v", false] }]
}, then: {
"passed_tests": "$$el.v"
}, else: {
"id": "$$el.v"
}
}
}
}
},
"count": "$count,
"_id": 0
}
},
], function (error, data) {
if (error) {
logger.error(error);
callback(null);
} else {
callback(data);
}
});
}
Which returns this JSON to my node app:
"data": [
{
"partner_values": [
{
"id": 2
},
{
"passed_tests": false
}
],
"count": 3
},
{
"partner_values": [
{
"id": 6
},
{
"passed_tests": true
}
],
"count": 1
},
{
"partner_values": [
{
"id": 3
},
{
"passed_tests": true
}
],
"count": 1
},
{
"partner_values": [
{
"id": 1
},
{
"passed_tests": true
}
],
"count": 1
}
]
If I don't use the projection pipeline in my aggregation, I get this:
"data": [
{
"_id": {
"partner": [
{
"k": "partner_id",
"v": 2
},
{
"k": "passed_tests",
"v": false
}
],
"partner_id": null
},
"count": 3
},
{
"_id": {
"partner": [
{
"k": "partner_id",
"v": 6
},
{
"k": "passed_tests",
"v": true
}
],
"partner_id": null
},
"count": 1
},
{
"_id": {
"partner": [
{
"k": "partner_id",
"v": 3
},
{
"k": "passed_tests",
"v": true
}
],
"partner_id": null
},
"count": 1
},
{
"_id": {
"partner": [
{
"k": "partner_id",
"v": 1
},
{
"k": "passed_tests",
"v": true
}
],
"partner_id": null
},
"count": 1
}
]
which is quite understandable as I am introducing "_id" and other extra fields to look into the array and find the value. However, the output I want is:
"data": {
"partners": [{
"id": 2,
"passed_tests": false,
"count": 3
}, {
"id": 6,
"passed_tests": false,
"count": 1
}, {
"id": 3,
"passed_tests": false,
"count": 1
}, {
"id": 1,
"passed_tests": false,
"count": 1
}]
}
Can I get some help on this, please? Thanks.
You can use below aggregation query.
TestService.aggregate([
{"$match":{"is_approved":true}},
{"$unwind":"$partners"},
{"$group":{
"_id":{"partner_id":"$partners.partner_id","passed_tests":"$partners.passed_tests"},
"count":{"$sum":1}
}},
{"$sort":{"count":-1}},
{"$group":{
"_id":null,
"partners":{"$push":{"id":"$_id.partner_id","passed_tests":"$_id.passed_tests","count":"$count"}}
}},
{"$project":{"partners":1}}
])

nodejs + mongodb error exception: FieldPath 'progress' doesn't start with $

I'm trying to modify the second pipeline from this query (which I got from here nodejs + mongoose - query aggregate
db.todos.aggregate([
{
"$group": {
"_id": "$pic",
"open_count": {
"$sum": {
"$cond": [ { "$eq": [ "$status", "open" ] }, 1, 0 ]
}
},
"progress_count": {
"$sum": {
"$cond": [ { "$eq": [ "$status", "progress" ] }, 1, 0 ]
}
},
"done_count": {
"$sum": {
"$cond": [ { "$eq": [ "$status", "done" ] }, 1, 0 ]
}
},
"archive_count": {
"$sum": {
"$cond": [ { "$eq": [ "$status", "archive" ] }, 1, 0 ]
}
}
}
},
{
"$group": {
"_id": "$_id",
"detail": {
"$push": {
"name": "open",
"$todos": "$open_count"
},
"$push": {
"name": "progress",
"$todos": "$progress_count"
},
"$push": {
"name": "done",
"$todos": "$done_count"
},
"$push": {
"name": "archive",
"$todos": "$archive_count"
}
}
}
},
{
"$project": {
"_id": 0, "pic": "$_id", "detail": 1
}
}
])
I want this kind of JSON structure so I can put it on google chart, which the format is like this:
[
{
"pic": "A",
"detail": [
{
"name": "open",
"todos": 2
},
{
"name": "progress",
"todos": 1
},
{
"name": "done",
"todos": 8
},
{
"name": "archive",
"todos": 20
}
],
"pic": "B",
"detail": [
{
"name": "open",
"todos": 5
},
{
"name": "progress",
"todos": 2
},
{
"name": "done",
"todos": 5
},
{
"name": "archive",
"todos": 10
}
],
}
]
But I got this error
exception: FieldPath 'progress' doesn't start with $
Try with this aggregation query:
db.todos.aggregate([
{
"$group": {
"_id": {
"pic": "$pic",
"name": "$status"
},
"todos": {
"$sum": 1
}
}
},
{
"$project": {
"_id": 0,
"pic": "$_id.pic",
"detail": {
"name": "$_id.name",
"todos": "$todos"
}
}
},
{
"$group": {
"_id": "$pic",
"detail": {
"$push": "$detail"
}
}
},
{
"$project": {
"_id": 0, "pic": "$_id", "detail": 1
}
}])

Resources