Shout! Shout!
Let it all out!
These are the things I can do with Scout!

Hi, I'm Steven Maguire

  • I've been building software since 2004.
  • Contribute to open source.
  • Author courses for Pluralsight.com.
  • VP, Technology at Earth Class Mail.
  • Tweet from @stevenmaguire.

Search is hard.

Basic SQL

select * from table where column like '%keyword%';

Basic-ish SQL

SELECT * FROM table WHERE
(
    column_one LIKE '%keyword%'
    OR column_two LIKE '%keyword%'
);

What about ranking by relevance?

This isn't a SQL talk.

Luckily other people solved this problem.

Advantages

  • They do the work
  • HTTP API (mostly)
  • Scale with growth
  • Reliable (mostly)

Disadvantages

  • You do the work to
    keep records fresh
  • Non-customizable (mostly)
  • Expensive to scale
  • Unreliable (sometimes)

We can't address all disadvantages.

Except one!

You do the work to
keep records fresh!

Laravel + Search = :)

Scout

Laravel Scout provides a simple, driver based solution for adding full-text search to your Eloquent models.

laravel.com/docs/5.3/scout

Basically...

  • Listen for CRUD events on models then "upsert" or remove documents in third-party search service
  • Search models using keyword, fetch results from third-party search service

Also

  • Not compatible for Laravel < 5.3
  • Scout is a separate package
  • Third-party search drivers are extendable,
    may require separate packages
  • Syncing can be synchronous or asynchronous using queues
  • Model relationships are not very well supported
  • Speed varies service to service

Let's build something!

I'm a fan of mascots.

and films by
Christopher Guest.

As a fan, I want a great experience when researching mascots.

We can help!

$ composer create-project --prefer-dist laravel/laravel mascots
...
$ cd mascots

New Laravel Project

$ php artisan make:model Mascot --migration
Model created successfully.
Created Migration: 2016_09_25_181801_create_mascots_table

New Laravel Model

<?php

//...

public function up()
{
    Schema::create('mascots', function (Blueprint $table) {
        $table->increments('id');
        $table->string('name');
        $table->string('image_url');
        $table->string('domain');
        $table->text('description');
        $table->integer('popularity');
        $table->timestamps();
    });
}

public function down()
{
    Schema::dropIfExists('mascots');
}

Define Migration

database/migrations/YYYY_MM_DD_HHMMSS_create_mascots_table.php

$ php artisan migrate
Migration table created successfully.
Migrated: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_100000_create_password_resets_table
Migrated: 2016_09_25_181801_create_mascots_table

Run Migration

<?php

//...

Route::resource('mascots', 'MascotController');

Create Resource Route

routes/api.php

$ php artisan make:controller MascotController --resource
Controller created successfully.

Create Resource Controller

Let's add Scout

laravel.com/docs/5.3/scout

<?php

//...

    'providers' => [
        //...
        Laravel\Scout\ScoutServiceProvider::class,
        //...
    ];

Include Scout Service Provider

config/app.php

$ composer require laravel/scout

Include Scout Package

<?php

namespace App;

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

class Mascot extends Model
{
    use Searchable;
}

Add Searchable Trait to Model

app/Mascot.php

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

Publish Scout Config

<?php

return [
    //...
    'driver' => env('SCOUT_DRIVER', 'algolia'),
    //...
    'queue' => false,
    //...
    'algolia' => [
        'id' => env('ALGOLIA_APP_ID', ''),
        'secret' => env('ALGOLIA_SECRET', ''),
    ],
];

Update Configuration

config/scout.php

Create an Algolia App

$ composer require algolia/algoliasearch-client-php

Include Algolia Package

ALGOLIA_APP_ID=XXXXXXXXXX
ALGOLIA_SECRET=XXXXXXXXXX

Include Algolia API Keys

.env

Indexing Scenarios

Batch import all existing models

$ php artisan scout:import "App\Mascot"

Add new records

<?php

$mascot = new App\Mascot;

// ...

$mascot->save();

Conditionally add records for existing models

<?php

// Adding via Eloquent query...
App\Mascot::where('popularity', '>', 10)->searchable();

// You may also add records via relationships...
$actor->mascots()->searchable();

// You may also add records via collections...
$mascots->searchable();

Update records

<?php

$mascot = App\Mascot::find(1);

// ...

$mascot->save();

Remove records

<?php

$mascot = App\Mascot::find(1);

// ...

$mascot->delete();

Conditionally remove records

<?php

// Removing via Eloquent query...
App\Mascot::where('popularity', '>', 10)->unsearchable();

// You may also remove via relationships...
$actor->mascots()->unsearchable();

// You may also remove via collections...
$mascots->unsearchable();

Skip/pause syncing

<?php

App\Mascot::withoutSyncingToSearch(function () {
    // Perform model actions without fear 
    // of syncing data with third-party 
    // search service
});

Searching Scenarios

Keyword

<?php

$mascots = App\Mascot::search('cereal')->get();

Performs search against third-party search service, then queries database for all models associated with results; returns Collection.

local.INFO: select * from `mascots` where `id` in ('2', '1') 

Limiting Results

<?php

$mascots = App\Mascot::search('cereal')
    ->where('popularity', 10)
    ->get();

Currently, these clauses only support basic numeric equality checks, and are primarily useful for scoping search queries by a tenant ID. Since a search index is not a relational database, more advanced "where" clauses are not currently supported.

Pagination

<?php

$mascots = App\Mascot::search('cereal')->paginate();

// Specific per page

$mascots = App\Mascot::search('cereal')->paginate(10);

Demo

$ composer require laravel/passport
...
$ php artisan migrate
Migrated: 2016_06_01_000001_create_oauth_auth_codes_table
Migrated: 2016_06_01_000002_create_oauth_access_tokens_table
Migrated: 2016_06_01_000003_create_oauth_refresh_tokens_table
Migrated: 2016_06_01_000004_create_oauth_clients_table
Migrated: 2016_06_01_000005_create_oauth_personal_access_clients_table
$ php artisan passport:install
Encryption keys generated successfully.
Personal access client created successfully.
Password grant client created successfully.

Include Passport
laravel.com/docs/5.3/passport

Performance Tip

Use a queue!

<?php

return [
    //...
    'queue' => true,
    //...
];

config/scout.php

More Tips

  • Consider leveraging search service conditionally,
    don't use for every look up operation. Services
    cost $$$.
  • The solution does not defer load from the database.
  • Usage looks similar to Eloquent; it's different!
  • Use a queue to defer syncing tasks.
  • Experiment; not for everyone.

Extend!

laravel.com/docs/5.3/scout#custom-engines

Questions?

Thank you!

@stevenmaguire
on twitter

stevenmaguire@gmail.com
on electronic mail

stevenmaguire
on github

Shout! Shout! Let it all out! These are the things I can do with Scout!

By Steven Maguire

Shout! Shout! Let it all out! These are the things I can do with Scout!

Deck originally created for a presentation to a gathering of the Chicago Laravel Meetup group - https://vcr.bz/laravel-scout

  • 3,380