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>