Mongodb lookup array of elements with combined result - node.js

So these are my two documents
Order document:
{
"_id":"02a33b9a-284c-4869-885e-d46981fdd679",
"context":{
"products":[
{
"id": "e68fc86a-b4ad-4588-b182-ae9ee3db25e4",
"version": "2020-03-14T13:18:41.296+00:00"
}
],
},
}
Product document:
{
"_id":"e68fc86a-b4ad-4588-b182-ae9ee3db25e4",
"context":{
"name": "My Product",
"image": "someimage"
},
}
So I'm trying to do a lookup for a products in order document, but the result should contain combined fields, like so:
"products":[
{
"_id": "e68fc86a-b4ad-4588-b182-ae9ee3db25e4",
"version": "2020-03-14T13:18:41.296+00:00",
"name": "My Product",
"image": "someimage"
}
],
Not sure how to do this, should I do it outside of the lookup, or inside? This is my aggregation
Orders.aggregate([
{
"$lookup":{
"from":"products",
"let":{
"products":"$context.products"
},
"pipeline":[
{
"$match":{
"$expr":{
"$in":[
"$_id",
"$$products.id"
]
}
}
},
{
"$project":{
"_id":0,
"id":1,
"name":"$context.name"
}
}
],
"as":"mergedProducts"
}
},
{
"$project":{
"context":"$context",
"mergedProducts":"$mergedProducts"
}
},
]);

You need to run that mapping outside of $lookup by running $map along with $arrayElemAt to get single pair from both arrays and then apply $mergeObjects to get one object as a result:
db.Order.aggregate([
{
$lookup: {
from: "products",
localField: "context.products.id",
foreignField: "_id",
as: "productDetails"
}
},
{
$addFields: {
productDetails: {
$map: {
input: "$productDetails",
in: {
_id: "$$this._id",
name: "$$this.context.name"
}
}
}
}
},
{
$project: {
_id: 1,
"context.products": {
$map: {
input: "$context.products",
as: "prod",
in: {
$mergeObjects: [
"$$prod",
{ $arrayElemAt: [ { $filter: { input: "$productDetails", cond: { $eq: [ "$$this._id", "$$prod.id" ] } } }, 0 ] }
]
}
}
}
}
}
])
Mongo Playground
The goals of the last step is to take take two arrays: products and productDetails (the output of $lookup) and find matches between them. We know there's always one match so we can get only one item $arrayElemAt 0. As an output of $map there will be single array containing "merged" documents.

Related

Removing Dynamic Fields by Association in MongoDB Aggregation

I'm trying to display a MongoDB aggregation result via react chartjs. in aggregation, I can remove one field whose value is static via the set operator. is there a way to remove a second field by an association whose value is dynamic? in the example below, {"A": "N"} denotes the field that is readily removed by the set operator, whereas {"A_count":1} denotes the corresponding dynamic field that I am trying to remove.
starting aggregation output
[{
"_id":"Fubar",
"A_set":[{"A":"Y"},{"A":"N"}],
"A_count_set":[{"A_count":0},{"A_count":1}]
}]
set operation for static field removal
{$set: {
A_set: {
$filter: {
input: "$A_set",
as: "x",
cond: { "$ne": [ "$$x", {"A":"N"}] }
}
}
}}
current aggregation output
[{
"_id":"Fubar",
"A_set":[{"A":"Y"}],
"A_count_set":[{"A_count":0},{"A_count":1}]
}]
target aggregation output
[{
"_id":"Fubar",
"A_set":[{"A":"Y"}],
"A_count_set":[{"A_count":0}]
}]
$project merge two array with the same position
$set filter array
$addFields recover the original array
$project remove the merge array
aggregate
db.collection.aggregate([
{
$project: {
anotherValue: {
$map: {
input: {
$range: [
0,
{
$size: "$A_set"
}
]
},
as: "idx",
in: {
$mergeObjects: [
{
$arrayElemAt: [
"$A_set",
"$$idx"
]
},
{
$arrayElemAt: [
"$A_count_set",
"$$idx"
]
}
]
}
}
}
}
},
{
$set: {
anotherValue: {
$filter: {
input: "$anotherValue",
as: "x",
cond: {
"$ne": [
"$$x.A",
"N"
]
}
}
}
}
},
{
$addFields: {
"A_set": {
$map: {
input: "$anotherValue",
as: "a",
in: {
"A": "$$a.A"
}
}
},
"A_count_set": {
$map: {
input: "$anotherValue",
as: "a",
in: {
"A_count": "$$a.A_count"
}
}
}
}
},
{
"$project": {
"anotherValue": 0
}
}
])
mongoplayground

nodejs/mongodb - Aggregate items by substring

I have two related collections that contain documents as follows:
/* heroes */
{ id: "HID_1", name: "A" }
{ id: "HID_2", name: "B" }
/* weapons */
{ name: "WHID_1", weapon: "Sword" }
{ name: "WHID_2", weapon: "Lance" }
How can I aggregate them so I get a single document where I know "A" uses a Sword and "B" uses a Lance? I can't directly join them by id and name because their value isn't exactly the same, but Weapon has a W-prefix on it.
I made some attempts with $substr but no success so far.
db.heroes.aggegate( [
{
$lookup: {
from: 'weapons',
let: { heroId: '$id' },
pipeline: [
{
$match: {
$expr: {
$eq: [ '$$heroId', { $substr: [ '$name', 1, -1 ] } ]
}
}
}
],
as: 'weapon'
}
}
] )
For reference, I also tried just hard-coding an ID with { $match: { $expr: { $eq: [ '$$heroId', 'HID_1' ] } } } and it didn't work. I could just rename all WHID to HID, but I am curious about whether it is possible or not.
Use $project to append the "W" to the heroID and then do a regular lookup like described here:
https://stackoverflow.com/a/46969468
I am laughing so hard right now, the query I posted is not the same I have in my code, and apparently I fixed it without knowing while I was copying it into the question. My let was wrong and defined weapons.name instead of heroes.id.
For anyone having a similar issue, the aggregate in the original post works as it should. I didn't notice it until #varman pointed it out, so thank you! And sorry for the silly mistake.
Try this...
db.heroes.aggregate([
{
$project: {
_id: 1,
name: 1,
newID: {
$concat: [
"W",
"$_id"
]
}
}
},
{
"$lookup": {
"from": "weapons",
localField: "newID",
foreignField: "name",
"as": "data"
}
},
{
$unwind: "$data"
},
{
$replaceRoot: {
newRoot: {
$mergeObjects: [
"$data",
"$$ROOT"
]
}
}
},
{
$project: {
data: 0,
newID: 0
}
}
])
or
db.heroes.aggregate([
{
$lookup: {
from: "weapons",
let: {
heroId: "$id"
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$$heroId",
{
$substr: [
"$name",
1,
-1
]
}
]
}
}
}
],
as: "data"
}
},
{
$unwind: "$data"
},
{
$replaceRoot: {
newRoot: {
$mergeObjects: [
"$data",
"$$ROOT"
]
}
}
},
{
$project: {
data: 0
}
}
])
output:
[
{
"_id": "HID_1",
"name": "A",
"weapon": "Sword"
},
{
"_id": "HID_2",
"name": "B",
"weapon": "Lance"
}
]
Mongoplayground

I want to display only one product image

This is Code in node js
const result = await OrderDB.aggregate([
{ $match: { _id: mongoose.Types.ObjectId(id) } },
{
$lookup: {
from: 'products',
localField: 'product',
foreignField: '_id',
as: 'productDetail',
},
},
{
$project: {
productDetail: {
name: 1,
price: 1,
productImage: 1,
},
},
},
])
This is the response of code
{
"message": "Get Order Successfully",
"result": [
{
"_id": "5ff47348db5f5917f81871aa",
"productDetail": [
{
"name": "Camera",
"productImage": [
{
"_id": "5fe9b8a26720f728b814e246",
"img": "uploads\\product\\7Rq1v-app-7.jpg"
},
{
"_id": "5fe9b8a26720f728b814e247",
"img": "uploads\\product\\FRuVb-app-8.jpg"
}
],
"price": 550
}
]
}
]
}
I want to display only one productImage from the response using nodejs and mongoose
This is using in aggregate projection
I was use $arrayElemAt but it is don't work
I also use $first but it is don't work
so projection method I use to display only one *productImage*
Data looks like multi level nested.
You have array of results, each result contains array of productDetails
play
You need to unwind the data to get the first productImage
db.collection.aggregate([
{
"$unwind": "$result"
},
{
"$unwind": "$result.productDetail"
},
{
$project: {
pImage: {
"$first": "$result.productDetail.productImage"
}
}
}
])
With the above response
db.collection.aggregate([
{
"$project": {
productDetails: {
$map: {
input: "$productDetail",
in: {
"$mergeObjects": [
"$$this",
{
productImage: {
"$arrayElemAt": [
"$$this.productImage",
0
]
}
}
]
}
}
}
}
}
])
Working Mongo playground

MongoDB Mongoose aggregate query deeply nested array remove empty results and populate references

This question is a follow up to a previous question for which I have accepted an answer already. I have an aggregate query that returns the results of a deeply nested array of subdocuments based on a date range. The query returns the correct results within the specified date range, however it also returns an empty array for the results that do not match the query.
Technologies: MongoDB 3.6, Mongoose 5.5, NodeJS 12
Question 1:
Is there any way to remove the results that don't match the query?
Question 2:
Is there any way to 'populate' the Person db reference in the results? For example to get the Person Display Name I usually use 'populate' such as find().populate({ path: 'Person', select: 'DisplayName'})
Records schema
let RecordsSchema = new Schema({
RecordID: {
type: Number,
index: true
},
RecordType: {
type: String
},
Status: {
type: String
},
// ItemReport array of subdocuments
ItemReport: [ItemReportSchema],
}, {
collection: 'records',
selectPopulatedPaths: false
});
let ItemReportSchema = new Schema({
// ObjectId reference
ReportBy: {
type: Schema.Types.ObjectId,
ref: 'people'
},
ReportDate: {
type: Date,
required: true
},
WorkDoneBy: [{
Person: {
type: Schema.Types.ObjectId,
ref: 'people'
},
CompletedHours: {
type: Number,
required: true
},
DateCompleted: {
type: Date
}
}],
});
Query
Works but also returns empty results and also need to populate the Display Name property of the Person db reference
db.records.aggregate([
{
"$project": {
"ItemReport": {
$map: {
input: "$ItemReport",
as: "ir",
in: {
WorkDoneBy: {
$filter: {
input: "$$ir.WorkDoneBy",
as: "value",
cond: {
"$and": [
{ "$ne": [ "$$value.DateCompleted", null ] },
{ "$gt": [ "$$value.DateCompleted", new Date("2017-01-01T12:00:00.000Z") ] },
{ "$lt": [ "$$value.DateCompleted", new Date("2018-12-31T12:00:00.000Z") ] }
]
}
}
}
}
}
}
}
}
])
Actual Results
{
"_id": "5dcb6406e63830b7aa5427ca",
"ItemReport": [
{
"WorkDoneBy": [
{
"_id": "5dcb6406e63830b7aa53d8ea",
"PersonID": 111,
"ReportID": 8855,
"CompletedHours": 3,
"DateCompleted": "2017-01-20T05:00:00.000Z",
"Person": "5dcb6409e63830b7aa54fdba"
}
]
}
]
},
{
"_id": "5dcb6406e63830b7aa5427f1",
"ItemReport": [
{
"WorkDoneBy": [
{
"_id": "5dcb6406e63830b7aa53dcdc",
"PersonID": 4,
"ReportID": 9673,
"CompletedHours": 17,
"DateCompleted": "2017-05-18T04:00:00.000Z",
"Person": "5dcb6409e63830b7aa54fd69"
},
{
"_id": "5dcb6406e63830b7aa53dcdd",
"PersonID": 320,
"ReportID": 9673,
"CompletedHours": 3,
"DateCompleted": "2017-05-18T04:00:00.000Z",
"Person": "5dcb6409e63830b7aa54fe88"
}
]
}
]
},
{
"_id": "5dcb6406e63830b7aa5427f2",
"ItemReport": [
{
"WorkDoneBy": []
}
]
},
{
"_id": "5dcb6406e63830b7aa5427f3",
"ItemReport": [
{
"WorkDoneBy": []
}
]
},
{
"_id": "5dcb6406e63830b7aa5427f4",
"ItemReport": [
{
"WorkDoneBy": []
}
]
},
{
"_id": "5dcb6406e63830b7aa5427f5",
"ItemReport": [
{
"WorkDoneBy": []
}
]
},
Desired results
Note the results with an empty "WorkDoneBy" array are removed (question 1), and the "Person" display name is populated (question 2).
{
"_id": "5dcb6406e63830b7aa5427f1",
"ItemReport": [
{
"WorkDoneBy": [
{
"_id": "5dcb6406e63830b7aa53dcdc",
"CompletedHours": 17,
"DateCompleted": "2017-05-18T04:00:00.000Z",
"Person": {
_id: "5dcb6409e63830b7aa54fe88",
DisplayName: "Joe Jones"
}
},
{
"_id": "5dcb6406e63830b7aa53dcdd",
"CompletedHours": 3,
"DateCompleted": "2017-05-18T04:00:00.000Z",
"Person": {
_id: "5dcb6409e63830b7aa54fe88",
DisplayName: "Alice Smith"
}
}
]
}
]
},
First question is relatively easy to answer and there are multiple ways to do that. I would prefer using $anyElementTrue along with $map as those operators are pretty self-explanatory.
{
"$match": {
$expr: { $anyElementTrue: { $map: { input: "$ItemReport", in: { $gt: [ { $size: "$$this.WorkDoneBy" }, 0 ] } } } }
}
}
MongoPlayground
Second part is a bit more complicated but still possible. Instead of populate you need to run $lookup to bring the data from other collection. The problem is that your Person values are deeply nested so you need to prepare a list of id values before using $reduce and $setUnion. Once you get the data you need to merge your nested objects with people entities using $map and $mergeObjects.
{
$addFields: {
people: {
$reduce: {
input: "$ItemReport",
initialValue: [],
in: { $setUnion: [ "$$value", "$$this.WorkDoneBy.Person" ] }
}
}
}
},
{
$lookup: {
from: "people",
localField: "peopleIds",
foreignField: "_id",
as: "people"
}
},
{
$project: {
_id: 1,
ItemReport: {
$map: {
input: "$ItemReport",
as: "ir",
in: {
WorkDoneBy: {
$map: {
input: "$$ir.WorkDoneBy",
as: "wdb",
in: {
$mergeObjects: [
"$$wdb",
{
Person: { $arrayElemAt: [{ $filter: { input: "$people", cond: { $eq: [ "$$this._id", "$$wdb.Person" ] } } } , 0] }
}
]
}
}
}
}
}
}
}
}
Complete Solution

Query by data already in the object

I'm writing a query that gets data from "coll2" based on data that is inside "coll1".
Coll1 has the following data structure:
{
"_id": "asdf",
"name": "John",
"bags": [
{
"type": "typ1",
"size": "siz1"
},
{
"type": "typ2",
"size": "siz2"
}
]
}
Coll2 has the following data structure:
{
_id: "qwer",
coll1Name: "John",
types: ["typ1", "typ3"],
sizes: ["siz1", "siz4"]
}
{
_id: "zxcv",
coll1Name: "John",
types: ["typ2", "typ3"],
sizes: ["siz1", "siz2"]
}
{
_id: "fghj",
coll1Name: "John",
types: ["typ2", "typ3"],
sizes: ["siz1", "siz4"]
}
I want to get all the documents in coll2 that have the same Type+Size combo as in coll1 using the $lookup stage of the aggregation pipeline. I understand that this can be achieved by using the $lookup pipeline and $expr but I cant seem to figure out how to dynamically make a query to pass into the $match stage.
The output I would like to get for the above data would be:
{
_id: "qwer",
coll1Name: "John",
types: ["typ1", "typ3"],
sizes: ["siz1", "siz4"]
}
{
_id: "zxcv",
coll1Name: "John",
types: ["typ2", "typ3"],
sizes: ["siz1", "siz2"]
}
You can use $lookup to get the data from Col2. Then you need to check if there's any element in Col2 ($anyElemenTrue) that matches with Col1. $map and $in can be used here. Then you just need to $unwind and promote Col2 to root level using $replaceRoot
db.Col1.aggregate([
{
$lookup: {
from: "Col2",
localField: "name",
foreignField: "coll1Name",
as: "Col2"
}
},
{
$project: {
Col2: {
$filter: {
input: "$Col2",
as: "c2",
cond: {
$anyElementTrue: {
$map: {
input: "$bags",
as: "b",
in: {
$and: [
{ $in: [ "$$b.type", "$$c2.types" ] },
{ $in: [ "$$b.size", "$$c2.sizes" ] },
]
}
}
}
}
}
}
}
},
{
$unwind: "$Col2"
},
{
$replaceRoot: {
newRoot: "$Col2"
}
}
])
You are correct in your approach to use $lookup with the pipeline field to filter the input documents in the $match pipeline
The $expr expression should typically follow
"$expr": {
"$and": [
{ "$eq": [ "$name", "$$coll1_name" ] },
{ "$setEquals": [ "$bags.type", "$$types" ] },
{ "$setEquals": [ "$bags.size", "$$sizes" ] }
]
}
where the first match expression in the $and conditional { "$eq": [ "$name", "$$coll1_name" ] } checks to see if the name field in coll1 collection matches the coll1Name field in the input documents from coll2.
Of course the fields from coll2 should be defined in a variable in the pipeline with the let field for the $lookup pipeline to access them.
The other match filters are basically checking if the arrays are equal where "$bags.type" from coll1 resolves to an array of types i.e. [ "typ1", "typ3" ] for example.
On getting the output field from $lookup which happens to be an array, you can filter the documents in coll2 on that array field where there can be some empty lists as a resul of the above $lookup pipeline $match filter:
{ "$match": { "coll1Data.0": { "$exists": true } } }
Overall your aggregate pipeline operation would be as follows:
db.getCollection('coll2').aggregate([
{ "$lookup" : {
"from": "coll1",
"let": { "coll1_name": "$coll1Name", "types": "$types", "sizes": "$sizes" },
"pipeline": [
{ "$match": {
"$expr": {
"$and": [
{ "$eq": [ "$name", "$$coll1_name" ] },
{ "$setEquals": [ "$bags.type", "$$types" ] },
{ "$setEquals": [ "$bags.size", "$$sizes" ] }
]
}
} }
],
"as": "coll1Data"
} },
{ "$match": { "coll1Data.0": { "$exists": true } } },
{ "$project": { "coll1Data": 0 } }
])

Resources