MongoDb: How to aggregate linked documents? - mongodb

I have two collections, Product and Stock
Below are example values
Product
{
"_id" : ObjectId("63513c705f31b4bcb75b80ce"),
"name" : "Coca-cola",
"stocks" : [
ObjectId("63513c705f31b4bcb75b80d0")
ObjectId("63513c705f31b4bcb75b80d1")
]
}
Stock
[{
"_id" : ObjectId("63513c705f31b4bcb75b80d0"),
"count" : 9,
"remaining" : 6,
"costPerItem" : 10,
"createdAt" : ISODate("2022-10-20T12:17:52.985+0000"),
},
{
"_id" : ObjectId("63513c705f31b4bcb75b80d1"),
"count" : 10,
"remaining" : 3,
"costPerItem" : 10,
"createdAt" : ISODate("2022-10-20T12:17:52.985+0000"),
}]
How do I query products whose sum of remaining stock (remaining field of stocks) is less than for example 100?

One option is to use:
$lookup with pipeline to get all the remaining stock count per stock
Sum it up using $sum
$match the relevant products
db.products.aggregate([
{$lookup: {
from: "stock",
let: {stocks: "$stocks"},
pipeline: [
{$match: {$expr: {$in: ["$_id", "$$stocks"]}}},
{$project: {_id: 0, remaining: 1}}
],
as: "remaining"
}},
{$set: {remaining: {$sum: "$remaining.remaining"}}},
{$match: {remaining: {$lt: 100}}}
])
See how it works on the playground example

Related

MongoDB Closest Match on properties

Let says I have a Users collection in MongoDB whose schema looks like this:
{
name: String,
sport: String,
favoriteColor: String
}
And lets say I passed in values like this to match a user on:
{ name: "Thomas", sport: "Tennis", favoriteColor:"blue" }
What I would like to do is match the user based off all those properties. However, if no user comes back, I would like to match a user on just these properties:
{sport: "Tennis", favoriteColor:"blue" }
And if no user comes back, I would like to match a user on just this property:
{ favoriteColor: "blue" }
Is it possible to do something like this in one query with Mongo? I saw the $switch condition in Mongo that will match on a case and then immediately return, but the problem is that I can't access the document it would have retrieved in the then block. It looks like you can only write strings in there.
Any suggestions on how to accomplish what I'm looking for?
Is the best thing (and only way) to just execute multiple User.find({...}) queries?
This is a good case to use MongoDB text index:
First you need to create text index on those fields:
db.users.ensureIndex({ name: "text", sport: "text", favoriteColor: "text" });
Then you can search the best match with "$text" limited by a number to show:
db.users.find( { $text: { $search: "Tennis blue Thomas" } } ).limit(10)
Try adding rank to all documents with weightage in aggregation pipeline, and sum the rank, $sort descending to get most matched documents on top
name -> 1
sport -> 2
favoriteColor -> 4
by doing this matching favoriteColor will always have higher weightage then sport and name or combination of both
aggregate pipeline
db.col.aggregate([
{$match : {
$or : [
{"name" : {$eq :"Thomas"}},
{"sport" : {$eq : "Tennis"}},
{"favoriteColor" : {$eq : "blue"}}
]
}},
{$addFields : {
rank : {$sum : [
{$cond : [{$eq : ["$name", "Thomas"]}, 1, 0]},
{$cond : [{$eq : ["$sport", "Tennis"]}, 2, 0]},
{$cond : [{$eq : ["$favoriteColor", "blue"]}, 4, 0]}
]}
}},
{$match : {rank :{$gt : 0}}},
{$sort : {rank : -1}}
])
Hope this query will satisfy your require condition, you can get relevant result in single db hit. Just create a query in aggregate pipeline
db.collection.aggregate([
{$match : {
$or : [
{"name" : {$eq :"Thomas"}},
{"sport" : {$eq : "Tennis"}},
{"favoriteColor" : {$eq : "blue"}}
]
}},
{$addFields : {
rank : {$sum : [
{$cond : [{$and:[{$eq : ["$name", "Thomas"]},{$eq : ["$sport", "Tennis"]},{$eq : ["$favoriteColor", "blue"]}] } , 1, 0]},
{$cond : [{$and:[{$eq : ["$name", "Thomas"]},{$eq : ["$sport", "Tennis"]}] } , 2, 0]},
{$cond : [{$and:[{$eq : ["$name", "Thomas"]}] } , 3, 0]},
]}
}},
{$group:{
_id:null,
doc:{$push:'$$ROOT'},
rank:{$max:'$rank'}
}},
{$unwind:'$doc'},
{$redact: {
$cond: {
if: { $eq: [ "$doc.rank", '$rank' ] },
then: "$$KEEP",
else: "$$PRUNE"
}
}},
{
$project:{
name:'$doc.name',
sport:'$doc.sport',
favoriteColor:'$doc.favoriteColor',
}}
])
Simply create a query builder for $match pipe in mongoDB aggregate pipeline or use it for find also, create JavaScript object variable and build your query dynamically.
var query={};
if(name!=null){
query['name']={ '$eq': name};
}
if(sport!=null){
query['sport']={ '$eq': sport};
}
if(favoriteColor!=null){
query['favoriteColor']={ '$eq': favoriteColor};
}
db.collection.find(query)
It will give exactly matched result on dynamic basis
Did you try with $or:https://docs.mongodb.com/manual/reference/operator/query/or/
I used it when I wanted to check if username or email exists..

Need a distinct count on multiple fields that were joined from another collection using mongodb aggregation query

I'm trying to use a mongodb aggregation query to join($lookup) two collections and then distinct count all the unique values in the joined array.
So my two collections look like this:
events-
{
"_id" : "1",
"name" : "event1",
"objectsIds" : [ "1", "2", "3" ],
}
Objects
{
"_id" : "1",
"name" : "object1",
"metaDataMap" : {
"SOURCE" : ["ABC", "DEF"],
"DESTINATION" : ["XYZ", "PDQ"],
"TYPE" : []
}
},
{
"_id" : "2",
"name" : "object2",
"metaDataMap" : {
"SOURCE" : ["RST", "LNE"],
"TYPE" : ["text"]
}
},
{
"_id" : "3",
"name" : "object3",
"metaDataMap" : {
"SOURCE" : ["NOP"],
"DESTINATION" : ["PHI", "NYC"],
"TYPE" : ["video"]
}
}
What I want to come out is when I do a $match on event _id=1 I want to join the metaDataMap and then distinct count all the keys like this:
Counts for event _id=1
SOURCE : 5
DESTINATION: 4
TYPE: 2
What I have so far is this:
db.events.aggregate([
{$match: {"_id" : id}}
,{$lookup: {"from" : "objects",
"localField" : "objectsIds",
"foreignField" : "_id",
"as" : "objectResults"}}
,{$project: {x: {$objectToArray: "$objectResults.metaDataMap"}}}
,{$unwind: "$x"}
,{$match: {"x.k": {$ne: "_id"}}}
,{$group: {_id: "$x.k", y: {$addToSet: "$x.v"}}}
,{$addFields: {size: {"$size":"$y"}} }
]);
This fails because $objectResults.metaDataMap is not an object it's an array. Any suggestions on how to solve this or a different way to do what I want to do?
Also I don't necessarily know what fields(keys) are in the metaDataMap array. And I don't want to count or include fields that might or might not exist in the Map.
This should do the trick. I tested it on your input set and deliberately added some dupe values like NYCshowing up in more than one DESTINATIONto ensure it got de-duped (i.e. distinct count as asked for).
For fun, comment out all the stages, then top down UNcomment it out to see the effect of each stage of the pipeline.
var id = "1";
c=db.foo.aggregate([
// Find a thing:
{$match: {"_id" : id}}
// Do the lookup into the objects collection:
,{$lookup: {"from" : "foo2",
"localField" : "objectsIds",
"foreignField" : "_id",
"as" : "objectResults"}}
// OK, so we've got a bunch of extra material now. Let's
// get down to just the metaDataMap:
,{$project: {x: "$objectResults.metaDataMap"}}
,{$unwind: "$x"}
,{$project: {"_id":0}}
// Use $objectToArray to get all the field names dynamically:
// Replace the old x with new x (don't need the old one):
,{$project: {x: {$objectToArray: "$x"}}}
,{$unwind: "$x"}
// Collect unique field names. Interesting note: the values
// here are ARRAYS, not scalars, so $push is creating an
// array of arrays:
,{$group: {_id: "$x.k", tmp: {$push: "$x.v"}}}
// Almost there! We have to turn the array of array (of string)
// into a single array which we'll subsequently dedupe. We will
// overwrite the old tmp with a new one, too:
,{$addFields: {tmp: {$reduce:{
input: "$tmp",
initialValue:[],
in:{$concatArrays: [ "$$value", "$$this"]}
}}
}}
// Now just unwind and regroup using the addToSet operator
// to dedupe the list:
,{$unwind: "$tmp"}
,{$group: {_id: "$_id", uniqueVals: {$addToSet: "$tmp"}}}
// Add size for good measure:
,{$addFields: {size: {"$size":"$uniqueVals"}} }
]);
I was able to generate required result using following query.
db.events.aggregate(
[
{$match: {"_id" : id}} ,
{$lookup: {
"from" : "objects",
"localField" : "objectsIds",
"foreignField" : "_id",
"as" : "objectResults"
}},
{$unwind: "$objectResults"},
{$project:{"A":"$objectResults.metaDataMap"}},
{$unwind: {path: "$A.SOURCE", preserveNullAndEmptyArrays: true}},
{$unwind:{ path: "$A.DESTINATION", preserveNullAndEmptyArrays: true}},
{$unwind:{ path: "$A.TYPE", preserveNullAndEmptyArrays: true}},
{$group:{"_id":"$_id","SOURCE":{$addToSet:"$A.SOURCE"},"DESTINATION":{$addToSet:"$A.DESTINATION"},"TYPE":{$addToSet:"$A.TYPE"}}},
{$addFields: {"SOURCE":{$size:"$SOURCE"},"DESTINATION":{$size:"$DESTINATION"},"TYPE":{$size:"$TYPE"}}},
{$project:{"_id":0}}]
).pretty()
Updated query for dynamic fields.
db.events.aggregate([
{
$match: {"_id" : id}} ,
{$lookup: {"from" : "objects","localField" : "objectsIds","foreignField" : "_id","as" : "objectResults"}},
{$unwind: "$objectResults"},
{$project:{"A":"$objectResults.metaDataMap"}},
{$project: {x: {$objectToArray: "$A"}}},
{$unwind: "$x"},
{$match: {"x.k": {$ne: "_id"}}},
{$unwind:"$x.v"},
{$group: {_id: "$x.k", y: {$addToSet: "$x.v"}}},
{$project:{"size":{$size:"$y"}}}]
).pretty()

Sorting aggregation addToSet result

Is there a way to get result of $addToSet as sorted array ?
I tried to expand the pipeline and $unwind the array, sort it and group it again ,
but still the result isn't sorted.
The arrays are pretty big and i try to avoid sort them in the the application.
Document Example :
{
"_id" : ObjectId("52a84825cc8391ab188b4567"),
"id" : 129624
"message" : "Sample",
"date" : "12-09-2013,17:34:34",
"dt" : ISODate("2013-12-09T17:34:34.000Z"),
}
Query :
db.uEvents.aggregate(
[
{$match : {dt : {$gte : new Date(2014,01,01) , $lt : new Date(2015,01,17)}}}
,{$sort : {dt : 1}}
, {$group : {
_id : {
id : "$id"
, year : {'$year' : "$dt"}
, month : {'$month' : "$dt"}
, day : {'$dayOfMonth' : "$dt"}
}
,dt : {$addToSet : "$dt"}
}}
]
);
Yes it is possible, but approach it differently. I'm just provide my own data for this, but you'll get the concept.
My Sample:
{ "array" : [ 2, 4, 3, 5, 2, 6, 8, 1, 2, 1, 3, 5, 9, 5 ] }
I'm going to "semi-quote" the CTO on this and state that Sets are considered to be unordered.
There is an actual JIRA, Google groups statement that goes something like that. So let's take it from "Elliot" and accept that this will be the case.
So if you want an ordered result, you have to massage that way with stages like this
db.collection.aggregate([
// Initial unwind
{"$unwind": "$array"},
// Do your $addToSet part
{"$group": {"_id": null, "array": {"$addToSet": "$array" }}},
// Unwind it again
{"$unwind": "$array"},
// Sort how you want to
{"$sort": { "array": 1} },
// Use $push for a regular array
{"$group": { "_id": null, "array": {"$push": "$array" }}}
])
And then do whatever. But now your array is sorted.
Since mongoDB version 5.2 you can do it without $unwind:
The $setIntersection create a set out of the array and the $sortArray sorts it:
db.collection.aggregate([
{$set: {array: {$setIntersection: ["$array"]}}},
{$set: {$sortArray: {input: "$array", sortBy: 1}}}
])

Summing Mongo Sub-Document Array

db.test3.find()
{ "_id" : 1, "results" : [{"result" : {"cost" : [ { "priceAmt" : 100 } ] } } ] }
I tried the following unsucessfully:
db.test3.aggregate({$group : {_id: "", total : {$sum:
$results.result.cost.priceAmt"}}}, {$project: {_id: 0, total: 1}})
{ "result" : [ { "total" : 0 } ], "ok" : 1 }
EDIT
Desired output:
100 // sum of each "priceAmt"
You'll have to use the $unwind operator to turn array items into individual documents.
db.test3.aggregate({$unwind: "$results"}, {$unwind: "$results.result.cost"}, {$group : {_id: "", total : {$sum: "$results.result.cost.priceAmt"}}}, {$project: {_id: 0, total: 1}})
The $unwind needs to be applied twice because you have a nested array.

group operations over arrays using Mongo aggregation framework

I'm using mongodb 2.2. I would like to use the new Aggregation Framework to do queries over my documents, but the elements are arrays.
Here an example of my $project result:
{
"type" : [
"ads-get-yyy",
"ads-get-zzz"
],
"count" : [
NumberLong(0),
NumberLong(10)
],
"latency" : [
0.9790918827056885,
0.9790918827056885
]
}
I want to group by type, so for "ads-get-yyy" to know how much is the average of count and how much is the average of the latency.
I would like to have something similar to the next query, but that works inside of the elements of every array:
db.test.aggregate(
{
$project : {
"type" : 1,
"count" : 1,
"latency" : 1
}
},{
$group : {
_id: {type : "$type"},
count: {$avg: "$count"},
latency: {$avg: "$latency"}
}
});
I'm just learning the new AF too, but I think you need to first $unwind the types so that you can group by them. So something like:
db.test.aggregate({
$project : {
"type" : 1,
"count" : 1,
"latency" : 1
}
},{
$unwind : "$type"
},{
$group : {
_id: {type : "$type"},
count: {$avg: "$count"},
latency: {$avg: "$latency"}
}
});