Paulund
2017-12-27 #laravel

Laravel & VueJS Algolia Search

I wanted to use an Algolia search box on my website and found that they've already done a lot of the hard work for me, by creating the vue-instantsearch package. This allows me to quickly hook into the Algolia API and query the index for the results in real time.

<section>
    <ais-index app-id="{{ config('scout.algolia.id') }}"
               api-key="{{ config('scout.algolia.search') }}"
               index-name="posts_index">

        <div class="relative">
            <ais-input placeholder="Search for tutorials..."></ais-input>
            <ais-results>
                <template scope="{ result }">
                    <div>
                        <h4><a :href="result.slug">@{{ result.title }}</a></h4>
                        <p><a :href="result.slug">@{{ result.excerpt }}</a></p>
                    </div>
                </template>
                <ais-powered-by slot="footer" />
            </ais-results>
        </div>
    </ais-index>
</section>

The problem I had is the vue instant search library will on page load make a blank search to the index and show the last 20 items in the index. I wanted the search to only show the results when the user types into the search box. There's probably other packages that already exist for this functionality but I thought it would be a good exercise to build my own VueJS component that will display the search results as the user types. Therefore let's built our own VueJS component and Laravel API that will only display the results when the user types something into the search box.

Install Laravel Scout

There's a Laravel package that makes it very easy to use Algolia wit Models, all you have to do is install the Laravel Scout package, add a trait to the model and import the model into algolia index. To install Laravel Scout you can use composer with the following command. composer require laravel/scout

Then you can publish the scout.php config file by using the following command php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider". This will add a new file to your Laravel config folder.

To use Algolia with Laravel Scout you need to install the following package. This will add the Algolia SDK which is used by Laravel Scout. composer require algolia/algoliasearch-client-php Now open up the file config/scout.php and make sure that algolia is selected as the default scout driver. At the bottom of the file is where you need to enter the Algolia API ID and secret.

'algolia' => [
    'id' => env('ALGOLIA_APP_ID', ''),
    'secret' => env('ALGOLIA_SECRET', ''),
    'search' => env('ALGOLIA_SEARCH', '')
]

We can now add these API settings in the .env file.

Create A Searchable Model

When the Algolia SDK is installed we can create a blog post Model and add the Searchable trait. After the trait has been added you need to add a new method called toSearchableArray which is used to tell Algolia what data to import. For this method we're going to return title, slug, any tags used and the post excerpt.

<?php
namespace App\Models;

use Laravel\Scout\Searchable;

/**
 * Post model
 */
class Post extends Model
{
    use Searchable;
  
    /**
     * Get the indexable data array for the model.
     *
     * @return array
     */
    public function toSearchableArray()
    {
        return [
            'title' => $this->title,
            'slug'  => $this->slug,
            'tags'  => $this->tags()->pluck('title'),
            'excerpt' => $this->excerpt
        ];
    }
}

Import The Data Into Algolia Index

From the Laravel Scout package we have a command available to us to import the Post into Algolia, to use this command you can use the following. php artisan scout:import 'App\Models\Post' You can now log into Algolia and see your posts have been added to the search index

Create Search Controller

With the posts in the Algolia index we can start to create a new route to query the Algolia API and bring back the results from the search query. We can then use this route to display the search results in a new VueJS search component. This Search controller will have one method which will take a request object to validate that there is a querystring parameter of q. We then take the value in the querystring and query Algolia for the index with the value of the search keyword. Because we added the Searchable trait we have access to a new method of search on the Post model. $posts = Post::search( $request->get('q') )->raw(); The result of the search will return which posts hit the keyboard phrase, we can then return this from the controller in a json format so that we can use this in VueJS.

<?php

namespace App\Http\Controllers\Search;

use App\Http\Controllers\Controller;
use App\Models\Post;
use Illuminate\Http\Request;

/**
 * Class SearchController
 * @package App\Http\Controllers\Search
 */
class SearchController extends Controller
{
    public function search(Request $request)
    {
        $this->validate($request, [
            'q' => 'required'
        ]);

        $posts = Post::search( $request->get('q') )->raw();

        if(empty($posts['hits'])) {
            $posts['hits'] = [];
        }

        return response()->json(['data' => $posts['hits']]);
    }
}

Create Search Route

With the controller built we can setup a route for /search to call this search controller. Route::get('/search', 'Search\SearchController@search');

Search Vue Component

With the endpoint ready to query we can start building the VueJS component to search for the posts in realtime and display the results. We're going to split this up into 3 different components, a parent Search.vue, a SearchBox.vue and a Results.vue. The first component we need to create is the Search.vue parent component that will hold the search box and an area to display the results. In the template area we need to display both these components and pass in a query data property that will store the keyboard typed into the search box.

<template>
    <section class="search">
        <search-box v-model="query" placeholder="Search for a tutorial" :show-results.sync="showResults"></search-box>
        <div class="relative" v-show="showResults">
            <search-results :query="query"></search-results>
        </div>
    </section>
</template>

<script>
    import SearchBox from './SearchBox'
    import SearchResults from './Results'
    export default {
      data() {
        return {
          query: '',
          showResults: true
        };
      },
      components: {
        SearchBox,
        SearchResults
      }
    }
</script>

Search Box Vue Component

The Searchbox.vue is a component that will just have a textbox that's going to be used to hold the keyword that the user types in. On the input event of the textbox we're going to run a method called updateValue that will emit an event to update the query data property on the parent Search.vue component.

<template>
    <div>
        <input type="search"
               :value="value"
               :placeholder="placeholder"
               v-on:input="updateValue($event.target.value)"
               id="s"
               name="s"
               autocomplete="off">
    </div>
</template>

<script>
    export default {
      props: ['value', 'placeholder'],
      methods: {
        updateValue: function (value) {
          this.$emit('input', value)
        }
      }
    }
</script>

Search Results Vue Component

In the search results component we need to take the query keyword as a prop and make a query to the Search endpoint which will then query Algolia for the results of the keyword. From the return of the endpoint we then need to display a list of posts and a link to the post for the end user. As this is going to a real time search we need to fetch the results of the query as the user is typing the keyword, therefore we need to watch the query prop for any changes and search Algolia for the results. The search results are split up into 3 parts the HTML template, the CSS styling and the JS component.

The HTML will only be displayed if the results have posts in them therefore we need to the v-show directive v-show="results.length > 0". Then we'll loop through the results and display a link and the result title.

<template>
    <section class="search-results" v-show="results.length > 0">
        <div v-for="result in results" class="search-result">
            <a :href="'/' + result.slug">{{ result.title }}</a>
        </div>
    </section>
</template>

Then we can style the results in a scrolling div.

<style lang="scss">
.search-results {
    position: absolute;
    top: 0;
    background: #FFF;
    border: 1px solid #dae4e9;
    width: 100%;
    max-height: 20rem;
    overflow-y: scroll;
    > div {
        margin-bottom: 0.5rem;
        text-align: left;
        &:hover {
            background-color: #F1F5F8;
        }
        a {
            display: block;
            padding: 1rem;
        }
    }
}
</style>

Now we need to do the VueJS code to query the the endpoint when the user types in the keyword in the search box. The first thing we need to do is accept a prop of query which will be used to send through to the endpoint. We'll need to store the results of the query in a results data point. We'll need to watch the query prop for any changes and query the endpoint each time this changes.

As this is a prop a new value will enter into the component on the change event of the text box, using the watch property any changes to this variable will then make a new query to the endpoint. Finally we create a method to make a query to the endpoint with the keyword. At the start of the method we need to check if the query keyword is empty as we don't want to call the endpoint for an empty keyword.

Then we add a condition to make sure that the keyword has more than 3 characters before we start searching. If the query keyword is not empty and more than 3 characters then we can query the /search endpoint and store the result of the endpoint in the results data point. This will automatically display the results in the search-results div.

<script>
  import axios from 'axios'
    export default {
      props: ['query'],
      data() {
        return {
          results: []
        };
      },
      watch: {
        query () {
          this.getSearchResults();
        }
      },
      methods: {
        getSearchResults () {
          if(this.query === '' && this.query.length < 3) {
            this.results = []
            return false
          }
          axios.get( '/search?q=' + this.query )
            .then(response => {
              this.results = response.data.data
            });
        }
      }
    }
</script>

The full file of the Results.vue component is

<template>
    <section class="search-results" v-show="results.length > 0">
        <div v-for="result in results" class="search-result">
            <a :href="'/' + result.slug">{{ result.title }}</a>
        </div>
    </section>
</template>

<style lang="scss">
.search-results {
    position: absolute;
    top: 0;
    background: #FFF;
    border: 1px solid #dae4e9;
    width: 100%;
    max-height: 20rem;
    overflow-y: scroll;
    > div {
        margin-bottom: 0.5rem;
        text-align: left;
        &:hover {
            background-color: #F1F5F8;
        }
        a {
            display: block;
            padding: 1rem;
        }
    }
}
</style>

<script>
  import axios from 'axios'
    export default {
      props: ['query'],
      data() {
        return {
          results: []
        };
      },
      watch: {
        query () {
          this.getSearchResults();
        }
      },
      methods: {
        getSearchResults () {
          if(this.query === '' && this.query.length < 3) {
            this.results = []
            return false
          }
          axios.get( '/search?q=' + this.query )
            .then(response => {
              this.results = response.data.data
            });
        }
      }
    }
</script>