Skip to content

Commit 20e18e4

Browse files
committed
re-index refactor
1 parent 061c12a commit 20e18e4

3 files changed

Lines changed: 157 additions & 47 deletions

File tree

src/Commands/ReIndexCommand.php

Lines changed: 147 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,21 @@ public function handle(): int
7777

7878
// Safe zone: create temp, copy, verify
7979
if ($resumeAt === 'CREATE_TEMP') {
80+
if (! $this->confirmContinue('Phase 2: Create Temp Index')) {
81+
return self::SUCCESS;
82+
}
8083
if (! $this->createTempIndex()) {
8184
return self::FAILURE;
8285
}
86+
if (! $this->confirmContinue('Phase 3: Copy to Temp')) {
87+
return self::SUCCESS;
88+
}
8389
if (! $this->copyToTemp()) {
8490
return self::FAILURE;
8591
}
92+
if (! $this->confirmContinue('Phase 4: Verify Temp')) {
93+
return self::SUCCESS;
94+
}
8695
if (! $this->verifyTemp()) {
8796
return self::FAILURE;
8897
}
@@ -114,18 +123,30 @@ public function handle(): int
114123
$this->omni->warning('Resuming in danger zone — original already gone, temp is source of truth');
115124
}
116125

126+
if (! $this->confirmContinue('Phase 6: Create Original (New Mapping)')) {
127+
return self::SUCCESS;
128+
}
117129
if (! $this->createOriginal()) {
118130
return self::FAILURE;
119131
}
120132

133+
if (! $this->confirmContinue('Phase 7: Copy Back')) {
134+
return self::SUCCESS;
135+
}
121136
if (! $this->copyBack()) {
122137
return self::FAILURE;
123138
}
124139

140+
if (! $this->confirmContinue('Phase 8: Verify Final')) {
141+
return self::SUCCESS;
142+
}
125143
if (! $this->verifyFinal()) {
126144
return self::FAILURE;
127145
}
128146

147+
if (! $this->confirmContinue('Phase 9: Cleanup')) {
148+
return self::SUCCESS;
149+
}
129150
$this->cleanup();
130151
$this->summary();
131152

@@ -177,15 +198,18 @@ private function resolveModel(): ?Model
177198
$this->newLine();
178199
$this->omni->statusError('Missing mapping definition', $class, [
179200
'Your model must override mappingDefinition():',
180-
'',
181-
'use PDPhilip\Elasticsearch\Schema\Blueprint;',
182-
'',
183-
'public static function mappingDefinition(Blueprint $index): void',
184-
'{',
185-
' $index->keyword(\'status\');',
186-
' $index->geoPoint(\'location\');',
187-
'}',
188201
]);
202+
$this->omni->render(<<<'HTML'
203+
<code line="3" start-line="1">
204+
use PDPhilip\Elasticsearch\Schema\Blueprint;
205+
206+
public static function mappingDefinition(Blueprint $index): void
207+
{
208+
$index->keyword('status');
209+
$index->geoPoint('location');
210+
}
211+
</code>
212+
HTML);
189213
$this->newLine();
190214

191215
return null;
@@ -286,24 +310,23 @@ private function validate(): string|false
286310
return $this->handleEmptyIndex();
287311
}
288312

289-
$this->omni->tableHeader('Check', 'Status');
290-
$this->omni->tableRowSuccess('Index exists', $this->indexName);
291-
$this->omni->tableRowSuccess('Record count', number_format($this->originalCount));
292-
$this->omni->tableRowSuccess('No leftover temp');
313+
$analysis = $this->mappingAnalysis();
293314

294-
$this->showMappings($this->indexName);
295-
296-
$mismatches = $this->mappingMismatches();
297-
if (empty($mismatches)) {
315+
if (empty($analysis['mismatches'])) {
298316
$this->omni->success('Mapping already matches — nothing to re-index');
299317
$this->newLine();
300318

301319
return false;
302320
}
303321

304-
$this->omni->divider('Fields to update');
305-
foreach ($mismatches as $field => $info) {
306-
$this->omni->tableRowWarning($field, $info['current'].''.$info['desired']);
322+
$fieldsToUpdate = [];
323+
foreach ($analysis['mismatches'] as $field => $info) {
324+
$fieldsToUpdate[$field] = $info['current'].''.$info['desired'];
325+
}
326+
$this->omni->dataList($fieldsToUpdate, 'Fields to Update', 'text-emerald-500');
327+
328+
if (! empty($analysis['unmapped'])) {
329+
$this->omni->dataList($analysis['unmapped'], 'Unmapped Fields', 'text-rose-500');
307330
}
308331

309332
return 'CREATE_TEMP';
@@ -659,7 +682,7 @@ private function showMappings(string $index): void
659682
$this->omni->dataList($mapping, $index.' mapping');
660683
}
661684

662-
private function mappingMismatches(): array
685+
private function mappingAnalysis(): array
663686
{
664687
$currentMapping = $this->schema->getFieldsMapping($this->indexName);
665688

@@ -668,23 +691,110 @@ private function mappingMismatches(): array
668691
: new Blueprint($this->indexName); // @phpstan-ignore arguments.count
669692
($this->mappingDefinition)($blueprint);
670693

694+
$definedFields = [];
671695
$mismatches = [];
672696
foreach ($blueprint->getAddedColumns() as $column) {
673697
$field = $column->name;
674698
$desiredType = $column->type;
699+
$definedFields[] = $field;
675700
$currentType = $currentMapping[$field] ?? null;
676701

677-
if ($currentType === $desiredType) {
702+
if ($currentType !== $desiredType) {
703+
$mismatches[$field] = [
704+
'current' => $currentType ?? 'missing',
705+
'desired' => $desiredType,
706+
];
707+
678708
continue;
679709
}
680710

681-
$mismatches[$field] = [
682-
'current' => $currentType ?? 'missing',
683-
'desired' => $desiredType,
684-
];
711+
$subMismatch = $this->detectSubFieldMismatch($column, $field, $currentMapping);
712+
if ($subMismatch) {
713+
$mismatches[$field] = $subMismatch;
714+
}
715+
}
716+
717+
$unmapped = [];
718+
foreach ($currentMapping as $field => $type) {
719+
if (in_array($field, $definedFields)) {
720+
continue;
721+
}
722+
if ($this->isSubFieldOfDefined($field, $definedFields)) {
723+
continue;
724+
}
725+
$unmapped[$field] = $type;
685726
}
686727

687-
return $mismatches;
728+
return [
729+
'mismatches' => $mismatches,
730+
'unmapped' => $unmapped,
731+
];
732+
}
733+
734+
private function detectSubFieldMismatch($column, string $field, array $currentMapping): ?array
735+
{
736+
$expectedSubs = $this->getExpectedSubFields($column);
737+
$currentSubs = $this->getCurrentSubFields($field, $currentMapping);
738+
739+
if ($expectedSubs === $currentSubs) {
740+
return null;
741+
}
742+
743+
$format = fn (array $subs) => empty($subs)
744+
? $column->type
745+
: $column->type.' [+'.implode(', ', array_keys($subs)).']';
746+
747+
return [
748+
'current' => $format($currentSubs),
749+
'desired' => $format($expectedSubs),
750+
];
751+
}
752+
753+
private function getExpectedSubFields($column): array
754+
{
755+
if (! ($column->fields instanceof Closure)) {
756+
return [];
757+
}
758+
759+
$subBlueprint = Helpers::getLaravelCompatabilityVersion() >= 12
760+
? new Blueprint($this->connection, '_sub')
761+
: new Blueprint('_sub'); // @phpstan-ignore arguments.count
762+
($column->fields)($subBlueprint);
763+
764+
$subs = [];
765+
foreach ($subBlueprint->getAddedColumns() as $subCol) {
766+
$subs[$subCol->name] = $subCol->type;
767+
}
768+
769+
return $subs;
770+
}
771+
772+
private function getCurrentSubFields(string $field, array $mapping): array
773+
{
774+
$prefix = $field.'.';
775+
$subs = [];
776+
foreach ($mapping as $key => $type) {
777+
if (! str_starts_with($key, $prefix)) {
778+
continue;
779+
}
780+
$subName = substr($key, strlen($prefix));
781+
if (! str_contains($subName, '.')) {
782+
$subs[$subName] = $type;
783+
}
784+
}
785+
786+
return $subs;
787+
}
788+
789+
private function isSubFieldOfDefined(string $field, array $definedFields): bool
790+
{
791+
foreach ($definedFields as $defined) {
792+
if (str_starts_with($field, $defined.'.')) {
793+
return true;
794+
}
795+
}
796+
797+
return false;
688798
}
689799

690800
private function confirmSettings(): bool
@@ -701,7 +811,7 @@ private function confirmSettings(): bool
701811
$valid = ['yes', 'y', 'edit', 'e', 'cancel', 'c', 'no', 'n'];
702812

703813
while (! in_array(strtolower($answer), $valid)) {
704-
$answer = $this->omni->ask('Continue with these settings?', ['Yes', 'Edit', 'Cancel']);
814+
$answer = $this->omni->ask('Continue with these settings?', ['yes', 'edit', 'cancel']);
705815
}
706816

707817
$answer = strtolower($answer);
@@ -735,13 +845,22 @@ private function editSettings(): void
735845
$this->omni->success('Settings updated — tolerance: '.($this->tolerance * 100).'%, retries: '.$this->maxRetries);
736846
}
737847

848+
private function confirmContinue(string $nextPhase): bool
849+
{
850+
if ($this->option('force')) {
851+
return true;
852+
}
853+
854+
return $this->promptYesNo('Continue to '.$nextPhase.'?');
855+
}
856+
738857
private function promptYesNo(string $question): bool
739858
{
740859
$answer = '';
741860
$valid = ['yes', 'y', 'n', 'no'];
742861

743862
while (! in_array(strtolower($answer), $valid)) {
744-
$answer = $this->omni->ask($question, ['Yes', 'No']);
863+
$answer = $this->omni->ask($question, ['yes', 'no']);
745864
}
746865

747866
return in_array(strtolower($answer), ['yes', 'y']);

tests/ReIndexCommandTest.php

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
use PDPhilip\Elasticsearch\Schema\Schema;
88
use PDPhilip\Elasticsearch\Tests\Models\ReIndexTarget;
99

10+
function refreshIndex(string $index): void
11+
{
12+
DB::connection('elasticsearch')->getClient()->indices()->refresh(['index' => $index]);
13+
}
14+
1015
beforeEach(function () {
1116
$schema = Schema::connection('elasticsearch');
1217
$schema->dropIfExists('re_index_targets');
@@ -32,14 +37,12 @@
3237
['name' => 'Charlie', 'status' => 'active'],
3338
]);
3439

35-
sleep(1);
36-
3740
$this->artisan('elastic:re-index', [
3841
'model' => ReIndexTarget::class,
3942
'--force' => true,
4043
])->assertSuccessful();
4144

42-
sleep(1);
45+
refreshIndex('re_index_targets');
4346

4447
$count = DB::connection('elasticsearch')->table('re_index_targets')->count();
4548
expect($count)->toBe(3);
@@ -88,8 +91,6 @@
8891
['name' => 'Alpha', 'status' => 'active'],
8992
]);
9093

91-
sleep(1);
92-
9394
// Mapping matches — exits early (no re-index needed)
9495
$this->artisan('elastic:re-index', [
9596
'model' => ReIndexTarget::class,
@@ -104,8 +105,6 @@
104105
['name' => 'Alpha', 'status' => 'active'],
105106
]);
106107

107-
sleep(1);
108-
109108
$this->artisan('elastic:re-index', [
110109
'model' => ReIndexTarget::class,
111110
'--force' => true,
@@ -121,16 +120,14 @@
121120
}
122121
ReIndexTarget::insert($records);
123122

124-
sleep(1);
125-
126123
$countBefore = DB::connection('elasticsearch')->table('re_index_targets')->count();
127124

128125
$this->artisan('elastic:re-index', [
129126
'model' => ReIndexTarget::class,
130127
'--force' => true,
131128
])->assertSuccessful();
132129

133-
sleep(1);
130+
refreshIndex('re_index_targets');
134131

135132
$countAfter = DB::connection('elasticsearch')->table('re_index_targets')->count();
136133
expect($countAfter)->toBe($countBefore);
@@ -142,8 +139,6 @@
142139
['name' => 'Bravo', 'status' => 'inactive'],
143140
]);
144141

145-
sleep(1);
146-
147142
$schema = Schema::connection('elasticsearch');
148143
$schema->create('re_index_targets_temp', function (Blueprint $index) {
149144
$index->keyword('status');
@@ -153,14 +148,12 @@
153148
});
154149
$schema->reindex('re_index_targets', 're_index_targets_temp');
155150

156-
sleep(1);
157-
158151
$this->artisan('elastic:re-index', [
159152
'model' => ReIndexTarget::class,
160153
'--force' => true,
161154
])->assertSuccessful();
162155

163-
sleep(1);
156+
refreshIndex('re_index_targets');
164157

165158
$count = DB::connection('elasticsearch')->table('re_index_targets')->count();
166159
expect($count)->toBe(2);
@@ -173,8 +166,6 @@
173166
['name' => 'Alpha', 'status' => 'active'],
174167
]);
175168

176-
sleep(1);
177-
178169
$schema = Schema::connection('elasticsearch');
179170
$schema->create('re_index_targets_temp', function (Blueprint $index) {
180171
$index->keyword('status');
@@ -186,7 +177,7 @@
186177
'--force' => true,
187178
])->assertSuccessful();
188179

189-
sleep(1);
180+
refreshIndex('re_index_targets');
190181

191182
$count = DB::connection('elasticsearch')->table('re_index_targets')->count();
192183
expect($count)->toBe(1);

tests/ReindexTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
use PDPhilip\Elasticsearch\Tests\Factories\ProductFactory;
88
use PDPhilip\Elasticsearch\Tests\Models\Product;
99

10-
it('re-indexs data', function () {
10+
it('re-indexes data', function () {
1111
// Drop the Schema
1212
Schema::deleteIfExists('products');
1313
Schema::deleteIfExists('holding_products');

0 commit comments

Comments
 (0)