Achieving Geo-search with Laravel Scout and Algolia

Julien Bourdeau
πŸ‘οΈ 5,105 views
πŸ’¬ comments

Laravel Scout makes it very easy to setup an external search engine to create consumer-grade search quickly. The package comes with Algolia as a default search engine. I'd like to demonstrate how to make use of the geo-location search feature with Scout.

In this tutorial, you'll learn how to prepare your data for Algolia and Laravel Scout to retrieve items based on location.

We recommend you use front-end search for your app because this implementation model is most efficient. That being said, there are cases where you need backend search, for instance, if you want to generate reports.

Getting Started

For this tutorial, I quickly set up a Laravel app with dummy data of airports around the world. You can find the project on GitHub. You can install it by following the notes in the readme.

If your use case is different or your data isn’t organised this way, feel free to post to our community forum.

Step 1: Install Scout

Scout comes as a separate package, we'll use composer to pull it into our project.

The whole point of Scout is to add an abstraction layer so your code will be the same for any search engine you choose. Since we're using Algolia, this will require our PHP client.

composer require laravel/scout
composer require algolia/algoliasearch-client-php

Once the PHP client is installed, let’s configure our configuration and specify Algolia as a search provider.. Simply open up the config/app.php file and add Scout in the providers array.

Laravel\Scout\ScoutServiceProvider::class,

Lastly, publish the package configuration. You don't have to edit the file, everything can be set in your .env configuration file.

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

Now, open up your .env file and add your Algolia credentials. You can find these details in your Algolia dashboard, under the API keys tab.

ALGOLIA_APP_ID=QWERTY12AB
ALGOLIA_SECRET=YOUR_ADMIN_API_KEY

Step 2: Format your data for indexing

Algolia’s storage is schema-less, meaning all your data will be represented as a JSON object.

Algolia requires a special attributes to store the coordinates. It has to be set as follows:

{
    other: 'attributes', 
    _geoloc: {
        lat: 0,
        lng: 0
    }
}

Laravel Scout transforms your model into an array before sending data to Algolia, using the method [toSearchableArray()](https://laravel.com/docs/5.4/scout#configuring-searchable-data). So we'll simply override this method to set the _geoloc attribute.

<?php

namespace App;

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

class Airport extends Model
{
    use Searchable;

    public function toSearchableArray()
    {
        $record = $this->toArray();

        $record['_geoloc'] = [
            'lat' => $record['latitude'],
            'lng' => $record['longitude'],
        ];

        unset($record['created_at'], $record['updated_at']); // Remove unrelevant data
        unset($record['latitude'], $record['longitude']);

        return $record;
    }
}

Once it's imported, you will see your data in your Algolia Dashboard.

Your imported data in Algolia dashboard

ProTips

It’s possible to index multiple locations for one model, it doesn't make sense for an airport but you could have:

$record['_geoloc'] = [
    ['lat' => $record['lat1'], 'lng' => $record['lng1']],
    ['lat' => $record['lat2'], 'lng' => $record['lng2']],
    ['lat' => $record['lat3'], 'lng' => $record['lng3']],
];

Step 3: Search for your records

Because we use the Searchable trait, we can use the search() static method to look for our data.

Airport::search('CDG');

Here’s what happens when this code is executed:

  1. Laravel calls Algolia to get relevant results
  2. It creates a collection based on the IDs of the records retrieved
  3. And finally pull data from the app database to instantiate proper models

The good news is that search() takes a callback as a second arguments to alter the request made to Algolia. We'll hook just before step 1 to search by locations.

Searching around a location (reverse geocoding)

You need two pieces of information to search per location: the coordinates and a radius. This is what we'll pass to Algolia using the callback.

Let's say we want the airports around Paris. In this example, we'll not search in the text, but only per location. Hence, we'll leave the query empty.

By default, Algolia returns the results sorted by ascending distance: the closest comes first.

$lat = 48.8588377;
$lng = 2.2775175;
$radius = 150000; // Value has to be in meters

Airport::search('', function ($algolia, $query, $options) use ($lat, $lng, $radius) {
    $location =  [
        'aroundLatLng' => $lat.','.$lng,
        'aroundRadius' => $radius,
    ];

    $options = array_merge($options, $location);

    return $algolia->search($query, $options);
});

The radius is, in fact, optional. If you don't provide radius, Algolia will return the closest 1,000 results.

If you want to know more about how to set the options to search by location, you can refer to the official documentation.

Searching inside a zone

Algolia can also search inside a zone. A zone, called a bounding box here, is defined by a set of at least 2 coordinates. In this case the rectangle where the line between those 2 points is the diagonal.

For this example we'll look for an airport in the UK. I formed a polygon using Google Maps. I could have used France for this example, but the borders were too complicated. Now that I think about it, I probably should have chosen the state of Colorado.

Airport::search($query, function ($algolia, $query, $options) {
    $polygonAroundTheUK = [
        52.9332312, -1.9937525, // Point 1 lat, lng
        52.1312677, -2.0434894, // Point 2 lat, lng
        52.7029492,-1.2530685, // Point 3 lat, lng
        51.5262792, 0.1192389, // Point 4 lat, lng
        48.8364598, 1.7006836, // Point 5 lat, lng
    ];

    $location = [
        'insidePolygon' => [$polygonAroundTheUK]
    ];

    $options = array_merge($options, $location);

    return $algolia->search($query, $options);
})

Leveraging Macros

Laravel Scout 3.0.5 added the Macroable capability to the builder. This is a great way to add new method to a class without overriding it. A good place to add your custom macros would be in the boot method of your AppServiceProvider.

if (! Builder::hasMacro('aroundLatLng')) {
    Builder::macro('aroundLatLng', function ($lat, $lng) {
        $callback = $this->callback;

        $this->callback = function ($algolia, $query, $options) use ($lat, $lng, $callback) {
            $options['aroundLatLng'] = (float) $lat . ',' . (float) $lng;

            if ($callback) {
                return call_user_func(
                    $callback,
                    $algolia,
                    $query,
                    $options
                );
            }

            return $algolia->search($query, $options);
        };

        return $this;
    });
}

This macro will hide all the burden of the callback behind a nice function, then you get:

// Return closest 1,000 airport from paris, ordered by distance
Airport::search('')->aroundLatLng(48.8588377, 2.2775175);

I created a little project to gather useful algolia-specific macro. Feel free to contribute!

Conclusion

This tutorial is only a small example of what you can do with Scout and Algolia. If you have any suggestions or if you want to share your use case, feel free to leave a comment or join our community. I'll be happy to answer.

Julien Bourdeau

1 post

Software engineer @ Algolia