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.
Related
I have the following MongoDB query:
const vaccination = await Schedule.aggregate([
{ $match: { status: ScheduleStatus.Published } },
{ "$unwind": { "path": "$vaccines", "preserveNullAndEmptyArrays": true } },
{
"$group": {
"_id": "$vaccines.vaccine",
"count": { "$sum": "$vaccines.stok" },
}
},
{
$lookup: { from: 'vaccines', localField: '_id', foreignField: '_id', as: 'vaccine' },
},
{
$project: {
"count": 1,
"vaccine": { "$arrayElemAt": ["$vaccine.name", 0] }
}
}
]);
and return the following results :
[
{
"_id": "61efd8a812432135c08a748d",
"count": 20,
"vaccine": "Sinovac"
}
]
is there a way I can make the output to be an array of values like:
[["Sinovac",20]]
Thanks sorry for my bad english
So you can't get the exact structure you asked for, by definition the aggregation framework returns an array of "documents", a document is in the form of {key: value}, What you can do however is return the following structure:
[
{
"values": [
"61efd8a812432135c08a748d",
20,
"Sinovac"
]
}
]
With this pipeline:
db.collection.aggregate([
{
$project: {
_id: 0,
values: {
$map: {
input: {
"$objectToArray": "$$ROOT"
},
as: "item",
in: "$$item.v"
}
}
}
}
])
Mongo Playground
Current MongoDB query, takes upto 5 mins to search through 2 documents, when each document has 10,000 contacts, Please suggest ways to improve this significantly.
I am trying to search for a phone number in hundreds of documents.
Each document belongs to a user and each user has a contacts array (as you can see in the below code) with 10,000 objects and each object can have 2 to 3 phone numbers. (See below document structure).
If a phone number is found in multiple documents, I need the MongoDB query to return an array with userNumber’s found in those documents.
Below is the structure of the document I have in MongoDB collection. For simplicity, I showed only one object in contacts array, infact there are thousands of objects
{
"_id": { "$oid": "61d1f04266289f003452d705" },
"userID": { "$oid": "61d1efea2c0fab00340f47c8" },
"contacts": [
{
"emailAddresses": [
{ "id": "6884", "label": "email1", "email": "addedemail#gmail.com" }
],
"phoneNumbers": [
{
"label": "other",
"id": "4594",
"number": "+918984292930"
},
{
"label": "other",
"id": "4595",
"number": "+911234567890"
}
],
"_id": { "$oid": "61d1f04266289f003452d744" },
"ContactName": "Sample User 1 Name Changed",
"ContactNumber": "+918984292930",
"recordID": "833"
}
],
"userNumber": "+911234567890",
"__v": 7
}
Current MongoDB Query:
await ContactModel.aggregate([
{
$match: {
userNumber: userNumber,
},
},
{
$unwind: "$contacts",
},
{
$lookup: {
from: "phonenumbers",
let: {
contactNumberVar: "$contacts.ContactNumber",
},
pipeline: [
{ $unwind: "$contacts" },
{
$project: {
userNumber: 1,
"contacts.ContactNumber": 1,
},
},
{
$match: {
$and: [
{ $expr: { $eq: ["$$contactNumberVar", "$userNumber"] } },
{
$expr: {
$eq: [contactNumber, "$contacts.ContactNumber"],
},
},
],
},
},
],
as: "mutualContacts",
},
},
{
$project: {
userID: 1,
"mutualContacts.userNumber": 1,
},
},
{
$group: {
_id: "$userID",
mutualContacts: {
$push: {
$cond: [
{ $gt: [{ $size: "$mutualContacts" }, 0] },
{ $arrayElemAt: ["$mutualContacts.userNumber", 0] },
"$$REMOVE",
],
},
},
},
},
]).exec()
First of all ensure you have indexes that support the query on both collections.
{userNumber:1}
Should be a good candidate, but please test other options.
Next - query optimisation. In the lookup pipeline:
pipeline: [
{ $unwind: "$contacts" },
{
$project: {
userNumber: 1,
"contacts.ContactNumber": 1,
},
},
{
$match: {
$and: [
{ $expr: { $eq: ["$$contactNumberVar", "$userNumber"] } },
{
$expr: {
$eq: [contactNumber, "$contacts.ContactNumber"],
},
},
],
},
},
],
You unwind whole phonenumbers collection.
Match it first and unwind/project only matching documents instead:
pipeline: [
{
$match: {
$and: [
{ $expr: { $eq: ["$$contactNumberVar", "$userNumber"] } },
{
$expr: {
$eq: [contactNumber, "$contacts.ContactNumber"],
},
},
],
},
},
{ $unwind: "$contacts" },
{
$project: {
userNumber: 1,
"contacts.ContactNumber": 1,
},
},
{
$match: {
$and: [
{ $expr: { $eq: ["$$contactNumberVar", "$userNumber"] } },
{
$expr: {
$eq: [contactNumber, "$contacts.ContactNumber"],
},
},
],
},
},
],
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
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
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