Filter on collection and merge results in another collection in MongoDB - mongodb

I am using MongoDB 4.2.9 and have the following requirements:
Collection 'A' has multiple documents with a string field 'status' that I need to filter on
Collection 'B' has multiple documents
Collection A
{ _id: "1",
status: "Report",
type: "Academy",
rating: "Excellent",
ReportNo: "A1"
},
{ _id: "2",
status: "Open",
type: "Academy",
rating: "",
ReportNo: ""
},
{ _id: "3",
status: "Draft",
type: "Academy",
rating: "",
ReportNo: ""
},
{ _id: "4",
status: "Report",
type: "Academy",
rating: "Great",
ReportNo: "A4"
}
Collection B
{ _id: "98",
status: "Archived",
type: "Academy",
rating: "So So",
ReportNo: "X2"
},
{ _id: "99",
status: "Archived",
type: "Academy",
rating: "Great",
ReportNo: "X1"
}
Resulting View
{ _id: "1",
status: "Report",
type: "Academy",
rating: "Excellent",
ReportNo: "A1"
},
{ _id: "4",
status: "Report",
type: "Academy",
rating: "Great",
ReportNo: "A4"
},
{ _id: "98",
status: "Archived",
type: "Academy",
rating: "So So",
ReportNo: "X2"
},
{ _id: "99",
status: "Archived",
type: "Academy",
rating: "Great",
ReportNo: "X1"
}
My goal is to create an aggregation view so that I can filter on a status value in Collection 'A' and then merge those results with Collection 'B' and show in the view ?
I can filter on Collection 'A' using the match call, just can't see how to merge resulting documents into Collection 'B'

From my understandings, your "merge" behaviour is actually a union view of filtered view of collection A and collection B.
With MongoDB v4.2, you can use $facet to handle collection A and collection B separately.
simply perform filtering on A
perform uncorrelated $lookup on B
wrangle the result and merge them together to get the union view that you are looking for.
db.createCollection(
"unionView",
{
"viewOn" : "A",
"pipeline" : [
{
"$facet": {
"A": [
{
"$match": {
status: "Report"
}
}
],
"B": [
{
$limit: 1
},
{
"$lookup": {
"from": "B",
"pipeline": [],
"as": "B"
}
},
{
$unwind: "$B"
},
{
"$replaceRoot": {
"newRoot": "$B"
}
}
]
}
},
{
$project: {
all: {
"$setUnion": [
"$A",
"$B"
]
}
}
},
{
$unwind: "$all"
},
{
"$replaceRoot": {
"newRoot": "$all"
}
}
]
}
)
Here is the Mongo Playground for your reference.
With MongoDB v4.4+, you can create a view with $unionWith
db.createCollection(
"unionView",
{
"viewOn" : "A",
"pipeline" : [
{
"$match": {
status: "Report"
}
},
{
"$unionWith": {
"coll": "B"
}
}
]
}
)
Here is the Mongo playground for your reference.

Related

mongodb - Merge object arrays based on key

In a mongodb database, I have the following data:
// db.people
[
{
_id: ObjectId("..."),
id: 111111111,
name: "George",
relatedPeople: [{ id: 222222222, relation: "child" }],
// A bunch of other data I don't care about
},
{
_id: ObjectId("..."),
id: 222222222,
name: "Jacob",
relatedPeople: [{ id: 111111111, relation: "father" }],
// A bunch of other data I don't care about
},
{
_id: ObjectId("..."),
id: 333333333,
name: "some guy",
relatedPeople: [],
// A bunch of other data I don't care about
},
]
I would like to query the people, and select only the fields I've shown, but have extra data in relatedPeople (id + relation + name)
So the desired output would be:
[
{
_id: ObjectId("..."),
id: 111111111,
name: "George",
relatedPeople: [{ id: 222222222, relation: "child", name: "Jacob" }],
},
{
_id: ObjectId("..."),
id: 222222222,
name: "Jacob",
relatedPeople: [{ id: 111111111, relation: "father", name: "George" }],
},
{
_id: ObjectId("..."),
id: 333333333,
name: "some guy",
relatedPeople: [],
},
]
I can get something close, with this query:
db.people.aggregate([
// { $match: { /** ... */ }, },
{
$lookup: {
from: "people",
let: { relatedPeopleIds: "$relatedPeople.id" },
pipeline: [
{ $match: { $expr: { $in: ["$id", "$$relatedPeopleIds"] } } },
{
$project: {
id: 1,
name: 1,
},
},
],
as: "relatedPeople2",
},
},
{
$project: {
id: 1,
name: 1,
relatedPeople: 1,
relatedPeople2: 1,
}
}
]);
But the data is split between two fields. I want to merge each object in the arrays by their id, and place the result array in relatedPeople
I found this question, but that merge is done over a range and uses $arrayElementAt which I can't use
I also tried looking at this question, but I couldn't get the answer to work (Kept getting empty results)
You can add one step using $arrayElementAt with $indexOfArray:
db.people.aggregate([
// { $match: { /** ... */ }, },
{$project: {id: 1, name: 1, relatedPeople: 1}},
{$lookup: {
from: "people",
let: { relatedPeopleIds: "$relatedPeople.id" },
pipeline: [
{ $match: { $expr: { $in: ["$id", "$$relatedPeopleIds"] } } },
{
$project: {
id: 1,
name: 1,
},
},
],
as: "relatedPeople2",
},
},
{$set: {
relatedPeople: {$map: {
input: "$relatedPeople",
in: {$mergeObjects: [
"$$this",
{$arrayElemAt: [
"$relatedPeople2",
{$indexOfArray: ["$relatedPeople2.id", "$$this.id"]}
]}
]}
}}
}},
{$unset: "relatedPeople2"}
])
See how it works on the playground example

Search Solution Mongodb lookup and filter fields

I´m looking a simple solution for my MongoDB Lookup-Problem.
I have two collections:
Disp with the following structure:
{"_id" : ObjectId("5f748869487b4d0013ee50b4"),
"productId": ObjectId("5f9a96f85b909923e8530f0c"),
"subproductId" ObjectId("5e8b3684a82c2a00134e507a"),
},
{"_id" : ObjectId("5f74870e487b4d0013ee50b3"),
"productId": ObjectId("5f7b4b17e8ec6a00158bb5d8"),
"subproductId" ObjectId("5f78303a82f45e0013afebc1"),
}
and a Collection "Product with the following structure:
{_id: ObjectId("5f9a96f85b909923e8530f0c"),
subproduct: [ {
_id:ObjectId("5e8b3684a82c2a00134e507a"),
title: "Test"
},
{_id: ObjectId("5f369a94018c040013c76ede"),
title: "Test 1"}
]},
{_id:ObjectId("5f7b4b17e8ec6a00158bb5d8"),
subproduct: [ {
_id: ObjectId("5f7b43efe8ec6a00158bb5cc"),
title: "Test3"
}, {
_id: ObjectId("5f78303a82f45e0013afebc1"),
title: "Test 4"}
]
As you can see I have to do a lookup to an array "subproduct" with its ObjectID.
But if I do a normal lookup I will receice all other array-elements as well.
How do I filter the result so that the result will be only the subproduct-array element it matches.
Same as an inner join.
My expected result will be:
{"_id" : ObjectId("5f748869487b4d0013ee50b4"),
"productId": ObjectId("5f9a96f85b909923e8530f0c"),
"subproductId" ObjectId("5e8b3684a82c2a00134e507a"),
"subproduct": {
_id:ObjectId("5e8b3684a82c2a00134e507a"),
title: "Test"
},
{"_id" : ObjectId("5f74870e487b4d0013ee50b3"),
"productId": ObjectId("5f7b4b17e8ec6a00158bb5d8"),
"subproductId" ObjectId("5f78303a82f45e0013afebc1"),
},
"subproduct": {
_id: ObjectId("5f78303a82f45e0013afebc1"),
title: "Test 4"
},
Thx,
Alex
You can use the below query:
db.disp.aggregate([
{
"$lookup": {
"from": "product",
"let": {
subprodId: "$subproductId"
},
"pipeline": [
{
"$unwind": "$subproduct"
},
{
"$match": {
$expr: {
$eq: [
"$$subprodId",
"$subproduct._id"
]
}
}
}
],
as: "subDetails"
}
},
{
$unwind: "$subDetails"
},
{
$project: {
_id: 1,
productId: 1,
subproductId: 1,
subproduct: {
_id: "$subDetails.subproduct._id",
title: "$subDetails.subproduct.title"
}
}
}
])

Can we push object value into $project using mongodb

db.setting.aggregate([
{
$match: {
status: true,
deleted_at: 0,
_id: {
$in: [
ObjectId("5c4ee7eea4affa32face874b"),
ObjectId("5ebf891245aa27c290672325")
]
}
}
},
{
$lookup: {
from: "site",
localField: "_id",
foreignField: "admin_id",
as: "data"
}
},
{
$project: {
name: 1,
status: 1,
price: 1,
currency: 1,
numberOfRecord: {
$size: "$data"
}
}
},
{
$sort: {
numberOfRecord: 1
}
}
])
how to push the currency into price object using project please guide thanks a lot, also eager to know what is difference between $addtoSet and $push, what is good option to opt it from project or fix it from $addField
https://mongoplayground.net/p/RiWnnRtksb4
Output should be like this:
[
{
"_id": ObjectId("5ebf891245aa27c290672325"),
"currency": "USD",
"name": "Menz",
"numberOfRecord": 0,
"price": {
"numberDecimal": "20",
"currency": "USD",
},
"status": true
},
{
"_id": ObjectId("5c4ee7eea4affa32face874b"),
"currency": "USD",
"name": "Dave",
"numberOfRecord": 2,
"price": {
"numberDecimal": "10",
"currency": "USD"
},
"status": true
}
]
You can insert a field into an object with project directly, like this (field price):
$project: {
name: 1,
status: 1,
price: {
numberDecimal: "$price.numberDecimal",
currency: "$currency"
},
numberOfRecord: {
$size: "$data"
}
}
By doing it with project, there is no need to use $addField.
For the difference between $addToSet and $push, read this great answer.
You can just set the object structure while projecting, so in this case there's no need for either $push or $addToSet.
{
$project: {
name: "1",
status: 1,
price: {
currency: "$currency",
numberDecimal: "$price.numberDecimal"
},
currency: 1,
numberOfRecord: {
$size: "$data",
}
}
}
Now the difference between $push and $addToSet is pretty trivial and derived from the name, $push saves all items while $addToSet will just create a set of them, for example:
input:
[
//doc1
{
item: 1
},
//doc2
{
item: 2
},
//doc3
{
item: 1
}
]
Now this:
{
$group: {
_id: null,
items: {$push: "$item"}
}
}
Will result in:
{_id: null, items: [1, 2, 1]}
While:
{
$group: {
_id: null,
items: {$addToSet: "$item"}
}
}
Will result in:
{_id: null, items: [1, 2]}

Mongoose aggregate the property of subdocument and display the result

I have a document with a subdocument (not referenced). I want to apply the aggregation on the field of the subdocument.
Schema
const MFileSchema = new Schema({
path: String,
malwareNames: [String],
title: String,
severity: String // i want to aggregate bases on this field
});
const ScanSchema = new Schema({
agent: { type: Schema.Types.ObjectId, ref: "Agent" },
completedAt: Date,
startedAt: { type: Date, default: Date.now() },
mFiles: [MFileSchema] // array of malicious files schema
});
Model
let Scan = model("Scan", ScanSchema);
Task
Find the sum of severity in all scan documents of particular agents.
// agents is an array Agents (the schema is not important to show, consider the _id)
The Aggregation Query I am using
let c = await Scan.aggregate([
{ $match: { agent: agents } },
{ $project: { "mFiles.severity": true } },
{ $group: { _id: "$mFiles.severity", count: { $sum: 1 } } }
]);
console.log(c);
Actual Output
[]
Expected Output
// The value of count in this question is arbitrary
[
{ _id: "Critical", count: 30 },
{ _id: "Moderate", count: 33 },
{ _id: "Clean", count: 500 }
]
PS: Also I would appreciate if you could suggest me the best resources to learn MongoDB aggregations
You need to use $in query operator in the $match stage, and add $unwind stage before $group stage.
db.collection.aggregate([
{
$match: {
agent: {
$in: [
"5e2c98fc3d785252ce5b5693",
"5e2c98fc3d785252ce5b5694"
]
}
}
},
{
$project: {
"mFiles.severity": true
}
},
{
$unwind: "$mFiles"
},
{
$group: {
_id: "$mFiles.severity",
count: {
$sum: 1
}
}
}
])
Playground
Sample data:
[
{
"agent": "5e2c98fc3d785252ce5b5693",
"mFiles": [
{
"title": "t1",
"severity": "Critical"
},
{
"title": "t2",
"severity": "Critical"
},
{
"title": "t3",
"severity": "Moderate"
},
{
"title": "t4",
"severity": "Clean"
}
]
},
{
"agent": "5e2c98fc3d785252ce5b5694",
"mFiles": [
{
"title": "t5",
"severity": "Critical"
},
{
"title": "t6",
"severity": "Critical"
},
{
"title": "t7",
"severity": "Moderate"
}
]
}
]
Output:
[
{
"_id": "Moderate",
"count": 2
},
{
"_id": "Critical",
"count": 4
},
{
"_id": "Clean",
"count": 1
}
]
For mongoose integration:
//agents must be an array of objectIds like this
// [ObjectId("5e2c98fc3d785252ce5b5693"), ObjectId("5e2c98fc3d785252ce5b5694")]
//or ["5e2c98fc3d785252ce5b5693","5e2c98fc3d785252ce5b5694"]
const ObjectId = require("mongoose").Types.ObjectId;
let c = await Scan.aggregate([
{
$match: {
agent: {
$in: agents
}
}
},
{
$project: {
"mFiles.severity": true
}
},
{
$unwind: "$mFiles"
},
{
$group: {
_id: "$mFiles.severity",
count: {
$sum: 1
}
}
}
]);
Best place for learning mongodb aggregation is the official docs.

MongoDB/Mongoose: append related records to each aggreation result

Given the following Mongo collection called "members"
{
{name: "Joe", hobby: "Food"}, {name: "Lyn", hobby: "Food"},
{name: "Rex", hobby: "Play"}, {name: "Rex", hobby: "Shop"},...
}
I have an aggregation query that returns a paged set of records along with metadata for the total records found:
db.members.aggregate([
{
$facet: {
pipe1: [{ $count: 'count' }],
pipe2: [{ $skip: 0 }, { $limit: 4 }],
},
},
{
$unwind: '$pipe1',
},
{
$project: {
count: '$pipe1.count',
results: '$pipe2',
},
},
])
This gives me:
{count: 454, results: [<First 4 records here>]}
I am now trying to add to each record, an array of all member names that have the same hobby. So for the collection above, something like:
{
count: 454,
results: [
{name: "Joe", hobby: "Food", fanClub: ["Joe", "Lyn", "Alfred"]},
{name: "Lyn", hobby: "Food", fanClub: ["Joe", "Lyn", "Alfred"]},
{name: "Rex", hobby: "Play", fanClub: ["Rex"]},
{name: "Rex", hobby: "Shop", fanClub: ["Rex", "Rita"]}
]
}
I can't figure out how to run the follow up query within the aggregate. I've tried:
db.members.aggregate([
{
$facet: {
pipe1: [{ $count: 'count' }],
pipe2: [
{ $skip: 0 },
{ $limit: 2 },
{
$lookup: {
from: 'members',
pipeline: [{ $match: { hobby: '$hobby' } }],
as: 'fanClub',
},
},
],
},
},
{
$unwind: '$pipe1',
},
{
$project: {
count: '$pipe1.count',
results: '$pipe2',
},
},
])
Alas, the fanClub array is always empty.
Update 1
If I hardcode the hobby, for instance replace
{ $match: { hobby: '$hobby' }
with
{ $match: { hobby: 'Food' }
Then I do get results and all the fanClub arrays contain the results for Joe, Lyn and Alfred. So I must not be referring to the value within the pipeline correctly
Please try this :
db.membersHobby.aggregate([
{
$facet: {
pipe1: [{ $count: 'count' }],
pipe2: [{
$lookup:
{
from: "membersHobby",
let: { hobby: "$hobby" },
pipeline: [
{
$match:
{ $expr: { $eq: ["$hobby", "$$hobby"] } }
},
{ $project: { name: 1, _id: 0 } }
],
as: "fanClub"
}
}, { $skip: 0 }, { $limit: 4 }]
}
},
{
$unwind: '$pipe1'
},
{
$project: {
count: '$pipe1.count',
results: '$pipe2'
}
}
])
Result :
/* 1 */
{
"count" : 4,
"results" : [
{
"_id" : ObjectId("5e20a63ed3c98f2a7100fd4a"),
"name" : "Joe",
"hobby" : "Food",
"fanClub" : [
{
"name" : "Joe"
},
{
"name" : "Lyn"
}
]
},
{
"_id" : ObjectId("5e20a63ed3c98f2a7100fd4b"),
"name" : "Lyn",
"hobby" : "Food",
"fanClub" : [
{
"name" : "Joe"
},
{
"name" : "Lyn"
}
]
},
{
"_id" : ObjectId("5e20a63ed3c98f2a7100fd4c"),
"name" : "Rex",
"hobby" : "Play",
"fanClub" : [
{
"name" : "Rex"
}
]
},
{
"_id" : ObjectId("5e20a63ed3c98f2a7100fd4d"),
"name" : "Rex",
"hobby" : "Shop",
"fanClub" : [
{
"name" : "Rex"
}
]
}
]
}
If #srinivasy's answer meets your requierements, please grant my points him :)
If you want to get such structure:
{
count: 454,
results: [
{name: "Joe", hobby: "Food", fanClub: ["Joe", "Lyn", "Alfred"]},
{name: "Lyn", hobby: "Food", fanClub: ["Joe", "Lyn", "Alfred"]},
{name: "Rex", hobby: "Play", fanClub: ["Rex"]},
{name: "Rex", hobby: "Shop", fanClub: ["Rex", "Rita"]}
]
}
Use this query ($reduce is used to return single value, in you case fanClub as array):
db.members.aggregate([
{
$facet: {
pipe1: [
{
$count: "count"
}
],
pipe2: [
{
$skip: 0
},
{
$limit: 4
},
{
$lookup: {
from: "members",
let: {
hobby: "$hobby"
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$hobby",
"$$hobby"
]
}
}
}
],
as: "fanClub"
}
}
]
}
},
{
$unwind: "$pipe1"
},
{
$project: {
count: "$pipe1.count",
results: {
$map: {
input: "$pipe2",
as: "pipe2",
in: {
_id: "$$pipe2._id",
hobby: "$$pipe2.hobby",
name: "$$pipe2.name",
fanClub: {
$reduce: {
input: "$$pipe2.fanClub",
initialValue: [],
in: {
$concatArrays: [
"$$value",
[
"$$this.name"
]
]
}
}
}
}
}
}
}
}
])
MongoPlayground