In MongoDB I have the following document:
directConfigs: [
{
id: ObjectId('627279d3ba7aef5d6418c867')
name: "Config A"
},
{
id: ObjectId('628e32777b9e83619746ee3f')
name: "Config B"
}
],
indirectConfigs: [
{
id: ObjectId('627279d3ba7aef5d6418c867')
name: "Config A"
},
{
id: ObjectId('628b4d3ff5b1c1736c0b654a')
name: "Config C"
}
]
I want to make a project that says if the config exist in both directConfigs and indirectConfigs then add a field to it called type and set the value to "Both", and if it only exists in one of the fields, add a value called direct or indirect depending on what field it exist in. And this should return an array looking like this:
configs: [
{
id: ObjectId('627279d3ba7aef5d6418c867')
name: "Config A"
type: "Both"
},
{
id: ObjectId('628e32777b9e83619746ee3f')
name: "Config B"
type: "direct"
},
{
id: ObjectId('628b4d3ff5b1c1736c0b654a')
name: "Config C",
type: "indirect"
}
]
One option is to use $setIntersection and $setDifference:
db.collection.aggregate([
{$set: {both: {$setIntersection: ["$directConfigs", "$indirectConfigs"]}}},
{$set: {
direct: {$setDifference: ["$directConfigs", "$both"]},
indirect: {$setDifference: ["$indirectConfigs", "$both"]}
}},
{$project: {
configs: {$concatArrays: [
{$map: {input: "$both", in: {$mergeObjects: ["$$this", {type: "both"}]}}},
{$map: {input: "$direct", in: {$mergeObjects: ["$$this", {type: "direct"}]}}},
{$map: {input: "$indirect", in: {$mergeObjects: ["$$this", {type: "indirect"}]}}}
]}
}}
])
See how it works on the playground example
Related
My model :
const scheduleTaskSchema = new Schema({
activity: { type: Object, required: true },
date: { type: Date, required: true },
crew: Object,
vehicle: Object,
pickups: Array,
details: String,
});
const ScheduleTaskModel = mongoose.model("schedule_task", scheduleTaskSchema),
and this aggregation pipeline :
let aggregation = [
{
$sort: {
"pickups.0.time": 1,
},
},
{
$group: {
_id: "$date",
tasks: { $push: "$$ROOT" },
},
},
{ $sort: { _id: -1 } },
];
if (hasDateQuery) {
aggregation.unshift({
$match: {
date: { $gte: new Date(start_date), $lte: new Date(end_date) },
},
});
} else {
aggregation.push({ $limit: 2 });
}
const scheduledTasksGroups = await ScheduleTaskModel.aggregate(aggregation);
the crew object can have arbitrary number of keys with this structure :
crew : {
drivers: [
{
_id: "656b1e9cf5b894a4f2v643bc",
name: "john"
},
{
_id: "567b1e9cf5b954a4f2c643bhh",
name: "bill"
}
],
officers: [
{
_id: "655b1e9cf5b6632a4f2c643jk",
name: "mark"
},
{
_id: "876b1e9af5b664a4f2c234bb",
name: "jane"
}
],
//...any number of keys that contain an array of objects that all have an _id
}
I'm looking for a way to return all documents (before sorting/grouping) that contain a given _id anywhere within the crew object without knowing which key to search,it can be many different keys that all contain an array of objects that all have an _id
Any ideas ?
You can use $objectToArray for this:
db.collection.aggregate([
{$addFields: {crewFilter: {$objectToArray: "$crew"}}},
{$set: {
crewFilter: {$size: {
$reduce: {
input: "$crewFilter",
initialValue: [],
in: {$concatArrays: [
"$$value",
{$filter: {
input: "$$this.v",
as: "member",
cond: {$eq: ["$$member._id", _id]}
}
}
]
}
}
}}
}},
{$match: {crewFilter: {$gt: 0}}}
])
See how it works on the playground example
I have three documents that looks like this:
_id: ObjectId('61e1312ad435c7124aa883a1')
name: "Brian"
languages: "English,Spanish,French"
_id: ObjectId('52e1312ad435c7124aa883a2')
name: "Max"
languages: "English"
_id: ObjectId('37e1312ad435c7124aa883a9')
name: "Mike"
languages: ""
As you can see, the languages field can either be empty, have one item, or multiple items separated by commas.
I want to create a new field, that is an Array of objects. Each object should consist of a "name" property and an "active" boolean. "name" should be the language and "active" should just be set to false. The end result should look like this:
_id: ObjectId('61e1312ad435c7124aa883a1')
name: "Brian"
languages: "English,Spanish,French"
newLanguages: [
{ name: "English", active: false },
{ name: "Spanish", active: false },
{ name: "French", active: false }
]
_id: ObjectId('52e1312ad435c7124aa883a2')
name: "Max"
languages: "English,Spanish,French"
newLanguages: [
{ name: "English", active: false }
]
_id: ObjectId('37e1312ad435c7124aa883a9')
name: "Mike"
languages: ""
newLanguages: []
I've managed to turn the comma-separated strings into the objects I want, but it doesn't create a new property, it just overwrites the languages property:
db.collection.updateMany({},
[
{
$set: {
languages: {
$filter: {
input: {$split: ["$languages", ","]},
cond: {$gt: [{$strLenCP: "$$this"}, 0]}
}
}
}
},
{
$set: {
languages: {
$map: {
input: "$languages",
as: "item",
in: {name: "$$item", active: false}
}
}
}
}
])
Simply set a new key newLanguages, instead of the existing key languages, which will stay as it was:
db.collection.updateMany({},
[
{
$set: {
newLanguages: {
$filter: {
input: {$split: ["$languages", ","]},
cond: {$gt: [{$strLenCP: "$$this"}, 0]}
}
}
}
},
{
$set: {
newLanguages: {
$map: {
input: "$newLanguages",
as: "item",
in: {name: "$$item", active: false}
}
}
}
}
])
See how it works on the playground example
Sample of my database:
{
name: 'The Perks of Being a Wallflower'
genres: ['Drama', 'Romance']
rating: 8
},
{
name: 'The Edge of Seventeen'
genres: ['Drama', 'Comedy']
rating: 7.3
},
{
name: 'Little Women',
genres: ['Drama', 'Romance']
rating: 7.8
}
I want to project the first element of the genres array if it's Comedy or Romance.
I tried this :
db.shows.find(
{},
{ genres: { $elemMatch: { $or: [{ $eq: 'Drama' }, {$eq: 'Comedy'}] } } }
);
But it gives me this error "unknown top level operator: $eq".
Use $in
The $in operator selects the documents where the value of a field equals any value in the specified array. To specify an $in expression, use the following prototype:
Demo - https://mongoplayground.net/p/MAuWUeP9GIE
db.collection.find({
genres: {
$elemMatch: {
"$in": [ "Drama", "Comedy" ]
}
}
})
Demo - https://mongoplayground.net/p/JJJkoHuz_DZ
db.collection.find({},
{
genres: {
$elemMatch: {
"$in": [ "Drama", "Comedy" ]
}
}
})
let's say I have a collection called pages as
{
_id: "pageid",
name: "Mongodb"
},
{
_id: "pageid2",
name: "Nodejs"
}
and user collection as follows
{
_id : "userid1",
following: ["pageid"],
...
},
{
_id : "userid2",
following: ["pageid", "pageid2"],
...
}
how could I make a query to retrieve the pages information along with the number of users follow each page in mongodb, expected result as follows
[
{
_id: "pageid",
name: "MongoDB",
followers: 2
},
{
_id: "pageid2",
name: "Nodejs",
followers: 1
},
]
You can use $lookup and $size to count total followers,
db.pages.aggregate([
{
$lookup: {
from: "user",
localField: "_id",
foreignField: "following",
as: "followers"
}
},
{
$addFields: {
followers: { $size: "$followers" }
}
}
])
Playground
I'm using node.js and mongodb, I have an array of objects which holds the names of an id. Let's say below is my array
let names = [
{ value: 1, text: 'One' },
{ value: 2, text: 'Two' },
{ value: 3, text: 'Three' },
{ value: 4, text: 'Gour' }
]
And this is my query result of a collection using $group which gives me the distinct values.
[
{ _id: { code: '1', number: 5 } },
{ _id: { code: '2', number: 5 } },
{ _id: { code: '3', number: 2 } },
{ _id: { code: '4', number: 22 } },
]
$lookup let's us to join the data from a different collection, but in my case I have an array which holds the text value for each of the codes which I got from the query.
Is there a way we can map the text from the array to the results from mongodb?
Any help will be much appreciated.
EDIT
MongoDB query which I was trying
db.collection.aggregate([
{
$match: {
_Id: id
}
},
{
$lookup: {
localField: "code",
from: names,
foreignField: "value",
as: "renderedNames"
}
},
{
"$group" : {
"_id": {
code: "$code",
number: "$number"
}
}
}
]);
Local variable lives in nodejs app, and mongodb knows nothing about it.
It looks like it belongs to representation layer, where you want to show codes as meaningful names. The mapping should be done there. I believe find is the most suitable here:
names.find(name => name.code === doc._id.code).text
If the names are not truly variable but quite constant, you can move it to own collection, e.g. codeNames:
db.codeNames.insert([
{ _id: "1", text: 'One' },
{ _id: "2", text: 'Two' },
{ _id: "3", text: 'Three' },
{ _id: "4", text: 'Gour' }
]);
and use $lookup as following:
db.collection.aggregate([
{
$match: {
_Id: id
}
},
{
"$group" : {
"_id": {
code: "$code",
number: "$number"
}
}
},
{
$lookup: {
localField: "_id.code",
from: "codeNames",
foreignField: "_id",
as: "renderedNames"
}
}
]);
If none of the above suit your usecase, you can pass the names to the database in each request to map names db-side, but you must be really really sure you cannot use 2 previous options:
db.collection.aggregate([
{
$match: {
_Id: id
}
},
{
"$group" : {
"_id": {
code: "$code",
number: "$number"
}
}
},
{
$project: {
renderedNames: { $filter: {
input: [
{ value: "1", text: 'One' },
{ value: "2", text: 'Two' },
{ value: "3", text: 'Three' },
{ value: "4", text: 'Gour' }
],
as: "name",
cond: { $eq: [ "$$name.value", "$_id.code" ] }
}
}
}
},
]);
As a side note, I find $match: {_Id: id} quite confusing, especially followed by $group. If _Id is _id, it is unique. You can have no more than 1 document after this stage, so there is not too much to group really.