MongoDB aggregate fill/replace array of ObjectId - arrays

I have the following MongoDB structure:
campaign:{
_id: '5e4eee638552043e60e5073b',
name: 'Test campaign',
partners: [
{
_id: '5e4fa9fbbdeb4a5878ac2a35',
options: [Object],
partner: '5e4e9bc2101ecb2e8764190c',
},
{
_id: '5e4ff7c6e54b6d676d97b7a6',
options: [Object],
partner: '5e4ff51c23f44266a8add39e',
}
]
}
Using the following aggregate function, I can lookup the ID of each of the partners and add to a new array:
{
$lookup:
{
from: 'campaigns',
let: { campaign: '$campaign' },
pipeline: [
{ $match: { $expr: { $and: [{ $eq: ['$_id', '$$campaign'] }] } } },
{
$lookup:
{
from: 'partners',
let: { partner: '$partner.partner' },
pipeline: [
{ $match: { $expr: { $in: ['$_id', '$$partner'] } } },
{ $project: { _id: 1, name: 1 } }
],
as: 'partner1'
}
},
{
$project: {
_id: 1, shortName: 1, userInterface: 1, options: 1, check: 1, description: 1, partner: 1, partner1: 1 } }
],
as: 'campaign'
}
}
This is new output:
campaign: {
_id: '5e4eee638552043e60e5073b',
name: 'Test campaign',
partners: [
...
],
partner1: [
{ _id: '5e4e9bc2101ecb2e8764190c', name: '123' },
{ _id: '5e4ff51c23f44266a8add39e', name: '456' }
]
}
But what I want to do is have the _id of each element in partner array be replaced with the full partner object.
Partner object:
partner
{
_id: '5e4e9bc2101ecb2e8764190c',
name: '123',
etc: ...
},
{
_id: '5e4ff51c23f44266a8add39e',
name: '456'
etc: ...
}
This is what the final object should look like:
[
{
"campaign": [
{
"partner": [
{
"_id": "5e4eee638552043e60e5073b",
"options": [
{}
],
"partner": {
"_id": "5e4e9bc2101ecb2e8764190c",
"name": "123"
}
}
]
}
]
}
]

You need to add extra 3 stages:
db.collection.aggregate([
//your $lookup,
{
$unwind: "$campaign"
},
{
$project: {
_id: 1,
"campaign.partner": {
$map: {
input: "$campaign.partner",
as: "partner",
in: {
$let: {
vars: {
partner1: {
$filter: {
input: "$campaign.partner1",
cond: {
$eq: [
"$$partner.partner",
"$$this._id"
]
}
}
}
},
in: {
// we merge partner[i] with partner1[j]
$mergeObjects: [
"$$partner",
{
partner: {
$arrayElemAt: [
"$$partner1",
0
]
}
}
]
}
}
}
}
}
}
},
{
$group: {
_id: "$_id",
campaign: {
$push: "$campaign"
}
}
}
])
MongoPlayground

Related

Remove 3 similar items from an array using mongodb

I have this document and i need remove only 3 items with name "Test" using only 1 request.
{ "_id" : 1, "items" : ["Test, "Test", "Test", "Test", "Test", "Sword", "Sword]}
It must become { "_id" : 1, "items" : ["Test", "Test", "Sword", "Sword]} after request.
Maybe something like this:
db.collection.aggregate([
{
"$unwind": "$items"
},
{
$group: {
_id: "$items",
cnt: {
$sum: 1
},
it: {
$push: "$items"
},
origid: {
"$first": "$_id"
}
}
},
{
$project: {
_id: "$origid",
items: {
$slice: [
"$it",
0,
2
]
}
}
},
{
$group: {
_id: "$_id",
items: {
$push: "$items"
}
}
},
{
$addFields: {
items: {
"$reduce": {
"input": "$items",
"initialValue": [],
"in": {
"$concatArrays": [
"$$this",
"$$value"
]
}
}
}
}
}
])
Explained:
unwind the items array so you can group after
group by item so you get the repeating values
project with slice to remove more then 2x items in array
group to form back the document but without the >2 repeating values
project with concatArrays to concat arrays of items.
Playground
You can use this aggregation to get the desired result. The query uses the $reduce Aggregate Array Operator to iterate over the items array, and discards the first 3 matching "Test" items.
db.collection.aggregate([
{
$set: {
items: {
$reduce: {
input: "$items",
initialValue: { result: [], count: 0 },
in: {
$cond: [ { $and: [ { $eq: [ "$$this", "Test" ] }, { $lt: [ "$$value.count", 3 ] } ] },
{ result: "$$value.result", count: { $add: [ "$$value.count", 1 ] } },
{ result: { $concatArrays: [ "$$value.result", [ "$$this" ] ] }, count: "$$value.count" }
]
}
}
}
}
},
{
$set: {
items: "$items.result"
}
}
])

mongoose aggregate how to map multiple collections into one Array

I have four different collections. From which three are connected to one:
Collection_A = {
_id: 1
name: A
includes: [
{
_id: 1,
includes_id: 222,
},
{
_id: 2,
includes_id: 333
}
]
}
Collection_B = {
_id: 222,
type: Computer,
name: Computer,
ref_id: 1
}
Collection_C = {
_id: 333,
type: Human,
name: Human,
ref_id: 1
}
Collection_D = {
_id: 444,
type: Animal,
name: Animal,
ref_id: 1
}
So collection A can include Collection B, C and D in the includes object. It includes minimum one of the collections.
So in the includes object in Collection A is the includes_id, which is the _id in Collection B, C and D.
The _id in Collection A is the ref_id in Collection B, C and D.
What my problem right now is, that aggregate takes only the last mapped collection.
My code right now is following:
Collection_A.aggregate([
{
$lookup: {
from: "collectionb",
localField: "includes.includes_id",
foreignField: "_id",
as: "colb",
},
},
{
$lookup: {
from: "collectionc",
localField: "includes.includes_id",
foreignField: "_id",
as: "colc",
},
},
{
$project: {
_id: 1,
status: 1,
type: 1,
includes_list: {
$map: {
input: "$includes",
as: "i",
in: {
$arrayElemAt: [
{
$filter: {
input: "$colb",
cond: {
$eq: ["$$this._id", "$$i.includes_id"],
},
},
},
0,
],
$arrayElemAt: [
{
$filter: {
input: "$colc",
cond: {
$eq: ["$$this._id", "$$i.includes_id"],
},
},
},
0,
],
},
},
},
},
},
]);
I tried to make the $lookup as the same on every lookup, but so it only took the last looked up data, and the others where shown as null.
So I made $lookup as unique, and put two ins in map, but then also the last looked up data was shown, and the others where null.
When I do the mapping like that:
includes_list: {
$map: {
input: "$icludes",
as: "i",
in: {
{
Col_A : {
$arrayElemAt: [
{
$filter: {
input: "$A",
cond: {
$eq: ["$$this._id", "$$i.includes"],
},
},
},
0,
],
},
Col_B : {
$arrayElemAt: [
{
$filter: {
input: "$B",
cond: {
$eq: ["$$this._id", "$$i.includes"],
},
},
},
0,
],
}
}
},
},
}
It workes. But not with the right output, because I need includes_list within one array.
My desired output is like following:
{
includes: [
{
_id: 1,
name: Computer,
includes_list: [
{
_id: 222,
type: Computer,
name: Computer,
ref_id: 1
},
{
_id: 333,
type: Human,
name: Human,
ref_id: 1
},
]
},
{
_id: 2,
name: Animal,
includes_list: [
{
_id: 333,
type: Human,
name: Human,
ref_id: 2
},
]
}
]
}
Would appreciate any help!
For this kind of situation,
$facet help to categorize the incoming data
db.Collection_A.aggregate([
{ $unwind: "$includes },
{
"$facet": {
"joinB": [
{
"$lookup": {
"from": "Collection_B", "localField": "includes.includes_id",
"foreignField": "_id", "as": "includes.includes_list"
}
},
{
"$group": {
"_id": "$_id",
"name": { "$first": "$name" },
includes: { $push: "$includes" }
}
}
],
"joinC": [
{
"$lookup": {
"from": "Collection_C", "localField": "includes.includes_id",
"foreignField": "_id", "as": "includes.includes_list"
}
},
{
"$group": {
"_id": "$_id",
"name": { "$first": "$name" },
includes: { $push: "$includes" }
}
}
],
"joinD": [
{
"$lookup": {
"from": "Collection_D", "localField": "includes.includes_id",
"foreignField": "_id", "as": "includes.includes_list"
}
},
{
"$group": {
"_id": "$_id",
"name": { "$first": "$name" },
includes: { $push: "$includes" }
}
}
],
}
},
{
$project: {
combined: {
"$concatArrays": [ "$joinB", "$joinC", "$joinD" ]
}
}
},
{ "$unwind": "$combined" },
{
"$replaceRoot": { "newRoot": "$combined" }
},
{
"$project": {
_id: 1,
name: 1,
includes: {
$filter: {
input: "$includes",
cond: {
$ne: [ "$$this.includes_list",[] ]
}
}
}
}
}
])
Working Mongo playground
Note: I feel this is kind of Anti pattern you follow. If you are in early stage of the project, better change the structure if I'm not mistaken.

How to divide documents values into same document, however key name of the divisor is same in MongoDB

Explanation: want to divide the name: Alex , name: petr value by name: hr value.
name: Alex , name: petr and name: hr are my parameters names.
also want to see the value of name: hr in the output document.
[
{
"name": "Alex",
"value": 65
},
{
"name": "petr",
"value": 8
},
{
"name": "hr",
"value": 20
}
]
Expected Output :
[
{
"name": "Alex/hr",
"value": 3.25
},
{
"name": "petr/hr",
"value": 0.4
},
{
"name": "hr",
"value": 20
}
]
Demo - https://mongoplayground.net/p/38G_Loo8V86
Use $facet
db.collection.aggregate([
{
$facet: {
hrVal: [
{ $match: { name: "hr" } }, // filter
{ $project: { _id: 0, value: 1 } } // take only value
],
allValues: [] // all documents or you can add match pipeline to filter here
}
},
{ $unwind: "$hrVal" }, // break into individual documents - now every document will have hrVal.value
{ $unwind: "$allValues" }, // break into individual documents
{
$set: { //
"allValues": {
"$cond": [
{ $eq: [ "$allValues.name", "hr" ] }, // condition
"$allValues", // true
{ // false
_id: "$allValues._id",
name: { "$concat": [ "$allValues.name", "/hr" ] }, // set name
value: { "$divide": [ "$allValues.value", "$hrVal.value" ] } // divide by hr value
}
]
}
}
},
{ $replaceRoot: { "newRoot": "$allValues" } } // reset to orignal document shape
])
$lookup with same collection and match for name: hr and return single result
$unwind deconstruct hr array
$project to check condition if name is hr then return current name and value of not then concat name using $concat and divide value by $divide
$project to move doc object to root
db.collection.aggregate([
{
$lookup: {
from: "collection",
pipeline: [
{ $match: { name: "hr" } },
{ $limit: 1 }
],
as: "hr"
}
},
{ $unwind: "$hr" },
{
$project: {
doc: {
$cond: [
{ $eq: ["$name", "hr"] },
{
name: "$name",
value: "$value"
},
{
name: { $concat: ["$name","/","$hr.name"] },
value: { $divide: ["$value", "$hr.value"] }
}
]
}
}
},
{
$project: {
name: "$doc.name",
value: "$doc.value"
}
}
])
Playground
Second option without lookup,
$facet to separate both result
$map to iterate loop of result array and check name is hr then concat name and divide value otherwise return same
$unwind deconstruct result array
$project to show fields
db.collection.aggregate([
{
$facet: {
result: [{ $match: {} }],
hr: [{ $match: { name: "hr" } }]
}
},
{
$project: {
result: {
$map: {
input: "$result",
in: {
$cond: [
{ $eq: ["$$this.name", "hr"] },
"$$this",
{
name: { $concat: ["$$this.name", "/", { $first: "$hr.name" }] },
value: { $divide: ["$$this.value", { $first: "$hr.value" }] }
}
]
}
}
}
}
},
{ $unwind: "$result" },
{
$project: {
name: "$result.name",
value: "$result.value"
}
}
])
Playground

How to group an array of subdocuments by multiple fields?

I have spent the better part of the day on this and am now out of ideas. Here is my collection:
[
{
"_id": "ID_XXX",
"logs": [
{
"lead_id": 123,
"list_id": "list_44",
"order_id": "order_1"
},
{
"lead_id": 124,
"list_id": "list_44",
"order_id": "order_2"
}
]
},
{
"_id": "ID_YYY",
"logs": [
{
"lead_id": 125,
"list_id": "list_44",
"order_id": "order_2"
},
{
"lead_id": 126,
"list_id": "list_44",
"order_id": "order_2"
},
{
"lead_id": 127,
"list_id": "list_44",
"order_id": "order_3"
},
{
"lead_id": 128,
"list_id": "list_66",
"order_id": "order_3"
}
]
}
]
I'm just trying to get the counts for list_id and order_id while preserving the _id of the document they are in. Here is my desired output:
[
{
"_id": "ID_XXX",
"counts": [
{
"lists": {"list_44": 2},
},
{
"orders": {"order_1": 1, "order_2": 1}
}
]
},
{
"_id": "ID_YYY",
"counts": [
{
"lists": {"list_44": 3, "list_66": 1},
},
{
"orders": {"order_2": 2, "order_3": 2}
}
]
}
]
I have tried way too many aggregate variations to list here, but the latest is this:
db.collection.aggregate([
{
$unwind: "$logs"
},
{
$group: {
_id: "$_id",
lists: {
$push: "$logs.list_id"
},
orders: {
$push: "$logs.order_id"
}
}
}
])
Which does not give me what I want. Can anyone point me in the right direction? Here is the playground link: https://mongoplayground.net/p/f-jk7lbSrJ0
$reduce to iterate loop of logs, convert logs object to array in k(key) v(value) format using $objectToArray, $concatArrays with initialValue in $reduce,
$filter above reduce result as input and filter required fields from logs
$unwind deconstruct logs array
db.collection.aggregate([
{
$addFields: {
logs: {
$filter: {
input: {
$reduce: {
input: "$logs",
initialValue: [],
in: { $concatArrays: ["$$value", { $objectToArray: "$$this" }] }
}
},
cond: { $in: ["$$this.k", ["list_id", "order_id"]] }
}
}
}
},
{ $unwind: "$logs" },
$group by _id and logs object and get the total count using $sum
{
$group: {
_id: {
_id: "$_id",
logs: "$logs"
},
counts: { $sum: 1 }
}
},
$group by _id and make lists array if logs.k is list_id and return in k and v format otherwise $$REMOVE, same as for orders make an array of order on the base of order_id
$addFields to convert lists array from k and v format to object format using $arrayToObjectand same as fororders` array
{
$group: {
_id: "$_id._id",
lists: {
$push: {
$cond: [
{ $eq: ["$_id.logs.k", "list_id"] },
{
k: "$_id.logs.v",
v: "$counts"
},
"$$REMOVE"
]
}
},
orders: {
$push: {
$cond: [
{ $eq: ["$_id.logs.k", "order_id"] },
{
k: "$_id.logs.v",
v: "$counts"
},
"$$REMOVE"
]
}
}
}
},
{
$addFields: {
lists: { $arrayToObject: "$lists" },
orders: { $arrayToObject: "$orders" }
}
}
])
Playground

Combine multiple projections in MongoDB aggregation

My MongoDB (v4.2) pipeline consists of multiple projections to subtract, divide, ceil and multiply a value.
Is there a way to combine them without using multiple projections?
My pipeline:
[
{
$project: {
duration: {
$subtract: [ '$updated', '$created' ]
}
}
},
{
$project: {
duration: {
$divide: [ '$duration', precision ]
}
}
},
{
$project: {
duration: {
$ceil: [ '$duration' ]
}
}
},
{
$project: {
duration: {
$multiply: [ '$duration', precision ]
}
}
},
{
$group: {
_id: '$duration',
count: {
$sum: 1
}
}
}
]
Example data:
[
{ created: 1569763367, updated: 1569773367 },
{ created: 1569760000, updated: 1569770000 },
{ created: 1569772415, updated: 1569773519 },
]
It's seems to be possible, but it's not very readable:
[
{
$project: {
duration: {
$multiply: [
{
$ceil: [
{
$divide: [
{
$subtract: [ '$updated', '$created' ]
},
precision
]
}
]
},
precision
]
}
}
},
{
$group: {
_id: '$duration',
count: {
$sum: 1
}
}
}
]

Resources