// Tutorial //

Build search functionality with Laravel Scout and Vue.js

Draft updated on Invalid Date
Default avatar
By rachidlaasri
Developer and author at DigitalOcean.
Build search functionality with Laravel Scout and Vue.js

This tutorial is out of date and no longer maintained.

Introduction

Today, we are going to learn how to build a simple search functionality using Laravel Scout and Vue.js.

If you don’t know what Laravel Scout is, here is what the Laravel documentation says:

Laravel Scout provides a simple, driver-based solution for adding full-text search to your Eloquent models. Using model observers, Scout will automatically keep your search indexes in sync with your Eloquent records.

It’s an official package, not included in Laravel by default but you can still pull it in with Composer and use it in your Laravel apps. It is shipped with Algolia driver, but you can swipe drivers easily as the documentation says:

Currently, Scout ships with an Algolia driver; however, writing custom drivers is simple and you are free to extend Scout with your own search implementations.

Before diving into the code, let’s take a look at what we are going to be building:

Installing Laravel

To install Laravel, open your terminal and cd into your folder and run this command:

  1. composer create-project --prefer-dist laravel/laravel search

After executing the command, change your document root to point to the public folder and make sure directories within the storage and the bootstrap/cache directories are writable by your web server.

The last step in installing Laravel is generating an application key that is used to encrypt your user sessions and other data, and you can do that by running:

  1. php artisan key:generate

If you open your website in the browser you should see this exact same page:

Database configuration

Rename the .env.example file to .env and add your database name, user, and password.

I will be using SQLite for this demo, feel free to use MySQL or any database management system you prefer.

DB_CONNECTION=sqlite

If you don’t specify a database name, Laravel assumes it is located in database/database.sqlite.

Models and migrations

For this small app, we will only be needing a Product model and a products table.

Go ahead and generate those two:

  1. php artisan make:model Product -m

When passing the -m flag to the php artisan make:model command a migration will be generated for you. That’s a cool trick to know!

These are the fields we are currently interested in:

class CreateProductsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('products', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title');
            $table->string('image');
            $table->integer('price');
            $table->text('description');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('products');
    }
}

Save your file and migrate your tables:

  1. php artisan migrate

Dummy data

Using Laravel Model Factories we will be generating some fake data to test with. add these lines to database/factories/ModelFactory.php:

$factory->define(App\Product::class, function (Faker\Generator $faker) {
    return [
        'title' => $faker->sentence(),
        'image' => 'http://loremflickr.com/400/300?random='.rand(1, 100),
        'price' => $faker->numberBetween(3, 100),
        'description' => $faker->paragraph(2)
    ];
});

Our model factory is ready, let’s create some data. In your command line run:

  1. php artisan tinker

And then:

factory(App\Product::class, 100)->create();

You can create as many records as you want, 100 sounds perfect to me.

Routes and Controllers

These two routes are all that we need for this app, so let’s go ahead and create them.

  • GET : / the home route, this route will render our website home page.
routes/web.php
Route::get('/', function () {
    return view('home');
});

You can use a controller, but I really don’t think it’s necessary for this route.

  • GET : api/search this route is responsible for handling search requests.
routes/api.php
Route::get('/search', [
    'as' => 'api.search',
    'uses' => 'Api\SearchController@search'
]);

To create the SearchController class, simply run:

  1. php artisan make:controller Api\SearchController

If you didn’t already know, when adding a namespace to your controller’s class name Laravel will automatically create the folder for you and put the generated controller inside it.

class SearchController extends Controller
{
    public function search(Request $request)
    {
        // we will be back to this soon!
    }
}

Installing Laravel Scout

Installing and configuring Laravel Scout is a breeze, you can pull the package using:

  1. composer require laravel/scout

When Composer is done doing its thing add the ScoutServiceProvider to the providers array of your config/app.php configuration file:

Laravel\Scout\ScoutServiceProvider::class,

Next, you should publish the Scout configuration using the vendor:publish Artisan command. This command will publish the scout.php configuration file to your config directory:

  1. php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"

Finally, add the Laravel\Scout\Searchable trait to the Product to make it searchable:

<?php

namespace App;

use Laravel\Scout\Searchable;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    use Searchable;
}

Importing data to Algolia

Now that Laravel Scout is installed and ready, it’s time to install the Algolia driver

  1. composer require algolia/algoliasearch-client-php

all we need to do now is to import the data we generated earlier to Algolia, You can create a free account ( No credit card is required ) if you want to follow up with this tutorial.

When you are done creating your account browse to https://www.algolia.com/api-keys and copy your Application ID and Admin API Key and put them in your .env file like this:

ALGOLIA_APP_ID=ALGOLIA_APP_ID
ALGOLIA_SECRET=ALGOLIA_SECRET

In your terminal run

  1. php artisan scout:import "App\Product"

If you did everything correctly, you should see this message, which means that all your products table data got copied to Algolia’s servers.

You can verify that by browsing to the Indices page in your account.

Writing the search method

Back to the search method in app\Http\Controllers\Api\SearchController.php.

    /**
     * Search the products table.
     *
     * @param  Request $request
     * @return mixed
     */
    public function search(Request $request)
    {
        // First we define the error message we are going to show if no keywords
        // existed or if no results found.
        $error = ['error' => 'No results found, please try with different keywords.'];

        // Making sure the user entered a keyword.
        if($request->has('q')) {

            // Using the Laravel Scout syntax to search the products table.
            $posts = Product::search($request->get('q'))->get();

            // If there are results return them, if none, return the error message.
            return $posts->count() ? $posts : $error;

        }

        // Return the error message if no keywords existed
        return $error;
    }

Don’t forget to import the Product model

use App\Product;

Now, browsing to http://example.com/api/search?q=code should return a JSON representation of your data.

The frontend setup

We won’t focus too much on the design in this tutorial, so here is the template we will be working with, copy/paste it in your resources/home.blade.php file.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
        <title>Search with Laravel Scout and Vue.js!</title>
    </head>
    <body>
        <div class="container">
            <div class="well well-sm">
                <div class="form-group">
                    <div class="input-group input-group-md">
                        <div class="icon-addon addon-md">
                            <input type="text" placeholder="What are you looking for?" class="form-control">
                        </div>
                        <span class="input-group-btn">
                            <button class="btn btn-default" type="button">Search!</button>
                        </span>
                    </div>
                </div>
            </div>
            <div id="products" class="row list-group">
            </div>
        </div>
    </body>
</html>

You can see that all I have done is set up some boilerplate and import Twitter Bootstrap, you shouldn’t be using the CDN for your real-life apps, but since this is just a quick demo it’s fine.

Importing Vue.js and vue-resource

Add these lines before the body closing tag (</body>):

<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.26/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-resource/1.0.1/vue-resource.min.js"></script>
<script src="/js/app.js"></script>

Again, you should use a dependency manager for these kinds of things but it’s beyond the scope of this tutorial so it is fine for now.

The next step is creating the app.js file inside public/js/ folder (delete it if does already exist) and this is what it will hold for now:

new Vue({
    el: 'body',
});

What this means is we are binding the Vue instance to the HTML body tag making our JavaScript only recognized between the HTML body tags.

Vue.js data and bindings

To capture what the user wrote in the search input, display the results, show the error message or display the searching button we will need to add these lines just under the el property:

data: {
    products: [],
    loading: false,
    error: false,
    query: ''
},

This will force us to change our HTML a little bit, here is what we need to do:

  1. Add the v-model attribute to the search input to bind it to the query property in our data object
<input type="text" placeholder="What are you looking for?" class="form-control" v-model="query">
  1. Display the Searching… button when an action is being performed and show the normal search button if not.
<button class="btn btn-default" type="button" v-if="!loading">Search!</button>
<button class="btn btn-default" type="button" disabled="disabled" v-if="loading">Searching...</button>
  1. Same as above for the error message, we will add this under the closing tag of the div with the class of well well-sm:
<div class="alert alert-danger" role="alert" v-if="error">
    <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
    @{{ error }}
</div>

To display data with Vue.js we use the Mustache syntax {{ }}, but since Laravel is using the same syntax we need to tell not to parse it by adding the @ at its beginning.

Fetching and displaying data

The remaining and most important step is to fetch the data. We will start by attaching an event listener to our search button and we do that by adding the @click attribute to it:

<button class="btn btn-default" type="button" @click="search()" v-if="!loading">Search!</button>

Let’s create the search function and insert it under the data object:

methods: {
    search: function() {
        // Clear the error message.
        this.error = '';
        // Empty the products array so we can fill it with the new products.
        this.products = [];
        // Set the loading property to true, this will display the "Searching..." button.
        this.loading = true;

        // Making a get request to our API and passing the query to it.
        this.$http.get('/api/search?q=' + this.query).then((response) => {
            // If there was an error set the error message, if not fill the products array.
            response.body.error ? this.error = response.body.error : this.products = response.body;
            // The request is finished, change the loading to false again.
            this.loading = false;
            // Clear the query.
            this.query = '';
        });
    }
}

Finally, the HTML for the product, should go bellow the div with the ID of products.

<div class="item col-xs-4 col-lg-4" v-for="product in products">
    <div class="thumbnail">
        <img class="group list-group-image" :src="product.image" alt="@{{ product.title }}" />
        <div class="caption">
            <h4 class="group inner list-group-item-heading">@{{ product.title }}</h4>
            <p class="group inner list-group-item-text">@{{ product.description }}</p>
            <div class="row">
                <div class="col-xs-12 col-md-6">
                    <p class="lead">$@{{ product.price }}</p>
                </div>
                <div class="col-xs-12 col-md-6">
                    <a class="btn btn-success" href="#">Add to cart</a>
                </div>
            </div>
        </div>
    </div>
</div>

What we did there is we created a loop using the v-for directive and displayed the data just like we did before using the Mustache syntax.

And with that, you should have this same exact website running on your server:

Conclusion

If you reached this part, it means you completed the tutorial and you have built or are ready to build your own search system. The code source of this tutorial is available here. If you have any problems or questions, please feel free to write a comment down in the comments section and I will be happy to help.


Want to learn more? Join the DigitalOcean Community!

Join our DigitalOcean community of over a million developers for free! Get help and share knowledge in our Questions & Answers section, find tutorials and tools that will help you grow as a developer and scale your project or business, and subscribe to topics of interest.

Sign up
About the authors
Default avatar
Developer and author at DigitalOcean.

Still looking for an answer?

Was this helpful?
Leave a comment