Skip to content
This repository was archived by the owner on Nov 4, 2021. It is now read-only.

Commit f281e6e

Browse files
committed
Added bulk indexing support
1 parent d0eff70 commit f281e6e

File tree

9 files changed

+317
-70
lines changed

9 files changed

+317
-70
lines changed

README.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ There are information about Elasticsearch installation and the package usage exa
4343
* Lots of different ways to implement your search algorithm: using [search rules](#search-rules) or a [raw search](#usage).
4444
* [Various filter types](#available-filters) to make a search query more specific.
4545
* [Zero downtime migration](#zero-downtime-migration) from an old index to a new index.
46+
* Bulk indexing, see [the configuration section](#configuration).
4647

4748
## Requirements
4849

@@ -70,12 +71,15 @@ php artisan vendor:publish --provider="ScoutElastic\ScoutElasticServiceProvider"
7071
```
7172

7273
Then, set the driver setting to `elastic` in the `config/scout.php` file and configure the driver itself in the `config/scout_elastic.php` file.
73-
There are two available options:
74+
The available options are:
7475

7576
Option | Description
7677
--- | ---
7778
client | A setting hash to build Elasticsearch client. More information you can find [here](https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/_configuration.html#_building_the_client_from_a_configuration_hash). By default the host is set to `localhost:9200`.
7879
update_mapping | The option that specifies whether to update a mapping automatically or not. By default it is set to `true`.
80+
indexer | Set to `single` for the single document indexing and to `bulk` for the bulk document indexing. By default is set to `true`.
81+
82+
Note, that if you use the bulk document indexing you'll probably want to change the chunk size, you can do that in the `config/scout.php` file.
7983

8084
## Index configurator
8185

@@ -190,7 +194,7 @@ class MyModel extends Model
190194
Each searchable model represents an Elasticsearch type.
191195
By default a type name is the same as a table name, but you can set any type name you want through the `searchableAs` method.
192196
You can also specify fields which will be indexed by the driver through the `toSearchableArray` method.
193-
More information about these options you will find in [the scout official documentation](https://laravel.com/docs/5.4/scout#configuration).
197+
More information about these options you will find in [the scout official documentation](https://laravel.com/docs/5.5/scout#configuration).
194198

195199
The last important option you can set in the `MyModel` class is the `$searchRules` property.
196200
It allows you to set different search algorithms for a model.
@@ -205,9 +209,19 @@ php artisan elastic:update-mapping App\\MyModel
205209
## Usage
206210

207211
Once you've created an index configurator, an Elasticsearch index itself and a searchable model, you are ready to go.
208-
Now you can [index](https://laravel.com/docs/5.4/scout#indexing) and [search](https://laravel.com/docs/5.4/scout#searching) data according to the documentation.
212+
Now you can [index](https://laravel.com/docs/5.5/scout#indexing) and [search](https://laravel.com/docs/5.5/scout#searching) data according to the documentation.
213+
214+
Basic search usage example:
215+
216+
```php
217+
App\MyModel::search('phone')
218+
->where('color', 'red')
219+
->orderBy('price', 'asc')
220+
->take(10)
221+
->get();
222+
```
209223

210-
In addition to standard functionality the package offers you the possibility to filter data in Elasticsearch without specifying query string:
224+
In addition to standard functionality the package offers you the possibility to filter data in Elasticsearch without specifying a query string:
211225

212226
```php
213227
App\MyModel::search('*')

config/scout_elastic.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@
66
env('SCOUT_ELASTIC_HOST', 'localhost:9200')
77
]
88
],
9-
'update_mapping' => env('SCOUT_ELASTIC_UPDATE_MAPPING', true)
9+
'update_mapping' => env('SCOUT_ELASTIC_UPDATE_MAPPING', true),
10+
'indexer' => env('SCOUT_ELASTIC_INDEXER', 'single')
1011
];

src/ElasticEngine.php

Lines changed: 27 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,65 +2,59 @@
22

33
namespace ScoutElastic;
44

5-
use Artisan;
5+
use Illuminate\Support\Facades\Artisan;
66
use Laravel\Scout\Builder;
77
use Laravel\Scout\Engines\Engine;
88
use ScoutElastic\Builders\SearchBuilder;
99
use ScoutElastic\Facades\ElasticClient;
1010
use Illuminate\Database\Eloquent\Collection;
1111
use Illuminate\Database\Eloquent\Model;
12-
use ScoutElastic\Payloads\DocumentPayload;
12+
use ScoutElastic\Indexers\IndexerInterface;
1313
use ScoutElastic\Payloads\TypePayload;
1414
use stdClass;
1515

1616
class ElasticEngine extends Engine
1717
{
18-
protected $updateMapping = false;
18+
protected $indexer;
1919

20-
public function __construct()
20+
protected $updateMapping;
21+
22+
static protected $updatedMappings = [];
23+
24+
public function __construct(IndexerInterface $indexer, $updateMapping)
2125
{
22-
$this->updateMapping = config('scout_elastic.update_mapping');
26+
$this->indexer = $indexer;
27+
28+
$this->updateMapping = $updateMapping;
2329
}
2430

2531
public function update($models)
2632
{
27-
$models->each(function ($model) {
28-
if ($this->updateMapping) {
29-
Artisan::call(
30-
'elastic:update-mapping',
31-
['model' => get_class($model)]
32-
);
33-
}
34-
35-
$array = $model->toSearchableArray();
36-
37-
if (empty($array)) {
38-
return true;
39-
}
33+
if ($this->updateMapping) {
34+
$self = $this;
4035

41-
$indexConfigurator = $model->getIndexConfigurator();
36+
$models->each(function ($model) use ($self) {
37+
$modelClass = get_class($model);
4238

43-
$payload = (new DocumentPayload($model))
44-
->set('body', $array);
39+
if (in_array($modelClass, $self::$updatedMappings)) {
40+
return true;
41+
}
4542

46-
if (in_array(Migratable::class, class_uses_recursive($indexConfigurator))) {
47-
$payload->useAlias('write');
48-
}
43+
Artisan::call(
44+
'elastic:update-mapping',
45+
['model' => $modelClass]
46+
);
4947

50-
ElasticClient::index($payload->get());
51-
});
48+
$self::$updatedMappings[] = $modelClass;
49+
});
50+
}
5251

53-
$this->updateMapping = false;
52+
$this->indexer->update($models);
5453
}
5554

5655
public function delete($models)
5756
{
58-
$models->each(function ($model) {
59-
$payload = (new DocumentPayload($model))
60-
->get();
61-
62-
ElasticClient::delete($payload);
63-
});
57+
$this->indexer->delete($models);
6458
}
6559

6660
protected function buildSearchQueryPayload(Builder $builder, $queryPayload, array $options = [])

src/Indexers/BulkIndexer.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
namespace ScoutElastic\Indexers;
4+
5+
use Illuminate\Database\Eloquent\Collection;
6+
use ScoutElastic\Facades\ElasticClient;
7+
use ScoutElastic\Migratable;
8+
use ScoutElastic\Payloads\RawPayload;
9+
10+
class BulkIndexer implements IndexerInterface
11+
{
12+
public function update(Collection $models)
13+
{
14+
$bulkPayload = new RawPayload();
15+
16+
$models->each(function ($model) use ($bulkPayload) {
17+
$modelData = $model->toSearchableArray();
18+
19+
if (empty($modelData)) {
20+
return true;
21+
}
22+
23+
$indexConfigurator = $model->getIndexConfigurator();
24+
25+
$actionPayload = (new RawPayload())
26+
->set('index._type', $model->searchableAs())
27+
->set('index._id', $model->getKey());
28+
29+
if (in_array(Migratable::class, class_uses_recursive($indexConfigurator))) {
30+
$actionPayload->set('index._index', $indexConfigurator->getWriteAlias());
31+
} else {
32+
$actionPayload->set('index._index', $indexConfigurator->getName());
33+
}
34+
35+
$bulkPayload->add('body', $actionPayload->get())
36+
->add('body', $modelData);
37+
});
38+
39+
ElasticClient::bulk($bulkPayload->get());
40+
}
41+
42+
public function delete(Collection $models)
43+
{
44+
$bulkPayload = new RawPayload();
45+
46+
$models->each(function ($model) use ($bulkPayload) {
47+
$indexConfigurator = $model->getIndexConfigurator();
48+
49+
$actionPayload = (new RawPayload())
50+
->set('delete._index', $indexConfigurator->getName())
51+
->set('delete._type', $model->searchableAs())
52+
->set('delete._id', $model->getKey());
53+
54+
$bulkPayload->add('body', $actionPayload->get());
55+
});
56+
57+
ElasticClient::bulk($bulkPayload->get());
58+
}
59+
}

src/Indexers/IndexerInterface.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace ScoutElastic\Indexers;
4+
5+
use Illuminate\Database\Eloquent\Collection;
6+
7+
interface IndexerInterface
8+
{
9+
public function update(Collection $models);
10+
11+
public function delete(Collection $models);
12+
}

src/Indexers/SingleIndexer.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace ScoutElastic\Indexers;
4+
5+
use Illuminate\Database\Eloquent\Collection;
6+
use ScoutElastic\Facades\ElasticClient;
7+
use ScoutElastic\Migratable;
8+
use ScoutElastic\Payloads\DocumentPayload;
9+
10+
class SingleIndexer implements IndexerInterface
11+
{
12+
public function update(Collection $models)
13+
{
14+
$models->each(function ($model) {
15+
$modelData = $model->toSearchableArray();
16+
17+
if (empty($modelData)) {
18+
return true;
19+
}
20+
21+
$indexConfigurator = $model->getIndexConfigurator();
22+
23+
$payload = (new DocumentPayload($model))
24+
->set('body', $modelData);
25+
26+
if (in_array(Migratable::class, class_uses_recursive($indexConfigurator))) {
27+
$payload->useAlias('write');
28+
}
29+
30+
ElasticClient::index($payload->get());
31+
});
32+
}
33+
34+
public function delete(Collection $models)
35+
{
36+
$models->each(function ($model) {
37+
$payload = (new DocumentPayload($model))
38+
->get();
39+
40+
ElasticClient::delete($payload);
41+
});
42+
}
43+
}

src/Payloads/RawPayload.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,32 @@ public function setIfNotEmpty($key, $value)
2424
return $this->set($key, $value);
2525
}
2626

27+
public function add($key, $value)
28+
{
29+
if (!is_null($key)) {
30+
$currentValue = array_get($this->payload, $key, []);
31+
32+
if (!is_array($currentValue)) {
33+
$currentValue = array_wrap($currentValue);
34+
}
35+
36+
$currentValue[] = $value;
37+
38+
array_set($this->payload, $key, $currentValue);
39+
}
40+
41+
return $this;
42+
}
43+
44+
public function addIfNotEmpty($key, $value)
45+
{
46+
if (empty($value)) {
47+
return $this;
48+
}
49+
50+
return $this->add($key, $value);
51+
}
52+
2753
public function get($key = null)
2854
{
2955
return array_get($this->payload, $key);

src/ScoutElasticServiceProvider.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Config;
66
use Illuminate\Support\ServiceProvider;
77
use Elasticsearch\ClientBuilder;
8+
use InvalidArgumentException;
89
use ScoutElastic\Console\ElasticIndexCreateCommand;
910
use ScoutElastic\Console\ElasticIndexDropCommand;
1011
use ScoutElastic\Console\ElasticIndexUpdateCommand;
@@ -39,7 +40,19 @@ public function boot()
3940

4041
$this->app->make(EngineManager::class)
4142
->extend('elastic', function () {
42-
return new ElasticEngine();
43+
$indexerType = config('scout_elastic.indexer', 'single');
44+
$updateMapping = config('scout_elastic.update_mapping', true);
45+
46+
$indexerClass = '\\ScoutElastic\\Indexers\\'.ucfirst($indexerType).'Indexer';
47+
48+
if (!class_exists($indexerClass)) {
49+
throw new InvalidArgumentException(sprintf(
50+
'The %s indexer doesn\'t exist.',
51+
$indexerType
52+
));
53+
}
54+
55+
return new ElasticEngine(new $indexerClass(), $updateMapping);
4356
});
4457
}
4558

0 commit comments

Comments
 (0)