Awwwards
Announcing Full-Text, Site-Wide Search's Hero Image

Announcing Full-Text, Site-Wide Search

Laravel 5.3 was a huge release and I can see why Taylor says it’s his favorite one recently. While not necessarily part of the 5.3 release, one of things I was most excited about, was the introduction of Laravel Scout. Scout is a “simple, driver based solution for adding full-text search to your Eloquent models”. It ships with an Algolia driver but the community has already come together and created more drivers for other search providers.

When we built the current version of the Metric Loop website, we knew we wanted to implement full-text, site-wide search to help our viewers find relevant information. But there were so many other things we needed to build that search took a distant backseat. We built a learning library, a customer portal, and a custom lead generation tool that allows Customers to fill out a tailored form so we can respond to their needs quickly and accurately. As our analytics rolled in after launch, we realized that our tech library wasn’t getting traffic because of how many clicks it takes to find relevant content. For instance, if you wanted to find out if virtual desktop infrastructure (VDI) was right for your company, would that be under “Network”, “Virtualization”, or “Data Center”?

When Scout was announced I knew that it would be a feature I rolled into the upgrade from 5.2 to 5.3. If I was going to spend time upgrading, refactoring, and re-implementing features, I might as well add search to the laundry list.

Make All The Things Searchable!

Like everything Laravel, it was stupid-easy to set up and get started. Using Algolia made the setup even easier because I didn’t have to change the scout.php file at all (except to flip queue to true instead of the default false). Then I added a few Searchable traits to my models and overrided the toSearchableArray() to make sure Users aren’t searching on the options or image attributes.

There was a weird situation I had to cover, though. Making the blog articles searchable was something I really wanted. For our blog, every Article has many Revisions, but only one is active (deleted_at == null) at a time. In the search, however, I wanted the full body of the article from the revision to be searchable but I didn’t want it to exist in a separate searchable index. I found a simple solution in the toSearchableArray() for Article.

public function toSearchableArray()
{
    $array = $this->toArray();
    $array[‘body’] = $this->revisions()->first()->body;
    return $array;
}

You, like me, are probably thinking this is totally fine and nothing will break and this will index perfectly. Nope.

Why doesn’t it work? Well, first I create a new Article and then use the new Article object to create a new Revision. Because Scout uses model observers, it tries to index the new Article immediately on create. But the Revision doesn’t exist yet. So I get a pesky little “body not found” error.

This was where I muttered “man, Taylor really thought of everything”.

Here’s what I mean:

Article::withoutSyncingToSearch(function () use ($request) {
    Auth::user()->articles()->create($request->all());
});

Then I create the revision

$this->createRevision($article, $revision_attr);

And then

$article->save();

Which, thanks to the model observers, automatically indexes the Article and makes it searchable!

Now Search

To prove that it was working, I did a simple text box in the main nav that submitted a form to /search. I would take the query input and then

$articles = Article::search($query)->get();
$subjects = Subject::search($query)->get();
$topics = Topic::search($query)->get();

I would also do the same for Article Tags; find all the Articles with those Tags, and then merge the results into one collection of Articles.

The results page was snappy and because I was retrieving the entire Eloquent model, it was really easy to format in a way that made a lot of sense. But who wants to wait for the entire page to reload?? This is 2016 dammit!

And now I get to say “man, Algolia really thought of everything”.

I wanted to do real time search which meant using the autocomplete and multi-category options in Algolia’s docs. Everything about this real-time search is in its own search.blade.php file that gets included in the main nav. There’s a div with the search box, and then <script> tags to pull in all the dependencies. We might add these to our gulpfile.js later but right now it’s not a priority. So, the setup.

<script>
    var client = algoliasearch("ALGOLIA_APP_ID", "ALGOLIA_PUBLIC");
    var articles = client.initIndex('articles');
    var subjects = client.initIndex('subjects');
    var topics = client.initIndex(‘topics');
    var tags = client.initIndex('tags');

    // define templates and initialize autocomplete.js
</script>

Keep in mind that Scout requires using the secret Algolia key because it needs write access. If you’re implementing this real-time autocompleting search, make sure you use the public search-only key so you’re not broadcasting your private key to the world.

Tip: set SCOUT_PREFIX in your .env file so that development and production environments aren’t using the same indices. We use dev_ for dev and an empty string for production.

We then customize the Hogan.js templates, add each source to the autocomplete init and we’re off to the races.

Self Promotion

We also wanted to point to Metric Loop social accounts and other ways that customers can get in touch with us. This kind of information isn’t stored in Eloquent models, which means that Scout wouldn’t help with indexing. Luckily, we can manually create indices and records in the Algolia dashboard. So, we created a contact index with records for phone, email, address, facebook, twitter, instagram, youtube, etc. Now, if you search for “tweet” or “subscribe” or “physical location” you’ll get results that point you in the right direction. Then we just needed to add the new “source” and template for the contact index.

    var contact = client.initIndex('contact');
    var templateContact = Hogan.compile('<div class="search-result">' +
            '<div class="name">' +
            '<a href="url" target="_blank">_highlightResult.title.value</a>' +
            '</div>' +
            '</div>');

Making it Look Good

Now that we had the search bar working, it was time to make it fit our user experience. We took a look at our main navigation and realized that a full search bar wouldn’t fit with the other menu items - especially on mobile. So, we opted for an icon toggle that would allow us to keep our original navigation and add in the search bar. Additionally, while the search bar is super useful for quickly finding information, we still wanted our users to use our navigation as much as possible. So having the icon increases the friction of using the search bar just enough so users only use it after they’ve tried the navigation.

Once we had our UI/UX sorted out, we dived right into the code and added the search bar’s HTML into the navigation. After that, it was simply a matter of getting the toggling to work and styling it up. We used our old friend jQuery to get the toggle to work, but we ran into a slight problem with the timing of the transition. When the search bar appears, the input field appeared faster than the height of its parent div. So we broke up the appear animation into pieces and set a small delay on the input’s animation so it would appear seamlessly with its parent. After that, we cooked up some tasty Sass to make the search bar look nice, and we were finally ready to deploy.

Wrapping Up

We hope that by implementing real-time, full-text, site-wide search we can improve our user experience, drive more traffic, and attract more customers. We want the search bar to cast a wide net and provide a lot of value to those browsing our site.


Patrick Guevara's Profile Picture

Patrick Guevara


Chief Software Engineer

Patrick cofounded Metric Loop on the dream of building really great software with a clean, transparent approach. He lives in Austin, Texas with his wife Jess and their dog named Moose.