Skip to content

Commit 19d77a4

Browse files
[9.x] Fix custom scout keys not being utilized when deleting from queue (#656)
* Add getUnqualifiedScoutKeyName method * Add RemoveableScoutCollection tests * Change order of update indexing so a custom key cannot be overridden and resolve deleting with custom key * Return a RemoveableScoutCollection instance and fix key type when scout key is a string * Add `RemoveFromSearch` meilisearch tests * Flush the container on tear down * Remove unneeded tearDown * Update algolia engine to delete using custom keys * Add Algolia deletion tests with custom keys * Rename "$values" to "$keys" for clarity * Spacing * Remove unused imports * CS fixes * Comment clarification * Move scout key array merge priority * Update RemoveFromSearch.php Co-authored-by: Taylor Otwell <taylor@laravel.com>
1 parent 3352fbc commit 19d77a4

File tree

8 files changed

+227
-42
lines changed

8 files changed

+227
-42
lines changed

src/Builder.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use Illuminate\Container\Container;
66
use Illuminate\Pagination\LengthAwarePaginator;
77
use Illuminate\Pagination\Paginator;
8-
use Illuminate\Support\Str;
98
use Illuminate\Support\Traits\Macroable;
109
use Laravel\Scout\Contracts\PaginatesEloquentModels;
1110

@@ -444,7 +443,7 @@ protected function getTotalCount($results)
444443

445444
$ids = $engine->mapIdsFrom(
446445
$results,
447-
Str::afterLast($this->model->getScoutKeyName(), '.')
446+
$this->model->getUnqualifiedScoutKeyName()
448447
)->all();
449448

450449
if (count($ids) < $totalCount) {

src/Engines/AlgoliaEngine.php

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Illuminate\Database\Eloquent\SoftDeletes;
88
use Illuminate\Support\LazyCollection;
99
use Laravel\Scout\Builder;
10+
use Laravel\Scout\Jobs\RemoveableScoutCollection;
1011

1112
class AlgoliaEngine extends Engine
1213
{
@@ -63,9 +64,9 @@ public function update($models)
6364
}
6465

6566
return array_merge(
66-
['objectID' => $model->getScoutKey()],
6767
$searchableData,
68-
$model->scoutMetadata()
68+
$model->scoutMetadata(),
69+
['objectID' => $model->getScoutKey()],
6970
);
7071
})->filter()->values()->all();
7172

@@ -82,13 +83,17 @@ public function update($models)
8283
*/
8384
public function delete($models)
8485
{
86+
if ($models->isEmpty()) {
87+
return;
88+
}
89+
8590
$index = $this->algolia->initIndex($models->first()->searchableAs());
8691

87-
$index->deleteObjects(
88-
$models->map(function ($model) {
89-
return $model->getScoutKey();
90-
})->values()->all()
91-
);
92+
$keys = $models instanceof RemoveableScoutCollection
93+
? $models->pluck($models->first()->getUnqualifiedScoutKeyName())
94+
: $models->map->getScoutKey();
95+
96+
$index->deleteObjects($keys->all());
9297
}
9398

9499
/**

src/Engines/MeiliSearchEngine.php

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
namespace Laravel\Scout\Engines;
44

55
use Illuminate\Support\LazyCollection;
6-
use Illuminate\Support\Str;
76
use Laravel\Scout\Builder;
7+
use Laravel\Scout\Jobs\RemoveableScoutCollection;
88
use MeiliSearch\Client as MeiliSearchClient;
99
use MeiliSearch\MeiliSearch;
1010
use MeiliSearch\Search\SearchResult;
@@ -64,9 +64,9 @@ public function update($models)
6464
}
6565

6666
return array_merge(
67-
[$model->getKeyName() => $model->getScoutKey()],
6867
$searchableData,
69-
$model->scoutMetadata()
68+
$model->scoutMetadata(),
69+
[$model->getKeyName() => $model->getScoutKey()],
7070
);
7171
})->filter()->values()->all();
7272

@@ -83,13 +83,17 @@ public function update($models)
8383
*/
8484
public function delete($models)
8585
{
86+
if ($models->isEmpty()) {
87+
return;
88+
}
89+
8690
$index = $this->meilisearch->index($models->first()->searchableAs());
8791

88-
$index->deleteDocuments(
89-
$models->map->getScoutKey()
90-
->values()
91-
->all()
92-
);
92+
$keys = $models instanceof RemoveableScoutCollection
93+
? $models->pluck($models->first()->getUnqualifiedScoutKeyName())
94+
: $models->map->getScoutKey();
95+
96+
$index->deleteDocuments($keys->all());
9397
}
9498

9599
/**
@@ -244,7 +248,7 @@ public function mapIdsFrom($results, $key)
244248
*/
245249
public function keys(Builder $builder)
246250
{
247-
$scoutKey = Str::afterLast($builder->model->getScoutKeyName(), '.');
251+
$scoutKey = $builder->model->getUnqualifiedScoutKeyName();
248252

249253
return $this->mapIdsFrom($this->search($builder), $scoutKey);
250254
}

src/Jobs/RemoveFromSearch.php

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44

55
use Illuminate\Bus\Queueable;
66
use Illuminate\Contracts\Queue\ShouldQueue;
7-
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
87
use Illuminate\Queue\SerializesModels;
9-
use Illuminate\Support\Str;
108

119
class RemoveFromSearch implements ShouldQueue
1210
{
@@ -15,7 +13,7 @@ class RemoveFromSearch implements ShouldQueue
1513
/**
1614
* The models to be removed from the search index.
1715
*
18-
* @var \Illuminate\Database\Eloquent\Collection
16+
* @var \Laravel\Scout\Jobs\RemoveableScoutCollection
1917
*/
2018
public $models;
2119

@@ -46,35 +44,24 @@ public function handle()
4644
* Restore a queueable collection instance.
4745
*
4846
* @param \Illuminate\Contracts\Database\ModelIdentifier $value
49-
* @return \Illuminate\Database\Eloquent\Collection
47+
* @return \Laravel\Scout\Jobs\RemoveableScoutCollection
5048
*/
5149
protected function restoreCollection($value)
5250
{
5351
if (! $value->class || count($value->id) === 0) {
54-
return new EloquentCollection;
52+
return new RemoveableScoutCollection;
5553
}
5654

57-
return new EloquentCollection(
55+
return new RemoveableScoutCollection(
5856
collect($value->id)->map(function ($id) use ($value) {
5957
return tap(new $value->class, function ($model) use ($id) {
60-
$keyName = $this->getUnqualifiedScoutKeyName(
61-
$model->getScoutKeyName()
62-
);
63-
64-
$model->forceFill([$keyName => $id]);
58+
$model->setKeyType(
59+
is_string($id) ? 'string' : 'int'
60+
)->forceFill([
61+
$model->getUnqualifiedScoutKeyName() => $id,
62+
]);
6563
});
6664
})
6765
);
6866
}
69-
70-
/**
71-
* Get the unqualified Scout key name.
72-
*
73-
* @param string $keyName
74-
* @return string
75-
*/
76-
protected function getUnqualifiedScoutKeyName($keyName)
77-
{
78-
return Str::afterLast($keyName, '.');
79-
}
8067
}

src/Searchable.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Illuminate\Database\Eloquent\SoftDeletes;
66
use Illuminate\Support\Collection as BaseCollection;
7+
use Illuminate\Support\Str;
78

89
trait Searchable
910
{
@@ -389,6 +390,16 @@ public function getScoutKeyName()
389390
return $this->getQualifiedKeyName();
390391
}
391392

393+
/**
394+
* Get the unqualified Scout key name.
395+
*
396+
* @return string
397+
*/
398+
public function getUnqualifiedScoutKeyName()
399+
{
400+
return Str::afterLast($this->getScoutKeyName(), '.');
401+
}
402+
392403
/**
393404
* Determine if the current class should use soft deletes with searching.
394405
*

tests/Unit/AlgoliaEngineTest.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
namespace Laravel\Scout\Tests\Unit;
44

55
use Algolia\AlgoliaSearch\SearchClient;
6+
use Illuminate\Container\Container;
67
use Illuminate\Database\Eloquent\Collection;
78
use Illuminate\Support\Facades\Config;
89
use Illuminate\Support\LazyCollection;
910
use Laravel\Scout\Builder;
11+
use Laravel\Scout\EngineManager;
1012
use Laravel\Scout\Engines\AlgoliaEngine;
13+
use Laravel\Scout\Jobs\RemoveFromSearch;
1114
use Laravel\Scout\Tests\Fixtures\EmptySearchableModel;
1215
use Laravel\Scout\Tests\Fixtures\SearchableModel;
1316
use Laravel\Scout\Tests\Fixtures\SoftDeletedEmptySearchableModel;
@@ -25,6 +28,7 @@ protected function setUp(): void
2528

2629
protected function tearDown(): void
2730
{
31+
Container::getInstance()->flush();
2832
m::close();
2933
}
3034

@@ -51,6 +55,59 @@ public function test_delete_removes_objects_to_index()
5155
$engine->delete(Collection::make([new SearchableModel(['id' => 1])]));
5256
}
5357

58+
public function test_delete_removes_objects_to_index_with_a_custom_search_key()
59+
{
60+
$client = m::mock(SearchClient::class);
61+
$client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(Indexes::class));
62+
$index->shouldReceive('deleteObjects')->once()->with(['my-algolia-key.5']);
63+
64+
$engine = new AlgoliaEngine($client);
65+
$engine->delete(Collection::make([new AlgoliaCustomKeySearchableModel(['id' => 5])]));
66+
}
67+
68+
public function test_delete_with_removeable_scout_collection_using_custom_search_key()
69+
{
70+
$job = new RemoveFromSearch(Collection::make([
71+
new AlgoliaCustomKeySearchableModel(['id' => 5]),
72+
]));
73+
74+
$job = unserialize(serialize($job));
75+
76+
$client = m::mock(SearchClient::class);
77+
$client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class));
78+
$index->shouldReceive('deleteObjects')->once()->with(['my-algolia-key.5']);
79+
80+
$engine = new AlgoliaEngine($client);
81+
$engine->delete($job->models);
82+
}
83+
84+
public function test_remove_from_search_job_uses_custom_search_key()
85+
{
86+
$job = new RemoveFromSearch(Collection::make([
87+
new AlgoliaCustomKeySearchableModel(['id' => 5]),
88+
]));
89+
90+
$job = unserialize(serialize($job));
91+
92+
Container::getInstance()->bind(EngineManager::class, function () {
93+
$engine = m::mock(AlgoliaEngine::class);
94+
95+
$engine->shouldReceive('delete')->once()->with(m::on(function ($collection) {
96+
$keyName = ($model = $collection->first())->getUnqualifiedScoutKeyName();
97+
98+
return $model->getAttributes()[$keyName] === 'my-algolia-key.5';
99+
}));
100+
101+
$manager = m::mock(EngineManager::class);
102+
103+
$manager->shouldReceive('engine')->andReturn($engine);
104+
105+
return $manager;
106+
});
107+
108+
$job->handle();
109+
}
110+
54111
public function test_search_sends_correct_parameters_to_algolia()
55112
{
56113
$client = m::mock(SearchClient::class);

tests/Unit/MeiliSearchEngineTest.php

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22

33
namespace Laravel\Scout\Tests\Unit;
44

5+
use Illuminate\Container\Container;
56
use Illuminate\Database\Eloquent\Collection;
7+
use Illuminate\Support\Facades\Config;
68
use Illuminate\Support\LazyCollection;
79
use Laravel\Scout\Builder;
10+
use Laravel\Scout\EngineManager;
811
use Laravel\Scout\Engines\MeiliSearchEngine;
12+
use Laravel\Scout\Jobs\RemoveFromSearch;
913
use Laravel\Scout\Tests\Fixtures\EmptySearchableModel;
1014
use Laravel\Scout\Tests\Fixtures\SearchableModel;
1115
use Laravel\Scout\Tests\Fixtures\SoftDeletedEmptySearchableModel;
@@ -18,6 +22,18 @@
1822

1923
class MeiliSearchEngineTest extends TestCase
2024
{
25+
protected function setUp(): void
26+
{
27+
Config::shouldReceive('get')->with('scout.after_commit', m::any())->andReturn(false);
28+
Config::shouldReceive('get')->with('scout.soft_delete', m::any())->andReturn(false);
29+
}
30+
31+
protected function tearDown(): void
32+
{
33+
Container::getInstance()->flush();
34+
m::close();
35+
}
36+
2137
public function test_update_adds_objects_to_index()
2238
{
2339
$client = m::mock(Client::class);
@@ -43,6 +59,59 @@ public function test_delete_removes_objects_to_index()
4359
$engine->delete(Collection::make([new SearchableModel(['id' => 1])]));
4460
}
4561

62+
public function test_delete_removes_objects_to_index_with_a_custom_search_key()
63+
{
64+
$client = m::mock(Client::class);
65+
$client->shouldReceive('index')->with('table')->andReturn($index = m::mock(Indexes::class));
66+
$index->shouldReceive('deleteDocuments')->once()->with(['my-meilisearch-key.5']);
67+
68+
$engine = new MeiliSearchEngine($client);
69+
$engine->delete(Collection::make([new MeiliSearchCustomKeySearchableModel(['id' => 5])]));
70+
}
71+
72+
public function test_delete_with_removeable_scout_collection_using_custom_search_key()
73+
{
74+
$job = new RemoveFromSearch(Collection::make([
75+
new MeiliSearchCustomKeySearchableModel(['id' => 5]),
76+
]));
77+
78+
$job = unserialize(serialize($job));
79+
80+
$client = m::mock(Client::class);
81+
$client->shouldReceive('index')->with('table')->andReturn($index = m::mock(Indexes::class));
82+
$index->shouldReceive('deleteDocuments')->once()->with(['my-meilisearch-key.5']);
83+
84+
$engine = new MeiliSearchEngine($client);
85+
$engine->delete($job->models);
86+
}
87+
88+
public function test_remove_from_search_job_uses_custom_search_key()
89+
{
90+
$job = new RemoveFromSearch(Collection::make([
91+
new MeiliSearchCustomKeySearchableModel(['id' => 5]),
92+
]));
93+
94+
$job = unserialize(serialize($job));
95+
96+
Container::getInstance()->bind(EngineManager::class, function () {
97+
$engine = m::mock(MeiliSearchEngine::class);
98+
99+
$engine->shouldReceive('delete')->once()->with(m::on(function ($collection) {
100+
$keyName = ($model = $collection->first())->getUnqualifiedScoutKeyName();
101+
102+
return $model->getAttributes()[$keyName] === 'my-meilisearch-key.5';
103+
}));
104+
105+
$manager = m::mock(EngineManager::class);
106+
107+
$manager->shouldReceive('engine')->andReturn($engine);
108+
109+
return $manager;
110+
});
111+
112+
$job->handle();
113+
}
114+
46115
public function test_search_sends_correct_parameters_to_meilisearch()
47116
{
48117
$client = m::mock(Client::class);
@@ -171,7 +240,7 @@ public function test_returns_primary_keys_when_custom_array_order_present()
171240
$builder = m::mock(Builder::class);
172241

173242
$model = m::mock(stdClass::class);
174-
$model->shouldReceive(['getScoutKeyName' => 'table.custom_key']);
243+
$model->shouldReceive(['getUnqualifiedScoutKeyName' => 'custom_key']);
175244
$builder->model = $model;
176245

177246
$engine->shouldReceive('keys')->passthru();
@@ -300,7 +369,7 @@ public function test_a_model_is_indexed_with_a_custom_meilisearch_key()
300369
$client = m::mock(Client::class);
301370
$client->shouldReceive('index')->with('table')->andReturn($index = m::mock(Indexes::class));
302371
$index->shouldReceive('addDocuments')->once()->with([[
303-
'id' => 5,
372+
'id' => 'my-meilisearch-key.5',
304373
]], 'id');
305374

306375
$engine = new MeiliSearchEngine($client);

0 commit comments

Comments
 (0)