I am doing a query with $or aggregation on two fields in the database. The first condition I want to be more relevant than the second, so I want to sort those that matches by first condition first and then return the latter after.
Here is some code:
const regexp = new RegExp('^' + req.query.search, 'i')
const query = {
$or: [{ word: regexp }, { 'lang.english': regexp }],
}
const words = await collection
.find(query, {
collation: {
locale: 'sv',
strength: 1,
},
projection: {
'-_id': 1,
'word': 1,
'lang.english': 1,
'lang.definitionEnglish': 1,
'lang.definition': 1,
},
})
.skip(skips)
.limit(page_size)
.toArray()
So basicly, those results that matches on the word regexp should come first, then lang.english
To start with let's throw some data in to a MongoDB test collection to play around with.
db.test.insertMany([
{ word: 'TEST123', lang: { english: 'wibble1' } },
{ word: 'wibble2', lang: { english: 'Test123' } },
{ word: 'test345', lang: { english: 'wibble3' } },
{ word: 'wibble4', lang: { english: 'testy101' } }
]);
{
"acknowledged" : true,
"insertedIds" : [
ObjectId("5f6924b271e546940acc10f7"),
ObjectId("5f6924b271e546940acc10f8"),
ObjectId("5f6924b271e546940acc10f9"),
ObjectId("5f6924b271e546940acc10fa")
]
}
This has the same structure as mentioned in the question.
Next we can build up an aggregation query that will match the same as your find but this time append an extra field that we can sort on. We'll make this 1 if word field matches then 2 for anything else.
db.test.aggregate([
{ $match: { $or: [{ word: regexp }, { 'lang.english': regexp }] } },
{ $addFields: { sortingValue: { $cond: { if: { $regexMatch: { input: "$word" , regex: regexp } }, then: 1, else: 2 } } } }
]);
{
"_id" : ObjectId("5f6924b271e546940acc10f7"),
"word" : "TEST123",
"lang" : {
"english" : "wibble1"
},
"sortingValue" : 1
}
{
"_id" : ObjectId("5f6924b271e546940acc10f8"),
"word" : "wibble2",
"lang" : {
"english" : "Test123"
},
"sortingValue" : 2
}
{
"_id" : ObjectId("5f6924b271e546940acc10f9"),
"word" : "test345",
"lang" : {
"english" : "wibble3"
},
"sortingValue" : 1
}
{
"_id" : ObjectId("5f6924b271e546940acc10fa"),
"word" : "wibble4",
"lang" : {
"english" : "testy101"
},
"sortingValue" : 2
}
Now we can add the normal sort, skip and limting.
var regexp = new RegExp('^' + 'test', 'i')
var skip = 0;
var limit = 3;
db.test.aggregate([
{ $match: { $or: [{ word: regexp }, { 'lang.english': regexp }] } },
{ $addFields: { sortingValue: { $cond: { if: { $regexMatch: { input: "$word" , regex: regexp } }, then: 1, else: 2 } } } },
{ $sort: { sortingValue: 1 } },
{ $skip: skip },
{ $limit: limit }
]);
{
"_id" : ObjectId("5f6924b271e546940acc10f9"),
"word" : "test345",
"lang" : {
"english" : "wibble3"
},
"sortingValue" : 1
}
{
"_id" : ObjectId("5f6924b271e546940acc10f7"),
"word" : "TEST123",
"lang" : {
"english" : "wibble1"
},
"sortingValue" : 1
}
{
"_id" : ObjectId("5f6924b271e546940acc10f8"),
"word" : "wibble2",
"lang" : {
"english" : "Test123"
},
"sortingValue" : 2
}
Related
This MongoDB aggregation is failing:
Attendance.aggregate([
{ $match: { cohort_id: cohort_id} },
{ $unwind: "$absences" },
{
$group: {
_id: {
term: "$absences.term",
$function:
{
body: function (day) {
return day.getDay();
},
args: ["$absences.formatted_date.day"],
lang: "js",
},
},
count: { $sum: 1 },
},
},
{ $sort: { count: 1 } },
])
with this error:
uncaught exception: Error: command failed: {
"ok" : 0,
"errmsg" : "FieldPath field names may not start with '$'. Consider using $getField or $setField.",
"code" : 16410,
"codeName" : "Location16410"
} with original command request: {
"aggregate" : "attendances",
"pipeline" : [
{
"$match" : {
"cohort_id" : "61858e13dc5e0d1ce0238abd"
}
},
{
"$unwind" : "$absences"
},
{
"$group" : {
"_id" : {
"term" : "$absences.term",
"$function" : {
"body" : function (day) { return day.getDay(); },
"args" : [
"$absences.formatted_date.day"
],
"lang" : "js"
}
},
"count" : {
"$sum" : 1
}
}
},
{
"$sort" : {
"count" : 1
}
}
],
"cursor" : {
},
"lsid" : {
"id" : UUID("b4505aa0-e65e-46cd-8e31-03e4ecdbfe3b")
}
}
...
Not the most helpful error message.
Where am I referencing a field name wrong? Looks like it's expecting a field name without $ somewhere, but I can't seem to find where.
I've seen similar posts about this error, but they generally have to do with $project and $sort which does not seem to be the problem here
Thank you!
It considers $function as field name. I think it should be like this:
{
$group: {
_id: {
term: "$absences.term",
day: {
$function: {
body: function (day) {
return day.getDay();
},
args: ["$absences.formatted_date.day"],
lang: "js",
},
},
count: { $sum: 1 },
},
}
Is this a school homework? day.getDay() sounds to be a very simple function which should be available native in MongoDB Query Language.
Found a solution that's simpler and that works:
Attendance.aggregate([
{ $match: { cohort_id: cohort_id} },
{ $unwind: "$absences" },
{
$group: {
_id: {
term: "$absences.term",
day: {
$dayOfWeek: "$absences.formatted_date.day"
},
},
count: { $sum: 1 },
},
},
{ $sort: { count: 1 } },
])
I am trying to update an embedded document in MongoDB using mongoose in nodejs. The document is simplified and shown below (The names in friendList is assumed to be unique):
{
"_id" : ObjectId("5eb0617f3aec924ff42249cd"),
"friendList" : [
{
"name" : "Alex",
"flag" : false,
},
{
"name" : "Bob",
"flag" : false,
},
{
"name" : "Caleb",
"flag" : true,
},
{
"name" : "Debbie",
"flag" : false,
}
]
}
I would like to update this collection by:
accepting a Patch API with a request body containing a subset of friendList and
update the nested field flag.
For example, if I were to do a patch call from postman with the request body:
{
"friendList":[
{
"name":"Alex",
"flag":true
},
{
"name":"Caleb",
"flag":false
},
{
"name":"Debbie",
"flag":false
}
]
}
then I should expect my document in MongoDB to look like this:
{
"_id" : ObjectId("5eb0617f3aec924ff42249cd"),
"friendList":[
{
"name":"Alex",
"flag":true
},
{
"name":"Bob",
"flag":false
},
{
"name":"Caleb",
"flag":false
},
{
"name":"Debbie",
"flag":false
}
]
}
What I have tried on nodejs is updating the entire request body:
function updateUser(req){
User.findOneAndUpdate({'_id':req.params._id},req.body,{new:true});
}
which replaces the entire friendList array:
{
"_id" : ObjectId("5eb0617f3aec924ff42249cd"),
"friendList":[
{
"name":"Alex",
"flag":true
},
{
"name":"Caleb",
"flag":false
},
{
"name":"Debbie",
"flag":false
}
]
}
I have also tried using array operators like $:
function updateUser(req){
User.findOneAndUpdate(
{'_id':req.params._id},
{$addToSet:{
"friendList":{
$each:req.body.friendList}
}
},
{new:true}
);
}
which gave me the output:
{
"_id" : ObjectId("5eb0617f3aec924ff42249cd"),
"friendList" : [
{
"name" : "Alex",
"flag" : false,
},
{
"name" : "Bob",
"flag" : false,
},
{
"name" : "Caleb",
"flag" : true,
},
{
"name" : "Debbie",
"flag" : false,
},
{
"name" : "Alex",
"flag" : true,
},
{
"name" : "Caleb",
"flag" : false,
},
]
}
which $addToSet considers both name and flag when making a comparison to check if the values exist in the array. It might work if I am able to intercept at this comparison phase such that only the name field is checked.
I have been exploring concepts like $[<identifier>] and arrayFilter but can't seem to make it work.
Simple $addToSet does not work, because your array is not ["Alex","Caleb","Debbie"]. Your array is
[
{name: "Alex", flag: true},
{name: "Caleb", flag: false},
{name: "Debbie", flag: false}
]
Element {name:"Alex", flag: true} is different to element {name: "Alex", flag: false}, that's the reason why your approach failed. I think you have to use aggregation pipeline, e.g. this one:
db.collection.aggregate([
{ $addFields: { newFriends: friendList } },
{
$set: {
friendList: {
$map: {
input: "$friendList",
in: {
name: "$$this.name",
flag: {
$cond: [
{ $eq: [{ $indexOfArray: ["$newFriends.name", "$$this.name"] }, - 1] },
"$$this.flag",
{ $arrayElemAt: [ "$newFriends.flag", { $indexOfArray: ["$newFriends.name", "$$this.name"] } ] }
]
}
}
}
}
}
},
{ $unset: "newFriends" }
])
Or if you like to work with index variable:
db.collection.aggregate([
{ $addFields: { newFriends: friendList } },
{
$set: {
friendList: {
$map: {
input: "$friendList",
in: {
$let: {
vars: { idx: { $indexOfArray: ["$newFriends.name", "$$this.name"] } },
in: {
name: "$$this.name",
flag: {
$cond: [
{ $eq: ["$$idx", - 1] },
"$$this.flag",
{ $arrayElemAt: ["$newFriends.flag", "$$idx"] }
]
}
}
}
}
}
}
}
},
{ $unset: "newFriends" }
])
Note, this will update only existing names. New names are not added to the array, your question is not clear in this regard. If you like to add also new elements then insert
{
$set: {
friendList: { $setUnion: ["$friendList", "$newFriends"] }
}
},
just before { $unset: "newFriends" }
The aggregation pipeline can be used in an update:
User.findOneAndUpdate(
{'_id':req.params._id},
[
{ $addFields: { newFriends: req.body.friendList } },
{
$set: { ...
}
]
);
My db cofiguration looks like:
{
"_id" : ObjectId("5ece47aa6510a611b47aac5a"),
"boats" : [
{
"_id" : ObjectId("5ece47aa6510a611b47aac6e"),
"model" : "Dufour",
"year" : 2019,
"about" : [
{
"_id" : ObjectId("5ece47aa6510a611b47aac71"),
"Capacity" : 14,
"characteristics" : [
{
"_id" : ObjectId("5ece47aa6510a611b47aac73"),
"fuel" : "petrol",
"fuelCap" : 200
},
{
"_id" : ObjectId("5ece47aa6510a611b47aac73"),
"fuel" : "petrol",
"fuelCap" : 120
},
]
},
{
"_id" : ObjectId("5ece47aa6510a611b47aac71"),
"Capacity" : 8,
"characteristics" : [
{
"_id" : ObjectId("5ece47aa6510a611b47aac73"),
"fuel" : "benzin",
"fuelCap" : 180
},
{
"_id" : ObjectId("5ece47aa6510a611b47aac73"),
"fuel" : "petrol",
"fuelCap" : 100
},
]
},
{...},
{...},
]
}
Now i am trying to count the number of boats which have "fuel" : "petrol", so i use the code bellow:
router.get('/boat', async(req, res)=>{
try{
const fuelData = await Boat.aggregate([
{
$project: {
fuelData: {
$filter: {
input: "$boats",
as: "boats",
cond: {
$filter:{
input:"$$boats.about",
as:"about",
cond:{
$filter:{
input:"$$about.characteristics",
as:"characteristics",
cond:{
$eq:["$$activity1.activity.type", "STILL"]
}
}
}
}
}
}
}
}
},
{
$project: {
boatsCount: {$size : "$fuelData" }
}
}
])
res.status(201).send(fuelData)
}catch(e){
res.send(e)
}
})
The problem is that return wrong number of boatCount. And it seems like it returns the number of the boats which are inside the db. Any help how to count correctly the boats which have "fuel" : "petrol"?
Is there anything wrong in my code?
https://mongoplayground.net/p/D0FEhTMJEJ1
Hope this is what you need.
The sample data you have provided is missing a ] & }.
So I added 1 additional boat with 2 about.
db.collection.aggregate([
{
$match: {
"boats.about.characteristics.fuel": "petrol"
}
},
{
$unwind: "$boats"
},
{
$unwind: "$boats.about"
},
{
$match: {
"boats.about.characteristics.fuel": "petrol"
}
},
{
$group: {
_id: null,
count: {
$sum: 1
}
}
}
])
I have following documents in Agriculture model
{
"_id" : ObjectId("5e5c9c0a0cfcdb1538406000"),
"agricultureProductSalesType" : [
"cash_crops",
"vegetable",
"fruit"
],
"flag" : false,
"agricultureDetail" : [
{
"production" : {
"plantCount" : 0,
"kg" : 0,
"muri" : 0,
"pathi" : 0
},
"sale" : {
"plantCount" : 0,
"kg" : 0,
"muri" : 0,
"pathi" : 0
},
"_id" : ObjectId("5e5c9c0a0cfcdb1538406001"),
"title" : "alaichi",
"agricultureProductionSalesType" : "cash_crops"
},
{
"production" : {
"plantCount" : 0,
"kg" : 40,
"muri" : 0,
"pathi" : 0
},
"sale" : {
"plantCount" : 0,
"kg" : 0,
"muri" : 0,
"pathi" : 0
},
"_id" : ObjectId("5e5c9c0a0cfcdb1538406002"),
"title" : "amriso",
"agricultureProductionSalesType" : "cash_crops"
}
],
"agricultureParent" : [
{
"area" : {
"ropani" : 10,
"aana" : 0,
"paisa" : 0
},
"_id" : ObjectId("5e5c9c0a0cfcdb1538406005"),
"title" : "cash_crops",
"income" : 50000,
"expense" : 6000
}
],
"house" : ObjectId("5e5c9c090cfcdb1538405fa9"),
"agricultureProductSales" : true,
"insecticides" : false,
"fertilizerUse" : true,
"seedNeed" : "local",
"__v" : 0
I want result from above document with condition if agricultureDetail.title is not empty or blank string AND agricultureDetail.production.plantcount equals zero or null or exists false AND agricultureDetail.production.kg equals zero or null or exists false AND (all remaining elements inside production zero or null or exists flase).
I tried $elemMatch as bellow:
$and: [
{ agricultureProductSales: true },
{ agricultureDetail: { $exists: true, $ne: [] } },
{
$or: [
{ agricultureDetail: { $elemMatch: { title: { $ne: "" } } } },
{
agricultureDetail: { $elemMatch: { title: { $exists: true } } }
}
]
},
{
$or: [
{
agricultureDetail: {
$elemMatch: { production: { $exists: true } }
}
},
{
agricultureDetail: {
$elemMatch: { production: { $ne: [] } }
}
}
]
},
{
$or: [
{
"agricultureDetail.production": {
$elemMatch: { plantCount: { $exists: false } }
}
},
{
"agricultureDetail.production": {
$elemMatch: { plantCount: { $eq: 0 } }
}
},
{
"agricultureDetail.production": {
$elemMatch: { plantCount: { $eq: null } }
}
}
]
}
]
But it reurns empty result. Any help? THankyou so much.
Breaking down this query a bit:
{ agricultureProductSales: true },
Selects only true values.
{ agricultureDetail: { $exists: true, $ne: [] } }
Is extraneous. Since you are testing fields of sub-documents within this array, those later tests could not possibly succeed if the array were empty or didn't exist.
{
$or: [
{ agricultureDetail: { $elemMatch: { title: { $ne: "" } } } },
{
agricultureDetail: { $elemMatch: { title: { $exists: true } } }
}
]
},
This tests to see if title either doesn't equal "" (which includes elements where the field doesn't exist) or if the title field exists. One of these is always true, so this $or will always match. If you wanted to match only documents that contain an element with a non-empty title, test to see if it is greater than "" - since the query operators are type-sensitive, this will fail to match any title that doesn't exist, doesn't contain a string, or contains the empty string.
"agricultureDetail.title": { $gt: "" }
Similarly with plantCount, if you were to test $gt: 0, that would match only documents that contain a plantCount that is numeric and greater than 0. What you want is the logical inverse of that, so:
"agricultureDetail.production.plantCount": {$not: {$gt: 0}}
In this case, that would match elements that do not contain a production field, or those that have an empty array for the production field.
An existence test for plantCount will eliminate both of those possibilities, so
"agricultureDetail.production.plantCount": {$exists:true, $not: {$gt: 0}}
As written, all of these are testing if any element in the array matches any of the criteria.
If your intent is to match a document that contains a single element that matches all of the criteria, you would collect them together in an $elemMatch of the agricultureDetail fields. So the final query could look something like:
db.collection.find({
agricultureProductSales: true,
agricultureDetail:{$elemMatch:{
title: {$gt: ""},
"production.plantCount": {$exists:true, $not: {$gt: 0}}
}}
})
Playground
my query is returning me an empty array when I try to text search in mongodb I already created an index into my database.
For example:
I have 2 data type of String declared in my model status and mac_address both of them already included in the text index. When I search for a mac_address it gives me the correct data but when I tried to search for the status it returns an empty array.
--Model--
const PhoneSchema = new mongoose.Schema({
status: {
type: String,
default: "DOWN"
},
mac_address: {
type: String,
unique: true
},
});
--Index--
db.phones.createIndex({
status: "text",
mac_address: "text"
});
--Route--
router.get('/search/:searchForData',
async function (req, res) {
try {
const searchPhone = await Phone.find({
$text: {
$search: req.params.searchForData
}
}, {
score: {
$meta: "textScore"
}
}).sort({
score: {
$meta: "textScore"
}
})
res.status(200).json(searchPhone);
} catch (err) {
return res.status(404).json({
error: err.message
});
}
});
db.phones.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "pingphony.phones"
},
{
"v" : 2,
"unique" : true,
"key" : {
"ip" : 1
},
"name" : "ip_1",
"ns" : "pingphony.phones",
"background" : true
},
{
"v" : 2,
"unique" : true,
"key" : {
"mac" : 1
},
"name" : "mac_1",
"ns" : "pingphony.phones",
"background" : true
},
{
"v" : 2,
"key" : {
"_fts" : "text",
"_ftsx" : 1
},
"name" : "$**_text",
"ns" : "pingphony.phones",
"weights" : {
"$**" : 1
},
"default_language" : "english",
"language_override" : "language",
"textIndexVersion" : 3
}
]
I expect the output of /phone/search/DOWN to be the data consisting of DOWN status but the actual output I get is []
Try making your query directly from mongo console:
db.phones.find({ $text: { $search: "DOWN" }})
Try using aggregation pipeline:
const searchPhone = await Phone.aggregate(
[
{ $match: { $text: { $search: "DOWN" } } },
{ $sort: { score: { $meta: "textScore" } } },
]
);
If you tried everything and it just didn't go well try using regexp:
const searchQuery = req.params.searchForData.replace(/[.*+?^${}()|[]\]/g, '\$&'); // escape regexp symbols
const searchPhone = await Phone.find({ status: new RegExp(${searchQuery}, 'i') });