I am trying to find all the trips provided by a company, grouped by the driver of the bus, and check if a given passenger was part of the trip.
Content is an array that can have reference to multiple models: User, Cargo, etc.
I can somewhat achieve my desired result using:
traveled: { $in: [ passengerId, "$content.item" ] },
But i want to confirm that the matched id is indeed a 'User'(passenger). I have tried:
traveled: { $and: [
{ $in: [ passengerId, "$content.item" ] },
{ $in: [ `Passenger`, "$content.kind" ] },
]},
But it also matches if the passed id has a kind of 'Cargo' when there is another content with a kind of 'User' is inside the array.
// Trip
const schema = Schema({
company: { type: Schema.Types.ObjectId, ref: 'Company', required: false },
driver: { type: Schema.Types.ObjectId, ref: 'User', required: true },
description: { type: String, required: true },
....
content: [{
kind: { type: String, required: true },
item: { type: Schema.Types.ObjectId, refPath: 'content.kind', required: true }
}]
});
const Trip = mongoose.model('Trip', schema, 'trips');
Trip.aggregate([
{ $match: { company: companyId } },
{
$project: {
_id: 1,
driver: 1,
description: 1,
traveled: { $in: [ passengerId, "$content.item" ] },
// traveled: { $and: [
// { $in: [ passengerId, "$content.item" ] },
// { $in: [ `Passenger`, "$content.kind" ] },
// ]},
}
},
{
$group : {
_id: "$driver",
content: {
$push: {
_id: "$_id",
description: "$description",
traveled: "$traveled",
}
}
},
}
]).then(console.log).catch(console.log);
There is no $elemMatch operator in $project stage. So to use mimic similar functionality you can create $filter with $size being $gt > 0.
Something like
"traveled":{
"$gt":[
{"$size":{
"$filter":{
"input":"$content",
"as":"c",
"cond":{
"$and":[
{"$eq":["$$c.item",passengerId]},
{"$eq":["$$c.kind","User"]}
]
}
}
}},
0
]
}
Related
In my project I use MongoDB as a database (specifically the mongoose driver for typescript, but this shouldn't matter) and I have a collection of posts that follow this schema:
export const PostSchema = new Schema({
author: { type: Types.ObjectId, required: true, ref: 'User' },
text: { type: String, required: true },
tags: [{ type: Types.ObjectId, required: true, ref: 'Tag' }],
location: { type: PointSchema, required: true },
}
export const PointSchema = new Schema({
type: {
type: String,
enum: ['Point'],
required: true,
},
coordinates: {
type: [Number],
required: true,
},
locationName: {
type: String,
required: true,
},
});
My question is if it is possible to write a query (I think an aggregation is needed) that returns all posts that meet a condition (such as that the position must be at a specific distance) and orders the results by a specific array of tags passed as an argument (in my case the array varies from user to user and represents their interests).
That is, choosing the array ["sport", "music", "art"] as an example, I would like a query that retrieves from the database all the posts that meet a certain condition (irrelevant in this question) and orders the results so that first are documents whose array of tags share elements with the array ["sport", "music", "art"], and only at the end the documents without any correspondence.
That is, something like this:
[
{
_id: "507f191e810c19729de860ea",
tags: ["sport", "art", "tennis"] // 2 matches
},
{
_id: "507f191e810c1975de860eg",
tags: ["sport", "food"] // 1 matches
},
{
_id: "607f191e810c19729de860ea",
tags: ["animals", "art"] // 1 matches
},
{
_id: "577f191e810c19729de860ea",
tags: ["animals", "zoo"] //0 matches
}
]
if your collection looks like this:
[
{
"author": "John",
"tags": [
ObjectId("60278ce8b370ff29b83226e2"), // Sport
ObjectId("60278ce8b370ff29b83226e8"), // Music
ObjectId("60278ce8b370ff29b83226e5"), // Food
]
},
{
"author": "Dheemanth",
"tags": [
ObjectId("60278ce8b370ff29b83226e7"), // Tech
ObjectId("60278ce8b370ff29b83226e5"), // Food
ObjectId("60278ce8b370ff29b83226e2") // Sport
]
},
{
"author": "Niccolo",
"tags": [
ObjectId("60278ce8b370ff29b83226e2"), // Sport
ObjectId("60278ce8b370ff29b83226e8"), // Music
ObjectId("60278ce8b370ff29b83226e3") // Art
]
}
]
then this is the solution:
db.posts.aggregate([
{
$lookup: {
from: "tags",
let: { "tags": "$tags" },
pipeline: [
{
$match: {
$expr: { $in: ["$_id", "$$tags"] }
}
}
],
as: "tags"
}
},
{
$addFields: {
"tagCount": {
$size: {
$filter: {
input: "$tags",
as: "tag",
cond: { $in: ["$$tag.name", ["sport", "music", "art"]] }
}
}
}
}
},
{
$sort: { tagCount: -1 }
},
{
$project: {
_id: 1,
tags: "$tags.name"
}
}
])
Output:
[
{
"_id": ObjectId("60278e14b370ff29b83226eb"),
"tags": ["sport", "art", "music"]
},
{
"_id": ObjectId("60278e14b370ff29b83226e9"),
"tags": ["sport", "food", "music"]
},
{
"_id": ObjectId("60278e14b370ff29b83226ea"),
"tags": ["sport", "food", "tech"]
}
]
I have a collection in Mongoose called Points where I have a history of all points of a user
This is the point schema
const PointSchema = mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
reason: {
type: String,
required: true,
},
points: {
type: Number,
required: true,
},
time: {
type: Date,
default: Date.now,
},
});
I want a JSON output like this
{
"points": 30,
"history": [
{ Point object },
{ Point object },
{ Point object },
]
}
How can I get the sum of all the points queried by a particular user, and get the output as above if I have the particular user's id?
You could do a simple aggregation where you first $match the user-id, then $sum all the points and finally push each document to the history array:
db.collection.aggregate([
{
"$match": {
user: "<user-id-to-match>"
}
},
{
"$group": {
"_id": "$user",
"points": {
$sum: "$points"
},
history: {
"$push": "$$ROOT"
}
}
},
{
$project: {
_id: 0
}
}
])
Here's an example I've created on mongoplayground: https://mongoplayground.net/p/Q_TtcI_dkZu
I'm getting a list of 'spot' with mongoose filtered by location and other stuff, with the code below which works fine.
But I want the value of 'rate' to be a $avg (average) of all reviews and not the list of the reviews. It's an aggregation of another collection.
this is what I get:
{
"_id":"5f0ade7d1f84460434524d3d",
"name":"Fly",
...
"rate":[
{"_id":"5f0bfca64ca1cc02ffe48faf","spot_id":"5f0ade7d1f84460434524d3d","rate":3},
{"_id":"5f0bfdb44ca1cc02ffe48fb0","spot_id":"5f0ade7d1f84460434524d3d","rate":2},
{"_id":"5f0bfdb44ca1cc02ffe48fb1","spot_id":"5f0ade7d1f84460434524d3d","rate":1}
]
},
but I would like this kind of result:
{
"_id":"5f0ade7d1f84460434524d3d",
"name":"Fly",
...
"rate": 2
},
I tried many different things, and I guess I need to use $group but can't figure out how to get the right output.
the reviews schema:
const reviewsSchema = new mongoose.Schema({
_id: {
type: ObjectId,
required: true,
},
spot_id: {
type: ObjectId,
required: true,
},
rate: {
type: Number,
required: true,
},
})
the spot Schema
const spotsSchema = new mongoose.Schema({
_id: {
type: ObjectId,
required: true,
},
name: {
type: String,
required: true,
},
...
})
The code:
Spots.aggregate
([
{
$geoNear: {
near: { type: "Point", coordinates: [ parseFloat(longitude), parseFloat(latitude) ] },
distanceField: "location",
maxDistance: parseInt(distance) * 1000,
query: {
$and: [
{ $or : filter },
{ $or : closed },
{ published: true }
]
}
}
},
{ $match: {} },
{
$lookup:{
from: 'reviews',
localField: '_id',
foreignField: 'spot_id',
as: 'rate'
}
},
])
You're really close, you just have to actually calculate the avg value which can be done using $map and $avg like so:
{
$addFields: {
rate: {
$avg: {
$map: {
input: "$rate",
as: "datum",
in: "$$datum.rate"
}
}
}
}
}
MongoPlayground
So the issue that I'm having is that I want to like user comment only one time, currently im using addToSet operator, since by definition it doesn't add value if that value is already present.
But in my case it adds, probably because I am adding object instead of value and when I add mongo generates _id?
This is my event model:
creator: {
type: Schema.Types.ObjectId,
ref: 'User'
},
comments: [
{
user: {
type: Schema.Types.ObjectId,
ref: 'User'
},
text: {
type: String,
required: true
},
likes: [
{
user: {
type: Schema.Types.ObjectId,
ref: 'User'
}
}
]
}
]
}
And this is my addLike function:
commentLike: async (req, res) => {
console.log('working', req.params.id, req.params.idas, req.params.commentID);
Events.findOneAndUpdate(
{ _id: req.params.idas, comments: { $elemMatch: { _id: req.params.commentID } } },
{ $addToSet: { 'comments.$.likes': { user: req.params.id } } },
(result) => {
res.json(result);
}
);
}
My likes array looks like this when I add like:
"likes" : [
{
"user" : ObjectId("5b53442c1f09f525441dac31"),
"_id" : ObjectId("5b54a5263e65324504035eac")
},
{
"user" : ObjectId("5b4b4725a3a5583ba8f8d513"),
"_id" : ObjectId("5b54a5bb3e65324504035eb0")
},
]
You can follow this
db.collection.update(
{ "_id": req.params.idas, "comments": { "$elemMatch": { "_id": req.params.commentID, "likes.user": { "$ne": req.params.id } } } },
{ "$push": { "comments.$.likes": { "user": req.params.id } } }
})
And if you just started with your project then you should follow JohnnyHK opinion and make your array some thing like this to make $addToSet workable
likes: [{ type: Schema.Types.ObjectId, ref: 'User' }]
Another option is to change your schema definition to add "_id: false" to prevent the _id field from being generated for subdocs in the array.
In this case, $addToSet will work as you expect, even with nested subdocs as long as your doc exactly matches.
comments: [
{
user: {
type: Schema.Types.ObjectId,
ref: 'User'
},
text: {
type: String,
required: true
},
likes: [
{
_id: false, //add this
user: {
type: Schema.Types.ObjectId,
ref: 'User'
}
}
]
}
]
I'm making an in app messaging system in which I have to show the list of conversations with their last message and the unread count. My schema is as follows--
var schema = new Schema({
senderID: {
type: Schema.Types.ObjectId,
ref: 'Member'
},
receiversID: [{
type: Schema.Types.ObjectId,
ref: 'Member'
}],
content: {
type: String,
default: ''
},
isRead: {
type: Boolean,
default: false,
},
createdAt: {
type: Number,
default: Date.now
}
});
I did this initially to get all the conversations with their last message --
messageModel.aggregate(
[{ $match: { senderID: userId } },
{ $unwind: '$receiversID' },
{ $sort: { createdAt: -1 } },
{ $group: { _id: '$receiversID', unreadCount: { $sum: { $cond: [{ $eq: ["$isRead", false] }, 1, 0] } }, senderID: { $first: '$senderID' }, receiversID: { $first: '$receiversID' }, content: { $first: '$content' } } },
{ $skip: pagingData.pageSize * (pagingData.pageIndex - 1) },
{ $limit: pagingData.pageSize }
], function (err, docs) {
resolve(docs);
}
);
But it doesn't shows the messages if you are a receiver. I want to show the conversation whether you are receiver or sender.
i use something like this:
{
'$or': [
{
'$and': [
{
'receiversID': userId
}, {
'senderID': toUserId
}
]
}, {
'$and': [
{
'receiversID': toUserId
}, {
'senderID': userId
}
]
},
],
}