diff --git a/changelog.md b/changelog.md index 8d4c7847..10c6a669 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added +- Updating the index alias is now done through a (queueable) job. + ## [3.7.0] ### Added diff --git a/docs/index-aliases.md b/docs/index-aliases.md index 338d4f6e..f5d3c873 100644 --- a/docs/index-aliases.md +++ b/docs/index-aliases.md @@ -54,3 +54,11 @@ return [ Be aware that if you currently already have indices and would like to move to using aliases you will need to delete those indices before configuring the aliases. In Elasticsearch a given name can only be either an index or alias, not both and this cannot be changed on-the-fly. + +### Note on updating aliases +When you update a model, Laravel Scouts will update the index. +When you use index aliases, a new index is created and the alias is being pointed to the nex one. +What you don't want is for the alias to be pointing to the new index before Elasticsearch is done with indexing all documents. +To prevent this, the alias update is done in a job that is dispatched to the queue. +If there is no queue it will still be done in the background, but it will be done synchronously. +This could still be enough of a "delay" for Elasticsearch to finish indexing, so there is no immediate need to set up a queue. diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 324b9a21..ff74d5f9 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -10,11 +10,6 @@ parameters: count: 1 path: src/Infrastructure/Console/ElasticSearch.php - - - message: "#^Argument of an invalid type JeroenG\\\\Explorer\\\\Domain\\\\IndexManagement\\\\IndexConfigurationInterface supplied for foreach, only iterables are supported\\.$#" - count: 1 - path: src/Infrastructure/Console/ElasticUpdate.php - - message: "#^Call to an undefined method JeroenG\\\\Explorer\\\\Domain\\\\IndexManagement\\\\IndexConfigurationInterface\\:\\:getAliasConfiguration\\(\\)\\.$#" count: 1 diff --git a/src/Application/Results.php b/src/Application/Results.php index 3eda4280..7d2d8b45 100644 --- a/src/Application/Results.php +++ b/src/Application/Results.php @@ -30,6 +30,15 @@ public function aggregations(): array $aggregations = []; foreach ($this->rawResults['aggregations'] as $name => $rawAggregation) { + if (array_key_exists('doc_count', $rawAggregation)) { + foreach ($rawAggregation as $nestedAggregationName => $rawNestedAggregation) { + if (isset($rawNestedAggregation['buckets'])) { + $aggregations[] = new AggregationResult($nestedAggregationName, $rawNestedAggregation['buckets']); + } + } + continue; + } + $aggregations[] = new AggregationResult($name, $rawAggregation['buckets']); } diff --git a/src/Infrastructure/Console/ElasticUpdate.php b/src/Infrastructure/Console/ElasticUpdate.php old mode 100644 new mode 100755 index 2b8ac17e..4df8032d --- a/src/Infrastructure/Console/ElasticUpdate.php +++ b/src/Infrastructure/Console/ElasticUpdate.php @@ -4,12 +4,14 @@ namespace JeroenG\Explorer\Infrastructure\Console; +use Illuminate\Bus\Dispatcher; use Illuminate\Console\Command; use Illuminate\Support\Facades\Artisan; use JeroenG\Explorer\Application\IndexAdapterInterface; use JeroenG\Explorer\Domain\IndexManagement\AliasedIndexConfiguration; use JeroenG\Explorer\Domain\IndexManagement\IndexConfigurationInterface; use JeroenG\Explorer\Domain\IndexManagement\IndexConfigurationRepositoryInterface; +use JeroenG\Explorer\Infrastructure\IndexManagement\Job\UpdateIndexAlias; final class ElasticUpdate extends Command { @@ -19,16 +21,17 @@ final class ElasticUpdate extends Command public function handle( IndexAdapterInterface $indexAdapter, - IndexConfigurationRepositoryInterface $indexConfigurationRepository + IndexConfigurationRepositoryInterface $indexConfigurationRepository, + Dispatcher $dispatcher ): int { $index = $this->argument('index'); - /** @var IndexConfigurationInterface $allConfigs */ + /** @var IndexConfigurationInterface[] $allConfigs */ $allConfigs = is_null($index) ? $indexConfigurationRepository->getConfigurations() : [$indexConfigurationRepository->findForIndex($index)]; foreach ($allConfigs as $config) { - $this->updateIndex($config, $indexAdapter); + $this->updateIndex($config, $indexAdapter, $dispatcher); } return 0; @@ -36,7 +39,8 @@ public function handle( private function updateIndex( IndexConfigurationInterface $indexConfiguration, - IndexAdapterInterface $indexAdapter + IndexAdapterInterface $indexAdapter, + Dispatcher $dispatcher, ): void { if ($indexConfiguration instanceof AliasedIndexConfiguration) { $indexAdapter->createNewWriteIndex($indexConfiguration); @@ -51,6 +55,8 @@ private function updateIndex( } } - $indexAdapter->pointToAlias($indexConfiguration); + $dispatcher->dispatch( + UpdateIndexAlias::createFor($indexConfiguration) + ); } } diff --git a/src/Infrastructure/IndexManagement/Job/UpdateIndexAlias.php b/src/Infrastructure/IndexManagement/Job/UpdateIndexAlias.php new file mode 100755 index 00000000..e3ab73ef --- /dev/null +++ b/src/Infrastructure/IndexManagement/Job/UpdateIndexAlias.php @@ -0,0 +1,39 @@ +getModel(); + $model = new $modelClassName(); + + return (new self($indexConfiguration->getName())) + ->onQueue($model->syncWithSearchUsingQueue()) + ->onConnection($model->syncWithSearchUsing()); + } + + public function handle( + IndexAdapterInterface $indexAdapter, + IndexConfigurationRepositoryInterface $indexConfigurationRepository + ): void { + $indexConfiguration = $indexConfigurationRepository->findForIndex($this->index); + $indexAdapter->pointToAlias($indexConfiguration); + } +} \ No newline at end of file diff --git a/src/Infrastructure/Scout/ScoutSearchCommandBuilder.php b/src/Infrastructure/Scout/ScoutSearchCommandBuilder.php index 5611450f..8c29493f 100644 --- a/src/Infrastructure/Scout/ScoutSearchCommandBuilder.php +++ b/src/Infrastructure/Scout/ScoutSearchCommandBuilder.php @@ -12,6 +12,7 @@ use JeroenG\Explorer\Domain\Syntax\Compound\QueryType; use JeroenG\Explorer\Domain\Syntax\MultiMatch; use JeroenG\Explorer\Domain\Syntax\Sort; +use JeroenG\Explorer\Domain\Syntax\SyntaxInterface; use JeroenG\Explorer\Domain\Syntax\Term; use JeroenG\Explorer\Domain\Syntax\Terms; use Laravel\Scout\Builder; @@ -33,7 +34,7 @@ class ScoutSearchCommandBuilder implements SearchCommandInterface private ?string $minimumShouldMatch = null; - /** @var Sort[] */ + /** @var SyntaxInterface[] */ private array $sort = []; private array $aggregations = []; @@ -207,7 +208,7 @@ public function setLimit(?int $limit): void public function setSort(array $sort): void { - Assert::allIsInstanceOf($sort, Sort::class); + Assert::allIsInstanceOf($sort, SyntaxInterface::class); $this->sort = $sort; } @@ -304,9 +305,11 @@ public function getOffset(): ?int return $this->offset; } - /** @return Sort[] */ + /** @return SyntaxInterface[] */ private static function getSorts(Builder $builder): array { - return array_map(static fn ($order) => new Sort($order['column'], $order['direction']), $builder->orders); + return array_map(static function($order) { + return $order instanceof SyntaxInterface ? $order : new Sort($order['column'], $order['direction']); + }, $builder->orders); } } diff --git a/tests/Support/Models/SyncableModel.php b/tests/Support/Models/SyncableModel.php new file mode 100644 index 00000000..d682b70a --- /dev/null +++ b/tests/Support/Models/SyncableModel.php @@ -0,0 +1,23 @@ +expects('search') + ->with([ + 'index' => self::TEST_INDEX, + 'body' => [ + 'query' => [ + 'bool' => [ + 'must' => [], + 'should' => [], + 'filter' => [], + ], + ], + 'aggs' => [ + 'nestedAggregation' => [ + 'nested' => [ + 'path' => 'nestedAggregation', + ], + 'aggs' => [ + 'someField' => [ + 'terms' => [ + 'field' => 'nestedAggregation.someField', + 'size' => 10, + ], + ], + ], + ], + 'anotherAggregation' => ['terms' => ['field' => 'anotherField', 'size' => 10]] + ], + ], + ]) + ->andReturn([ + 'hits' => [ + 'total' => ['value' => 1], + 'hits' => [$this->hit()], + ], + 'aggregations' => [ + 'nestedAggregation' => [ + 'doc_count' => 42, + 'someField' => [ + 'doc_count_error_upper_bound' => 0, + 'sum_other_doc_count' => 0, + 'buckets' => [ + ['key' => 'someKey', 'doc_count' => 6,] + ], + ], + ], + 'specificAggregation' => [ + 'buckets' => [ + ['key' => 'myKey', 'doc_count' => 42] + ] + ], + ], + ]); + + $query = Query::with(new BoolQuery()); + $query->addAggregation('anotherAggregation', new TermsAggregation('anotherField')); + $nestedAggregation = new NestedAggregation('nestedAggregation'); + $nestedAggregation->add('someField', new TermsAggregation('nestedAggregation.someField')); + $query->addAggregation('nestedAggregation',$nestedAggregation); + $builder = new SearchCommand(self::TEST_INDEX, $query); + $builder->setIndex(self::TEST_INDEX); + + $subject = new Finder($client, $builder); + $results = $subject->find(); + + self::assertCount(2, $results->aggregations()); + + $nestedAggregation = $results->aggregations()[0]; + + self::assertInstanceOf(AggregationResult::class, $nestedAggregation); + self::assertEquals('someField', $nestedAggregation->name()); + self::assertCount(1, $nestedAggregation->values()); + + $nestedAggregationValue = $nestedAggregation->values()[0]; + + self::assertEquals(6, $nestedAggregationValue['doc_count']); + self::assertEquals('someKey', $nestedAggregationValue['key']); + } + private function hit(int $id = 1, float $score = 1.0): array { return [ diff --git a/tests/Unit/IndexManagement/Job/UpdateIndexAliasTest.php b/tests/Unit/IndexManagement/Job/UpdateIndexAliasTest.php new file mode 100644 index 00000000..8f180de9 --- /dev/null +++ b/tests/Unit/IndexManagement/Job/UpdateIndexAliasTest.php @@ -0,0 +1,60 @@ +queue); + Assert::assertSame(':connection:', $subject->connection); + Assert::assertSame(':index:', $subject->index); + + } + + public function testHandleCallsPointToIndex(): void + { + $adapter = Mockery::mock(IndexAdapterInterface::class); + $repository = Mockery::mock(IndexConfigurationRepositoryInterface::class); + + $config = DirectIndexConfiguration::create( + name: ':index:', + properties: [], + settings: [], + model: SyncableModel::class, + ); + + $repository + ->expects('findForIndex') + ->with(':index:') + ->andReturn($config); + + $adapter + ->expects('pointToAlias') + ->with($config); + + UpdateIndexAlias::createFor($config) + ->handle($adapter, $repository); + + } +} diff --git a/tests/Unit/ScoutSearchCommandBuilderTest.php b/tests/Unit/ScoutSearchCommandBuilderTest.php index 3e78f3da..93d843e1 100644 --- a/tests/Unit/ScoutSearchCommandBuilderTest.php +++ b/tests/Unit/ScoutSearchCommandBuilderTest.php @@ -140,12 +140,12 @@ public function test_it_can_set_the_sort_order(): void $command->setSort([new Sort('id', 'invalid')]); } - public function test_it_only_accepts_sort_classes(): void + public function test_it_only_accepts_syntax_interface_classes(): void { $command = new ScoutSearchCommandBuilder(); $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Expected an instance of JeroenG\Explorer\Domain\Syntax\Sort. Got: string'); + $this->expectExceptionMessage('Expected an instance of JeroenG\Explorer\Domain\Syntax\SyntaxInterface. Got: string'); $command->setSort(['not' => 'a class']); } @@ -174,11 +174,11 @@ public function test_it_can_get_the_sorting_from_the_scout_builder(): void $builder->model = Mockery::mock(Model::class); $builder->index = self::TEST_INDEX; - $builder->orders = [[ 'column' => 'id', 'direction' => 'asc']]; + $builder->orders = [['column' => 'id', 'direction' => 'asc'], new Sort('name')]; $subject = ScoutSearchCommandBuilder::wrap($builder); - self::assertSame([['id' => 'asc']], $subject->getSort()); + self::assertSame([['id' => 'asc'], ['name' => 'asc']], $subject->getSort()); } public function test_it_can_get_the_fields_from_scout_builder(): void