How to get sum of child entries for hierarchical documents? - mongodb

I have a document of the following form:
{
"name": "root1",
"children": [{
"name": "A",
"children": [{
"name": "A1",
"items": 20
}, {
"name": "A2",
"items": 19
}],
"items": 8
}, {
"name": "B",
"items": 12
}],
"items": 1
}
That is, each level has a "name" field, an "items" field, and optionally a children field. I would like to run a query which returns the total number of items for each root. In this example, it should return (since 20+19+8+12+1=60)
{ "_id" : "root1", "items" : 60 }
However, each document can have arbitrarily many levels. That is, this example has two to three children below the root, but other documents may have more. That is, I cannot do something like
db.myCollection.aggregate( { $unwind : "$children" },
{ $group : { _id : "$name", items: { $sum : "$items" } } } )
What sort of query will work?

There really is no way to descend arrays to arbitrary depths using the aggregation framework. For this sort of structure you need to use mapReduce where you can programatically do this:
db.collection.mapReduce(
function () {
var items = 0;
var action = function(current) {
items += current.items;
if ( current.hasOwnProperty("children") ) {
current.children.forEach(function(child) {
action( child );
});
}
};
action( this );
emit( this.name, items );
},
function(){},
{ "out": { "inline": 1 } }
)
If you do not want mapReduce then consider another structure for your data and do things differently:
{ "name": "root1", "items": 1, "path": [], "root": null },
{ "name": "A", "items": 8, "path": ["root1"], "root": "root1" },
{ "name": "A1", "items": 20, "path": ["root1", "A"], "root": "root1" },
{ "name": "A2", "items": 19, "path": ["root1", "A"], "root": "root1" },
{ "name": "B", "items": 12, "path": ["root1"], "root": "root1" }
Then you just have a simple aggregate:
db.collection.aggregate([
{ "$group": {
"_id": {
"$cond": [
"$root",
"$root",
"$name"
]
},
"items": { "$sum": "$items" }
}}
])
So if you take a different approach to mapping a hierarchy then doing things such as aggregating totals for paths is much easier without the recursive inspection that would otherwise be required.
The approach that you need depends on your actual usage requirements.

Related

MongoDb Aggregate Total Count Before Grouping

I have an aggregation pipeline that groups objects and holds count for some specific field for grouped objects. You can reproduce the problem here: https://mongoplayground.net/p/2DGaiQDYDBP .
The schema is like this;
[
{
"_id": {
"$oid": "63ce93ffb6e06322db59fdc0"
},
"fruit": "apple",
"source": "tree",
"is_fruit_important": "true"
},
{
"_id": {
"$oid": "63ce93ffb6e06322db59fdc1"
},
"fruit": "orange",
"source": "tree",
"is_fruit_important": "false"
},
]
and the current query groups fruits by the source, and holds the count of important fruits for every group. After applying aggregation I get something like this after query:
[
{
"count": {
"number_of_important_fruits": 1
},
"objects": [
{
"fruit": "apple",
"id": "63ce93ffb6e06322db59fdc0",
"is_fruit_important": "true",
"source": "tree"
},
{
"fruit": "orange",
"id": "63ce93ffb6e06322db59fdc1",
"is_fruit_important": "false",
"source": "tree"
}
],
"source": {
"source-of": "tree"
}
}
]
Is there a way to put the number of all fruits in the database to the response object. For example like this:
{
"total-count": 2,
"result": [
{
"count": {
"number_of_important_fruits": 1
},
"objects": [
{
"fruit": "apple",
"id": "63ce93ffb6e06322db59fdc0",
"is_fruit_important": "true",
"source": "tree"
},
{
"fruit": "orange",
"id": "63ce93ffb6e06322db59fdc1",
"is_fruit_important": "false",
"source": "tree"
}
],
"source": {
"source-of": "tree"
}
}
]
}
They can be handled in separate aggregation pipelines but that's what I would not like to implement. Any help would be highly appreciated.
Add one additional group stage just before the final $project, using $sum with $size for a total count, or add up the important counts for a total important count.
{$group: {
_id: null,
result: {$push: "$$ROOT"},
"count_total": {$sum: {$size: "$objects"}},
"count_important": {$sum: "$count.number_of_important_fruits"}
}},
Playground
You can simply add a $facet stage to push all your results into result. Then perform a $size on result to get total-count.
db.collection.aggregate([
...,
{
"$facet": {
"result": [],
"total-important-count": [
{
$group: {
_id: null,
cnt: {
$sum: "$count.number_of_important_fruits"
}
}
}
]
}
},
{
"$addFields": {
"total-count": {
$size: "$result"
},
"total-important-count": {
$first: "$total-important-count.cnt"
}
}
}
])
Mongo Playground

Delete objects that met a condition inside an array in mongodb

My collection has array "name" with objects inside. I need to remove only those objects inside array where "name.x" is blank.
"name": [
{
"name.x": [
{
"_id": "607e7fcca57aa56e2a06b57b",
"name": "abc",
"type": "123"
}
],
"_id": {
"$oid": "62232cd70ce38c5007de31e6"
},
"qty": "1.0",
"Unit": "pound,lbs"
},
{
"name.x": [
{
"_id": "607e7fcca57aa56e2a06b430",
"name": "xyz",
"type": "123"
}
],
"_id": {
"$oid": "62232cd70ce38c5007de31e7"
},
"qty": "1.0",
"Unit": "pound,lbs"
},{
"name.x": []
,
"_id": {
"$oid": "62232cd70ce38c5007de31e7"
},
"qty": "1.0",
"Unit": "pound,lbs"
}
I tried to get all the ids where name.x is blank using python and used $pull to remove objects base on those ids.But the complete array got deleted.How can I remove the objects that meet the condition.
Think MongoDB update with aggregation pipeline meets your requirement especially to deal with the field name with ..
$set - Update the name array field by $filter name.x field is not an empty array.
db.collection.update({},
[
{
$set: {
name: {
$filter: {
input: "$name",
cond: {
$ne: [
{
$getField: {
field: "name.x",
input: "$$this"
}
},
[]
]
}
}
}
}
}
],
{
multi: true
})
Sample Mongo Playground

Filtering a mongodb query result based on the position of a field in an array

Apologies for the confusing title, I am not sure how to summarize this.
Suppose I have the following list of documents in a collection:
{ "name": "Lorem", "source": "A" }
{ "name": "Lorem", "source": "B" }
{ "name": "Ipsum", "source": "A" }
{ "name": "Ipsum", "source": "B" }
{ "name": "Ipsum", "source": "C" }
{ "name": "Foo", "source": "B" }
as well an ordered list of accepted sources, where lower indexes signify higher priority
sources = ["A", "B"]
My query should:
Take a list of available sources and a list of wanted names
Return a maximum of one document per name.
In case of multiple matches, the document with the most prioritized source should be chosen.
Example:
wanted_names = ['Lorem', 'Ipsum', 'Foo', 'NotThere']
Result:
{ "name": "Lorem", "source": "A" }
{ "name": "Ipsum", "source": "A" }
{ "name": "Foo", "source": "B" }
The results don't necessarily have to be ordered.
Is it possible to do this with a Mongo query alone? If so could someone point me towards a resource detailing how to accomplish it?
My current solution doesn't support a list of names, and instead relies on a Python script to execute multiple queries:
db.collection.aggregate([
{$match: {
"name": "Lorem",
"source": {
$in: sources
}}},
{$addFields: {
"order": {
$indexOfArray: [sources, "$source"]
}}},
{$sort: {
"order": 1
}},
{$limit: 1}
]);
Note: _id fields are omitted in this question for the sake of brevity
How about this: With $group we have $min operator which takes lower source
Note: If you prioritize as ['B', 'A'], use $max then
db.collection.aggregate([
{
$match: {
"name": {
$in: [
"Lorem",
"Ipsum",
"Foo",
"NotThere"
]
},
"source": {
$in: [
"A",
"B"
]
}
}
},
{
$group: {
_id: "$name",
source: {
$min: "$source"
}
}
},
{
$project: {
_id: 0,
name: "$_id",
source: 1
}
}
])
MongoPlayground

MongoDB filter for specific data in Array and return only specific fields in the output

I have a below structure maintained in a sample collection.
{
"_id": "1",
"name": "Stock1",
"description": "Test Stock",
"lines": [
{
"lineNumber": "1",
"priceInfo": {
"buyprice": 10,
"sellprice": 15
},
"item": {
"id": "BAT10001",
"name": "CricketBat",
"description": "Cricket bat"
},
"quantity": 10
},
{
"lineNumber": "2",
"priceInfo": {
"buyprice": 10,
"sellprice": 15
},
"item": {
"id": "BAT10002",
"name": "CricketBall",
"description": "Cricket ball"
},
"quantity": 10
},
{
"lineNumber": "3",
"priceInfo": {
"buyprice": 10,
"sellprice": 15
},
"item": {
"id": "BAT10003",
"name": "CricketStumps",
"description": "Cricket stumps"
},
"quantity": 10
}
]
}
I have a scenario where i will be given lineNumber and item.id, i need to filter the above collection based on lineNumber and item.id and i need to project only selected fields.
Expected output below:
{
"_id": "1",
"lines": [
{
"lineNumber": "1",
"item": {
"id": "BAT10001",
"name": "CricketBat",
"description": "Cricket bat"
},
"quantity": 10
}
]
}
Note: I may not get lineNumber all the times, if lineNumber is null then i should filter for item.id alone and get the above mentioned output.The main purpose is to reduce the number of fields in the output, as the collection is expected to hold huge number of fields.
I tried the below query,
db.sample.aggregate([
{ "$match" : { "_id" : "1"} ,
{ "$project" : { "lines" : { "$filter" : { "input" : "$lines" , "as" : "line" , "cond" :
{ "$and" : [ { "$eq" : [ "$$line.lineNumber" , "3"]} , { "$eq" : [ "$$line.item.id" , "BAT10001"]}]}}}}}
])
But i got all the fields, i'm not able to exclude or include the required fields.
I tried the below query and it worked for me,
db.Collection.aggregate([
{ $match: { _id: '1' } },
{
$project: {
lines: {
$map: {
input: {
$filter: {
input: '$lines',
as: 'line',
cond: {
$and: [
{ $eq: ['$$line.lineNumber', '3'] },
{ $eq: ['$$line.item.id', 'BAT10001'] },
],
},
},
},
as: 'line',
in: {
lineNumber: '$$line.lineNumber',
item: '$$line.item',
quantity: '$$line.quantity',
},
},
},
},
},
])
You can achieve it with $unwind and $group aggregation stages:
db.collection.aggregate([
{$match: {"_id": "1"}},
{$unwind: "$lines"},
{$match: {
$or: [
{"lines.lineNumber":{$exists: true, $eq: "1"}},
{"item.id": "BAT10001"}
]
}},
{$group: {
_id: "$_id",
lines: { $push: {
"lineNumber": "$lines.lineNumber",
"item": "$lines.item",
"quantity": "$lines.quantity"
}}
}}
])
$match - sets the criterias for the documents filter. The first stage is takes document with _id = "1", the second takes only documents which have lines.lineNumber equal to "1" or item.id equal to "BAT10001".
$unwind - splits the lines array into seperated documents.
$group - merges the documents by the _id element and puts the generated object with lineNumber, item and quantity elements into the lines array.

Nested query update in a loop in mongodb

I have collection the structure of which is :-
Subscribed.insert({
"name": "Manager1",
"emailId": "arora.priya4172#gmail.com",
"category": "Finance",
"designation": 'Head',
"done": false,
"categorySubscribedUsers": [
{
"_id": "u4._id",
"username": "u4.profile.name",
"issuesNotToDisplay": []
},
{
"_id": "u4._id",
"username": "u4.profile.name",
"issuesNotToDisplay": []
},
{
"_id": "u4._id",
"username": "u4.profile.name",
"issuesNotToDisplay": []
}
]
});
I want to add the value in issuesNotToDisplay field. The query which I am using to update the issuesNotToDisplay field is as follows:-
Subscribed.update(
{
"_id":Subscribed.findOne({
"category": "Finance",
"categorySubscribedUsers.username" :"abhi"
})._id
},
{ "$addToSet": {
"categorySubscribedUsers.0.issueNotToDisplay": "25PEgZoMamLSTDdw7"
}}
)
This one I used in the console (browser and mongo) and it is working fine but when I replace the numerical value with the loop index (j here). It is not working why so? and what should be done to make it work.
The query with j index is as follows:-
Subscribed.update(
{
"_id":Subscribed.findOne({
"category": "Finance",
"categorySubscribedUsers.username" :"abhi"
})._id
},{
"$addToSet": {
"categorySubscribedUsers.j.issueNotToDisplay": "25PEgZoMamLSTDdw7"
}
}
)
Please anyone let me know how we can update the collection field inside a loop in mongodb.
Subscribed.update({
"category": "Finance",
"categorySubscribedUsers.username" : "u4.profile.name"
},
{
"$set":{
"categorySubscribedUsers.$.issueNotToDisplay": "25PEgZoMamLSTDdw7"
}
});