Skip to content

Commit 623fe8a

Browse files
Merge pull request #3 from ginkelsoft-development/develop
Add chunked batch processing and stability improvements to index rebu…
2 parents 8e1ce0a + df90924 commit 623fe8a

File tree

3 files changed

+62
-79
lines changed

3 files changed

+62
-79
lines changed

src/Console/RebuildIndex.php

Lines changed: 31 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,40 +9,24 @@
99
/**
1010
* Class RebuildIndex
1111
*
12-
* This Artisan command rebuilds the encrypted search index for a given Eloquent model.
13-
* It is designed for maintenance operations where the search index may be outdated,
14-
* corrupted, or needs regeneration after schema or normalization changes.
12+
* Artisan command that rebuilds the encrypted search index for a given Eloquent model.
13+
* It now supports short model names (e.g. "Client") and automatically resolves them
14+
* under the `App\Models` namespace if not fully qualified.
1515
*
16-
* The command iterates over all records of the specified model and regenerates
17-
* both "exact" and "prefix" tokens as defined by the model’s
18-
* `HasEncryptedSearchIndex` trait configuration.
19-
*
20-
* Usage example:
16+
* Example:
17+
* php artisan encryption:index-rebuild Client
2118
* php artisan encryption:index-rebuild "App\Models\Client"
22-
*
23-
* Options:
24-
* --chunk=100 Number of records processed per batch (default: 100)
25-
*
26-
* Implementation details:
27-
* - Before rebuilding, all existing search index entries for the given model
28-
* are removed.
29-
* - Records are then reprocessed in chunks to prevent memory exhaustion.
30-
* - For each model instance, `updateSearchIndex()` is called to regenerate
31-
* the normalized token rows.
32-
*
33-
* This ensures a clean and consistent index aligned with the current model data.
3419
*/
3520
class RebuildIndex extends Command
3621
{
3722
/**
3823
* The name and signature of the console command.
3924
*
40-
* {model} The fully qualified class name (FQCN) of the Eloquent model.
41-
* {--chunk=100} The number of model records to process per batch.
42-
*
4325
* @var string
4426
*/
45-
protected $signature = 'encryption:index-rebuild {model : FQCN of the Eloquent model} {--chunk=100}';
27+
protected $signature = 'encryption:index-rebuild
28+
{model : Model name or FQCN of the Eloquent model}
29+
{--chunk=100 : Number of records processed per batch}';
4630

4731
/**
4832
* The console command description.
@@ -54,49 +38,54 @@ class RebuildIndex extends Command
5438
/**
5539
* Execute the console command.
5640
*
57-
* This method performs the following steps:
58-
* 1. Validates the provided model class.
59-
* 2. Deletes all existing search index entries for that model.
60-
* 3. Iterates through all model records in configurable chunks.
61-
* 4. Calls `updateSearchIndex()` for each record to regenerate tokens.
62-
* 5. Displays progress and a final summary of processed records.
63-
*
64-
* @return int Command exit code (0 on success, 1 on failure).
41+
* @return int
6542
*/
6643
public function handle(): int
6744
{
45+
$input = trim($this->argument('model'));
46+
47+
// Automatically resolve models under App\Models namespace if not fully qualified
48+
if (! class_exists($input)) {
49+
$guessed = "App\\Models\\{$input}";
50+
if (class_exists($guessed)) {
51+
$input = $guessed;
52+
}
53+
}
54+
6855
/** @var class-string<Model> $class */
69-
$class = $this->argument('model');
56+
$class = $input;
7057

7158
if (! class_exists($class)) {
72-
$this->error("Model class not found: {$class}");
59+
$this->error("Model class not found: {$this->argument('model')}");
7360
return self::FAILURE;
7461
}
7562

7663
$chunk = (int) $this->option('chunk');
64+
$this->info("Rebuilding encrypted search index for: {$class}");
65+
$this->line("Processing in chunks of {$chunk}...");
7766

7867
// Remove all existing search tokens for this model
7968
SearchIndex::where('model_type', $class)->delete();
8069

81-
/** @var \Illuminate\Database\Eloquent\Builder $q */
82-
$q = $class::query();
70+
/** @var \Illuminate\Database\Eloquent\Builder $query */
71+
$query = $class::query();
8372

8473
$count = 0;
8574

86-
// Process model data in chunks to minimize memory usage
87-
$q->chunk($chunk, function ($rows) use (&$count, $class) {
88-
foreach ($rows as $model) {
89-
if (method_exists($class, 'updateSearchIndex')) {
90-
$class::updateSearchIndex($model);
75+
$query->chunk($chunk, function ($models) use (&$count) {
76+
foreach ($models as $model) {
77+
if (method_exists($model, 'updateSearchIndex')) {
78+
$model->updateSearchIndex(); // <-- FIXED
9179
}
9280
$count++;
9381
}
82+
9483
// Write a dot to indicate progress
9584
$this->output->write('.');
9685
});
9786

9887
$this->newLine();
99-
$this->info("Rebuilt index for {$count} records of {$class}.");
88+
$this->info("Rebuilt index for {$count} records of {$class}.");
10089

10190
return self::SUCCESS;
10291
}

src/Observers/SearchIndexObserver.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ protected function rebuildIndex(Model $model): void
9595
{
9696
if (method_exists($model, 'updateSearchIndex')) {
9797
// @phpstan-ignore-next-line
98-
$model::updateSearchIndex($model);
98+
$model::updateSearchIndex();
9999
}
100100
}
101101

@@ -113,7 +113,7 @@ protected function removeIndex(Model $model): void
113113
{
114114
if (method_exists($model, 'removeSearchIndex')) {
115115
// @phpstan-ignore-next-line
116-
$model::removeSearchIndex($model);
116+
$model::removeSearchIndex();
117117
}
118118
}
119119
}

src/Traits/HasEncryptedSearchIndex.php

Lines changed: 29 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -47,23 +47,6 @@
4747
*/
4848
trait HasEncryptedSearchIndex
4949
{
50-
/**
51-
* Defines which fields should be included in the encrypted search index.
52-
*
53-
* Example:
54-
* ```php
55-
* protected array $encryptedSearch = [
56-
* 'first_names' => ['exact' => true, 'prefix' => true],
57-
* 'last_names' => ['exact' => true],
58-
* ];
59-
* ```
60-
*
61-
* Each entry specifies whether an exact or prefix index (or both)
62-
* should be generated for that field.
63-
*
64-
* @var array<string, array{exact?: bool, prefix?: bool}>
65-
*/
66-
protected array $encryptedSearch = [];
6750

6851
/**
6952
* Boot logic for the trait.
@@ -98,37 +81,34 @@ public static function bootHasEncryptedSearchIndex(): void
9881
* @param \Illuminate\Database\Eloquent\Model $model
9982
* @return void
10083
*/
101-
protected static function updateSearchIndex(Model $model): void
84+
public function updateSearchIndex(): void
10285
{
103-
if (empty($model->encryptedSearch)) {
86+
$config = $this->getEncryptedSearchConfiguration();
87+
88+
if (empty($config)) {
10489
return;
10590
}
10691

10792
$pepper = (string) config('encrypted-search.search_pepper', '');
10893
$max = (int) config('encrypted-search.max_prefix_depth', 6);
10994

110-
// Remove previous entries for this model record
111-
SearchIndex::where('model_type', get_class($model))
112-
->where('model_id', $model->getKey())
95+
SearchIndex::where('model_type', static::class)
96+
->where('model_id', $this->getKey())
11397
->delete();
11498

115-
// Generate new tokens
11699
$rows = [];
117-
foreach ($model->encryptedSearch as $field => $modes) {
118-
$raw = (string) $model->getAttribute($field);
119-
if ($raw === '') {
120-
continue;
121-
}
100+
101+
foreach ($config as $field => $modes) {
102+
$raw = (string) $this->getAttribute($field);
103+
if ($raw === '') continue;
122104

123105
$norm = Normalizer::normalize($raw);
124-
if (! $norm) {
125-
continue;
126-
}
106+
if (! $norm) continue;
127107

128108
if (! empty($modes['exact'])) {
129109
$rows[] = [
130-
'model_type' => get_class($model),
131-
'model_id' => $model->getKey(),
110+
'model_type' => static::class,
111+
'model_id' => $this->getKey(),
132112
'field' => $field,
133113
'type' => 'exact',
134114
'token' => Tokens::exact($norm, $pepper),
@@ -140,8 +120,8 @@ protected static function updateSearchIndex(Model $model): void
140120
if (! empty($modes['prefix'])) {
141121
foreach (Tokens::prefixes($norm, $max, $pepper) as $t) {
142122
$rows[] = [
143-
'model_type' => get_class($model),
144-
'model_id' => $model->getKey(),
123+
'model_type' => static::class,
124+
'model_id' => $this->getKey(),
145125
'field' => $field,
146126
'type' => 'prefix',
147127
'token' => $t,
@@ -236,4 +216,18 @@ public function scopeEncryptedPrefix(Builder $query, string $field, string $term
236216
->whereIn('token', $tokens);
237217
});
238218
}
219+
220+
/**
221+
* Get encrypted search configuration from the model.
222+
*/
223+
protected function getEncryptedSearchConfiguration(): array
224+
{
225+
// Laat modellen hun eigen configuratie bepalen
226+
if (method_exists($this, 'getEncryptedSearchFields')) {
227+
return $this->getEncryptedSearchFields();
228+
}
229+
230+
// Fallback: gebruik property als die er nog is
231+
return property_exists($this, 'encryptedSearch') ? $this->encryptedSearch : [];
232+
}
239233
}

0 commit comments

Comments
 (0)