diff --git a/CHANGELOG.md b/CHANGELOG.md index 66990202..37b89a8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/) ## [Unreleased] + +## [2.0.1] - 2019-05-06 ### Added -- Progress bar for console commands +- Progress report for console commands ## [2.0.0] - 2019-04-09 ### Added diff --git a/README.md b/README.md index 80d0d67e..bcd7690c 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Scout ElasticSearch + + Import progress report

Build Status diff --git a/composer.json b/composer.json index 2f4bda73..61463f23 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "matchish/laravel-scout-elasticsearch", "description": "This package extends Laravel Scout adding full power of ElasticSearch", - "version": "2.0.0", + "version": "2.0.1", "keywords": [ "laravel", "scout", @@ -20,7 +20,6 @@ "php": "^7.1.3", "elasticsearch/elasticsearch": "~6.0", "laravel/scout": "^6.1.1|^7.0", - "league/pipeline": "^1.0", "ongr/elasticsearch-dsl": "^6.0" }, "require-dev": { diff --git a/docs/demo.gif b/docs/demo.gif new file mode 100644 index 00000000..17ea84a3 Binary files /dev/null and b/docs/demo.gif differ diff --git a/resources/lang/en/flush.php b/resources/lang/en/flush.php new file mode 100644 index 00000000..7fe80295 --- /dev/null +++ b/resources/lang/en/flush.php @@ -0,0 +1,5 @@ + 'All [:searchable] records have been flushed.', +]; diff --git a/resources/lang/en/import.php b/resources/lang/en/import.php new file mode 100644 index 00000000..d71a44ce --- /dev/null +++ b/resources/lang/en/import.php @@ -0,0 +1,7 @@ + 'Importing [:searchable]', + 'done' => 'All [:searchable] records have been imported.', + 'done.queue' => 'Import job dispatched to the queue.', +]; diff --git a/src/Console/Commands/FlushCommand.php b/src/Console/Commands/FlushCommand.php index ed0622f6..2fafa0c7 100644 --- a/src/Console/Commands/FlushCommand.php +++ b/src/Console/Commands/FlushCommand.php @@ -32,7 +32,10 @@ public function handle(): void }); $searchableList->each(function ($searchable) { $searchable::removeAllFromSearch(); - $this->output->success('All ['.$searchable.'] records have been flushed.'); + $doneMessage = trans('scout::flush.done', [ + 'searchable' => $searchable, + ]); + $this->output->success($doneMessage); }); } } diff --git a/src/Console/Commands/ImportCommand.php b/src/Console/Commands/ImportCommand.php index a482ebe2..410ec314 100644 --- a/src/Console/Commands/ImportCommand.php +++ b/src/Console/Commands/ImportCommand.php @@ -6,6 +6,7 @@ use Illuminate\Console\Command; use Matchish\ScoutElasticSearch\Jobs\Import; +use Matchish\ScoutElasticSearch\Jobs\QueueableJob; use Matchish\ScoutElasticSearch\Searchable\SearchableListFactory; final class ImportCommand extends Command @@ -24,22 +25,41 @@ final class ImportCommand extends Command */ public function handle(): void { - $command = $this; - $searchableList = collect($command->argument('searchable'))->whenEmpty(function () { + $this->searchableList($this->argument('searchable')) + ->each(function ($searchable) { + $this->import($searchable); + }); + } + + private function searchableList($argument) + { + return collect($argument)->whenEmpty(function () { $factory = new SearchableListFactory(app()->getNamespace(), app()->path()); return $factory->make(); }); - $searchableList->each(function ($searchable) { - $job = new Import($searchable); - if (config('scout.queue')) { - dispatch($job)->allOnQueue((new $searchable)->syncWithSearchUsingQueue()) - ->allOnConnection(config((new $searchable)->syncWithSearchUsing())); - $this->output->success('All ['.$searchable.'] records have been dispatched to import job.'); - } else { - dispatch_now($job); - $this->output->success('All ['.$searchable.'] records have been searchable.'); - } - }); + } + + private function import($searchable) + { + $job = new Import($searchable); + + if (config('scout.queue')) { + $job = (new QueueableJob())->chain([$job]); + } + + $bar = (new ProgressBarFactory($this->output))->create(); + $job->withProgressReport($bar); + + $startMessage = trans('scout::import.start', ['searchable' => "$searchable"]); + $this->line($startMessage); + + dispatch($job)->allOnQueue((new $searchable)->syncWithSearchUsingQueue()) + ->allOnConnection(config((new $searchable)->syncWithSearchUsing())); + + $doneMessage = trans(config('scout.queue') ? 'scout::import.done.queue' : 'scout::import.done', [ + 'searchable' => $searchable, + ]); + $this->output->success($doneMessage); } } diff --git a/src/Console/Commands/ProgressBarFactory.php b/src/Console/Commands/ProgressBarFactory.php new file mode 100644 index 00000000..9f16be5f --- /dev/null +++ b/src/Console/Commands/ProgressBarFactory.php @@ -0,0 +1,34 @@ +output = $output; + } + + public function create(int $max = 0) + { + $bar = $this->output->createProgressBar($max); + $bar->setBarCharacter('⚬'); + $bar->setEmptyBarCharacter('⚬'); + $bar->setProgressCharacter('➤'); + $bar->setFormat( + "%message%\n%current%/%max% [%bar%] %percent:3s%%\n" + ); + + return $bar; + } +} diff --git a/src/Engines/ElasticSearchEngine.php b/src/Engines/ElasticSearchEngine.php index 49f075eb..260432ff 100644 --- a/src/Engines/ElasticSearchEngine.php +++ b/src/Engines/ElasticSearchEngine.php @@ -8,7 +8,6 @@ use Laravel\Scout\Builder as BaseBuilder; use ONGR\ElasticsearchDSL\Query\MatchAllQuery; use Matchish\ScoutElasticSearch\ElasticSearch\Index; -use Matchish\ScoutElasticSearch\Pipelines\ImportPipeline; use Matchish\ScoutElasticSearch\ElasticSearch\Params\Bulk; use Matchish\ScoutElasticSearch\ElasticSearch\SearchFactory; use Matchish\ScoutElasticSearch\ElasticSearch\SearchResults; @@ -62,10 +61,13 @@ public function delete($models) public function flush($model) { $indexName = $model->searchableAs(); - $body = (new Search())->addQuery(new MatchAllQuery())->toArray(); - $params = new SearchParams($indexName, $body); - $this->elasticsearch->deleteByQuery($params->toArray()); - $this->elasticsearch->indices()->refresh((new Refresh($indexName))->toArray()); + $exist = $this->elasticsearch->indices()->exists(['index' => $indexName]); + if ($exist) { + $body = (new Search())->addQuery(new MatchAllQuery())->toArray(); + $params = new SearchParams($indexName, $body); + $this->elasticsearch->deleteByQuery($params->toArray()); + $this->elasticsearch->indices()->refresh((new Refresh($indexName))->toArray()); + } } /** @@ -114,15 +116,6 @@ public function getTotalCount($results) return $results['hits']['total']; } - /** - * @internal - */ - public function sync($model) - { - $pipeline = new ImportPipeline($this->elasticsearch); - $pipeline->process([Index::fromSearchable($model), $model]); - } - /** * @param BaseBuilder $builder * @param array $options diff --git a/src/Jobs/Import.php b/src/Jobs/Import.php index 9483a0bf..139c09d6 100644 --- a/src/Jobs/Import.php +++ b/src/Jobs/Import.php @@ -2,16 +2,18 @@ namespace Matchish\ScoutElasticSearch\Jobs; +use Elasticsearch\Client; use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; -use Matchish\ScoutElasticSearch\Engines\ElasticSearchEngine; +use Illuminate\Support\Collection; +use Matchish\ScoutElasticSearch\ProgressReportable; /** * @internal */ -final class Import implements ShouldQueue +final class Import { use Queueable; + use ProgressReportable; /** * @var string @@ -27,10 +29,22 @@ public function __construct(string $searchable) } /** - * @param ElasticSearchEngine $engine + * @param Client $elasticsearch */ - public function handle(ElasticSearchEngine $engine): void + public function handle(Client $elasticsearch): void { - $engine->sync(new $this->searchable); + $stages = $this->stages(); + $estimate = $stages->sum->estimate(); + $this->progressBar()->setMaxSteps($estimate); + $stages->each(function ($stage) use ($elasticsearch) { + $this->progressBar()->setMessage($stage->title()); + $stage->handle($elasticsearch); + $this->progressBar()->advance($stage->estimate()); + }); + } + + private function stages(): Collection + { + return ImportStages::fromSearchable(new $this->searchable); } } diff --git a/src/Jobs/ImportStages.php b/src/Jobs/ImportStages.php new file mode 100644 index 00000000..86224595 --- /dev/null +++ b/src/Jobs/ImportStages.php @@ -0,0 +1,36 @@ +count()) { + return (new static([ + new CleanUp($searchable), + new CreateWriteIndex($searchable, $index), + PullFromSource::chunked($searchable), + new RefreshIndex($index), + new SwitchToNewAndRemoveOldIndex($searchable, $index), + ]))->flatten(); + } else { + return new static; + } + } +} diff --git a/src/Jobs/QueueableJob.php b/src/Jobs/QueueableJob.php new file mode 100644 index 00000000..8d4c9564 --- /dev/null +++ b/src/Jobs/QueueableJob.php @@ -0,0 +1,17 @@ +searchable = $searchable; + } + + public function handle(Client $elasticsearch): void + { + /** @var Searchable $searchable */ + $searchable = $this->searchable; + $params = GetAliasParams::anyIndex($searchable->searchableAs()); + try { + $response = $elasticsearch->indices()->getAliases($params->toArray()); + } catch (Missing404Exception $e) { + $response = []; + } + foreach ($response as $indexName => $data) { + foreach ($data['aliases'] as $alias => $config) { + if (array_key_exists('is_write_index', $config) && $config['is_write_index']) { + $params = new DeleteIndexParams((string) $indexName); + $elasticsearch->indices()->delete($params->toArray()); + continue 2; + } + } + } + } + + public function title(): string + { + return 'Clean up'; + } + + public function estimate(): int + { + return 1; + } +} diff --git a/src/Jobs/Stages/CreateWriteIndex.php b/src/Jobs/Stages/CreateWriteIndex.php new file mode 100644 index 00000000..6a7799e3 --- /dev/null +++ b/src/Jobs/Stages/CreateWriteIndex.php @@ -0,0 +1,60 @@ +searchable = $searchable; + $this->index = $index; + } + + public function handle(Client $elasticsearch): void + { + /** @var Searchable $searchable */ + $searchable = $this->searchable; + $this->index->addAlias(new WriteAlias(new DefaultAlias($searchable->searchableAs()))); + + $params = new Create( + $this->index->name(), + $this->index->config() + ); + + $elasticsearch->indices()->create($params->toArray()); + } + + public function title(): string + { + return 'Create write index'; + } + + public function estimate(): int + { + return 1; + } +} diff --git a/src/Jobs/Stages/PullFromSource.php b/src/Jobs/Stages/PullFromSource.php new file mode 100644 index 00000000..be34dfea --- /dev/null +++ b/src/Jobs/Stages/PullFromSource.php @@ -0,0 +1,73 @@ +query = $query; + } + + public function handle(): void + { + $results = $this->query->get(); + $results->filter->shouldBeSearchable()->searchable(); + } + + public function estimate(): int + { + return 1; + } + + public function title(): string + { + return 'Indexing...'; + } + + /** + * @param Model $searchable + * @return Collection + */ + public static function chunked(Model $searchable): Collection + { + /** @var Searchable $searchable */ + $softDelete = config('scout.soft_delete', false); + $query = $searchable->newQuery() + ->when($softDelete, function ($query) { + return $query->withTrashed(); + }) + ->orderBy($searchable->getKeyName()); + $totalSearchables = $query->count(); + if ($totalSearchables) { + $chunkSize = (int) config('scout.chunk.searchable', self::DEFAULT_CHUNK_SIZE); + $totalChunks = (int) ceil($totalSearchables / $chunkSize); + + return collect(range(1, $totalChunks))->map(function ($page) use ($query, $chunkSize) { + $clone = (clone $query)->forPage($page, $chunkSize); + + return new static($clone); + }); + } else { + return collect(); + } + } +} diff --git a/src/Jobs/Stages/RefreshIndex.php b/src/Jobs/Stages/RefreshIndex.php new file mode 100644 index 00000000..285b0919 --- /dev/null +++ b/src/Jobs/Stages/RefreshIndex.php @@ -0,0 +1,43 @@ +index = $index; + } + + public function handle(Client $elasticsearch): void + { + $params = new Refresh($this->index->name()); + $elasticsearch->indices()->refresh($params->toArray()); + } + + public function estimate(): int + { + return 1; + } + + public function title(): string + { + return 'Refreshing index'; + } +} diff --git a/src/Jobs/Stages/SwitchToNewAndRemoveOldIndex.php b/src/Jobs/Stages/SwitchToNewAndRemoveOldIndex.php new file mode 100644 index 00000000..025c7242 --- /dev/null +++ b/src/Jobs/Stages/SwitchToNewAndRemoveOldIndex.php @@ -0,0 +1,62 @@ +searchable = $searchable; + $this->index = $index; + } + + public function handle(Client $elasticsearch): void + { + /** @var Searchable $searchable */ + $searchable = $this->searchable; + $params = Get::anyIndex($searchable->searchableAs()); + $response = $elasticsearch->indices()->getAliases($params->toArray()); + + $params = new Update(); + foreach ($response as $indexName => $alias) { + if ($indexName != $this->index->name()) { + $params->removeIndex($indexName); + } else { + $params->add((string) $indexName, $searchable->searchableAs()); + } + } + $elasticsearch->indices()->updateAliases($params->toArray()); + } + + public function estimate(): int + { + return 1; + } + + public function title(): string + { + return 'Switching to the new index'; + } +} diff --git a/src/Pipelines/ImportPipeline.php b/src/Pipelines/ImportPipeline.php deleted file mode 100644 index a0cbb8de..00000000 --- a/src/Pipelines/ImportPipeline.php +++ /dev/null @@ -1,32 +0,0 @@ -elasticsearch = $elasticsearch; - } - - public function __invoke($payload): array - { - [$index, $source] = $payload; - $params = Get::anyIndex($source->searchableAs()); - try { - $response = $this->elasticsearch->indices()->getAliases($params->toArray()); - } catch (Missing404Exception $e) { - $response = []; - } - foreach ($response as $indexName => $data) { - foreach ($data['aliases'] as $alias => $config) { - if (array_key_exists('is_write_index', $config) && $config['is_write_index']) { - $params = new Delete((string) $indexName); - $this->elasticsearch->indices()->delete($params->toArray()); - continue 2; - } - } - } - - return [$index, $source]; - } -} diff --git a/src/Pipelines/Stages/CreateWriteIndex.php b/src/Pipelines/Stages/CreateWriteIndex.php deleted file mode 100644 index aa287b81..00000000 --- a/src/Pipelines/Stages/CreateWriteIndex.php +++ /dev/null @@ -1,40 +0,0 @@ -elasticsearch = $elasticsearch; - } - - public function __invoke($payload) - { - [$index, $source] = $payload; - - $index->addAlias(new WriteAlias(new DefaultAlias($source->searchableAs()))); - - $params = new Create( - $index->name(), - $index->config() - ); - - $this->elasticsearch->indices()->create($params->toArray()); - - return [$index, $source]; - } -} diff --git a/src/Pipelines/Stages/PullFromSource.php b/src/Pipelines/Stages/PullFromSource.php deleted file mode 100644 index f50b910c..00000000 --- a/src/Pipelines/Stages/PullFromSource.php +++ /dev/null @@ -1,48 +0,0 @@ -elasticsearch = $elasticsearch; - } - - public function __invoke($payload) - { - [$index, $source] = $payload; - - $softDelete = $source::usesSoftDelete() && config('scout.soft_delete', false); - $query = $source->newQuery() - ->when($softDelete, function ($query) { - $query->withTrashed(); - }) - ->orderBy($source->getKeyName()); - $totalSearchables = $query->count(); - if ($totalSearchables) { - $chunkSize = (int) config('scout.chunk.searchable', 500); - $totalChunks = (int) ceil($totalSearchables / $chunkSize); - collect(range(1, $totalChunks))->each(function ($page) use ($query, $chunkSize) { - $results = $query->forPage($page, $chunkSize)->get(); - $countResults = $results->count(); - if ($countResults == 0) { - return false; - } - $results->filter->shouldBeSearchable()->searchable(); - }); - } - - return [$index, $source]; - } -} diff --git a/src/Pipelines/Stages/RefreshIndex.php b/src/Pipelines/Stages/RefreshIndex.php deleted file mode 100644 index b4fa0e15..00000000 --- a/src/Pipelines/Stages/RefreshIndex.php +++ /dev/null @@ -1,32 +0,0 @@ -elasticsearch = $elasticsearch; - } - - public function __invoke($payload) - { - [$index, $source] = $payload; - - $params = new Refresh($index->name()); - $this->elasticsearch->indices()->refresh($params->toArray()); - - return [$index, $source]; - } -} diff --git a/src/Pipelines/Stages/SwitchToNewAndRemoveOldIndex.php b/src/Pipelines/Stages/SwitchToNewAndRemoveOldIndex.php deleted file mode 100644 index 66a45358..00000000 --- a/src/Pipelines/Stages/SwitchToNewAndRemoveOldIndex.php +++ /dev/null @@ -1,43 +0,0 @@ -elasticsearch = $elasticsearch; - } - - public function __invoke($payload) - { - [$index, $source] = $payload; - - $params = Get::anyIndex($source->searchableAs()); - $response = $this->elasticsearch->indices()->getAliases($params->toArray()); - - $params = new Update(); - foreach ($response as $indexName => $alias) { - if ($indexName != $index->name()) { - $params->removeIndex($indexName); - } else { - $params->add((string) $indexName, $source->searchableAs()); - } - } - $this->elasticsearch->indices()->updateAliases($params->toArray()); - - return [$index, $source]; - } -} diff --git a/src/ProgressReportable.php b/src/ProgressReportable.php new file mode 100644 index 00000000..e4edcdb4 --- /dev/null +++ b/src/ProgressReportable.php @@ -0,0 +1,21 @@ +progressBar = $progressBar; + } + + private function progressBar(): ProgressBar + { + return $this->progressBar ?: new ProgressBar(new NullOutput()); + } +} diff --git a/src/ScoutElasticSearchServiceProvider.php b/src/ScoutElasticSearchServiceProvider.php index 0925449a..ff1e3870 100644 --- a/src/ScoutElasticSearchServiceProvider.php +++ b/src/ScoutElasticSearchServiceProvider.php @@ -19,6 +19,8 @@ final class ScoutElasticSearchServiceProvider extends ServiceProvider */ public function boot(): void { + $this->loadTranslationsFrom(__DIR__.'/../resources/lang', 'scout'); + resolve(EngineManager::class)->extend(ElasticSearchEngine::class, function () { $elasticsearch = resolve(Client::class); diff --git a/tests/Feature/ImportCommandTest.php b/tests/Feature/ImportCommandTest.php index ff7e5f0c..ededcff4 100644 --- a/tests/Feature/ImportCommandTest.php +++ b/tests/Feature/ImportCommandTest.php @@ -4,9 +4,12 @@ namespace Tests\Feature; +use App\Book; +use stdClass; use App\Product; use Tests\IntegrationTestCase; use Illuminate\Support\Facades\Artisan; +use Symfony\Component\Console\Output\BufferedOutput; final class ImportCommandTest extends IntegrationTestCase { @@ -28,7 +31,7 @@ public function test_import_entites(): void 'index' => 'products', 'body' => [ 'query' => [ - 'match_all' => new \stdClass(), + 'match_all' => new stdClass(), ], ], ]; @@ -54,7 +57,7 @@ public function test_import_entites_in_queue(): void 'index' => 'products', 'body' => [ 'query' => [ - 'match_all' => new \stdClass(), + 'match_all' => new stdClass(), ], ], ]; @@ -78,7 +81,7 @@ public function test_import_all_pages(): void 'index' => (new Product())->searchableAs(), 'body' => [ 'query' => [ - 'match_all' => new \stdClass(), + 'match_all' => new stdClass(), ], ], ]; @@ -91,7 +94,7 @@ public function test_remove_old_index_after_switching_to_new(): void $params = [ 'index' => 'products_old', 'body' => [ - 'aliases' => ['products' => new \stdClass()], + 'aliases' => ['products' => new stdClass()], 'settings' => [ 'number_of_shards' => 1, 'number_of_replicas' => 0, @@ -112,4 +115,40 @@ public function test_remove_old_index_after_switching_to_new(): void $this->assertFalse($this->elasticsearch->indices()->exists(['index' => 'products_old']), 'Old index must be deleted'); } + + public function test_progress_report() + { + $output = new BufferedOutput(); + Artisan::call('scout:import', [], $output); + + $output = explode("\n", $output->fetch()); + $this->assertEquals( + trans('scout::import.start', ['searchable' => Product::class]), + trim($output[0])); + $this->assertEquals( + '[OK] '.trans('scout::import.done', ['searchable' => Product::class]), + trim($output[2])); + $this->assertEquals( + trans('scout::import.start', ['searchable' => Book::class]), + trim($output[4])); + $this->assertEquals( + '[OK] '.trans('scout::import.done', ['searchable' => Book::class]), + trim($output[6])); + } + + public function test_progress_report_in_queue() + { + $this->app['config']->set('scout.queue', ['connection' => 'sync', 'queue' => 'scout']); + + $output = new BufferedOutput(); + Artisan::call('scout:import', [], $output); + + $output = explode("\n", $output->fetch()); + $this->assertEquals( + trans('scout::import.start', ['searchable' => Product::class]), + trim($output[0])); + $this->assertEquals( + '[OK] '.trans('scout::import.done.queue', ['searchable' => Product::class]), + trim($output[2])); + } } diff --git a/tests/Feature/ScoutElasticSearchServiceProviderTest.php b/tests/Feature/ScoutElasticSearchServiceProviderTest.php index 0e2af790..99d7fb2e 100644 --- a/tests/Feature/ScoutElasticSearchServiceProviderTest.php +++ b/tests/Feature/ScoutElasticSearchServiceProviderTest.php @@ -3,10 +3,11 @@ namespace Matchish\ScoutElasticSearch; use Tests\TestCase; +use Elasticsearch\Client; class ScoutElasticSearchServiceProviderTest extends TestCase { - public function testConfigPublishing() + public function test_config_publishing() { \File::delete(config_path('elasticsearch.php')); $provider = new ElasticSearchServiceProvider($this->app); @@ -18,4 +19,10 @@ public function testConfigPublishing() $this->assertFileExists(config_path('elasticsearch.php')); } + + public function test_provides() + { + $provider = new ElasticSearchServiceProvider($this->app); + $this->assertEquals([Client::class], $provider->provides()); + } } diff --git a/tests/Integration/Jobs/ImportTest.php b/tests/Integration/Jobs/ImportTest.php new file mode 100644 index 00000000..60b55068 --- /dev/null +++ b/tests/Integration/Jobs/ImportTest.php @@ -0,0 +1,41 @@ +create(); + Product::setEventDispatcher($dispatcher); + + $job = new Import(Product::class); + $output = new DummyOutput(); + $outputStyle = new OutputStyle(new ArrayInput([]), $output); + $progressBar = $outputStyle->createProgressBar(); + $progressBar->setFormat('[%message%] %current%/%max%'); + $job->withProgressReport($progressBar); + + dispatch($job); + + $this->assertEquals([ + 'Clean up 1/7', + 'Create write index 2/7', + 'Indexing... 3/7', + 'Indexing... 4/7', + 'Indexing... 5/7', + 'Refreshing index 6/7', + 'Switching to the new index 7/7', + ], $output->getLogs()); + } +} diff --git a/tests/Integration/Pipelines/Stages/CleanUpTest.php b/tests/Integration/Jobs/Stages/CleanUpTest.php similarity index 56% rename from tests/Integration/Pipelines/Stages/CleanUpTest.php rename to tests/Integration/Jobs/Stages/CleanUpTest.php index c9d692ef..ca2c6442 100644 --- a/tests/Integration/Pipelines/Stages/CleanUpTest.php +++ b/tests/Integration/Jobs/Stages/CleanUpTest.php @@ -1,11 +1,11 @@ elasticsearch->indices()->create([ 'index' => 'products_old', - 'body' => ['aliases' => ['products' => new \stdClass()]], + 'body' => ['aliases' => ['products' => new stdClass()]], ]); $this->elasticsearch->indices()->create([ 'index' => 'products_new', @@ -24,22 +24,12 @@ public function test_remove_write_index() 'body' => ['aliases' => ['products' => ['is_write_index' => false]]], ]); - $stage = new CleanUp($this->elasticsearch); - $stage([new Index(new Product(), []), new Product()]); + $stage = new CleanUp(new Product()); + $stage->handle($this->elasticsearch); $writeIndexExist = $this->elasticsearch->indices()->exists(['index' => 'products_new']); $readIndexExist = $this->elasticsearch->indices()->exists(['index' => 'products_old']); $this->assertFalse($writeIndexExist); $this->assertTrue($readIndexExist); } - - public function test_return_same_payload() - { - $stage = new CleanUp($this->elasticsearch); - $payload = [new Index(new Product(), []), new Product()]; - $nextPayload = $stage($payload); - $this->assertEquals(2, count($nextPayload)); - $this->assertSame($payload[0], $nextPayload[0]); - $this->assertSame($payload[1], $nextPayload[1]); - } } diff --git a/tests/Integration/Pipelines/Stages/CreateWriteIndexTest.php b/tests/Integration/Jobs/Stages/CreateWriteIndexTest.php similarity index 79% rename from tests/Integration/Pipelines/Stages/CreateWriteIndexTest.php rename to tests/Integration/Jobs/Stages/CreateWriteIndexTest.php index 37ae1616..0877a117 100644 --- a/tests/Integration/Pipelines/Stages/CreateWriteIndexTest.php +++ b/tests/Integration/Jobs/Stages/CreateWriteIndexTest.php @@ -2,21 +2,21 @@ declare(strict_types=1); -namespace Tests\Integration\Pipelines\Stages; +namespace Tests\Integration\Jobs\Stages; use App\Product; use Elasticsearch\Client; use Tests\IntegrationTestCase; use Matchish\ScoutElasticSearch\ElasticSearch\Index; -use Matchish\ScoutElasticSearch\Pipelines\Stages\CreateWriteIndex; +use Matchish\ScoutElasticSearch\Jobs\Stages\CreateWriteIndex; final class CreateWriteIndexTest extends IntegrationTestCase { public function test_create_write_index(): void { $elasticsearch = $this->app->make(Client::class); - $stage = new CreateWriteIndex($elasticsearch); - $stage([Index::fromSearchable(new Product()), new Product()]); + $stage = new CreateWriteIndex(new Product(), Index::fromSearchable(new Product())); + $stage->handle($elasticsearch); $response = $elasticsearch->indices()->getAliases(['index' => '*', 'name' => 'products']); $this->assertTrue($this->containsWriteIndex($response, 'products')); } diff --git a/tests/Integration/Pipelines/Stages/PullFromSourceTest.php b/tests/Integration/Jobs/Stages/PullFromSourceTest.php similarity index 51% rename from tests/Integration/Pipelines/Stages/PullFromSourceTest.php rename to tests/Integration/Jobs/Stages/PullFromSourceTest.php index 8dec6af4..f54851d6 100644 --- a/tests/Integration/Pipelines/Stages/PullFromSourceTest.php +++ b/tests/Integration/Jobs/Stages/PullFromSourceTest.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Tests\Integration\Pipelines\Stages; +namespace Tests\Integration\Jobs\Stages; +use stdClass; use App\Product; use Tests\IntegrationTestCase; -use Matchish\ScoutElasticSearch\ElasticSearch\Index; -use Matchish\ScoutElasticSearch\Pipelines\Stages\PullFromSource; +use Matchish\ScoutElasticSearch\Jobs\Stages\PullFromSource; final class PullFromSourceTest extends IntegrationTestCase { @@ -23,10 +23,10 @@ public function test_put_all_entites_to_index(): void Product::setEventDispatcher($dispatcher); $this->elasticsearch->indices()->create([ 'index' => 'products_index', - 'body' => ['aliases' => ['products' => new \stdClass()]], + 'body' => ['aliases' => ['products' => new stdClass()]], ]); - $stage = new PullFromSource($this->elasticsearch); - $stage([Index::fromSearchable(new Product()), new Product()]); + $stage = new PullFromSource(Product::query()); + $stage->handle(); $this->elasticsearch->indices()->refresh([ 'index' => 'products', ]); @@ -34,7 +34,7 @@ public function test_put_all_entites_to_index(): void 'index' => 'products', 'body' => [ 'query' => [ - 'match_all' => new \stdClass(), + 'match_all' => new stdClass(), ], ], ]; @@ -46,10 +46,10 @@ public function test_dont_put_entities_if_no_entities_in_collection(): void { $this->elasticsearch->indices()->create([ 'index' => 'products_index', - 'body' => ['aliases' => ['products' => new \stdClass()]], + 'body' => ['aliases' => ['products' => new stdClass()]], ]); - $stage = new PullFromSource($this->elasticsearch); - $stage([Index::fromSearchable(new Product()), new Product()]); + $stage = new PullFromSource(Product::query()); + $stage->handle(); $this->elasticsearch->indices()->refresh([ 'index' => 'products', ]); @@ -57,7 +57,7 @@ public function test_dont_put_entities_if_no_entities_in_collection(): void 'index' => 'products', 'body' => [ 'query' => [ - 'match_all' => new \stdClass(), + 'match_all' => new stdClass(), ], ], ]; @@ -78,10 +78,10 @@ public function test_put_all_to_index_if_amount_of_entities_more_than_chunk_size Product::setEventDispatcher($dispatcher); $this->elasticsearch->indices()->create([ 'index' => 'products_index', - 'body' => ['aliases' => ['products' => new \stdClass()]], + 'body' => ['aliases' => ['products' => new stdClass()]], ]); - $stage = new PullFromSource($this->elasticsearch); - $stage([Index::fromSearchable(new Product()), new Product()]); + $stage = new PullFromSource(Product::query()); + $stage->handle(); $this->elasticsearch->indices()->refresh([ 'index' => 'products', ]); @@ -89,7 +89,7 @@ public function test_put_all_to_index_if_amount_of_entities_more_than_chunk_size 'index' => 'products', 'body' => [ 'query' => [ - 'match_all' => new \stdClass(), + 'match_all' => new stdClass(), ], ], ]; @@ -97,7 +97,7 @@ public function test_put_all_to_index_if_amount_of_entities_more_than_chunk_size $this->assertEquals($productsAmount, $response['hits']['total']); } - public function test_push_soft_delete_meta_data() + public function test_pull_soft_delete_meta_data() { $this->app['config']['scout.soft_delete'] = true; @@ -111,10 +111,10 @@ public function test_push_soft_delete_meta_data() Product::setEventDispatcher($dispatcher); $this->elasticsearch->indices()->create([ 'index' => 'products_index', - 'body' => ['aliases' => ['products' => new \stdClass()]], + 'body' => ['aliases' => ['products' => new stdClass()]], ]); - $stage = new PullFromSource($this->elasticsearch); - $stage([Index::fromSearchable(new Product()), new Product()]); + $stage = new PullFromSource(Product::query()); + $stage->handle(); $this->elasticsearch->indices()->refresh([ 'index' => 'products', ]); @@ -122,11 +122,82 @@ public function test_push_soft_delete_meta_data() 'index' => 'products', 'body' => [ 'query' => [ - 'match_all' => new \stdClass(), + 'match_all' => new stdClass(), ], ], ]; $response = $this->elasticsearch->search($params); $this->assertEquals(0, $response['hits']['hits'][0]['_source']['__soft_deleted']); } + + public function test_pull_soft_deleted() + { + $this->app['config']['scout.soft_delete'] = true; + + $dispatcher = Product::getEventDispatcher(); + Product::unsetEventDispatcher(); + + $productsAmount = 3; + + factory(Product::class, $productsAmount)->create(); + + Product::limit(1)->get()->first()->delete(); + + Product::setEventDispatcher($dispatcher); + $this->elasticsearch->indices()->create([ + 'index' => 'products_index', + 'body' => ['aliases' => ['products' => new stdClass()]], + ]); + $stages = PullFromSource::chunked(new Product()); + $stages->first()->handle(); + $this->elasticsearch->indices()->refresh([ + 'index' => 'products', + ]); + $params = [ + 'index' => 'products', + 'body' => [ + 'query' => [ + 'match_all' => new stdClass(), + ], + ], + ]; + $response = $this->elasticsearch->search($params); + $this->assertEquals(3, $response['hits']['total']); + } + + public function test_no_searchables_no_chunks() + { + $stages = PullFromSource::chunked(new Product()); + + $this->assertEquals(0, $stages->count()); + } + + public function test_chunked_pull_only_one_page() + { + $dispatcher = Product::getEventDispatcher(); + Product::unsetEventDispatcher(); + + $productsAmount = 5; + + factory(Product::class, $productsAmount)->create(); + + Product::setEventDispatcher($dispatcher); + + $chunks = PullFromSource::chunked(new Product()); + $chunks->first()->handle(); + $this->elasticsearch->indices()->refresh([ + 'index' => 'products', + ]); + $params = [ + 'index' => 'products', + 'body' => [ + 'query' => [ + 'match_all' => new stdClass(), + ], + ], + ]; + $response = $this->elasticsearch->search($params); + + $this->assertEquals(3, $response['hits']['total']); + } } diff --git a/tests/Integration/Pipelines/Stages/RefreshIndexTest.php b/tests/Integration/Jobs/Stages/RefreshIndexTest.php similarity index 71% rename from tests/Integration/Pipelines/Stages/RefreshIndexTest.php rename to tests/Integration/Jobs/Stages/RefreshIndexTest.php index e18c3349..6d32c0b3 100644 --- a/tests/Integration/Pipelines/Stages/RefreshIndexTest.php +++ b/tests/Integration/Jobs/Stages/RefreshIndexTest.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Tests\Integration\Pipelines\Stages; +namespace Tests\Integration\Jobs\Stages; -use App\Product; +use stdClass; use Tests\IntegrationTestCase; use Matchish\ScoutElasticSearch\ElasticSearch\Index; -use Matchish\ScoutElasticSearch\Pipelines\Stages\RefreshIndex; +use Matchish\ScoutElasticSearch\Jobs\Stages\RefreshIndex; final class RefreshIndexTest extends IntegrationTestCase { @@ -15,7 +15,7 @@ public function test_refresh_index(): void { $this->elasticsearch->indices()->create([ 'index' => 'products_index', - 'body' => ['aliases' => ['products' => new \stdClass()]], + 'body' => ['aliases' => ['products' => new stdClass()]], ]); $this->elasticsearch->bulk(['body' => [ ['index' => [ @@ -29,14 +29,14 @@ public function test_refresh_index(): void ], ], ]); - $stage = new RefreshIndex($this->elasticsearch); - $stage([new Index('products_index'), new Product()]); + $stage = new RefreshIndex(new Index('products_index')); + $stage->handle($this->elasticsearch); $params = [ 'index' => 'products', 'body' => [ 'query' => [ - 'match_all' => new \stdClass(), + 'match_all' => new stdClass(), ], ], ]; diff --git a/tests/Integration/Pipelines/Stages/SwitchToNewAndRemoveOldIndexTest.php b/tests/Integration/Jobs/Stages/SwitchToNewAndRemoveOldIndexTest.php similarity index 75% rename from tests/Integration/Pipelines/Stages/SwitchToNewAndRemoveOldIndexTest.php rename to tests/Integration/Jobs/Stages/SwitchToNewAndRemoveOldIndexTest.php index f1f0e7b7..f608f2ec 100644 --- a/tests/Integration/Pipelines/Stages/SwitchToNewAndRemoveOldIndexTest.php +++ b/tests/Integration/Jobs/Stages/SwitchToNewAndRemoveOldIndexTest.php @@ -2,12 +2,13 @@ declare(strict_types=1); -namespace Tests\Integration\Pipelines\Stages; +namespace Tests\Integration\Jobs\Stages; +use stdClass; use App\Product; use Tests\IntegrationTestCase; use Matchish\ScoutElasticSearch\ElasticSearch\Index; -use Matchish\ScoutElasticSearch\Pipelines\Stages\SwitchToNewAndRemoveOldIndex; +use Matchish\ScoutElasticSearch\Jobs\Stages\SwitchToNewAndRemoveOldIndex; final class SwitchToNewAndRemoveOldIndexTest extends IntegrationTestCase { @@ -19,11 +20,11 @@ public function test_switch_to_new_and_remove_old_index(): void ]); $this->elasticsearch->indices()->create([ 'index' => 'products_old', - 'body' => ['aliases' => ['products' => new \stdClass()]], + 'body' => ['aliases' => ['products' => new stdClass()]], ]); - $stage = new SwitchToNewAndRemoveOldIndex($this->elasticsearch); - $stage([new Index('products_new'), new Product()]); + $stage = new SwitchToNewAndRemoveOldIndex(new Product(), new Index('products_new')); + $stage->handle($this->elasticsearch); $newIndexExist = $this->elasticsearch->indices()->exists(['index' => 'products_new']); $oldIndexExist = $this->elasticsearch->indices()->exists(['index' => 'products_old']); diff --git a/tests/Unit/Jobs/ImportStagesTest.php b/tests/Unit/Jobs/ImportStagesTest.php new file mode 100644 index 00000000..764f5c84 --- /dev/null +++ b/tests/Unit/Jobs/ImportStagesTest.php @@ -0,0 +1,36 @@ +assertEquals(0, $stages->count()); + } + + public function test_stages() + { + factory(Product::class, 10)->create(); + $stages = ImportStages::fromSearchable(new Product()); + $this->assertEquals(8, $stages->count()); + $this->assertInstanceOf(CleanUp::class, $stages->get(0)); + $this->assertInstanceOf(CreateWriteIndex::class, $stages->get(1)); + $this->assertInstanceOf(PullFromSource::class, $stages->get(2)); + $this->assertInstanceOf(PullFromSource::class, $stages->get(3)); + $this->assertInstanceOf(PullFromSource::class, $stages->get(4)); + $this->assertInstanceOf(PullFromSource::class, $stages->get(5)); + $this->assertInstanceOf(RefreshIndex::class, $stages->get(6)); + $this->assertInstanceOf(SwitchToNewAndRemoveOldIndex::class, $stages->get(7)); + } +} diff --git a/tests/Unit/Pipelines/ImportPipelineTest.php b/tests/Unit/Pipelines/ImportPipelineTest.php deleted file mode 100644 index 66a6d819..00000000 --- a/tests/Unit/Pipelines/ImportPipelineTest.php +++ /dev/null @@ -1,55 +0,0 @@ -process(new \stdClass()); - $this->assertEquals(5, count($processor->stages)); - $this->assertInstanceOf(CleanUp::class, $processor->stages[0]); - $this->assertInstanceOf(CreateWriteIndex::class, $processor->stages[1]); - $this->assertInstanceOf(PullFromSource::class, $processor->stages[2]); - $this->assertInstanceOf(RefreshIndex::class, $processor->stages[3]); - $this->assertInstanceOf(SwitchToNewAndRemoveOldIndex::class, $processor->stages[4]); - } -} - -class Processor implements ProcessorInterface -{ - public $stages; - - /** - * Process the payload using multiple stages. - * - * @param mixed $payload - * - * @return mixed - */ - public function process($payload, callable ...$stages) - { - $this->stages = $stages; - - return $payload; - } -} diff --git a/tests/laravel/app/Book.php b/tests/laravel/app/Book.php new file mode 100644 index 00000000..aee0cbf8 --- /dev/null +++ b/tests/laravel/app/Book.php @@ -0,0 +1,28 @@ +getKeyName()); + } + + public function getScoutKey() + { + return $this->getAttribute($this->getScoutKeyName()); + } +} diff --git a/tests/laravel/database/factories/BookFactory.php b/tests/laravel/database/factories/BookFactory.php new file mode 100644 index 00000000..af23a933 --- /dev/null +++ b/tests/laravel/database/factories/BookFactory.php @@ -0,0 +1,15 @@ +define(Book::class, function (Faker $faker) { + return [ + 'custom_key' => $faker->uuid, + 'title' => $faker->text, + 'author' => $faker->name, + 'year' => $faker->year, + ]; +}); diff --git a/tests/laravel/database/migrations/2019_15_02_000002_create_books_table.php b/tests/laravel/database/migrations/2019_15_02_000002_create_books_table.php new file mode 100644 index 00000000..ccf193f0 --- /dev/null +++ b/tests/laravel/database/migrations/2019_15_02_000002_create_books_table.php @@ -0,0 +1,28 @@ +increments('id'); + $table->string('custom_key'); + $table->string('title'); + $table->string('author'); + $table->integer('year'); + $table->softDeletes(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('books'); + } +}