Skip to content

Commit

Permalink
Merge branch 'master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeroen-G authored Sep 15, 2023
2 parents 41369f8 + 1fdab85 commit f81123f
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 18 deletions.
3 changes: 3 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions docs/index-aliases.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
5 changes: 0 additions & 5 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/Application/Results.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
}

Expand Down
16 changes: 11 additions & 5 deletions src/Infrastructure/Console/ElasticUpdate.php
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -19,24 +21,26 @@ 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;
}

private function updateIndex(
IndexConfigurationInterface $indexConfiguration,
IndexAdapterInterface $indexAdapter
IndexAdapterInterface $indexAdapter,
Dispatcher $dispatcher,
): void {
if ($indexConfiguration instanceof AliasedIndexConfiguration) {
$indexAdapter->createNewWriteIndex($indexConfiguration);
Expand All @@ -51,6 +55,8 @@ private function updateIndex(
}
}

$indexAdapter->pointToAlias($indexConfiguration);
$dispatcher->dispatch(
UpdateIndexAlias::createFor($indexConfiguration)
);
}
}
39 changes: 39 additions & 0 deletions src/Infrastructure/IndexManagement/Job/UpdateIndexAlias.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php declare(strict_types=1);

namespace JeroenG\Explorer\Infrastructure\IndexManagement\Job;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use JeroenG\Explorer\Application\IndexAdapterInterface;
use JeroenG\Explorer\Domain\IndexManagement\IndexConfigurationInterface;
use JeroenG\Explorer\Domain\IndexManagement\IndexConfigurationRepositoryInterface;

final class UpdateIndexAlias implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

private function __construct(public string $index)
{
}

public static function createFor(IndexConfigurationInterface $indexConfiguration): self
{
$modelClassName = $indexConfiguration->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);
}
}
11 changes: 7 additions & 4 deletions src/Infrastructure/Scout/ScoutSearchCommandBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,7 +34,7 @@ class ScoutSearchCommandBuilder implements SearchCommandInterface

private ?string $minimumShouldMatch = null;

/** @var Sort[] */
/** @var SyntaxInterface[] */
private array $sort = [];

private array $aggregations = [];
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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);
}
}
23 changes: 23 additions & 0 deletions tests/Support/Models/SyncableModel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace JeroenG\Explorer\Tests\Support\Models;

use Illuminate\Database\Eloquent\Model;
use JeroenG\Explorer\Application\Aliased;
use JeroenG\Explorer\Application\Explored;
use Laravel\Scout\Searchable;

class SyncableModel extends Model
{
public function syncWithSearchUsingQueue(): string
{
return ':queue:';
}

public function syncWithSearchUsing(): string
{
return ':connection:';
}
}
82 changes: 82 additions & 0 deletions tests/Unit/FinderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use JeroenG\Explorer\Infrastructure\Scout\ScoutSearchCommandBuilder;
use Mockery;
use Mockery\Adapter\Phpunit\MockeryTestCase;
use JeroenG\Explorer\Domain\Aggregations\NestedAggregation;

class FinderTest extends MockeryTestCase
{
Expand Down Expand Up @@ -362,6 +363,87 @@ public function test_it_adds_aggregates(): void
self::assertEquals('myKey', $specificAggregationValue['key']);
}

public function test_it_adds_nested_aggregations(): void
{
$client = Mockery::mock(Client::class);
$client->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 [
Expand Down
60 changes: 60 additions & 0 deletions tests/Unit/IndexManagement/Job/UpdateIndexAliasTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace JeroenG\Explorer\Tests\Unit\IndexManagement\Job;

use JeroenG\Explorer\Application\IndexAdapterInterface;
use JeroenG\Explorer\Domain\IndexManagement\DirectIndexConfiguration;
use JeroenG\Explorer\Domain\IndexManagement\IndexConfigurationRepositoryInterface;
use JeroenG\Explorer\Infrastructure\IndexManagement\Job\UpdateIndexAlias;
use JeroenG\Explorer\Tests\Support\Models\SyncableModel;
use Mockery;
use Mockery\Adapter\Phpunit\MockeryTestCase;
use PHPUnit\Framework\Assert;

final class UpdateIndexAliasTest extends MockeryTestCase
{
public function testJobCreation(): void
{
$config = DirectIndexConfiguration::create(
name: ':index:',
properties: [],
settings: [],
model: SyncableModel::class,
);

$subject = UpdateIndexAlias::createFor($config);

Assert::assertSame(':queue:', $subject->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);

}
}
8 changes: 4 additions & 4 deletions tests/Unit/ScoutSearchCommandBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
}
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit f81123f

Please sign in to comment.