Mongodb loop through every distinct values and select tags using aggregate (facet) - mongodb

I have collection like this:
{
"labels": [{
"description": "Dog"
}, {
"description": "Red"
}, {
"description": "XXX"
}]
}
{
"labels": [{
"description": "Cat"
}, {
"description": "XXX"
}, {
"description": "Yellow"
}]
}
{
"labels": [{
"description": "Dog"
}, {
"description": "Red"
}, {
"description": "Yellow"
}]
}
{
"labels": [{
"description": "Bird"
}, {
"description": "XXX"
}, {
"description": "XXX"
}]
}
I want to filter for example only "Red" and "Yellow" colors from ALL elements and output document like this:
// because "Dog" appears 2 times so total = 2
{
description: "Dog",
total: 2,
colors: [
{ "_id": "Red", total: 2 },
{ "_id": "Yellow", total: 1 }
]
}
{
description: "Cat",
total: 1,
colors: [
{ "_id": "Yellow", total: 1 }
]
}
{
description: "Bird",
total: 1,
colors: []
}
{
description: "Red",
total: 2,
colors: [
{ _id: "Yellow", total: 1 }
]
}
{
description: "XXX",
total: 4,
colors: [
{ _id: "Yellow", total: 1 }
]
}
I can do this by using collection.distinct('labels.description') and then iterating through every single element + make a separate collection.count({ 'labels.description': 'Dog' }) like this:
for (...)
db.collection.aggregate([
{
"$match": {
"labels.description": valueFromLoop // (e.g. Dog)
}
},
{ $unwind : "$labels" },
{
"$group": {
"_id": "$labels.description",
"count": { "$sum": 1 }
}
},
{
"$match": {
"$or": [
{ "_id": "Red" },
{ "_id": "Yellow" }
]
}
},
{
"$sort": {
"count": -1
}
}
])
I want to do this in a single aggregation or mapReduce so that I could easily output it to new collection using $out instead of using Bulk operations separately, however I don't know if it's possible.

Try this:
let filter = ["Red", "Yellow"];
db.testcollection.aggregate([
{
$addFields: { bkp: "$labels" }
},
{ $unwind: "$labels" },
{
$addFields: {
bkp: {
$filter: {
input: "$bkp",
as: "item",
cond: {
$and: [
{ $ne: ["$$item.description", "$labels.description"] },
{ $in: ["$$item.description", filter] }
]
}
}
}
}
},
{
$unwind: {
path: "$bkp",
preserveNullAndEmptyArrays: true
}
},
{
$group: {
_id: {
key1: "$labels.description",
key2: { $ifNull: ["$bkp.description", false] }
},
total: { $sum: 1 }
}
},
{
$group: {
_id: "$_id.key1",
description: { $first: "$_id.key1" },
total: {
$sum: {
$cond: {
if: { $first: [["$_id.key2"]] },
then: 1,
else: "$total"
}
}
},
colors: {
$push: {
$cond: {
if: { $first: [["$_id.key2"]] },
then: {
_id: "$_id.key2",
total: "$total"
},
else: "$$REMOVE"
}
}
}
}
},
{ $project: { _id: 0 } }
]);

For some reason with code from both answers it does not count all tags properly.
I'm posting what works:
db.collection.aggregate([
{
$project: {
labels: 1,
result: {
$filter: {
input: "$labels",
as: "label",
cond: {
$or: [
{ $eq: ["$$label.description", "Blue"] },
{ $eq: ["$$label.description", "Red"] },
{ $eq: ["$$label.description", "Black-and-white"] },
{ $eq: ["$$label.description", "Purple"] },
{ $eq: ["$$label.description", "Orange"] },
{ $eq: ["$$label.description", "Yellow"] },
{ $eq: ["$$label.description", "Green"] },
{ $eq: ["$$label.description", "Teal"] }
]
}
}
}
}
},
{
$unwind: "$labels"
},
{
"$group": {
_id: "$labels.description",
x: {
$push: "$result.description"
},
total: { "$sum": 1 }
}
},
{
$project: {
x: {
$reduce: {
input: '$x',
initialValue: [],
in: {$concatArrays: ['$$value', '$$this']}
}
},
total: 1
}
},
{
$project: {
x: 1,
y: { $setUnion: "$x" },
total: 1
}
},
{
$project: {
_id: 0,
description: "$_id",
"colors": {
$map: {
input: "$y",
as: "item",
in: {
_id: "$$item",
count: {
$size: {
$filter: {
input: "$x",
as: "itemx",
cond: {
$eq: ["$$item", "$$itemx"]
}
}
}
}
}
}
},
total: 1
}
},
{
$out: "backgrounds_meta"
}
])

db.test2.aggregate([
{
$project: {
labels:1,
colours: {
$filter: {
input: "$labels",
as: "label",
cond: {
$or: [
{$eq:["Yellow","$$label.description"]},
{$eq:["Red", "$$label.description"]}
]
}
}
}
}
},
{$unwind:"$labels"},
{$group:{
_id: "$labels.description",
total: {$sum:1},
colours: {$addToSet:"$colours.description"}
}},
{
$project:{
_id:0,
description:"$_id",
total:1,
colours: {
$reduce:{
input: "$colours",
initialValue: [],
in: {$concatArrays: ["$$value", "$$this"]}
}
}
}
},
{
$unwind: {
path:"$colours",preserveNullAndEmptyArrays: true
}
},
{
$group:{
_id:{
description:"$description",
total:"$total",
colour:"$colours"
},
count: {
$sum: {$cond:[{$ifNull:["$colours",false]},1,0]}
}
}
},
{
$group:{
_id:{
description:"$_id.description",
total:"$_id.total"
},
colours: {
$push: {
$cond: [{$gt:["$count",0]},
{
"_id":"$_id.colour",
total:"$count"
},
"$$REMOVE"
]
}
}
}
},
{
$project: {
_id:0,
description: "$_id.description",
total: "$_id.total",
colours: 1
}
}
]);
**Edit In your answer, you are missing the Yellows for Red and Dog because you are taking the first item from $result with $arrayElemAt: ["$result.description", 0].
If description is a colour, do you also want to include the counts for itself in colours?
Never mind, you've updated the answer

Related

How to find and update a document in MongoDB

I am having a similar collection
db={
collectionA: [
{
"id": ObjectId("63b7c24c06ebe7a8fd11777b"),
"uniqueRefId": "UUID-2023-0001",
"products": [
{
"productIndex": 1,
"isProdApproved": false,
"productCategory": ObjectId("63b7c24c06ebe7a8fd11777b"),
"productOwners": [
{
_id: ObjectId("63b7c2fd06ebe7a8fd117781"),
iApproved: false
},
{
_id: ObjectId("63b7c2fd06ebe7a8fd117782"),
iApproved: false
}
]
},
{
"productIndex": 2,
"isProdApproved": false,
"productCategory": ObjectId("63b7c24c06ebe7a8fd11777b"),
"productOwners": [
{
_id: ObjectId("63b7c2fd06ebe7a8fd117781"),
iApproved: false
},
{
_id: ObjectId("63b7c2fd06ebe7a8fd117783"),
iApproved: false
}
]
},
{
"productIndex": 3,
"productCategory": "",
"productOwners": ""
}
]
}
]
}
I want to find the productOwner whose _id is 63b7c2fd06ebe7a8fd117781 in the productOwners and update the isApproved and isprodApproved to true. Other data will remain as it is.
I have tried this but it is only updating the first occurance
db.collectionA.update(
{
_id: ObjectId('63b7c24c06ebe7a8fd11777b'),
'products.productOwners._id': ObjectId('63b7c2fd06ebe7a8fd117781'),
},
{ $set: { 'products.$.productOwners.$[x].isApproved': true } },
{ arrayFilters: [{ 'x._id': ObjectId('63b7c2fd06ebe7a8fd117781') }] }
);
This one should work:
db.collection.updateMany({},
[
{
$set: {
products: {
$map: {
input: "$products",
as: "product",
in: {
$cond: {
if: { $eq: [{ $type: "$$product.productOwners" }, "array"] },
then: {
$mergeObjects: [
"$$product",
{ isProdApproved: { $in: [ObjectId("63b7c2fd06ebe7a8fd117781"), "$$product.productOwners._id"] } },
{
productOwners: {
$map: {
input: "$$product.productOwners",
as: 'owner',
in: {
$mergeObjects: [
"$$owner",
{ iApproved: { $eq: ["$$owner._id", ObjectId("63b7c2fd06ebe7a8fd117781")] } }
]
}
}
}
}
]
},
else: "$$product"
}
}
}
}
}
}
]
)
However, the data seem to be redundant. Better update only products.productOwners.iApproved and then derive products.isProdApproved from nested elements:
db.collection.aggregate([
{
$set: {
products: {
$map: {
input: "$products",
as: "product",
in: {
$cond: {
if: { $eq: [{ $type: "$$product.productOwners" }, "array"] },
then: {
$mergeObjects: [
"$$product",
{ isProdApproved: { $anyElementTrue: ["$$product.productOwners.iApproved"] } },
]
},
else: "$$product"
}
}
}
}
}
}
])

MongoDB match computed value

I've created an aggregate query but for some reason it doesn't seem to work for custom fields created in the aggregation pipeline.
return this.repository.mongo().aggregate([
{
$match: { q1_avg: { $regex: baseQuery['value'], $options: 'i' } }, // NOT WORKING
},
{
$group: {
_id: '$product_sku',
id: { $first: "$_id" },
product_name: { $first: '$product_name' },
product_category: { $first: '$product_category' },
product_sku: { $first: '$product_sku' },
q1_cnt: { $sum: 1 },
q1_votes: { $push: "$final_rating" }
},
},
{
$facet: {
pagination: [ { $count: 'total' } ],
data: [
{
$project: {
_id: 1,
id: 1,
product_name: 1,
product_category: 1,
product_sku: 1,
q1_cnt: 1,
q1_votes: {
$filter: {
input: '$q1_votes',
as: 'item',
cond: { $ne: ['$$item', null] }
}
},
},
},
{
$set: {
q1_avg: { $round: [ { $avg: '$q1_votes' }, 2 ] },
}
},
{ $unset: ['q1_votes'] },
{ $skip: skip },
{ $limit: limit },
{ $sort: sortList }
]
}
},
{ $unwind : "$pagination" },
]).next();
q1_avg value is an integer and as far as I know, regex only works with strings. Could that be the reason

Mongoose subquery

I have a collection that looks like below:
[
{
"orderNum": "100",
"createdTime": ISODate("2020-12-01T21:00:00.000Z"),
"amount": 100,
"memo": "100memo",
"list": [
1
]
},
{
"orderNum": "200",
"createdTime": ISODate("2020-12-01T21:01:00.000Z"),
"amount": 200,
"memo": "200memo",
"list": [
1,
2
]
},
{
"orderNum": "300",
"createdTime": ISODate("2020-12-01T21:02:00.000Z"),
"amount": 300,
"memo": "300memo"
},
{
"orderNum": "400",
"createdTime": ISODate("2020-12-01T21:03:00.000Z"),
"amount": 400,
"memo": "400memo"
},
]
and I'm trying to get the total amount of orders that were created before order# 300 (so order#100 and #200, total amount is 300).
Does anyone know how to get it via Mongoose?
You can use this one:
db.collection.aggregate([
{ $sort: { orderNum: 1 } }, // by default the order of documents in a collection is undetermined
{ $group: { _id: null, data: { $push: "$$ROOT" } } }, // put all documents into one document
{ $set: { data: { $slice: ["$data", { $indexOfArray: ["$data.orderNum", "300"] }] } } }, // cut desired elementes from array
{ $unwind: "$data" }, // transform back to documents
{ $replaceRoot: { newRoot: "$data" } },
{ $group: { _id: null, total_amount: { $sum: "$amount" } } } // make summary
])
Actually it is not needed to $unwind and $group, so the shortcut would be this:
db.collection.aggregate([
{ $sort: { orderNum: 1 } },
{ $group: { _id: null, data: { $push: "$$ROOT" } } },
{ $set: { data: { $slice: ["$data", { $indexOfArray: ["$data.orderNum", "300"] }] } } },
{ $project: { total_amount: { $sum: "$data.amount" } } }
])
But the answer from #turivishal is even better.
Update for additional field
{
$set: {
data: { $slice: ["$data", { $indexOfArray: ["$data.orderNum", "300"] }] },
memo: { $arrayElemAt: [ "$data.memo", { $indexOfArray: ["$data.orderNum", "300"] } ] }
}
}
or
{ $set: { data: { $slice: ["$data", { $indexOfArray: ["$data.orderNum", "300"] }] } } },
{ $set: { memo: { $last: { "$data.memo" } } },
$match orderNum less than 300
$group by null and get totalAmount using $sum of amount
YourSchemaModel.aggregate([
{ $match: { orderNum: { $lt: "300" } } },
{
$group: {
_id: null,
totalAmount: { $sum: "$amount" }
}
}
])
Playground

mongoDB updateMany based on nested array condition

I have following structure in users collection:
[
{ "name": "Ivan",
"payments": [
{"date": new Date("2019-01-01"), "details": [{"payment_system": "A", "spent": 95},
{"payment_system": "B", "spent": 123}]},
{"date": new Date("2019-01-03"), "details": [{"payment_system": "A", "spent": 12},
{"payment_system": "B", "spent": 11}]}]},
{ "name": "Mark",
"payments": [
{"date": new Date("2019-01-01"), "details": [{"payment_system": "D", "spent": 456},
{"payment_system": "B", "spent": 123}]},
{"date": new Date("2019-01-02"), "details": [{"payment_system": "A", "spent": 98},
{"payment_system": "C", "spent": 4}]}]}
]
Is it any way to add a field to users who spent more than, lets say 100 during the specific date range in specific payment system?
I tried updateMany, but have no idea how to filter "details" array element based on payment_system field.
For payment_system IN ("A", "C"), date >= "2019-01-02", spent_total >= 100 update should return
[
{ "name": "Ivan", ...},
{ "name": "Mark", "filter_passed": true, ... }
]
This this one:
db.collection.aggregate([
{
$set: {
payments: {
$filter: {
input: "$payments",
cond: { $gte: ["$$this.date", new Date("2019-01-02")] }
}
}
}
},
{
$set: {
spent_total: {
$reduce: {
input: "$payments.details.spent",
initialValue: [],
in: { $concatArrays: ["$$value", "$$this"] }
}
}
}
},
{ $set: { spent_total: { $sum: "$spent_total" } } },
{ $match: { "spent_total": { $gte: 100 } } }
])
Mongo Playground
Update:
Filter by payment_system is a bit longer. You have to $unwind and $group:
db.collection.aggregate([
{
$set: {
payments: {
$filter: {
input: "$payments",
cond: { $gte: ["$$this.date", new Date("2019-01-02")] }
}
}
}
},
{ $unwind: "$payments" },
{
$set: {
"payments.details": {
$filter: {
input: "$payments.details",
cond: { $in: ["$$this.payment_system", ["A", "C"]] }
},
},
}
},
{
$group: {
_id: { _id: "$_id", name: "$name", },
payments: { $push: "$payments" }
}
},
{
$set: {
spent_total: {
$reduce: {
input: "$payments.details.spent",
initialValue: [],
in: { $concatArrays: ["$$value", "$$this"] }
}
}
}
},
{ $set: { spent_total: { $sum: "$spent_total" } } },
{ $match: { "spent_total": { $gte: 100 } } },
{ // just some cosmetic
$project: {
_id: "$_id._id",
name: "$_id.name",
payments: 1
}
}
])
You cannot update your collection like db.collection.updateMany({}, [<the aggregation pipeline from above>]) because it contains $unwind and $group.
However, you can make $lookup or $out to save entire result into new collection.
If you need to sum up for each payment_system individually then try:
db.collection.aggregate([
{
$set: {
payments: {
$filter: {
input: "$payments",
cond: { $gte: ["$$this.date", new Date("2019-01-01")] }
}
}
}
},
{ $unwind: "$payments" },
{
$set: {
"payments.details": {
$filter: {
input: "$payments.details",
cond: { $in: ["$$this.payment_system", ["A", "B","C"]] }
},
},
}
},
{ $unwind: "$payments.details" },
{
$group: {
_id: {
_id: "$_id",
name: "$name",
payments: "$payments.details.payment_system"
},
spent_total: { $sum: "$payments.details.spent" }
}
},
{ $match: { "spent_total": { $gte: 100 } } },
{
$project: {
_id: "$_id._id",
name: "$_id.name",
payments: "$_id.payments",
spent_total: 1
}
}
])

MongoDB - group by on facet result

Provided I have following collections
Customers
[
{
uuid: "first",
culture: "it-it"
},
{
uuid: "second",
culture: "de-de"
}
]
Vehicles
[
{
model: "mymodel",
users: [
{
uuid: "first",
isOwner: true,
createdOn: "2019-05-15T06: 00: 00"
}
]
},
{
model: "mymodel",
users: [
{
uuid: "first",
isOwner: false,
createdOn: "2019-05-15T06: 00: 00"
},
{
uuid: "second",
isOwner: true,
createdOn: "2019-05-15T06: 00: 00"
}
]
}
]
And following query:
db.customers.aggregate([
{
$lookup: {
from: "vehicles",
let: {
uuid: "$uuid"
},
pipeline: [
{
$match: {
$expr: {
$in: [
"$$uuid",
"$users.uuid"
]
}
}
},
{
$project: {
model: 1,
users: {
$filter: {
input: "$users",
as: "user",
cond: {
$eq: [
"$$user.uuid",
"$$uuid"
]
}
}
}
}
},
{
$unwind: "$users"
},
{
$replaceRoot: {
newRoot: {
isOwner: "$users.isOwner",
createdOn: "$users.createdOn"
}
}
}
],
as: "vehicles"
}
},
{
$facet: {
"createdOn": [
{
$match: {
"vehicles": {
$elemMatch: {
isOwner: true,
$and: [
{
"createdOn": {
$gte: "2019-05-15T00: 00: 00"
}
},
{
"createdOn": {
$lt: "2019-05-16T00: 00: 00"
}
}
]
}
}
}
},
{
$project: {
culture: 1,
count: {
$size: "$vehicles"
}
}
},
{
$group: {
_id: 0,
"total": {
$sum: "$count"
}
}
}
]
}
},
{
$project: {
"CreatedOn": {
$arrayElemAt: [
"$CreatedOn.total",
0
]
}
}
}
])
I get following result:
[
{
"createdOn": 2
}
]
What I would like to achieve is a result as follows:
[
{
culture: "it-it",
results: {
"createdOn": 1
}
},
{
culture: "de-de",
results: {
"createdOn": 1
}
}
]
But I cannot seem to figure out where I can group so that I can get that result.
Can someone show me the way to do this?
The query is more complex with more metrics so this is a trimmed down version of what I have.
I tried grouping everywhere but fail to get the desired result I want.
The following query can get us the expected output:
db.customers.aggregate([
{
$lookup: {
"from": "vehicles",
"let": {
"uuid": "$uuid"
},
"pipeline": [
{
$unwind: "$users"
},
{
$match: {
$expr: {
$and: [
{
$eq: ["$users.uuid", "$$uuid"]
},
{
$eq: ["$users.isOwner", true]
},
{
$gte: ["$users.createdOn", "2019-05-15T00: 00: 00"]
},
{
$lte: ["$users.createdOn", "2019-05-16T00: 00: 00"]
}
]
}
}
},
{
$count:"totalVehicles"
}
],
as: "vehiclesInfo"
}
},
{
$unwind: {
"path": "$vehiclesInfo",
"preserveNullAndEmptyArrays": true
}
},
{
$group: {
"_id": "$culture",
"culture": {
$first: "$culture"
},
"createdOn": {
$sum: "$vehiclesInfo.totalVehicles"
}
}
},
{
$project: {
"_id": 0,
"culture": 1,
"results.createdOn": "$createdOn"
}
}
]).pretty()
Output:
{ "culture" : "de-de", "results" : { "createdOn" : 1 } }
{ "culture" : "it-it", "results" : { "createdOn" : 1 } }