Mongo Text Search with Meteor

Written by Pete Corey on Jan 26, 2015.

My most recent project, Suffixer, involves doing a primarily text-based search over more than 80k documents. Initially, I was using $regex to do all of my querying, but this approach was unacceptably slow. I decided to try out MongoDB’s text search functionality to see if I could get any performance gains.

I replaced my main query with something like this:

MyCollection.find({$text: {$search: searchText}});

Unfortunately, Meteor seemed very unhappy with this change. I immediately began setting errors in my server logs:

Exception from sub ZskAqGy2t2jJckpXK MongoError: invalid operator: $search

A quick investigation showed what was wrong. Meteor uses Mongo 2.4, instead of 2.6. You can check this by running db.version() in your Mongo shell (meteor mongo). Text search in 2.4 is syntactically significatly different than text search in 2.6.

If you insist on using Meteor’s bundled version of Mongo, this Meteorpedia post shows how to manually kick off the search command in a reactive context.

A much better solution is to simply use your own instance of Mongo 2.6. Follow the available installation guides to get an instance running on your machine (or remotely). Once Mongo is successfully installed, you can instruct Meteor to use this new instance of Mongo by pointing to it with the MONGO_URL environment variable.

Using Mongo’s text search coupled with a text index drastically improved the performance of my web-app.

The Dangers of Debouncing Meteor Subscriptions

Written by Pete Corey on Jan 19, 2015.

I’ve been working on a Meteor app with instant search functionality. When users to type data into an input box, the system updates a session value which kicks off a Meteor.subscribe.

Template.controls.events({
    'keyup #search': function(e) {
        Session.set('search', e.target.value);
    }
});

Meteor.autorun(function() {
    Meteor.subscribe('my-collection', Session.get('search'));
});

While this worked, triggering a new subscribe for every keypress put too much of an unneeded strain on the system. Typing the word “test” triggered 4 different subscriptions, and the first 3 sets of subscription results were thrown out in a fraction of a second. I needed to limit the rate at which I was triggering my new subscriptions and subsequent database queries. A great way to do that is with Lo-Dash’s debounce method.

Debounce Meteor.subscribe

My initial idea was to debounce the Meteor.subscribe function used within the Meteor.autorun callback. Since the session variables being tracked by the Tracker computation could be updated in other places in the app as well, I figured this would be a clean way to limit excessive subscriptions being made to the server.

I changed my code to look like this:

var debouncedSubscribe = _.debounce(Meteor.subscribe, 300);
Meteor.autorun(function() {
    debouncedSubscribe('my-collection', Session.get('search'));
});

This had a very interesting affect on my application. While changing the session variable did trigger a new subscription, and the call was being debounced as expected, I noticed that old subscription results were being kept around on the client. The collection was starting to balloon in size.

Down to Debugging

I fired up my trusty Chrome Dev Tools and started debugging within the subscribe method itself in livedata_connection.js. After comparing behavior with a normal subscription invocation and a debounced invocation, the problem made itself known on line 571.

When it was executed as a debounce callback, the Meteor.subscribe call was no longer part of a computation because it is executed asynchronously. Tracker.active was returning false within the context of the debounce callback. This means that the Tracker.onInvalidate and Tracker.afterFlush callbacks were never initiated within the subscribe call as they would have been if subscribe were called from directly within a computation. That caused the subscription to never “stop” and its subscription data stayed around forever. Effectively, I was piling up new subscriptions every time the search string changed.

Meteor.subscribe('my-collection', 't');
Meteor.subscribe('my-collection', 'te');
Meteor.subscribe('my-collection', 'tes');
Meteor.subscribe('my-collection', 'test');
...

The Solution

I spent some time trying to find a way to run an asynchronous callback under an existing computation, but I wasn’t able to find a good way to do this. Ultimately, my solution was to not debounce the Meteor.subscribe call, but to debounce the keyup event handler:

Template.controls.events({
    'keyup #search': _.debounce(function(e) {
        Session.set('search', e.target.value);
    }, 300)
});

Meteor.autorun(function() {
    Meteor.subscribe('my-collection', Session.get('search'));
});

Custom Block Helpers and Meteor Composability

Written by Pete Corey on Jan 13, 2015.

I’ll admit it; working with AngularJS has left me with a certain sense of entitlement. I expect my front-end framework to be able to let me build distinct elements that can be composed together and placed within each other. After reading about Meteor’s templating system, I was under the impression that content could not be transcluded into a template. After all, where would it go?

{{> myTemplate}}

It wasn’t until I read through Spacebar’s documentation that I found out about the amazingly useful custom block helpers. Custom block helpers let us inject or transclude content (HTML, text, variable interpolations, other templates) into a template. Let’s check it out!

Using Custom Block Helpers

If you read through the Custom Block Helpers section of the Spacebar docs, you’ll notice that Template.contentBlock behaves almost exactly like AngularJS’ ng-transclude directive. It marks the place where content will be injected into the template’s markup. Let’s look at an example:

<template name="myTemplate">
    {{#if this}}
    <h1>{{title}}</h1>
    <div>
        {{> Template.contentBlock}}
    </div>
    {{else}}
        <h1>No content provided</h1>
        <div>
            {{#if Template.elseBlock}}
                {{> Template.elseBlock}}
            {{else}}
                <p>No elseBlock provided</p>
            {{/if}}
        </div>
    {{/if}}
</template>

<body>
    {{#myTemplate title="My Title"}}
        <p>My custom content</p>
    {{else}}
        <p>This is my else block</p>
    {{/myTemplate}}
</body>

There’s a lot going on in this example. Let’s break it down.

Check Your Context

The first thing I do in my template is check the truthiness of the template’s data context (this). If a truthy data context is passed into the template, I render one set of markup (which includes data from the context), otherwise, I render an alternate set of markup.

Inject Your Content

You’ll notice that I’m using Template.contentBlock in the first set of output and Template.elseBlock in the next. Interestingly, when you define a custom block helper, you can also define an “else” block to go along with it. In the second set of output I’m checking if Template.elseBlock is truthy. If it is, I render it, otherwise, I render some markup explaining that an else block was not provided.

Use Your Template

To actually use your new template and inject content into it, you no longer use the inclusion tag syntax. Instead, you reference your template using block tag syntax. You can see this at the bottom of the above example. Within your block tag you include your content to be injected and an optional else block. Note that any arguments you pass into your template becomes that template’s data context.

Final Thoughts

While the above example is completely contrived and mostly useless, I think it shows some really useful functionality. I feel like custom blocks are going to be a big game changer in my Meteor development workflow. There’s a great article over at Discover Meteor that touches on custom blocks with a more practical example. Check it out!