The collection has two geo fields: fromLocation and toLocation. But only one Geonear is allowed.
The collection looks like:
...............
fromLocation: {
type: { type: String, default: "Point" },
coordinates: [Number],
},
toLocation: {
type: { type: String, default: "Point" },
coordinates: [Number],
},
.........................................
plese give example code, how to use Geonear for search by two fields.
My code for one field search:
[
{
'$geoNear': {
near: [Object],
key: 'fromLocation',
distanceField: 'fromDistance',
spherical: true
}
},
{
'$match': {
status: 2,
'from.data.city_fias_id': '27c5bc66-61bf-4a17-b0cd-ca0eb64192d6',
'to.data.city_fias_id': '27c5bc66-61bf-4a17-b0cd-ca0eb64192d6',
'car.paymentInfo.id': [Object],
budget: [Object]
}
},
{
'$lookup': {
from: 'users',
localField: 'autor',
foreignField: '_id',
as: 'autor'
}
},
{ '$unwind': '$autor' },
{ '$addFields': { sortBudget: [Object] } },
{ '$sort': { sortBudget: 1 } },
{ '$group': { _id: null, total: [Object], results: [Object] } },
{ '$project': { total: 1, results: [Object] } }
]
Distance calculation is rather straightforward. Use $geoNear for the more selective condition to take advantage of the geo index, use $match and $expr for the second condition.
Related
Hello I have the following collections
const TransactionSchema = mongoose.Schema({
schedule: {
type: mongoose.Schema.ObjectId,
required: true,
ref: "Schedule"
},
uniqueCode: {
type: String,
required: true
},
created: {
type: Date,
default: Date.now
},
status: {
type: String,
required: false
},
})
const ScheduleSchema = mongoose.Schema({
start: {
type: Date,
required: true,
},
end: {
type: Date,
required: false,
},
location: {
type: mongoose.Schema.ObjectId,
required: true,
ref: "Location"
},
})
and I want to return how many times the schedule appear in transaction ( where the status is equal to 'Active') and group it based on its location Id and then lookup the location collection to show the name.
For example I have the following data.
transaction
[
{
"_id":"identifier",
"schedule":identifier1,
"uniqueCode":"312312312312",
"created":"Date",
"status": 'Active'
},
{
"_id":"identifier",
"schedule":identifier1,
"uniqueCode":"1213123123",
"created":"Date",
"status": "Deleted"
}
]
schedule
[
{
"_id":identifier1,
"start":"date",
"end":"date",
"location": id1
},
{
"_id":identifier2,
"start":"date",
"end":"date",
"location": id2
}
]
and I want to get the following result and limit the result by 10 and sort it based on its total value:
[
{
"locationName":id1 name,
"total":1
},
{
"locationName":id2 name,
"total":0
}
]
thank you. Sorry for my bad english.
A bit complex and long query.
$lookup - schedule collection joins with transaction collection by matching:
_id (schedule) with schedule (transaction)
status is Active
and return a transactions array.
$lookup - schedule collection joins with location collection to return location array.
$set - Take the first document in location array so this field would be a document field instead of an array. [This is needed to help further stage]
$group - Group by location._id. And need the fields such as location and total.
$sort - Sort by total DESC.
$limit - Limit to 10 documents to be returned.
$project - Decorate the output documents.
db.schedule.aggregate([
{
$lookup: {
from: "transaction",
let: {
scheduleId: "$_id"
},
pipeline: [
{
$match: {
$expr: {
$and: [
{
$eq: [
"$schedule",
"$$scheduleId"
]
},
{
$eq: [
"$status",
"Active"
]
}
]
}
}
}
],
as: "transactions"
}
},
{
$lookup: {
from: "location",
localField: "location",
foreignField: "_id",
as: "location"
}
},
{
$set: {
location: {
$first: "$location"
}
}
},
{
$group: {
_id: "$location._id",
location: {
$first: "$location"
},
total: {
$sum: {
$size: "$transactions"
}
}
}
},
{
$sort: {
"total": -1
}
},
{
$limit: 10
},
{
$project: {
_id: 0,
locationName: "$location.name",
total: 1
}
}
])
Sample Mongo Playground
I use Atlas Search to return a list of documents (using Mongoose):
const searchResults = await Resource.aggregate()
.search({
text: {
query: searchQuery,
path: ["title", "tags", "link", "creatorName"],
},
}
)
.match({ approved: true })
.addFields({
score: { $meta: "searchScore" }
})
.exec();
These resources can be up and downvoted by users (like questions on Stackoverflow). I want to boost the search score depending on these votes.
I can use the boost operator for that.
Problem: The votes are not a property of the Resource document. Instead, they are stored in a separate collection:
const resourceVoteSchema = mongoose.Schema({
_id: { type: String },
userId: { type: mongoose.Types.ObjectId, required: true },
resourceId: { type: mongoose.Types.ObjectId, required: true },
upDown: { type: String, required: true },
After I get my search results above, I fetch the votes separately and add them to each search result:
for (const resource of searchResults) {
const resourceVotes = await ResourceVote.find({ resourceId: resource._id }).exec();
resource.votes = resourceVotes
}
I then subtract the downvotes from the upvotes on the client and show the final number in the UI.
How can I incorporate this vote points value into the score of the search results? Do I have to reorder them on the client?
Edit:
Here is my updated code. The only part that's missing is letting the resource votes boost the search score, while at the same time keeping all resource-votes documents in the votes field so that I can access them later. I'm using Mongoose syntax but an answer with normal MongoDB syntax will work for me:
const searchResults = await Resource.aggregate()
.search({
compound: {
should: [
{
wildcard: {
query: queryStringSegmented,
path: ["title", "link", "creatorName"],
allowAnalyzedField: true,
}
},
{
wildcard: {
query: queryStringSegmented,
path: ["topics"],
allowAnalyzedField: true,
score: { boost: { value: 2 } },
}
}
,
{
wildcard: {
query: queryStringSegmented,
path: ["description"],
allowAnalyzedField: true,
score: { boost: { value: .2 } },
}
}
]
}
}
)
.lookup({
from: "resourcevotes",
localField: "_id",
foreignField: "resourceId",
as: "votes",
})
.addFields({
searchScore: { $meta: "searchScore" },
})
.facet({
approved: [
{ $match: matchFilter },
{ $skip: (page - 1) * pageSize },
{ $limit: pageSize },
],
resultCount: [
{ $match: matchFilter },
{ $group: { _id: null, count: { $sum: 1 } } }
],
uniqueLanguages: [{ $group: { _id: null, all: { $addToSet: "$language" } } }],
})
.exec();
It could be done with one query only, looking similar to:
Resource.aggregate([
{
$search: {
text: {
query: "searchQuery",
path: ["title", "tags", "link", "creatorName"]
}
}
},
{$match: {approved: true}},
{$addFields: {score: {$meta: "searchScore"}}},
{
$lookup: {
from: "ResourceVote",
localField: "_id",
foreignField: "resourceId",
as: "votes"
}
}
])
Using the $lookup step to get the votes from the ResourceVote collection
If you want to use the votes to boost the score, you can replace the above $lookup step with something like:
{
$lookup: {
from: "resourceVote",
let: {resourceId: "$_id"},
pipeline: [
{
$match: {$expr: {$eq: ["$resourceId", "$$resourceId"]}}
},
{
$group: {
_id: 0,
sum: {$sum: {$cond: [{$eq: ["$upDown", "up"]}, 1, -1]}}
}
}
],
as: "votes"
}
},
{$addFields: { votes: {$arrayElemAt: ["$votes", 0]}}},
{
$project: {
"wScore": {
$ifNull: [
{$multiply: ["$score", "$votes.sum"]},
"$score"
]
},
createdAt: 1,
score: 1
}
}
As you can see on this playground example
EDIT: If you want to keep the votes on the results, you can do something like:
db.searchResults.aggregate([
{
$lookup: {
from: "ResourceVote",
localField: "_id",
foreignField: "resourceId",
as: "votes"
}
},
{
"$addFields": {
"votesCount": {
$reduce: {
input: "$votes",
initialValue: 0,
in: {$add: ["$$value", {$cond: [{$eq: ["$$this.upDown", "up"]}, 1, -1]}]}
}
}
}
},
{
$addFields: {
"wScore": {
$add: [{$multiply: ["$votesCount", 0.1]}, "$score"]
}
}
}
])
As can be seen here
I've been struggling to complete an aggregation process due to the timeout error and none of the solutions online have worked for me. I've reduced the workload as much as possible through match
Mongo version: 4.1.3
Using MongoDB Free tier
Attempt:
return this.abyssBattleModel
.aggregate([
{
$match: {
floor_level: floorLevel,
battle_index: battleIndex,
},
},
{
$lookup: {
from: 'playercharacters',
localField: 'party',
foreignField: '_id',
as: 'party',
},
},
{
$project: {
_id: 0,
party: {
$map: {
input: '$party',
as: 'pc',
in: '$$pc.character',
},
},
floor_level: 1,
battle_index: 1,
star: 1,
},
},
{
$match: {
party: { $all: characterIds },
},
},
{
$group: {
_id: {
party: '$party',
floorLevel: '$floor_level',
battleIndex: '$battle_index',
},
count: {
$sum: 1,
},
avgStar: {
$avg: '$star',
},
winCount: {
$sum: {
$cond: { if: { $eq: ['$star', 3] }, then: 1, else: 0 },
},
},
},
},
{
$sort: {
count: -1,
},
},
{
$limit: limit,
},
])
.option({ maxTimeMS: 21600000, allowDiskUse: true, noCursorTimeout: true })
.exec();
In this query the field party is an array of PlayerCharacter ObjectIds. A PlayerCharacter object has a field character which references a Character ObjectId. In this query I am using $lookup to change party to an array of PlayerCharacter ObjectIds to Character ObjectIds so that I can filter them by Character ObjectIds.
Models
// Filtered by floor_level and battle_index and party
// Party is an array of PlayerCharacter ObjectId
// which need to be joined with Character model to filter by Character id
export class AbyssBattle {
#Field(() => String)
_id: MongooseSchema.Types.ObjectId;
#Field(() => String)
#Prop({ required: true })
floor_level: string;
#Field(() => Number)
#Prop({ required: true })
battle_index: number;
#Field(() => Number)
#Prop({ required: true })
star: number;
#Field(() => [String])
#Prop({
type: [MongooseSchema.Types.ObjectId],
ref: 'PlayerCharacter',
required: true,
})
party: MongooseSchema.Types.ObjectId[];
}
export class PlayerCharacter {
#Field(() => String)
_id: MongooseSchema.Types.ObjectId;
#Field(() => String)
#Prop({
type: MongooseSchema.Types.ObjectId,
ref: 'Character',
required: true,
})
character: MongooseSchema.Types.ObjectId;
}
Is there another way to increase the timeout or is this a limitation of MongoDB free tier that I'm unaware of? Is this purely an optimization issue? Thank you in advance.
Apart from proper index on { floor_level: 1, battle_index: 1 } you can try to optimize the $lookup:
{
$lookup:
{
from: 'playercharacters',
pipeline: [
{ $match: { _id: { $all: characterIds } } },
{ $match: { $expr: { $eq: [ "$party", "$$_id" ] } }
],
as: 'party'
}
}
Or as concise correlated subquery (requires MongoDB 5.0)
{
$lookup: {
from: "playercharacters",
localField: "party",
foreignField: "_id",
pipeline: [ { $match: { _id: { $all: characterIds } } } ],
as: "party"
}
}
I'm trying to do an aggregation on two collections that has a linkage between them, and I need to access information in an array of objects in one of those collections.
Here are the schemas:
User Schema:
{
_id: ObjectId,
username: String,
password: String,
associatedEvents: [
{
event_id: ObjectId,
isCreator: boolean,
access_level: String,
}
]
}
Event Schema:
{
_id: ObjectId,
title: String,
associated_users: [
{
user_id: ObjectId
}
]
}
I'm attempting to get the users associated to an event for a specific user, and then get their access level information. Here's the aggregation I have:
const eventsJoined = await Event.aggregate([
{
$match: {
$expr: { $in: [id, "$associatedUserIds"] },
},
},
{
$lookup: {
from: "users",
localField: "associatedUserIds",
foreignField: "_id",
as: "user_info",
},
},
{ $unwind: "$user_info" },
{
$unwind: {
path: "$user_info.associatedEvents",
preserveNullAndEmptyArrays: true,
},
},
{
$group: {
_id: "$_id",
title: { $first: "$title" },
description: { $first: "$description" },
startDate: { $first: "$startdate" },
userInfo: { $first: "$user_info" },
usersAssociatedEvents: { $push: "$user_info.associatedEvents" },
},
},
{
$project: {
title: 1,
description: 1,
startDate: 1,
userInfo: 1,
usersAssociatedEvents: "$usersAssociatedEvents",
},
},
]);
And this is the result I'm getting:
[
{
_id: 609d5ad1ef4cdbeb32987739,
title: 'hello',
description: 'desc',
startDate: null,
usersAssociatedEvents: [ [Object] ]
}
]
As you can see, the query is already aggregating the correct data. But the last thing that's tripping me up is the fact that the aggregation is [ [Object] ] for usersAssociatedEvents instead of the actual contents of the object. Any idea on why that would be?
I cannot manage to get this to work:
db.library.update({
categories: {
$all: ['/movie/action', '/movie/comedy'],
$nin: ['/movie/cartoon']
},
location: {
$geoWithin: {
$centerSphere: [[48.8574946, 2.3476296000000048], 50/6378.1]
}
}
},
{
$setOnInsert: {
categories: ['/movie/action', '/movie/comedy'],
location: {
type: 'Point',
coordinates: [48.8574946, 2.3476296000000048]
}
},
$addToSet: {users: {_id: '1', date: '2018-04-06'}}
},
{ upsert: true })
It returns the following error:
cannot infer query fields to set, path 'categories' is matched twice
I understand that query part is moved to update part when upsert happens, but I'm not sure how to keep $all from having this effect
It does work when $all array is not set to more than 1 element.
I've found this solution, even though it's painful to be forced to list $all elements under { $elemMatch: { $eq:... :
db.library.update({
categories: {
$all: [
{ $elemMatch: { $eq: '/movie/action' } },
{ $elemMatch: { $eq: '/movie/comedy' } }
],
$nin: ['/movie/cartoon']
},
location: {
$geoWithin: {
$centerSphere: [[48.8574946, 2.3476296000000048], 50/6378.1]
}
}
},
{
$setOnInsert: {
categories: ['/movie/action', '/movie/comedy'],
location: {
type: 'Point',
coordinates: [48.8574946, 2.3476296000000048]
}
},
$addToSet: {users: {_id: '1', date: '2018-04-06'}}
},
{ upsert: true })
any simpler solution is welcome