MongoDB, aggregation query an Multipoint geoindexes - mongodb

I am trying to query a 2dsphere geo-index which has Multipoints in its coordinates. According to the mongodb documentation, it's a new feature from 2.6, and I did not find a good example for querying it.
My code works fine with a coordinate of Type Point, but not when I change it to MultiPoint.
I am using mongoose and nodejs.
This is the mongoose schema :
AnnonceSchema = mongoose.Schema({
[...]
geo: {type: Object, index: '2dsphere',name:String }
});
My search method :
AnnonceSchema.statics.search = function(long, lat, maxDistanceInKm, done){
var Annonce = this;
Annonce.aggregate(
[
{ $match: [...]
{ $match: { "geo": {$within: {$centerSphere: [
[long, lat],
maxDistanceInKm / 6371
]}}}},
{ $sort: { score: { $meta: "textScore" } } },
{ $project: { title: 1, _id: 1, score: { $meta: "textScore" }}},
{ $match: { score: { $gt: 1.0 } } }
], done);
};
My unit test:
var notReturned = {[...], geo:
{type: 'MultiPoint', coordinates:[[2.65, 48.79]]}};
var returned = {[...], geo: {type: 'Point', coordinates:[2.65, 48.79]}};
Model.create(doesNotWork, function(){
Model.create(doesWork, function(){
Annonce.searchAnnonce(2.65, 48.79, 30, function (err, annonces) {
assert.strictEqual(annonces.length, 2);
});
});
I don't know what I am missing, the coordinates are good, the 2 objects are the sames, the only thing that is different is their their type and that I put an array of arrays for MultiPoint.
Did anyone already tried this feature ? How to query MultiPoints geo indexes ?
Thank you

Related

mongodb aggregation where document field is less than another field

Using mongoose, I'm trying to make a query that searches for tasks where timeSpent is greater than timeBilled.
Task schema:
const myTaskSchema = new Schema({
date: { type: Date, default: Date.now },
timeSpent: { type: Number },
timeBilled: { type: Number }
})
The query I've tried:
myTaskSchema.aggregate([
{
$match: {
timeSpent: { $gt: '$timeBilled' }
}
}
])
.then(data => {
console.log(data)
})
But I'm getting zero results (I know there should be results)
NOTE: Not every task has a timeSpent or timeBilled.field if that matters.
here is my dirty solution. It'd be nice if I didnt have to add a field but this gets me where I want to be.
myTaskSchema.aggregate([
{
$addFields: {
needToBill: { $gt: ['$timeSpent', '$timeBilled'] }
}
},
{
$match: {
needToBill: true
}
},
{
$project: {
timeSpent: 1,
timeBilled: 1
}
}
])

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

findOneAndUpdate document with array

Two questions here.
What is the correct way to findOneAndUpdate when there is an array? The example below errors with err MongooseError [CastError]: Cast to embedded failed for value.
Should you arrays of objects become separate collections?
* Example *
var ProductSchema = new Schema({
_id: Schema.Types.ObjectId,
product_name: String
});
var purchaseOrderSchema = new Schema(
{
purchaseOrderNo: Number,
products: [ProductSchema]
}
);
const purchaseOrder = new PurchaseOrder(req.body);
PurchaseOrder.findOneAndUpdate(
{ _id: req.body._id },
{
$set: req.body,
$push: req.body.products
},
{ upsert: true, new: true }
)
.then((result) => {
console.log('result', result);
res.status(200).json(result);
})
.catch((err) => {
console.log('err', err);
res.status(500).json({ error: err });
});
const body = {
_id: 'skjdhflksjdf',
purchaseOrderNo: 1,
products: [
{
_id: '111',
product_name: 'Cup'
},
{
_id: '222',
product_name: 'Spoon'
}
]
}
In the ProductSchema the type of _id field to set to ObjectId. The product id 111 and 222 are not a valid ObjectId and it fails to cast it. You can update the type of _id in ProductSchema to Number for this to work
var ProductSchema = new Schema({
_id: Number,
product_name: String
});

How to Check current user's vote before votes are grouped and sumed in same aggregate function

var PostSchema = new mongoose.Schema({
item: {
type: mongoose.Schema.ObjectId,
ref: 'item',
required: true
},
user: {
type: mongoose.Schema.ObjectId,
ref: 'User',
required: true
},
vote: {
type: Number,
default: 0
},
total: {
type: Number,
default: 0
},
awsPostKey: {type: String},
picture: {type: String, required: true}
});
var data = function(){
return Post
.find({})
.then(function(post){
return post;
})
};
var userId = //mongo objectId for current user
//postVote schema:
var PostVoteSchema = new mongoose.Schema({
post: {
type: mongoose.Schema.ObjectId,
ref: 'Post',
required: true
},
user: {
type: mongoose.Schema.ObjectId,
ref: 'User',
required: true
},
vote: {
type: Number,
default: 0
}
});
//pass data from Post query to PostVote sum function:
PostVoteSchema.statics.sum = function (data, userId) {
var postIds = data.map(function (a) {
return a._id;
});
return PostVote
.aggregate(
[
{ $match: { 'post': {$in: postIds}}},
{ $group: { _id:'$post' ,vote:{$sum:'$vote'}}}
])
.execAsync()
.then(function(votes){
return votes;
//desired output to client, _id is for specific post
{_id: 5802ea4bc00cb0beca1972cc, vote: 3, currentUserVote: -1}
});
};
I'm successfully able to get the total sum of all votes with the same postId.
Now, I"m wanting to see if the current user (userId) has placed a vote for the given post as well, then to return how they voted (+1 or -1) along with the sum of all votes for the specific post.
Is it possible to do this, or will I have to do this outside of my aggregate pipeline -- within a second query? It just seems potentially taxing to have to query the collection again.
Yes, that's possible. Within the $group pipeline, you can use the $cond operator as the logic for feeding the $sum accumulator operator. For example:
return PostVote.aggregate([
{ "$match": { "post": { "$in": postIds } } },
{
"$group": {
"_id": "$post",
"votes": { "$sum": "$vote" },
"userVotes": {
"$sum": {
"$cond": [
{ "$eq": ["$user", userId] },
"$vote",
0
]
}
}
}
}
]).execAsync().then(function(votes){
return votes;
});

Mongoosejs populate and aggregate subdocuments

I have a '3-layered' relationship in MongooseJS like so, it's two one-to-many relationships between subdocuments. Like so:
var BroadcastSchema = new Schema({
...
_donationAddresses: [{
type: Schema.Types.ObjectId,
ref: 'DonationAddress'
}]
});
var DonationAddressSchema = new Schema({
...
_donations: [{
type: Schema.Types.ObjectId,
ref: 'Donation'
}]
});
var DonationSchema = new Schema({
...
amount: Number
});
I want to get the $sum total of the amount:Number on the DonationSchema
So far I've populated the Donation by using a work-around listed here (because as far as I know you can't populate a populate so far as I know)
Broadcast.find()
.exec(function(err, broadcasts) {
// this works
var iter = function(broadcast, callback) {
DonationAddress.populate(broadcast._donationAddresses, {
path: '_donations'
}, callback);
};
// tried to iterate over the donation address and
// aggregate the _donations.amount
var iter2 = function(broadcast, callback) {
DonationAddress.aggregate([{
$match: {
_id: broadcast._donationAddresses
}
}, {
$unwind: "$_donations"
}, {
$group: {
_id: "$_id",
total: {
$sum: "$_donations.amount"
}
}
}], callback);
};
async.each(broadcasts, iter, function done(err) {
async.each(broadcasts, iter2, function done(err) {
res.json(broadcasts);
});
});