Filtering items within object that is multiple level down with MongoQuery - mongodb

Please consider below model.
interface Model {
title: string
content: string
comments: {
content: string
likes?: number
subComments: {
parentCommentId: string
content: string
likes?: number
}[]
}[]
}
const post: Model = {
title: 'test',
content: 'content',
comments: [
{
content: 'level2',
likes: 3,
subComments: [
{
parentCommentId: '1',
content: 'level3',
likes: 3,
},
],
},
{
content: 'level2',
likes: 3,
subComments: [
{
parentCommentId: '1',
content: 'level3',
likes: 5,
},
{
parentCommentId: '1',
content: 'level3',
likes: 5,
},
],
},
],
}
Let's say we have a post that has comment that has subComment.
The level of subComment is at level 3 but the depth is fixed.
Is there a way to filter subComments that has optional key "likes" and whose value is greater than 3 with MongoClient?

I don't know exactly what output do you want but you can try something like this example using only projection into find query:
Here there are two $filter and one map.
The $map get each object into comments array and:
Set the content as it is: content: "$$comment.content"
Set the subcomment as an array filtered only by these one objects where likes is greater than 3.
*I've not added likes because it seems no congruent but can be added using likes: "$$comment.likes"
So this produce an array with only obbjects where there are more than 3 likes.
That's mean it can produce an empty subComments array. So the result of the $map is used into another $filter to get only object where subComments is not empty (i.e. there is at least one comment with more than 3 likes).
db.collection.find({},
{
title: 1,
content: 1,
comments: {
$filter: {
input: {
$map: {
input: "$comments",
as: "comment",
in: {
content: "$$comment.content",
subComments: {
$filter: {
input: "$$comment.subComments",
as: "subComment",
cond: {
$gt: [
"$$subComment.likes",
3
]
}
}
}
}
}
},
cond: {
$ne: [
"$$this.subComments",
[]
]
}
}
}
})
Example here.
Which is also the same to use $project in an aggregate query: example

Related

How can I get count of document in one to many relation with mongoose

I have two schemas.
// tutorial
export const TutorialSchema = new mongoose.Schema({
title: String,
author: String,
tags: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Tag"
}
]
})
// tag
export const TagSchema = new mongoose.Schema({
name: String,
companyId: Number
})
constructor(#InjectModel('Tutorial') private readonly _tutorialModel: Model<any>) { }
I want to get count of tags for each tutorial (in one query). How can I do that ?
I know how to get list of tutorial.
const result = await _tutorialModel.find()
You can use group aggregation in the following way -
_tutorialModel.aggregate([
{
$project: {
title: 1,
_id:1,
numberOfTags: { $cond: { if: { $isArray: "$tags" }, then: { $size: "$tags"
}, else: "NA"} }
}
}
] )
For Size operator you need to ensure first that tags is always an array! If for any reason tags is not present then error will be thrown.
If you make sure tags will be always present and is an array then you can simplify to following -
_tutorialModel.aggregate([
{
$project: {
title: 1,
_id:1,
numberOfTags: {$size: "$tags"}
}
}
] )
Take a look at -
Size aggregation operator -
https://www.mongodb.com/docs/manual/reference/operator/aggregation/size/

How to make and requests in mongodb queries

I've worked on this for about an hour now and I can't figure anything out that works so sorry if this is obvious.
I want my query to only return results where every result matches, but right now it returns a result if at least one match is found.
My document looks like this...
{
country: 'Great Britain',
data: [
{
school: 'King Alberts',
staff: [
{
name: 'John',
age: 37,
position: 'Head Teacher'
},
{
name: 'Grace',
age: 63,
position: 'Retired'
},
{
name: 'Bob',
age: 25,
position: 'Receptionist'
}
]
},
{
school: 'St Johns',
staff: [
{
name: 'Alex',
age: 22,
position: 'Head of Drama'
},
{
name: 'Grace',
age: 51,
position: 'English Teacher'
},
{
name: 'Jack',
age: 33,
position: 'Receptionist'
}
]
},
// ... more schools
]
}
The query I'm currently making looks like...
{ 'data.staff.name': { $in: names } }
and the 'names' array that is being provided looks like ['Bob', 'Grace', 'John', 'Will', 'Tom']. Currently both schools are being returned when I make this query, I think it's because the 'names' array contains 'Grace' which is a name present at both schools and so the document it matching. Does anyone know if there's a query I could make so mongodb only returns the school object if every name in the 'names' array is a member of staff at the school?
You need to use the aggregation pipeline for this, after matching the document we'll just filter out the none matching arrays, like so:
db.collection.aggregate([
{
$match: {
"data.staff.name": {
$in: names
}
}
},
{
$addFields: {
data: {
$filter: {
input: "$data",
cond: {
$eq: [
{
$size: {
"$setIntersection": [
"$$this.staff.name",
names
]
}
},
{
$size: "$$this.staff"
}
]
}
}
}
}
}
])
Mongo Playground

Mongoose - projection with $elemMatch on nested fields

I'm relatively new to MongoDB/Mongoose and I've only performed simple queries. Now I'm having some trouble trying to filter my database in a slightly more complex way. I already did some research to tackle my previous issues, but now I can't move forward. Here's what happening:
This is my schema:
const userSchema = new mongoose.Schema({
email: String,
password: String,
movies: [
{
title: String,
movieId: Number,
view_count: Number,
rating: Number,
review: String,
},
],
lists: {
watched_movies: [
{
title: String,
director: String,
genres: [{ type: String }],
runtime: Number,
date: Date,
},
],
},
});
I want to make a GET request that matches simultaneously "lists.watched_movies": { _id: req.params.entryId } and also "movies.title": req.body.title for a given email, so that the outcome of the findOne query would be just those elements and not the whole document. What I'm trying to accomplish is something like that:
{
email: "some.email#gmail.com",
movies: [
{
title: "Mongoose Strikes Back",
movieId: 123,
view_count: 1,
rating: 3,
review: "Very confusing movie!"
}
],
lists: {
watched_movies: [
{
_id: 4321
title: "Mongoose Strikes Back",
director: "Mongo",
genres: ["Drama"],
runtime: 150,
date: "2021-11-22"
}
]
}
}
My first attempt to tackle it, however, wasn't successful. Here's what I tried:
router.route("/:entryId").get((req, res) => {
User.findOne(
{ email: "some.email#gmail.com" },
{
"lists.watched_movies": { $elemMatch: { _id: req.params.entryId } },
movies: { $elemMatch: { title: req.body.title } },
},
(err, entry) => {
if (!err) {
res.send(entry);
console.log(entry);
} else {
console.log(err);
}
}
);
});
It says that Cannot use $elemMatch projection on a nested field. I thought that maybe I can solve it by changing my schema, but I'd like to avoid it if possible.
For your scenario, you can use $filter to filter document(s) in nested array field.
db.collection.find({
email: "some.email#gmail.com"
},
{
"lists.watched_movies": {
"$filter": {
"input": "$lists.watched_movies",
"cond": {
"$eq": [
"$$this._id",
4321// req.params.entryId
]
}
}
},
movies: {
$elemMatch: {
title: "Mongoose Strikes Back"// req.body.title
}
}
})
Sample Mongo Playground

Find MongoDB document with the latest date according to different fields

We have data stored in MongoDB by country code. Our document looks like the following,
[
{
title: '1',
US: {
data: { lastReportDate: '2021-09-09' } // will be fetched
},
GB: {
data: { lastReportDate: '2021-09-04' }
}
},
{
title: '2',
US: {
data: { lastReportDate: '2021-09-07' } // will NOT be fetched
}
},
{
title: '3',
US: {
data: null // will NOT be fetched
}
},
{
title: '4',
US: {
data: null
}
GB: {
data: { lastReportDate: '2021-09-08' } // will be fetched
},
NZ: {
data: { lastReportDate: '2021-09-04' }
}
},
{
title: '5',
GB: {
data: null
},
NZ: {
data: { lastReportDate: '2021-09-06' } // will be fetched
}
}
]
I want to fetch the titles which have the latest dates according to the countries.
For EX: in the above DB, we have the latest date for US as '2021-09-09', so I want to fetch all the titles which match this date in lastReportDate. For GB, the latest date is '2021-09-08' and for NZ, its '2021-09-06'.
We have around 180 countries in one document and I want to hit the DB minimum times. So can we build a query that can us latest dates for different countries and then query the Database according to that.
You can try below aggregation:
db.collection.aggregate([
{
$project: {
doc: {
$objectToArray:"$$ROOT"
},
title: "$title"
}
},
{
$unwind: "$doc"
},
{
$match: {
"doc.k": { $nin: [ "_id", "title" ] }
}
},
{
$group: {
_id: "$doc.k",
maxDate: { $max: "$doc.v.data.lastReportDate" },
titles: { $push: { date: "$doc.v.data.lastReportDate", title: "$title" } }
}
},
{
$project: {
_id: 0,
country: "$_id",
maxTitles: { $filter: { input: "$titles", cond: { $eq: [ "$$this.date", "$maxDate" ] } } }
}
}
])
The challenge here is that your countries are represented as keys of your document so you need to start with $obectToArray operator which in conjunction with $unwind will give you a list of countries with corresponding dates and titles.
Once you have them you can use $group to get $max date and then use $filter to get titles related to max date.
Mongo Playground

$in operator not working in MongoDB Aggregation

I'm trying to make a discover page for a social media website. The discover page queries the database for all posts that satisfy four things:
User has not already liked post
Post tags do not violate user's filtered tag content
Post text content does not violate user's filtered post content
And finally the part of the aggregation giving me trouble:
Post tagIds contain a given tagId from user (a post using the same tag that the user already follows)
Here's the function:
const asyncFetchTagPosts = async (
query,
//here's a given tag that a user already follows
tagId,
likedPostIds,
Post,
User,
mongoose,
handleFilterTagRegex,
handleFilterPostContentRegex
) => {
var recastTagId = mongoose.Types.ObjectId(tagId)
var user = await User.findOne({ blogName: query })
var filteredTagRegex = handleFilterTagRegex(user)
var filteredPostContentRegex = handleFilterPostContentRegex(user)
var posts = await Post.aggregate([
{
$lookup: {
from: 'posts',
let: {
likedPostIds: likedPostIds,
tagId: recastTagId,
filteredTagRegex: filteredTagRegex,
filteredPostContentRegex: filteredPostContentRegex
},
pipeline: [
{
$match: {
$expr: {
$and: [
{ $not: { $in: ["$_id", "$$likedPostIds"] } },
{ $not: [
{
$regexMatch: {
input: "$tagTitles",
regex: "$$filteredTagRegex"
}
}
]
},
{ $not: [
{
$regexMatch: {
input: "$allText",
regex: "$$filteredPostContentRegex"
}
}
]
},
{ $or: [
//here's the bad expression, $tagIds won't resolve to an array
{ $in: [ "$$tagId", "$tagIds" ] },
]
}
]
}
}
}
],
as: 'posts'
}
},
{ $unwind: '$posts' },
{ $replaceRoot: { "newRoot": "$posts" } },
{ $sort: { "notesHeatLastTwoDays": -1 } },
{ $limit: 5 }
])
return posts
}
Here's the Post model:
import mongoose from 'mongoose';
const Schema = mongoose.Schema;
const options = { discriminatorKey: 'kind' }
const PostSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'User'
},
allText: {
type: String
},
descriptions: [
{
kind: String,
content: String,
displayIdx: Number
}
],
descriptionImages: [
{
type: Schema.Types.ObjectId,
ref: 'Image'
}
],
tagIds: [
{
type: Schema.Types.ObjectId,
ref: 'Tag'
}
],
tagTitles: {
type: String
},
mentions: [
{
type: Schema.Types.ObjectId,
ref: 'Mention'
}
],
notesCount: {
type: Number,
default: 0
},
notesHeatLastTwoDays: {
type: Number,
default: 0
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
},
kind: {
type: String,
default: 'Post'
}
}, options)
const Post = mongoose.model('Post', PostSchema, 'posts')
export default Post;
I keep getting this error:
Error: $in requires an array as a second argument, found: missing
When I comment out the last part of the query the aggregation works. It returns data in this shape:
{
_id: 60c18ee43730198901cfae9b,
descriptionImages: [],
//here's the array I'm trying to get to resolve in the aggregation
tagIds: [],
mentions: [],
notesCount: 1,
notesHeatLastTwoDays: 0,
kind: 'VideoPost',
descriptions: [],
createdAt: 2021-06-10T04:02:44.744Z,
updatedAt: 2021-06-11T08:51:38.166Z,
user: 608f213bb4a094bd91e02936,
videoLink: 60c3241a6c9ed4d1fc908270,
allText: '',
__v: 1,
tagTitles: ''
},
I thought using the $ operator in the aggregation gave me access to each document, does it just not work if you try to use the variable as the first expression?
you need to handle missing "$tagIds" by setting it to empty array []
{
$ifNull: [
"$tagIds",
[]
]
}
https://docs.mongodb.com/manual/reference/operator/aggregation/ifNull/
so you pipeline stage would be
{ $or: [
//here's the bad expression, $tagIds won't resolve to an array
{ $in: [ "$$tagId", { $ifNull: [ "$tagIds", [] ] } ] },
]
}