I have the following MongoDB documents:
{
_id: ObjectId('09de14821345dda65c471c99'),
items: [
_id: ObjectId('34de64871345dfa655471c99'),
_id: ObjectId('34de64871345dfa655471c91'),
_id: ObjectId('34de64871345dfa655471c99'),
]
},
{
_id: ObjectId('09de14821345dda65c471c98'),
items: [
_id: ObjectId('24de64871345dfa61271c10'),
_id: ObjectId('24de64871345dfa61271c11'),
_id: ObjectId('24de64871345dfa61271c11'),
]
},
{
_id: ObjectId('09de14821345dda65c471c07'),
items: [
_id: ObjectId('24de64871345dfa61271c05'),
_id: ObjectId('24de64871345dfa61271c06'),
_id: ObjectId('24de64871345dfa61271c07'),
]
}
I need to find all documents with repeated items array elements. So from the documents above I want to get the following result:
db.collection.documents.find({/** need query*/}).toArray(function (err, documents) {
console.dir(documents); // documents with id's 09de14821345dda65c471c99 and 09de14821345dda65c471c98
});
How could I do that?
In order to group and match results you will need to use the Aggregation Framework or Map/Reduce rather than a simple find() query.
Example data
Your example document include some errors: a few of the ObjectIDs are too short and the array elements should either be embedded documents ({_id: ObjectId(...)}) or simple values.
For test data I've used:
db.mydocs.insert([
{
_id: ObjectId('09de14821345dda65c471c99'),
items: [
ObjectId('34de64871345dfa655471c99'),
ObjectId('34de64871345dfa655471c91'),
ObjectId('34de64871345dfa655471c99')
]
},
{
_id: ObjectId('09de14821345dda65c471c98'),
items: [
ObjectId('24de64871345ddfa61271c10'),
ObjectId('24de64871345ddfa61271c11'),
ObjectId('24de64871345ddfa61271c11')
]
},
{
_id: ObjectId('09de14821345dda65c471c07'),
items: [
ObjectId('24de64871345ddfa61271c05'),
ObjectId('24de64871345ddfa61271c06'),
ObjectId('24de64871345ddfa61271c07')
]
}])
Aggregation query
Here is an aggregation query using the mongo shell:
db.mydocs.aggregate(
// Unpack items array into stream of documents
{ $unwind: "$items" },
// Group by original document _id and item
{ $group: {
_id: { _id: "$_id", item: "$items" },
count: { $sum: 1 }
}},
// Limit to duplicated array items (1 or more count per document _id)
{ $match: {
count: { $gt: 1 }
}},
// (Optional) clean up the result formatting
{ $project: {
_id: "$_id._id",
item: "$_id.item",
count: "$count"
}}
)
Sample results
{
"_id" : ObjectId("09de14821345dda65c471c98"),
"count" : 2,
"item" : ObjectId("24de64871345ddfa61271c11")
}
{
"_id" : ObjectId("09de14821345dda65c471c99"),
"count" : 2,
"item" : ObjectId("34de64871345dfa655471c99")
}
Related
I have a need to use $elemMatch in an aggregation pipeline and I need to compare 2 fields of an object from a nested array of objects:
Example collection:
name: 'xxx',
steps: [
{
userId: 'abc',
senderId: 'abc'
},
...
]
What I'm trying to do is return all that have at least 1 step where userId = senderId.
I have tried the following, but I get an error that $expr isn't allowed as a child of $elemMatch:
{
$match: {
steps: {
$elemMatch: {
$expr: { $eq: ['$userId', '$senderId'] },
},
},
},
}
Thanks.
$elemMatch can only be used in projection.
You can workaround for comparing fields in the array as below:
$set - Create new field filteredCount with get the array size $size of filtered array.
$match - Get filteredCount greater than 0.
db.collection.aggregate({
$set: {
filteredCount: {
$size: {
$filter: {
input: "$steps",
cond: {
$eq: [
"$$this.userId",
"$$this.senderId"
]
}
}
}
}
}
},
{
$match: {
"filteredCount": {
$gt: 0
}
}
})
Sample Mongo Playground
I have a sample mongoose object that looks like this:
{
_id: 5fa849ad4f6be0382363809c,
ratings: {
ratedPersonId: 7,
rating: 7,
timeSpent: 30,
timestamp: 78,
userThreshold: 5
}
},
it contains an _id and a list of ratings which is a subdocument with the following features.
I have created an aggregation pipeline like this:
const ratedUser = await this.ratingModel
.aggregate([
{ $project: { ratings: 1 } },
{ $unwind: '$ratings' },
{
$match: {
$and: [{ 'ratings.ratedPersonId': userId }, { 'ratings.rating': { $gte: 5 } }],
},
},
])
.exec()
This works for the first condition ratings.ratedPersonId: userId
My problem is that my second condition is the rating should be greater than or equal to the userThreshold field in the same object.
whenever I type that in the query it returns nothing
$and: [{ 'ratings.ratedPersonId': userId }, { 'ratings.rating': { $gte: 'ratings.threshold'} }],
Demo - https://mongoplayground.net/p/AQMsJGkoFcu
Use $expr to compare the fields
Read aggregation-expressions
$expr can build query expressions that compare fields from the same document in a $match stage.
If the $match stage is part of a $lookup stage, $expr can compare fields using let variables. See Specify Multiple Join Conditions with $lookup for an example.
$expr only uses indexes on the from the collection for equality matches in a $match stage.
$expr does not support multikey indexes.
db.collection.aggregate([
{
$project: {
ratings: 1
}
},
{
$unwind: "$ratings"
},
{
$match: {
$and: [
{
"ratings.ratedPersonId": 7
},
{
$expr: {
$gte: [
"$ratings.rating",
"$ratings.userThreshold"
]
}
}
],
},
},
])
I need to get docs from MongoDB collection where ID's are in array:
[
'5f80a44d0179262f7c2e6a42',
'5f8c00762fae890e9c4d029c',
'5f802cf8abac1116a46bf9d4'
]
The issue is, docs are not coming in sequence of my array ID's. They are coming (1, 0, 2) for above array ID's.
How can I make them in sequence of my ID's array? I am using, NodeJs + Mongoose.
My code:
var ids = ['5f80a44d0179262f7c2e6a42','5f8c00762fae890e9c4d029c','5f802cf8abac1116a46bf9d4']
Product.find({
_id: {
$in: ids
}
})
I don't think its possible with find(), or any functionality available in MongoDB related to this,
It is possible with aggregate() but this will just fulfil your expected result,
I am not recommending to use this because this will affect performance of query response, this could be a heavy transaction.
$match your conditions
$group by null and make array of all matching objects in root
$addFields to add ids array that we search for
$unwind deconstruct ids array, this will deconstruct in sequence as per we provided in ids array
$project, $reduce input as root array and check condition if id match then return object
$replaceWith to replace root object to root
var ids = [
ObjectId("5f802cf8abac1116a46bf9d4"),
ObjectId("5f8c00762fae890e9c4d029c"),
ObjectId("5f80a44d0179262f7c2e6a42")
];
Product.aggregate([
{ $match: { _id: { $in: ids } } },
{
$group: {
_id: null,
root: { $push: "$$ROOT" }
}
},
{ $addFields: { ids: ids } },
{ $unwind: "$ids" },
{
$project: {
root: {
$reduce: {
input: "$root",
initialValue: {},
in: { $cond: [{ $eq: ["$$this._id", "$ids"] }, "$$this", "$$value"] }
}
}
}
},
{ $replaceWith: "$root" }
])
Playground
I am aggregating a large data where i need to group the data according to their types and also i need to lookup the data from another collections.inside $group i want my lookup's data.
my code for aggregation goes like :
NotificationSchema.aggregate([{
$match: condition
}, {
$group: {
_id: "$type",
details: {
$push: "$$ROOT"
},
count: {
$sum: 1
}
}
}, {
$sort: {
_id: -1
}
}, {
$lookup: {
from: "vehicles",
localField: "details.device_id",
foreignField: "device_id",
as: "vehicle"
}
}], function(err, result) {
if (err) {
res.status(500);
return res.json({
result: err
});
}
console.log('res', result[0].details[0]);
res.json({
result: result
});
});
if i remove or comment the $group code i get the data with Vehicle array but using $group i get vehicle array empty, as i have only two types in records in the database, i get two empty array of vehicles. but i have 102 records so i need 102 arrays of vehicles how can i get such result.
what i am getting in console right now is
res [ { _id: 'Vehicle Delay Alert!',
details:
[ [Object],
....57 object...
[Object] ],
count: 57,
vehicle: [] },
and inside every object i dont find vehicle array so i wish to remove vehicle array from here and get a vehicle array that is generated from $lookup inside every object.
Any suggestions are highly appreciated.
You $lookup from details.device_id which comes from an array. To $lookup from a regular field, you can place $lookup after the $match :
NotificationSchema.aggregate([{
$match: condition
}, {
$lookup: {
from: "vehicles",
localField: "device_id",
foreignField: "device_id",
as: "vehicle"
}
}, {
$group: {
_id: "$type",
details: {
$push: "$$ROOT"
},
count: {
$sum: 1
}
}
}, {
$sort: {
_id: -1
}
}])
I have a collection of songs and its metadata with the following structure:
[{
title:"title",
artist:"artist",
album:"album,
...
},...
Now I want to get a list of every artist with the number of songs and the number of albums it has using Node.js. So far, using the aggregation framework, I've been able to get an array of objects with each artist, its number of songs and an array with the album titles (instead of just the count), using the following pipeline:
collection.aggregate([
{ $project:{
artist:1,
album:1
}},
{ $group: {
_id: "$artist",
songs:{$sum: 1},
albums:{$addToSet:"$album"}
}},
{ $sort: { artist: 1 } }
]
If I replace $addToSet with $sum, I get albums:0 in every artist, because it expects numbers and not strings to sum.
I just can't get around it!
You need to add a couple of steps to your pipeline - the array of albums needs to be unwound and then counted. Here is what it would look like:
collection.aggregate([
{ $project:{
artist:1,
album:1
}},
{ $group: {
_id: "$artist",
songs:{$sum: 1},
albums:{$addToSet:"$album"}
}},
{ $unwind: "$albums"},
{ $group: {
_id: "$_id",
songs:{$first: 1},
albums:{$sum: 1}
}},
{ $sort: { artist: 1 } }
]
)