Skip to content
ElasticScout is an optimized Laravel Scout driver for Elasticsearch 7.1+
PHP
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
config
src
tests
.codecov.yml
.editorconfig
.gitignore
.styleci.yml
.travis.yml
README.md
composer.json
phpunit.xml

README.md

ElasticScout - Elasticsearch Driver for Laravel Scout

Latest Stable Version Total Downloads Monthly Downloads Build Status codecov StyleCI License

PayPal

This package was shaped from Babenko Ivan's Elasticscout Driver repo.

Contents

Install

Install the package using Composer CLI:

$ composer require rennokki/elasticscout

If your Laravel package does not support auto-discovery, add this to your config/app.php file:

'providers' => [
    ...
    Rennokki\ElasticScout\ElasticScoutServiceProvider::class,
    ...
];

Publish the config files:

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

Configuring Scout

In your .env file, set yout SCOUT_DRIVER to elasticscout, alongside with Elasticsearch configuration:

SCOUT_DRIVER=elasticscout

SCOUT_ELASTICSEARCH_HOST=localhost
SCOUT_ELASTICSEARCH_PORT=9200

Indexes

Creating an index

In Elasticsearch, the Index is the equivalent of a table in MySQL, or a collection in MongoDB. You can create an index class using artisan:

$ php artisan make:elasticscout:index PostIndex

You will have something like this in app/Indexes/PostIndex.php:

<?php

namespace App\Indexes;

use Rennokki\ElasticScout\Index;
use Rennokki\ElasticScout\Migratable;

class PostIndex extends Index
{
    use Migratable;

    /**
     * The settings applied to this index.
     *
     * @var array
     */
    protected $settings = [
        //
    ];

    /**
     * The mapping for this index.
     *
     * @var array
     */
    protected $mapping = [
        //
    ];
}

The key here is that you can set settings and a mapping for each index. You can find more on Elasticsearch's documentation website about mappings and settings.

Here's an example on creating a mapping for a field that is a geo-point datatype:

class RestaurantIndex extends Index
{
    ...
    protected $mapping = [
        'properties' => [
            'location' => [
                'type' => 'geo_point',
            ],
        ],
    ];
}

Here is an example on creating a new analyzer in the $settings variable for a whitespace tokenizer:

class PostIndex extends Index
{
    ...
    protected settings = [
        'analysis' => [
            'analyzer' => [
                'content' => [
                    'type' => 'custom',
                    'tokenizer' => 'whitespace',
                ],
            ],
        ],
    ];
}

If you wish to change the name of the index, you can do so by overriding the $name variable:

class PostIndex extends Index
{
    protected $name = 'posts_index_2';
}

Attach the index to a model

All the models that can be searched into should use the Rennokki\ElasticScout\Searchable trait and implement the Rennokki\ElasticScout\Index\HasElasticScoutIndex interface:

use Rennokki\ElasticScout\Contracts\HasElasticScoutIndex;
use Rennokki\ElasticScout\Searchable;

class Post extends Model implements HasElasticScoutIndex
{
    use Searchable;
}

Additionally, the model should also specify the index class:

use App\Indexes\PostIndex;
use Rennokki\ElasticScout\Contracts\HasElasticScoutIndex;
use Rennokki\ElasticScout\Index;
use Rennokki\ElasticScout\Searchable;

class Post extends Model implements HasElasticScoutIndex
{
    use Searchable;

    /**
     * Get the index instance class for Elasticsearch.
     *
     * @return \Rennokki\ElasticScout\Index
     */
    public function getElasticScoutIndex(): Index
    {
        return new PostIndex($this);
    }
}

Publish the index to Elasticsearch

To publish the index to Elasticsearch, you should sync the index:

$ php artisan elasticscout:index:sync App\\Post

Now, each time your model creates,updates or deletes new records, they will be automatically synced to Elasticsearch.

In case you want to import already-existing data, please use the scout:import command that is described in the Scout documentation.

Syncing the index can also be done within your code:

$restaurant = Restaurant::first();

$restaurant->getIndex()->sync(); // returns true/false

Search Query

To query data into Elasticsearch, you may use the search() method:

Post::search('Laravel')
    ->take(30)
    ->from(10)
    ->get();

In case you want just the number of the documents, you can do so:

$posts = Post::search('Lumen')->count();

Filter Query

ElasticScout allows you to create a custom query using built-in methods by going through the elasticsearch() method.

Must, Must not, Should, Filter

You can use Elasticsearch's must, must_not, should and filter keys directly in the builder. Keep in mind that you can chain as many as you want.

Post::elasticsearch()
    ->must(['term' => ['tag' => 'wow']])
    ->should(['term' => ['tag' => 'yay']])
    ->shouldNot(['term' => ['tag' => 'nah']])
    ->filter(['term' => ['tag' => 'wow']])
    ->get();

Append to body or query

You can append data to body or query keys.

// apend to the body payload
Post::elasticsearch()
    ->appendToBody('minimum_should_match', 1)
    ->appendToBody('some_field', ['array' => 'yes'])
    ->get();
// append to the query payload
Post::elasticsearch()
    ->appendToQuery('some_field', 'value')
    ->appendToQuery('some_other_field', ['array' => 'yes'])
    ->get();

Wheres

Post::elasticsearch()
    ->where('title.keyword', 'Elasticsearch')
    ->first();
Book::elasticsearch()
    ->whereBetween('price', [100, 200])
    ->first();
Book::elasticsearch()
    ->whereNotBetween('price', [100, 200])
    ->first();

Regex filters

Post::elasticsearch()
    ->whereRegexp('title.raw', 'A.+')
    ->get();

Existence check

Since Elasticsearch has a NoSQL structure, you should be able to check if a field exists.

Post::elasticsearch()
    ->whereExists('meta')
    ->whereNotExists('new_meta')
    ->get();

Geo-type searches

Restaurant::whereGeoDistance('location', [-70, 40], '1000m')
    ->get();
Restaurant::whereGeoBoundingBox(
    'location',
    [
        'top_left' => [-74.1, 40.73],
        'bottom_right' => [-71.12, 40.01],
    ]
)->get();
Restaurant::whereGeoPolygon(
    'location',
    [
        [-70, 40], [-80, 30], [-90, 20],
    ]
)->get();
Restaurant::whereGeoShape(
    'shape',
    [
        'type' => 'circle',
        'radius' => '1km',
        'coordinates' => [4, 52],
    ],
    'WITHIN'
)->get();

Rules

A search rule is a class that can be used on multiple queries, helping you to define custom payload only once. This works only for the Search Query builder.

To create a rule, use the artisan command:

$ php artisan make:elasticscout:rule NameRule

You will get something like this:

<?php

namespace App\SearchRules;

use Rennokki\ElasticScout\Builders\SearchQueryBuilder;
use Rennokki\ElasticScout\SearchRule;

class NameRule extends SearchRule
{
    /**
     * Initialize the rule.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Build the highlight payload.
     *
     * @param  SearchQueryBuilder  $builder
     * @return array
     */
    public function buildHighlightPayload(SearchQueryBuilder $builder)
    {
        return [
            //
        ];
    }

    /**
     * Build the query payload.
     *
     * @param  SearchQueryBuilder  $builder
     * @return array
     */
    public function buildQueryPayload(SearchQueryBuilder $builder)
    {
        return [
            //
        ];
    }
}

Query Payload

Within the buildQueryPayload(), you should define the query payload that will take place during the query.

For example, you can get started with some bool query. Details about the bool query you can find in the Elasticsearch documentation.

class NameRule extends SearchRule
{
    public function buildQueryPayload(SearchQueryBuilder $builder)
    {
        return [
            'must' => [
                'match' => [
                    // access the search phrase from the $builder
                    'name' => $builder->query,
                ],
            ],
        ];
    }
}

To apply by default on all search queries, define a getElasticScoutSearchRules() method in your model:

use App\SearchRules\NameRule;

class Restaurant extends Model
{
    /**
     * Get the search rules for Elasticsearch.
     *
     * @return array
     */
    public function getElasticScoutSearchRules(): array
    {
        return [
            new NameRule,
        ];
    }
}

To apply the rule at the query level, you can call the ->addRule() method:

use App\SearchRules\NameRule;

Restaurant::search('Dominos')
    ->addRule(new NameRule)
    ->get();

You can add multiple rules or set the rules to a specific value:

use App\SearchRules\NameRule;
use App\SearchRules\LocationRule;

Restaurant::search('Dominos')
    ->addRules([
        new NameRule,
        new LocationRule($lat, $lon),
    ])->get();

// The rule that will be aplied will be only LocationRule
Restaurant::search('Dominos')
    ->addRule(new NameRule)
    ->setRules([
        new LocationRule($lat, $lon),
    ])->get();

Highlight Payload

When building the highlight payload, you can pass the array to the buildHighlightPayload() method. More details on highlighting can be found in the Elasticsearch documentation.

class NameRule extends SearchRule
{
    public function buildHighlightPayload(SearchQueryBuilder $builder)
    {
        return [
            'fields' => [
                'name' => [
                    'type' => 'plain',
                ],
            ],
        ];
    }

    public function buildQueryPayload(SearchQueryBuilder $builder)
    {
        return [
            'should' => [
                'match' => [
                    'name' => $builder->query,
                ],
            ],
        ];
    }
}

To access the payload, you can use the $highlight attribute from the model (or from each model of the final collection).

use App\SearchRules\NameRule;

$restaurant = Restaurant::search('Dominos')->addRule(new NameRule)->first();

$name = $restaurant->elasticsearch_highlights->name;
$nameAsString = $restaurant->elasticsearch_highlights->nameAsString;

In case you need to pass arguments to the rules, you can do so by adding your construct method.

class NameRule extends SearchRule
{
    protected $name;

    public function __construct($name = null)
    {
        $this->name = $name;
    }

    public function buildQueryPayload(SearchQueryBuilder $builder)
    {
        // Override the name from the rule construct.
        $name = $this->name ?: $builder->query;

        return [
            'must' => [
                'match' => [
                    'name' => $name,
                ],
            ],
        ];
    }
}

Restaurant::search('Dominos')
    ->addRule(new NameRule('Pizza Hut'))
    ->get();

Debugging

You can debug by explaining the query.

Restaurant::search('Dominos')->explain();

You can see how the payload looks like by calling getPayload().

Restaurant::search('Dominos')->getPayload();
You can’t perform that action at this time.