Skip to content

Commit c80f4c6

Browse files
[10.x] Add scout:queue command to optimize large imports (#929)
* Extract searchable query * Add scout:queue command to optimize reindexing * Dispatch on appropriate queue and connection * Add tests * CS fixes * Remove strange assertion edge case in lower laravel versions * Fix test php 8.0 * Filter searchable models * Return early if searchable models is empty * Use collection isEmpty method for consistency across jobs * Add ability to enter custom range via --min and --max options * CS fixes * Fix comparison assertions * formatting --------- Co-authored-by: Taylor Otwell <[email protected]>
1 parent f90b696 commit c80f4c6

File tree

7 files changed

+701
-4
lines changed

7 files changed

+701
-4
lines changed

src/Console/QueueImportCommand.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
namespace Laravel\Scout\Console;
4+
5+
use Illuminate\Console\Command;
6+
use Laravel\Scout\Jobs\MakeRangeSearchable;
7+
use Symfony\Component\Console\Attribute\AsCommand;
8+
9+
#[AsCommand(name: 'scout:queue-import')]
10+
class QueueImportCommand extends Command
11+
{
12+
/**
13+
* The name and signature of the console command.
14+
*
15+
* @var string
16+
*/
17+
protected $signature = 'scout:queue-import
18+
{model : Class name of model to bulk queue}
19+
{--min= : The minimum ID to start queuing from}
20+
{--max= : The maximum ID to queue up to}
21+
{--c|chunk= : The number of records to queue in a single job (Defaults to configuration value: `scout.chunk.searchable`)}';
22+
23+
/**
24+
* The console command description.
25+
*
26+
* @var string
27+
*/
28+
protected $description = 'Import the given model into the search index via chunked, queued jobs';
29+
30+
/**
31+
* Execute the console command.
32+
*
33+
* @return void
34+
*/
35+
public function handle()
36+
{
37+
$class = $this->argument('model');
38+
39+
$model = new $class;
40+
41+
$query = $model::makeAllSearchableQuery();
42+
43+
$min = $this->option('min') ?? $query->min($model->getScoutKeyName());
44+
$max = $this->option('max') ?? $query->max($model->getScoutKeyName());
45+
46+
$chunk = max(1, (int) ($this->option('chunk') ?? config('scout.chunk.searchable', 500)));
47+
48+
if (! $min || ! $max) {
49+
$this->info('No records found for ['.$class.'].');
50+
51+
return;
52+
}
53+
54+
if (! is_numeric($min) || ! is_numeric($max)) {
55+
$this->error('The primary key for ['.$class.'] is not numeric.');
56+
57+
return;
58+
}
59+
60+
for ($start = $min; $start <= $max; $start += $chunk) {
61+
$end = min($start + $chunk - 1, $max);
62+
63+
dispatch(new MakeRangeSearchable($model, $start, $end))
64+
->onQueue($model->syncWithSearchUsingQueue())
65+
->onConnection($model->syncWithSearchUsing());
66+
67+
$this->line('<comment>Queued ['.$class.'] models up to ID:</comment> '.$end);
68+
}
69+
70+
$this->info('All ['.$class.'] records have been queued for importing.');
71+
}
72+
}

src/Jobs/MakeRangeSearchable.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
namespace Laravel\Scout\Jobs;
4+
5+
use Illuminate\Bus\Queueable;
6+
use Illuminate\Contracts\Queue\ShouldQueue;
7+
use Illuminate\Queue\SerializesModels;
8+
use Laravel\Scout\Scout;
9+
10+
class MakeRangeSearchable implements ShouldQueue
11+
{
12+
use Queueable, SerializesModels;
13+
14+
/**
15+
* The model to be made searchable.
16+
*
17+
* @var \Illuminate\Database\Eloquent\Model
18+
*/
19+
public $model;
20+
21+
/**
22+
* The start id to be made searchable.
23+
*
24+
* @var int
25+
*/
26+
public $start;
27+
28+
/**
29+
* The end id to be made searchable.
30+
*
31+
* @var int
32+
*/
33+
public $end;
34+
35+
/**
36+
* Create a new job instance.
37+
*
38+
* @param \Illuminate\Database\Eloquent\Model $model
39+
* @param int $start
40+
* @param int $end
41+
* @return void
42+
*/
43+
public function __construct($model, $start, $end)
44+
{
45+
$this->model = $model;
46+
$this->start = $start;
47+
$this->end = $end;
48+
}
49+
50+
/**
51+
* Handle the job.
52+
*
53+
* @return void
54+
*/
55+
public function handle()
56+
{
57+
$models = $this->model::makeAllSearchableQuery()
58+
->whereBetween($this->model->getScoutKeyName(), [$this->start, $this->end])
59+
->get()
60+
->filter
61+
->shouldBeSearchable();
62+
63+
if ($models->isEmpty()) {
64+
return;
65+
}
66+
67+
dispatch(new Scout::$makeSearchableJob($models))
68+
->onQueue($this->model->syncWithSearchUsingQueue())
69+
->onConnection($this->model->syncWithSearchUsing());
70+
}
71+
}

src/Jobs/MakeSearchable.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public function __construct($models)
3535
*/
3636
public function handle()
3737
{
38-
if (count($this->models) === 0) {
38+
if ($this->models->isEmpty()) {
3939
return;
4040
}
4141

src/ScoutServiceProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Laravel\Scout\Console\FlushCommand;
99
use Laravel\Scout\Console\ImportCommand;
1010
use Laravel\Scout\Console\IndexCommand;
11+
use Laravel\Scout\Console\QueueImportCommand;
1112
use Laravel\Scout\Console\SyncIndexSettingsCommand;
1213
use Meilisearch\Client as Meilisearch;
1314

@@ -48,6 +49,7 @@ public function boot()
4849
{
4950
if ($this->app->runningInConsole()) {
5051
$this->commands([
52+
QueueImportCommand::class,
5153
FlushCommand::class,
5254
ImportCommand::class,
5355
IndexCommand::class,

src/Searchable.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,12 +171,22 @@ public static function search($query = '', $callback = null)
171171
* @return void
172172
*/
173173
public static function makeAllSearchable($chunk = null)
174+
{
175+
static::makeAllSearchableQuery()->searchable($chunk);
176+
}
177+
178+
/**
179+
* Get a query builder for making all instances of the model searchable.
180+
*
181+
* @return \Illuminate\Database\Eloquent\Builder
182+
*/
183+
public static function makeAllSearchableQuery()
174184
{
175185
$self = new static;
176186

177187
$softDelete = static::usesSoftDelete() && config('scout.soft_delete', false);
178188

179-
$self->newQuery()
189+
return $self->newQuery()
180190
->when(true, function ($query) use ($self) {
181191
$self->makeAllSearchableUsing($query);
182192
})
@@ -185,8 +195,7 @@ public static function makeAllSearchable($chunk = null)
185195
})
186196
->orderBy(
187197
$self->qualifyColumn($self->getScoutKeyName())
188-
)
189-
->searchable($chunk);
198+
);
190199
}
191200

192201
/**

0 commit comments

Comments
 (0)