From 5db5669fb658abd3ed839efbda5abd2fd39f7cff Mon Sep 17 00:00:00 2001 From: tvercruysse Date: Wed, 10 Jun 2020 19:44:27 +0200 Subject: [PATCH 01/10] replace php-sonic with psonic --- .gitignore | 1 + composer.json | 6 +- src/Engines/SonicSearchEngine.php | 55 +++--- src/Providers/SonicScoutServiceProvider.php | 8 +- tests/SonicEngineTest.php | 185 ++------------------ 5 files changed, 48 insertions(+), 207 deletions(-) diff --git a/.gitignore b/.gitignore index 6c1fbd6..fad0db1 100644 --- a/.gitignore +++ b/.gitignore @@ -22,5 +22,6 @@ Homestead.json .phpunit.result.cache composer.lock +.idea/ # End of https://www.gitignore.io/api/laravel diff --git a/composer.json b/composer.json index 1280626..fb21a4d 100644 --- a/composer.json +++ b/composer.json @@ -9,10 +9,10 @@ } ], "require": { - "php": ">=7.0", + "php": ">=7.1", "laravel/scout": "^7.1|^8.0", - "php-sonic/php-sonic": "^2.0", - "laravel/helpers": "^1.0" + "laravel/helpers": "^1.0", + "ppshobi/psonic": "^1.2" }, "autoload": { "psr-4": { diff --git a/src/Engines/SonicSearchEngine.php b/src/Engines/SonicSearchEngine.php index 0b4ad1d..4123008 100644 --- a/src/Engines/SonicSearchEngine.php +++ b/src/Engines/SonicSearchEngine.php @@ -3,51 +3,53 @@ namespace james2doyle\SonicScout\Engines; use Laravel\Scout\Builder; -use Illuminate\Database\Eloquent\SoftDeletes; use Laravel\Scout\Engines\Engine; -use SonicSearch\ChannelFactory; +use Psonic\Client; +use Psonic\Control; +use Psonic\Ingest; +use Psonic\Search; class SonicSearchEngine extends Engine { /** * The Sonic search client. * - * @var \SonicSearch\SearchChannel + * @var Search */ protected $search; /** * The Sonic index/push client. * - * @var \SonicSearch\IngestChannel + * @var Ingest */ protected $ingest; /** * The Sonic index/push client. * - * @var \SonicSearch\ControlChannel + * @var Control */ protected $control; /** * Create a new engine instance. * - * @throws \SonicSearch\NoConnectionException If connecting to the sonic instance failed. - * @throws \SonicSearch\AuthenticationException If the given password was wrong. - * @throws \SonicSearch\ProtocolException If Sonic misbehaved or announced an unsupported protocol version. - * - * @return void + * @param string $host + * @param int $port + * @param string $password + * @param $timeout + * @throws \Psonic\Exceptions\ConnectionException */ - public function __construct(ChannelFactory $factory) + public function __construct(string $host = 'localhost', int $port = 1491, string $password = 'secretPassword', int $timeout = 30) { - $this->ingest = $factory->newIngestChannel(); - $this->search = $factory->newSearchChannel(); - $this->control = $factory->newControlChannel(); + $this->ingest = new Ingest(new Client($host, $port, $timeout)); + $this->search = new Search(new Client($host, $port, $timeout)); + $this->control = new Control(new Client($host, $port, $timeout)); - $this->ingest->connect(); - $this->search->connect(); - $this->control->connect(); + $this->ingest->connect($password); + $this->search->connect($password); + $this->control->connect($password); } /** @@ -55,18 +57,14 @@ public function __construct(ChannelFactory $factory) */ public function __destruct() { - $this->ingest->quit(); - $this->search->quit(); - $this->control->quit(); + $this->ingest->disconnect(); + $this->search->disconnect(); + $this->control->disconnect(); } - /** * Update the given model in the index. * * @param \Illuminate\Database\Eloquent\Collection $models - * @throws \SonicSearch\NoConnectionException If the connection to Sonic has been lost in the meantime. - * @throws \SonicSearch\CommandFailedException If execution of the command failed for which-ever reason. - * @throws \SonicSearch\InvalidArgumentException If the given set of terms could not fit into Sonics receive buffer. * * @return void */ @@ -107,9 +105,6 @@ public function update($models) * Remove the given model from the index. * * @param \Illuminate\Database\Eloquent\Collection|\Illuminate\Support\Collection $models - * @throws \SonicSearch\NoConnectionException If the connection to Sonic has been lost in the meantime. - * @throws \SonicSearch\CommandFailedException If execution of the command failed for which-ever reason. - * * @return void */ public function delete($models) @@ -118,17 +113,13 @@ public function delete($models) $models->map(function ($model) { $bucket = class_basename($model); - $this->ingest->flush(str_plural($bucket), $bucket, $model->getScoutKey()); + $this->ingest->flusho(str_plural($bucket), $bucket, $model->getScoutKey()); })->values()->all(); } /** * Perform the given search on the engine. * - * @param \Laravel\Scout\Builder $builder - * @throws \SonicSearch\NoConnectionException If the connection to Sonic has been lost in the meantime. - * @throws \SonicSearch\CommandFailedException If execution of the command failed for which-ever reason. - * * @return array */ private function performSearch(Builder $builder, int $limit = null, int $offset = null) diff --git a/src/Providers/SonicScoutServiceProvider.php b/src/Providers/SonicScoutServiceProvider.php index 5ffc298..78202a5 100644 --- a/src/Providers/SonicScoutServiceProvider.php +++ b/src/Providers/SonicScoutServiceProvider.php @@ -15,15 +15,11 @@ class SonicScoutServiceProvider extends ServiceProvider public function boot() { $this->app->make(EngineManager::class)->extend('sonic', function () { - $factory = new ChannelFactory( + return new SonicSearchEngine( \config('scout.sonic.address'), \config('scout.sonic.port'), \config('scout.sonic.password'), - \config('scout.sonic.connection_timeout'), - \config('scout.sonic.read_timeout') - ); - - return new SonicSearchEngine($factory); + \config('scout.sonic.connection_timeout')); }); } } diff --git a/tests/SonicEngineTest.php b/tests/SonicEngineTest.php index 643de8a..5afc2c3 100644 --- a/tests/SonicEngineTest.php +++ b/tests/SonicEngineTest.php @@ -1,189 +1,66 @@ shouldReceive('connect')->withNoArgs()->once(); - $ingest->shouldReceive('quit')->withNoArgs()->once(); - $search = Mockery::mock(SearchChannel::class); - $search->shouldReceive('connect')->withNoArgs()->once(); - $search->shouldReceive('quit')->withNoArgs()->once(); + /** + * @var SonicSearchEngine|null + */ + private $engine; - $control = Mockery::mock(ControlChannel::class); - $control->shouldReceive('connect')->withNoArgs()->once(); - $control->shouldReceive('quit')->withNoArgs()->once(); - - return [ - 'ingest' => $ingest, - 'search' => $search, - 'control' => $control, - ]; + protected function setUp(): void + { + $this->engine = new SonicSearchEngine(); } - /** @test */ - public function itCanCreateTheClient() + public function itCanInitiateThesearchEngine() { - $mocks = $this->mockFactory(); - - $mocks['ingest']->shouldReceive('ping')->withNoArgs()->once(); - $mocks['ingest']->shouldReceive('push')->once(); - $mocks['control']->shouldReceive('consolidate')->withNoArgs()->once(); - - $factory = Mockery::mock(ChannelFactory::class, [ - 'newIngestChannel' => $mocks['ingest'], - 'newSearchChannel' => $mocks['search'], - 'newControlChannel' => $mocks['control'], - ]); - - $engine = new SonicSearchEngine($factory); - $engine->update(Collection::make([new SearchableModel(['id' => 1])])); + $this->assertInstanceOf(SonicSearchEngine::class, $this->engine); } - /** @test */ - public function itCanAddObjectsToTheIndex() + public function itCanPushObjectsToTheIndex() { - $mocks = $this->mockFactory(); - - $mocks['ingest']->shouldReceive('ping')->withNoArgs()->once(); - $mocks['ingest']->shouldReceive('push')->withArgs(function () { - $args = func_get_args(); - $expected = [ - 'SearchableModels', - 'SearchableModel', - '1', - '1 searchable model', - ]; - - return $args == $expected; - }); - - $mocks['control']->shouldReceive('consolidate')->withNoArgs()->once(); - - $factory = Mockery::mock(ChannelFactory::class, [ - 'newIngestChannel' => $mocks['ingest'], - 'newSearchChannel' => $mocks['search'], - 'newControlChannel' => $mocks['control'], - ]); - - $engine = new SonicSearchEngine($factory); - $engine->update(Collection::make([new SearchableModel(['id' => 1])])); + $this->engine->update(Collection::make([new SearchableModel(['id' => 1])])); } /** @test */ public function itCanDeleteObjectsFromTheIndex() { - $mocks = $this->mockFactory(); - - $mocks['ingest']->shouldReceive('ping')->withNoArgs()->once(); - $mocks['ingest']->shouldReceive('flush')->withArgs(function () { - $args = func_get_args(); - $expected = [ - 'SearchableModels', - 'SearchableModel', - '1', - ]; - - return $args == $expected; - }); - - $factory = Mockery::mock(ChannelFactory::class, [ - 'newIngestChannel' => $mocks['ingest'], - 'newSearchChannel' => $mocks['search'], - 'newControlChannel' => $mocks['control'], - ]); - - $engine = new SonicSearchEngine($factory); - $engine->delete(Collection::make([new SearchableModel(['id' => 1])])); + $this->engine->delete(Collection::make([new SearchableModel(['id' => 1])])); } /** @test */ public function itCanSearchTheIndex() { - $mocks = $this->mockFactory(); - - $mocks['search']->shouldReceive('ping')->withNoArgs()->once(); - - $mocks['search']->shouldReceive('query')->withArgs(function () { - $args = func_get_args(); - $expected = [ - 'SearchableModels', - 'SearchableModel', - 'searchable', - null, - null, - ]; - - return $args == $expected; - }); - - $factory = Mockery::mock(ChannelFactory::class, [ - 'newIngestChannel' => $mocks['ingest'], - 'newSearchChannel' => $mocks['search'], - 'newControlChannel' => $mocks['control'], - ]); - - $engine = new SonicSearchEngine($factory); - $builder = new Builder(new SearchableModel, 'searchable'); - $engine->search($builder); + $this->engine->search($builder); } /** @test */ public function itCanMapCorrectlyToTheModels() { - $mocks = $this->mockFactory(); - - $factory = Mockery::mock(ChannelFactory::class, [ - 'newIngestChannel' => $mocks['ingest'], - 'newSearchChannel' => $mocks['search'], - 'newControlChannel' => $mocks['control'], - ]); - - $engine = new SonicSearchEngine($factory); - $model = Mockery::mock(stdClass::class); $model->shouldReceive('getScoutModelsByIds')->andReturn($models = Collection::make([ new SearchableModel(['id' => 1]), ])); $builder = Mockery::mock(Builder::class); - $results = $engine->map($builder, [1], $model); - $this->assertEquals(1, count($results)); + $results = $this->engine->map($builder, [1], $model); + $this->assertCount(1, $results); } /** @test */ public function itCanMapCorrectlyToTheModelsWhenFiltered() { - $mocks = $this->mockFactory(); - - $factory = Mockery::mock(ChannelFactory::class, [ - 'newIngestChannel' => $mocks['ingest'], - 'newSearchChannel' => $mocks['search'], - 'newControlChannel' => $mocks['control'], - ]); - - $engine = new SonicSearchEngine($factory); - $model = Mockery::mock(stdClass::class); $model->shouldReceive('getScoutModelsByIds')->andReturn($models = Collection::make([ new SearchableModel(['id' => 1]), @@ -194,41 +71,17 @@ public function itCanMapCorrectlyToTheModelsWhenFiltered() $builder = Mockery::mock(Builder::class); $builder->wheres = ['id' => 1]; - $results = $engine->map($builder, [1, 2, 3, 4], $model); - $this->assertEquals(1, count($results)); + $results = $this->engine->map($builder, [1, 2, 3, 4], $model); + $this->assertCount(1, $results); } /** @test */ public function itCanHandleDefaultSearchableArray() { - $mocks = $this->mockFactory(); - - $mocks['ingest']->shouldReceive('ping')->withNoArgs()->once(); - $mocks['ingest']->shouldReceive('push')->withArgs(function () { - $args = func_get_args(); - $expected = [ - str_plural($args[0]), // inject mockery class details - $args[1], // inject mockery class details - '1', - '1 hello@example.com' - ]; - - return $args == $expected; - }); - - $mocks['control']->shouldReceive('consolidate')->withNoArgs()->once(); - - $factory = Mockery::mock(ChannelFactory::class, [ - 'newIngestChannel' => $mocks['ingest'], - 'newSearchChannel' => $mocks['search'], - 'newControlChannel' => $mocks['control'], - ]); - $model = Mockery::mock(stdClass::class); $model->shouldReceive('getScoutKey')->andReturn(1); $model->shouldReceive('toSearchableArray')->andReturn(['id' => 1, 'email' => 'hello@example.com']); - $engine = new SonicSearchEngine($factory); - $engine->update(Collection::make([$model])); + $this->engine->update(Collection::make([$model])); } } From a1168e9cafb063d2be31c5c3912503a42e891d33 Mon Sep 17 00:00:00 2001 From: tvercruysse Date: Sat, 13 Jun 2020 21:24:55 +0200 Subject: [PATCH 02/10] refactor engine constructor and fix tests --- src/Engines/SonicSearchEngine.php | 15 +-- src/Providers/SonicScoutServiceProvider.php | 22 +++- tests/SonicEngineTest.php | 138 ++++++++++++++++---- 3 files changed, 137 insertions(+), 38 deletions(-) diff --git a/src/Engines/SonicSearchEngine.php b/src/Engines/SonicSearchEngine.php index 4123008..1357247 100644 --- a/src/Engines/SonicSearchEngine.php +++ b/src/Engines/SonicSearchEngine.php @@ -4,7 +4,6 @@ use Laravel\Scout\Builder; use Laravel\Scout\Engines\Engine; -use Psonic\Client; use Psonic\Control; use Psonic\Ingest; use Psonic\Search; @@ -35,17 +34,17 @@ class SonicSearchEngine extends Engine /** * Create a new engine instance. * - * @param string $host - * @param int $port + * @param Ingest $ingest + * @param Search $search + * @param Control $control * @param string $password - * @param $timeout * @throws \Psonic\Exceptions\ConnectionException */ - public function __construct(string $host = 'localhost', int $port = 1491, string $password = 'secretPassword', int $timeout = 30) + public function __construct(Ingest $ingest, Search $search, Control $control, string $password = 'secretPassword') { - $this->ingest = new Ingest(new Client($host, $port, $timeout)); - $this->search = new Search(new Client($host, $port, $timeout)); - $this->control = new Control(new Client($host, $port, $timeout)); + $this->ingest = $ingest; + $this->search = $search; + $this->control = $control; $this->ingest->connect($password); $this->search->connect($password); diff --git a/src/Providers/SonicScoutServiceProvider.php b/src/Providers/SonicScoutServiceProvider.php index 78202a5..136c6c0 100644 --- a/src/Providers/SonicScoutServiceProvider.php +++ b/src/Providers/SonicScoutServiceProvider.php @@ -5,7 +5,10 @@ use Illuminate\Support\ServiceProvider; use james2doyle\SonicScout\Engines\SonicSearchEngine; use Laravel\Scout\EngineManager; -use SonicSearch\ChannelFactory; +use Psonic\Client; +use Psonic\Control; +use Psonic\Ingest; +use Psonic\Search; class SonicScoutServiceProvider extends ServiceProvider { @@ -16,10 +19,19 @@ public function boot() { $this->app->make(EngineManager::class)->extend('sonic', function () { return new SonicSearchEngine( - \config('scout.sonic.address'), - \config('scout.sonic.port'), - \config('scout.sonic.password'), - \config('scout.sonic.connection_timeout')); + new Ingest($this->generateClient()), + new Search($this->generateClient()), + new Control($this->generateClient()), + \config('scout.sonic.password') + ); }); } + + private function generateClient(): Client { + return new Client( + \config('scout.sonic.address'), + \config('scout.sonic.port'), + \config('scout.sonic.connection_timeout') + ); + } } diff --git a/tests/SonicEngineTest.php b/tests/SonicEngineTest.php index 5afc2c3..40c49eb 100644 --- a/tests/SonicEngineTest.php +++ b/tests/SonicEngineTest.php @@ -2,6 +2,9 @@ namespace james2doyle\SonicScout\Tests; +use Psonic\Control; +use Psonic\Ingest; +use Psonic\Search; use stdClass; use Mockery; use Laravel\Scout\Builder; @@ -13,55 +16,129 @@ class SonicEngineTest extends TestCase { - /** - * @var SonicSearchEngine|null - */ - private $engine; - - protected function setUp(): void + protected function tearDown(): void { - $this->engine = new SonicSearchEngine(); + Mockery::close(); } - /** @test */ - public function itCanInitiateThesearchEngine() + + protected function mockChannels(): array { - $this->assertInstanceOf(SonicSearchEngine::class, $this->engine); + $ingest = Mockery::mock(Ingest::class); + $ingest->shouldReceive('connect')->withAnyArgs()->once(); + $ingest->shouldReceive('disconnect')->withNoArgs()->once(); + + $search = Mockery::mock(Search::class); + $search->shouldReceive('connect')->withAnyArgs()->once(); + $search->shouldReceive('disconnect')->withNoArgs()->once(); + + $control = Mockery::mock(Control::class); + $control->shouldReceive('connect')->withAnyArgs()->once(); + $control->shouldReceive('disconnect')->withNoArgs()->once(); + + return compact('ingest', 'search', 'control'); } - /** @test */ - public function itCanPushObjectsToTheIndex() + + public function testItCanPushObjectsToTheIndex() { - $this->engine->update(Collection::make([new SearchableModel(['id' => 1])])); + /** + * @var Search|Mockery\MockInterface $search + * @var Ingest|Mockery\MockInterface $ingest + * @var Control|Mockery\MockInterface $control + */ + extract($this->mockChannels()); + + $ingest->shouldReceive('ping')->withNoArgs()->once(); + $ingest->shouldReceive('push')->once(); + $control->shouldReceive('consolidate')->withNoArgs()->once(); + + $engine = new SonicSearchEngine($ingest, $search, $control); + $engine->update(Collection::make([new SearchableModel(['id' => 1])])); } - /** @test */ - public function itCanDeleteObjectsFromTheIndex() + public function testItCanDeleteObjectsFromTheIndex() { - $this->engine->delete(Collection::make([new SearchableModel(['id' => 1])])); + /** + * @var Search|Mockery\MockInterface $search + * @var Ingest|Mockery\MockInterface $ingest + * @var Control|Mockery\MockInterface $control + */ + extract($this->mockChannels()); + $ingest->shouldReceive('ping')->withNoArgs()->once(); + $ingest->shouldReceive('flusho')->withArgs(function () { + $args = func_get_args(); + $expected = [ + 'SearchableModels', + 'SearchableModel', + 1, + ]; + return $args === $expected; + }); + + $engine = new SonicSearchEngine($ingest, $search, $control); + $engine->delete(Collection::make([new SearchableModel(['id' => 1])])); } /** @test */ - public function itCanSearchTheIndex() + public function testItCanSearchTheIndex() { + /** + * @var Search|Mockery\MockInterface $search + * @var Ingest|Mockery\MockInterface $ingest + * @var Control|Mockery\MockInterface $control + */ + extract($this->mockChannels()); + $search->shouldReceive('ping')->withNoArgs()->once(); + + $search->shouldReceive('query')->withArgs(function () { + $args = func_get_args(); + $expected = [ + 'SearchableModels', + 'SearchableModel', + 'searchable', + null, + null, + ]; + + return $args === $expected; + }); + + $engine = new SonicSearchEngine($ingest, $search, $control); $builder = new Builder(new SearchableModel, 'searchable'); - $this->engine->search($builder); + $engine->search($builder); } /** @test */ - public function itCanMapCorrectlyToTheModels() + public function testItCanMapCorrectlyToTheModels() { + /** + * @var Search|Mockery\MockInterface $search + * @var Ingest|Mockery\MockInterface $ingest + * @var Control|Mockery\MockInterface $control + */ + extract($this->mockChannels()); + + $engine = new SonicSearchEngine($ingest, $search, $control); $model = Mockery::mock(stdClass::class); $model->shouldReceive('getScoutModelsByIds')->andReturn($models = Collection::make([ new SearchableModel(['id' => 1]), ])); $builder = Mockery::mock(Builder::class); - $results = $this->engine->map($builder, [1], $model); + $results = $engine->map($builder, [1], $model); $this->assertCount(1, $results); } /** @test */ - public function itCanMapCorrectlyToTheModelsWhenFiltered() + public function testItCanMapCorrectlyToTheModelsWhenFiltered() { + /** + * @var Search|Mockery\MockInterface $search + * @var Ingest|Mockery\MockInterface $ingest + * @var Control|Mockery\MockInterface $control + */ + extract($this->mockChannels()); $model = Mockery::mock(stdClass::class); + + $engine = new SonicSearchEngine($ingest, $search, $control); $model->shouldReceive('getScoutModelsByIds')->andReturn($models = Collection::make([ new SearchableModel(['id' => 1]), new SearchableModel(['id' => 2]), @@ -71,17 +148,28 @@ public function itCanMapCorrectlyToTheModelsWhenFiltered() $builder = Mockery::mock(Builder::class); $builder->wheres = ['id' => 1]; - $results = $this->engine->map($builder, [1, 2, 3, 4], $model); + $results = $engine->map($builder, [1, 2, 3, 4], $model); $this->assertCount(1, $results); } /** @test */ - public function itCanHandleDefaultSearchableArray() + public function testItCanHandleDefaultSearchableArray() { + /** + * @var Search|Mockery\MockInterface $search + * @var Ingest|Mockery\MockInterface $ingest + * @var Control|Mockery\MockInterface $control + */ + extract($this->mockChannels()); + + $ingest->shouldReceive('ping')->withNoArgs()->once(); + $ingest->shouldReceive('push')->once(); + $control->shouldReceive('consolidate')->withNoArgs()->once(); + $model = Mockery::mock(stdClass::class); $model->shouldReceive('getScoutKey')->andReturn(1); $model->shouldReceive('toSearchableArray')->andReturn(['id' => 1, 'email' => 'hello@example.com']); - - $this->engine->update(Collection::make([$model])); + $engine = new SonicSearchEngine($ingest, $search, $control); + $engine->update(Collection::make([$model])); } } From d9d994cc47a8c5549d7c0ee3a0516b1dbcb878c0 Mon Sep 17 00:00:00 2001 From: tvercruysse Date: Sun, 14 Jun 2020 12:48:55 +0200 Subject: [PATCH 03/10] remove laravel helpers from required packages --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index fb21a4d..b549ad0 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,6 @@ "require": { "php": ">=7.1", "laravel/scout": "^7.1|^8.0", - "laravel/helpers": "^1.0", "ppshobi/psonic": "^1.2" }, "autoload": { From 78f220a5ff6e0a65260ab7ee2ccc30aca45070d3 Mon Sep 17 00:00:00 2001 From: tvercruysse Date: Sun, 14 Jun 2020 13:05:03 +0200 Subject: [PATCH 04/10] Revert "remove laravel helpers from required packages" This reverts commit d9d994cc47a8c5549d7c0ee3a0516b1dbcb878c0. --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index b549ad0..fb21a4d 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,7 @@ "require": { "php": ">=7.1", "laravel/scout": "^7.1|^8.0", + "laravel/helpers": "^1.0", "ppshobi/psonic": "^1.2" }, "autoload": { From d7f3c1303474b9000ecf2462dd0f86409c17e6a5 Mon Sep 17 00:00:00 2001 From: tvercruysse Date: Wed, 19 Aug 2020 14:29:00 +0200 Subject: [PATCH 05/10] fix null result handling --- src/Engines/SonicSearchEngine.php | 4 ++-- tests/SonicEngineTest.php | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/Engines/SonicSearchEngine.php b/src/Engines/SonicSearchEngine.php index 1357247..a1ff646 100644 --- a/src/Engines/SonicSearchEngine.php +++ b/src/Engines/SonicSearchEngine.php @@ -175,7 +175,7 @@ public function mapIds($results) */ public function map(Builder $builder, $results, $model) { - if (count($results) === 0) { + if (count($results) === 1 && empty(reset($results))) { return $model->newCollection(); } @@ -193,7 +193,7 @@ public function map(Builder $builder, $results, $model) return $result->sortBy(function($model) use ($objectIdPositions) { return $objectIdPositions[$model->getScoutKey()]; - });; + }); } /** diff --git a/tests/SonicEngineTest.php b/tests/SonicEngineTest.php index 40c49eb..d070809 100644 --- a/tests/SonicEngineTest.php +++ b/tests/SonicEngineTest.php @@ -172,4 +172,25 @@ public function testItCanHandleDefaultSearchableArray() $engine = new SonicSearchEngine($ingest, $search, $control); $engine->update(Collection::make([$model])); } + + public function testItCanHandleAnEmptyResultset() + { + /** + * @var Search|Mockery\MockInterface $search + * @var Ingest|Mockery\MockInterface $ingest + * @var Control|Mockery\MockInterface $control + */ + extract($this->mockChannels()); + $model = Mockery::mock(stdClass::class); + + $engine = new SonicSearchEngine($ingest, $search, $control); + $model->shouldReceive('newCollection')->andReturn($models = Collection::make([ + new Collection() + ])); + + $builder = Mockery::mock(Builder::class); + $builder->wheres = ['id' => 1]; + $results = $engine->map($builder, [0 => ""], $model); + $this->assertEmpty($results->first()); + } } From 7ec4a9fa7bf6e291bc43ce228b1a4e68acdc7a5f Mon Sep 17 00:00:00 2001 From: Philip Manavopoulos Date: Wed, 14 Oct 2020 19:44:54 +0100 Subject: [PATCH 06/10] Fix tests --- tests/SonicEngineTest.php | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/tests/SonicEngineTest.php b/tests/SonicEngineTest.php index 0596f09..2d241c9 100644 --- a/tests/SonicEngineTest.php +++ b/tests/SonicEngineTest.php @@ -111,11 +111,15 @@ public function testItCanSearchTheIndex() /** @test */ public function testItCanSearchTheIndexWithTakeLimit() { - $mocks = $this->mockFactory(); - - $mocks['search']->shouldReceive('ping')->withNoArgs()->once(); + /** + * @var Search|Mockery\MockInterface $search + * @var Ingest|Mockery\MockInterface $ingest + * @var Control|Mockery\MockInterface $control + */ + extract($this->mockChannels()); + $search->shouldReceive('ping')->withNoArgs()->once(); - $mocks['search']->shouldReceive('query')->withArgs(function () { + $search->shouldReceive('query')->withArgs(function () { $args = func_get_args(); $expected = [ 'SearchableModels', @@ -125,17 +129,10 @@ public function testItCanSearchTheIndexWithTakeLimit() null, ]; - return $args == $expected; + return $args === $expected; }); - $factory = Mockery::mock(ChannelFactory::class, [ - 'newIngestChannel' => $mocks['ingest'], - 'newSearchChannel' => $mocks['search'], - 'newControlChannel' => $mocks['control'], - ]); - - $engine = new SonicSearchEngine($factory); - + $engine = new SonicSearchEngine($ingest, $search, $control); $builder = (new Builder(new SearchableModel, 'searchable'))->take(3); $engine->search($builder); } @@ -202,6 +199,7 @@ public function testItCanHandleDefaultSearchableArray() $model = Mockery::mock(stdClass::class); $model->shouldReceive('getScoutKey')->andReturn(1); $model->shouldReceive('toSearchableArray')->andReturn(['id' => 1, 'email' => 'hello@example.com']); + $model->shouldReceive('searchableAs')->andReturn('SearchableModel'); $engine = new SonicSearchEngine($ingest, $search, $control); $engine->update(Collection::make([$model])); } From 27aaf60ef6f275738805284aec0c035e78a60965 Mon Sep 17 00:00:00 2001 From: Philip Manavopoulos Date: Wed, 14 Oct 2020 19:45:21 +0100 Subject: [PATCH 07/10] Remove laravel/helpers --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index fb21a4d..b549ad0 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,6 @@ "require": { "php": ">=7.1", "laravel/scout": "^7.1|^8.0", - "laravel/helpers": "^1.0", "ppshobi/psonic": "^1.2" }, "autoload": { From 2644ea4ea6f0be5c798487c7334513212d45bb62 Mon Sep 17 00:00:00 2001 From: Philip Manavopoulos Date: Wed, 14 Oct 2020 20:01:15 +0100 Subject: [PATCH 08/10] Try and add Github Action to run tests automatically --- .github/workflows/tests.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..937bc47 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,17 @@ +name: Tests + +on: + push: + pull_request: + +jobs: + tests: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Install Dependencies + run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist --optimize-autoloader + - name: Execute tests + run: vendor/bin/phpunit From 1ffcca9c83a4f2c51f699ce9a7499a2f4fba7f5a Mon Sep 17 00:00:00 2001 From: Philip Manavopoulos Date: Wed, 14 Oct 2020 20:02:27 +0100 Subject: [PATCH 09/10] Remove github actions --- .github/workflows/tests.yml | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index 937bc47..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Tests - -on: - push: - pull_request: - -jobs: - tests: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Install Dependencies - run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist --optimize-autoloader - - name: Execute tests - run: vendor/bin/phpunit From 8566e4887e0a0bd45b957d0b94481e7b3de509e8 Mon Sep 17 00:00:00 2001 From: Philip Manavopoulos Date: Fri, 16 Oct 2020 11:46:37 +0100 Subject: [PATCH 10/10] Add support for passing locale to Sonic --- README.md | 10 +++++++ src/Engines/SonicSearchEngine.php | 11 ++++++- tests/Fixtures/SearchableModelWithLocale.php | 15 ++++++++++ tests/SonicEngineTest.php | 30 ++++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 tests/Fixtures/SearchableModelWithLocale.php diff --git a/README.md b/README.md index 7e2c276..57182aa 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,16 @@ public function toSearchableArray() For me, this builds a nice string for search that can match on a "name". In my application, the concept of "name" is either the User display name or first/last name. +--- + +If the locale is known, you can also create the `getSonicLocale()` method on your model, which returns the locale. It will then get passed to the Sonic `PUSH` calls: + +```php +// an ISO 639-3 locale code eg. eng for English (if set, the locale must be a valid ISO 639-3 code; if set to none, lexing will be disabled; if not set, the locale will be guessed from text) +public function getSonicLocale() { + return 'none'; +} +``` Installation
------------ diff --git a/src/Engines/SonicSearchEngine.php b/src/Engines/SonicSearchEngine.php index 589b1dd..b00ed6f 100644 --- a/src/Engines/SonicSearchEngine.php +++ b/src/Engines/SonicSearchEngine.php @@ -87,12 +87,21 @@ public function update($models) $collection = $self->getCollectionFromModel($model); $bucket = $self->getBucketFromModel($model); - return [ + $message = [ $collection, $bucket, $model->getScoutKey(), is_array($searchableData) ? implode(' ', array_values($searchableData)) : $searchableData, ]; + + if (method_exists($model, 'getSonicLocale')) { + $locale = $model->getSonicLocale(); + if ($locale) { + $message[] = $locale; + } + } + + return $message; })->filter()->all(); if (! empty($messages)) { diff --git a/tests/Fixtures/SearchableModelWithLocale.php b/tests/Fixtures/SearchableModelWithLocale.php new file mode 100644 index 0000000..af773f4 --- /dev/null +++ b/tests/Fixtures/SearchableModelWithLocale.php @@ -0,0 +1,15 @@ +update(Collection::make([new SearchableModel(['id' => 1])])); } + /** @test */ + public function itCanAddObjectsToTheIndexWithLocale() + { + /** + * @var Search|Mockery\MockInterface $search + * @var Ingest|Mockery\MockInterface $ingest + * @var Control|Mockery\MockInterface $control + */ + extract($this->mockChannels()); + + $ingest->shouldReceive('ping')->withNoArgs()->once(); + $ingest->shouldReceive('push')->withArgs(function () { + $args = func_get_args(); + $expected = [ + 'SearchableModels', + 'SearchableModel', + '1', + '1 searchable model', + 'none' + ]; + + return $args == $expected; + }); + $control->shouldReceive('consolidate')->withNoArgs()->once(); + + $engine = new SonicSearchEngine($ingest, $search, $control); + $engine->update(Collection::make([new SearchableModelWithLocale(['id' => 1])])); + } + public function testItCanDeleteObjectsFromTheIndex() { /**