Optimization - find on all fields in Mongoose MongoDB schema - javascript

We needed to challenge our database approach and need your help
We needed to search a word/phrase in all fields of a Mongoose schema.
Let's say the schema is like this:
var sampleSchema = new Schema({
fieldABC: String,
fieldDEF: String,
fieldGHI: String
});
We need to write a find query which will search for a word in all fields in a document of the collection:
db.sampleCollection.find({
$or: [{
fieldABC: "wordToSearch"
}, {
fieldDEF: "wordToSearch"
}, {
fieldGHI: "wordToSearch"
}]
})
It's possible for us to write the above query but it looks very inefficient - is there some better and faster approach to this?
In the year 2015, it was not supported, is there any change in this?

As suggested by #Veeram
Step 1:
Create a text index
db.sampleCollection.createIndex( { "$**": "text" } )
Step 2:
Use the text index to search the word in concern
db.sampleCollection.find( { $text: { $search: "wordToSearch" } })

Related

Mongoose search for items on array containing _id with aggregate [duplicate]

If I have this schema...
person = {
name : String,
favoriteFoods : Array
}
... where the favoriteFoods array is populated with strings. How can I find all persons that have "sushi" as their favorite food using mongoose?
I was hoping for something along the lines of:
PersonModel.find({ favoriteFoods : { $contains : "sushi" }, function(...) {...});
(I know that there is no $contains in mongodb, just explaining what I was expecting to find before knowing the solution)
As favouriteFoods is a simple array of strings, you can just query that field directly:
PersonModel.find({ favouriteFoods: "sushi" }, ...); // favouriteFoods contains "sushi"
But I'd also recommend making the string array explicit in your schema:
person = {
name : String,
favouriteFoods : [String]
}
The relevant documentation can be found here: https://docs.mongodb.com/manual/tutorial/query-arrays/
There is no $contains operator in mongodb.
You can use the answer from JohnnyHK as that works. The closest analogy to contains that mongo has is $in, using this your query would look like:
PersonModel.find({ favouriteFoods: { "$in" : ["sushi"]} }, ...);
I feel like $all would be more appropriate in this situation. If you are looking for person that is into sushi you do :
PersonModel.find({ favoriteFood : { $all : ["sushi"] }, ...})
As you might want to filter more your search, like so :
PersonModel.find({ favoriteFood : { $all : ["sushi", "bananas"] }, ...})
$in is like OR and $all like AND. Check this : https://docs.mongodb.com/manual/reference/operator/query/all/
In case that the array contains objects for example if favouriteFoods is an array of objects of the following:
{
name: 'Sushi',
type: 'Japanese'
}
you can use the following query:
PersonModel.find({"favouriteFoods.name": "Sushi"});
In case you need to find documents which contain NULL elements inside an array of sub-documents, I've found this query which works pretty well:
db.collection.find({"keyWithArray":{$elemMatch:{"$in":[null], "$exists":true}}})
This query is taken from this post: MongoDb query array with null values
It was a great find and it works much better than my own initial and wrong version (which turned out to work fine only for arrays with one element):
.find({
'MyArrayOfSubDocuments': { $not: { $size: 0 } },
'MyArrayOfSubDocuments._id': { $exists: false }
})
Incase of lookup_food_array is array.
match_stage["favoriteFoods"] = {'$elemMatch': {'$in': lookup_food_array}}
Incase of lookup_food_array is string.
match_stage["favoriteFoods"] = {'$elemMatch': lookup_food_string}
Though agree with find() is most effective in your usecase. Still there is $match of aggregation framework, to ease the query of a big number of entries and generate a low number of results that hold value to you especially for grouping and creating new files.
PersonModel.aggregate([
{
"$match": {
$and : [{ 'favouriteFoods' : { $exists: true, $in: [ 'sushi']}}, ........ ] }
},
{ $project : {"_id": 0, "name" : 1} }
]);
There are some ways to achieve this. First one is by $elemMatch operator:
const docs = await Documents.find({category: { $elemMatch: {$eq: 'yourCategory'} }});
// you may need to convert 'yourCategory' to ObjectId
Second one is by $in or $all operators:
const docs = await Documents.find({category: { $in: [yourCategory] }});
or
const docs = await Documents.find({category: { $all: [yourCategory] }});
// you can give more categories with these two approaches
//and again you may need to convert yourCategory to ObjectId
$in is like OR and $all like AND. For further details check this link : https://docs.mongodb.com/manual/reference/operator/query/all/
Third one is by aggregate() function:
const docs = await Documents.aggregate([
{ $unwind: '$category' },
{ $match: { 'category': mongoose.Types.ObjectId(yourCategory) } }
]};
with aggregate() you get only one category id in your category array.
I get this code snippets from my projects where I had to find docs with specific category/categories, so you can easily customize it according to your needs.
For Loopback3 all the examples given did not work for me, or as fast as using REST API anyway. But it helped me to figure out the exact answer I needed.
{"where":{"arrayAttribute":{ "all" :[String]}}}
In case You are searching in an Array of objects, you can use $elemMatch. For example:
PersonModel.find({ favoriteFoods : { $elemMatch: { name: "sushiOrAnytthing" }}});
With populate & $in this code will be useful.
ServiceCategory.find().populate({
path: "services",
match: { zipCodes: {$in: "10400"}},
populate: [
{
path: "offers",
},
],
});
If you'd want to use something like a "contains" operator through javascript, you can always use a Regular expression for that...
eg.
Say you want to retrieve a customer having "Bartolomew" as name
async function getBartolomew() {
const custStartWith_Bart = await Customers.find({name: /^Bart/ }); // Starts with Bart
const custEndWith_lomew = await Customers.find({name: /lomew$/ }); // Ends with lomew
const custContains_rtol = await Customers.find({name: /.*rtol.*/ }); // Contains rtol
console.log(custStartWith_Bart);
console.log(custEndWith_lomew);
console.log(custContains_rtol);
}
I know this topic is old, but for future people who could wonder the same question, another incredibly inefficient solution could be to do:
PersonModel.find({$where : 'this.favouriteFoods.indexOf("sushi") != -1'});
This avoids all optimisations by MongoDB so do not use in production code.

Sequelize wrap column with " in where condition

I have a JSONB column in DB.
I'd like to have request to DB where I can check if some value in this JSON it true or false:
SELECT *
FROM table
WHERE ("json_column"->'data'->>'data2')::boolean = true AND id = '00000000-1111-2222-3333-456789abcdef'
LIMIT 1
So, my sequelize request:
const someVariableWithColumnName = 'data2';
Model.findOne({
where: {
[`$("json_column"->'data'->>'${someVariableWithColumnName}')::boolean$`]: true,
id: someIdVariable,
},
order: [/* some order, doesn't matter */],
})
And sequelize generate bad result like:
SELECT *
FROM table
WHERE "(json_column"."->'data'->>'data2')::boolean" = true AND id = '00000000-1111-2222-3333-456789abcdef'
LIMIT 1
Split my column by . and add " to every element.
Any idea how to get rid of adding " to the column in where condition?
Edit:
Here is my query with sequelize.literal():
const someVariableWithColumnName = 'data2';
Model.findOne({
where: {
[sequelize.literal(`$("json_column"->'data'->>'${someVariableWithColumnName}')::boolean$`)]: true,
id: someIdVariable,
},
order: [/* some order, doesn't matter */],
})
You can use Sequelize.literal() to avoid spurious quotes. IMHO, wrapping the json handling in a db function might also be helpful.
I just came across a similar use case.
I believe you can use the static sequelize.where method in combination with sequelize.literal.
Here is the corresponding documentation in sequelize API reference: https://sequelize.org/master/class/lib/sequelize.js~Sequelize.html#static-method-where
And here is an example (although I will admit hard to find) in the regular documentation:
https://sequelize.org/master/manual/model-querying-basics.html#advanced-queries-with-functions--not-just-columns-
In the end for your specific sit try something like this:
const someVariableWithColumnName = 'data2';
Model.findOne({
where: {
[Op.and]: [
// We provide the virtual column sql as the first argument of sequelize.where with sequelize.literal.
// We provide the matching condition as the second argument of sequelize.where, with the usual sequelize syntax.
sequelize.where(sequelize.literal(`$("json_column"->'data'->>'${someVariableWithColumnName}')::boolean$`), { [Op.eq]: true }),
{ id: someIdVariable }
]
})

In mongoose, how to find records based on value in related collection?

In Mongoose, I have two collections, with one referencing the other. Is it possible to have a find query that selects records based on a value in the other. An example of what I am try to get at (not actual schemas):
const CarModelSchema = new mongoose.Schema({
name: String,
brand: { type: mongoose.Schema.Types.ObjectId, ref: 'CarBrand' }
});
const CarBrandSchema = new mongoose.Schema({
name: String,
country: String
});
I then want to perform a query of the form, without needing to do two queries:
CarModelSchema.find({ 'brand.country': 'GER' });
So far I haven't been able to make this work, so I am wondering whether this can be done in Mongo or whether I am approaching it wrong?
Yes it is possible.
I realize you don't have models for your schemas so add them like this:
const CarModel = mongoose.model('CarModel', CarModelSchema);
const CarBrand = mongoose.model('CarBrand', CarBrandSchema);
Also brands should be defined like this:
brand: [{ type: mongoose.Schema.Types.ObjectId, ref: 'CarBrand' }] //added the brackets
You can then run a find query to filter by country by doing the following:
CarModel.
find(...).
populate({
path: 'brand',
match: { country: { $eq: 'GER' }},
// You can even select the field you want using select like below,
select: 'name -_id',
//Even limit the amount of documents returned in the array
options: { limit: 5 }
}).
exec();
And that should do it, as long as the ObjectIds saved in brands array in the CarModel collection are valid or exist.
Using match in your population will do the work.
CarModel.find()
.populate({
path: 'brand',
model: CarBrandModel,
match: { country: { $eq: 'GER' }},
})
.exec()
Keep in mind you have to define CarModel and CarBrandModel like this:
const CarModel = mongoose.model('CarModel', CarModelSchema)
const CarBrandModel = mongoose.model('CarBrandModel', CarBrandSchema)
Yes, you are doing it wrong.
In CarModelSchema.brand there is not string saved, there is ObjectId saved, therefore you have to find that ObjectId (the reference).
You can do it manually - first finding the CarBrandSchema.find({ 'country': 'GER' }); and then use its ObjectId (=_id), or you can use https://mongoosejs.com/docs/populate.html to populate your CarModel with the CarBrand object.

Create a text index on MongoDB schema which references another schema

I am trying to add an index to a certain Schema with mongoose for text searches. If I add a text index to individual fields it works fine, also with compound indexes it is okay. For example the answer provided here is great:
Full text search with weight in mongoose
However, I am trying to add an index to fields which are references to other Schemas. For example my model looks like the following:
var Book = new Schema({
"title": String,
"createdAt": Date,
"publisher": {
"type": Schema.ObjectId,
"ref": "Publisher"
},
"author": {
"type": Schema.ObjectId,
"ref": "Author"
},
"isbn": String
});
So something like the following indexing doesn't work when you perform a search query as described below:
Indexing:
Book.index({"title": "text", "publisher.name": "text", "author.firstName": "text"});
Search query:
function searchBooks(req, res) {
var query = req.query.searchQuery;
Book.find({ $text: { $search: query } })
.populate('author publisher')
.limit(25)
.exec(function(err, books) {
if (err) {
res.json(err);
} else {
res.json(books);
}
}
);
}
Does anyone have any suggestions of how to add a text index in for the "publisher" and "author" fields, I am using the "populate" mongodb method to pull in the data for these schemas.
I think, what you are looking for is the ability to join tables of data and perform a query against the sum of that data. That is something you need a relational database for, which MongoDB isn't.
So I recommend you change your approach in how you would like to preform your search, e.g. search for your keyword within both author and title instead of attempting to search the whole dataset at the same time.

mongoosejs or statement with objectid

Can I "traverse" through an ObjectID with an OR statement using mongoose.js?
An example would be something like this:
var confName = 'something';
finding = finding.or([
{ 'home_team.conference.name': confName },
{ 'away_team.conference.name': confName }
]);
finding.exec(function(err, models) {
...
home_team and away_team are both ObjectID's pointing to the Team Schema which has an embedded doc conference in it.
Right now this isn't working for me and I'm not sure if I this isn't possible or if I'm just not doing it right.
No, you can't. You'll need to either store the name in addition to the id on that document (denormalized data is common in mongo schemas) or search for "teams" with that conference name, and then search for something like
finding.or([
{ home_team: {$in: teamIds}},
{ away_team: {$in: teamIds}}
])

Categories