Search for Substring in several fields with MongoDB and Mongoose
Your use case is a good fit for text search.
Define a text index on your schema over the searchable fields:
trackingSchema.index({ tracking_number: 'text', zip_code: 'text', 'delivery_info.recipient': 'text', 'delivery_info.street': 'text', 'delivery_info.city': 'text'}, {name: 'search'});
Join your search terms into a single string and execute the search using the $text
query operator:
var search = ['15232', 'julian'];Test.find({$text: {$search: search.join(' ')}}, function(err, docs) {...});
Even though this passes all your search values as a single string, this still performs a logical OR search of the values.
Why just dont try
var trackingSchema = mongoose.Schema({ tracking_number: String, zip_code: String, courier: String, user_id: Number, created: { type: Date, default: Date.now }, international_shipment: { type: Boolean, default: false }, delivery_info: { recipient: String, street: String, city: String }});var Tracking = mongoose.model('Tracking', trackingSchema );var search = [ "word1", "word2", ...]var results = []for(var i=0; i<search.length; i++){ Tracking.find({$or : [ { tracking_number : search[i]}, {zip_code: search[i]}, {courier: search[i]}, {delivery_info.recipient: search[i]}, {delivery_info.street: search[i]}, {delivery_info.city: search[i]}] }).map(function(tracking){ //it will push every unique result to variable results if(results.indexOf(tracking)<0) results.push(tracking); });
Okay, I came up with this.
My schema now has an extra field search
with an array of all my searchable fields:
var trackingSchema = mongoose.Schema({ ... search: [String]});
With a pre-save hook, I populate this field:
trackingSchema.pre('save', function(next) { this.search = [ this.tracking_number ]; var searchIfAvailable = [ this.zip_code, this.delivery_info.recipient, this.delivery_info.street, this.delivery_info.city ]; for (var i = 0; i < searchIfAvailable.length; i++) { if (!validator.isNull(searchIfAvailable[i])) { this.search.push(searchIfAvailable[i].toLowerCase()); } } next();});
In the hope of improving performance, I also index that field (also the user_id
as I limit search results by that):
trackingSchema.index({ search: 1 });trackingSchema.index({ user_id: 1 });
Now, when searching I first list all substrings I want to look for in an array:
var andArray = [];var searchTerms = searchRequest.split(" ");searchTerms.forEach(function(searchTerm) { andArray.push({ search: { $regex: searchTerm, $options: 'i' } });});
I use this array in my find()
and chain it with an $and
:
Tracking. find({ $and: andArray }). where('user_id').equals(userId). limit(pageSize). skip(pageSize * page). exec(function(err, docs) { // hooray! });
This works.