Using Magic Numbers To Build a Faster Search Pattern in Meteor


by David Woody

The code below is a standard pattern to perform a simple search.

// client event
'keypress': function (event, template) {
  var search = template.$('.search-input').val() + String.fromCharCode(event.keyCode);
  Meteor.subscribe('search', search);
}
// server
Meteor.publish('search', function (search) {
  check(search, String);
  return People.find({
      name: {
        $regex: search,
        $options: 'i'
      }
    });
});

Whenever this event is fired, the client sends a DDP message to the server, the server runs a query, and then sends a series of DDP messages back to the client for each document as the query matches fewer or more documents, whatever the case me be. For many documents, a DDP added message is sent within a few seconds of a removed message needing to be sent.

This is not ideal, as it creates unnecessary load on the server.

Aside: What is the String.fromCharCode(event.keyCode) doing? It is getting the value of the key that was pressed to trigger the event. The keypress event is fired before the input is updated, so template.$('.search-input').val() is always one keypress behind the current keypress. This is in contrast to the keyup event, which is triggered after the input is updated. I have found that keyup does not always work as expected on Cordova/mobile, but keypress seems to do the trick.

A pattern I've used recently is only a slight variation to the pattern above. However it has the effect of only publishing and subscribing once, and then doing all subsequent filtering on the client. It looks something like this:

'keypress': function (event, template) {
  var search = template.$('.search-input').val() + String.fromCharCode(event.keyCode);
  if(search.length === 3) {
    Meteor.subscribe('search', search);
  }
  template.filter.set(search);
}

Why did we use 3 instead of 2 or 4? Simply because 3 was our magic number. The template.filter.set(search) is setting a reactive variable which was added to the template instance inside the Template's created callback function 

Note: the subscription was actually handled the same way, but was not included for illustrative purposes

The client side cursor passed to the {{#each}} block looks similar to the publish function, but is passed the filter variable.

Template.search.helpers({
  search: function() {
    var filter = Template.instance().filter.get();
    return People.find({
      name: {
        $regex: filter,
        $options: 'i'
      }
    });
  }
});

Finding Your Magic Number

Figuring out how long to wait before subscribing can be tricky. Should you wait for one character or five? Here is a questions I ask myself...

How many characters are needed before the query has relevance to the user?

Another way to think about this question is to consider the number of matching documents in the database for a given search string. This can help guide you to the right answer, or at least a good place to start. 

In any case, finding your magic number and only publishing and subscribing once can make your Meteor app just a little more efficient at doing simple search.

Share Share on Twitter Share on Facebook Share on LinkedIn

How Can We Help?

Reaching out doesn’t mean you’re ready to start a project, but we’d love to learn more about the challenge you’re facing, answer any questions, and see if we might be a good fit for working together.

Contact Us