Skip to content

Commit c686fd5

Browse files
committed
Re-index bug - nested mapping
1 parent 4a0d7d3 commit c686fd5

5 files changed

Lines changed: 86 additions & 109 deletions

File tree

src/Commands/ReIndexCommand.php

Lines changed: 56 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
use PDPhilip\Elasticsearch\Eloquent\Model;
1414
use PDPhilip\Elasticsearch\Schema\Blueprint;
1515
use PDPhilip\Elasticsearch\Schema\Builder as SchemaBuilder;
16-
use PDPhilip\Elasticsearch\Utils\Helpers;
16+
use PDPhilip\Elasticsearch\Utils\Sanitizer;
1717

1818
class ReIndexCommand extends Command
1919
{
@@ -104,7 +104,7 @@ public function handle(): int
104104
// Confirmation gate before danger zone
105105
if ($resumeAt !== 'CREATE_ORIGINAL') {
106106
$this->omni->hr();
107-
$this->omni->warning('About to enter the danger zone original index will be dropped');
107+
$this->omni->warning('About to enter the danger zone - original index will be dropped');
108108
$this->omni->tableRow('Original', $this->indexName.' ('.$this->originalCount.' docs)');
109109
$this->omni->tableRow('Temp', $this->tempIndexName.' ('.$this->tempCount.' docs)');
110110
$this->omni->hr();
@@ -120,7 +120,7 @@ public function handle(): int
120120
return self::FAILURE;
121121
}
122122
} else {
123-
$this->omni->warning('Resuming in danger zone original already gone, temp is source of truth');
123+
$this->omni->warning('Resuming in danger zone - original already gone, temp is source of truth');
124124
}
125125

126126
if (! $this->confirmContinue('Phase 6: Create Original (New Mapping)')) {
@@ -287,7 +287,7 @@ private function validate(): string|false
287287
$tempExists = $this->schema->hasTable($this->tempIndexName);
288288

289289
if (! $originalExists && ! $tempExists) {
290-
$this->omni->error('Both indices missing catastrophic state, manual recovery needed');
290+
$this->omni->error('Both indices missing - catastrophic state, manual recovery needed');
291291
$this->newLine();
292292

293293
return false;
@@ -313,20 +313,17 @@ private function validate(): string|false
313313
$analysis = $this->mappingAnalysis();
314314

315315
if (empty($analysis['mismatches'])) {
316-
$this->omni->success('Mapping already matches nothing to re-index');
316+
$this->omni->success('Mapping already matches - nothing to re-index');
317317
$this->newLine();
318318

319319
return false;
320320
}
321321

322-
$fieldsToUpdate = [];
323-
foreach ($analysis['mismatches'] as $field => $info) {
324-
$fieldsToUpdate[$field] = $info['current'].''.$info['desired'];
325-
}
322+
$fieldsToUpdate = $this->formatMismatchesForDisplay($analysis['mismatches']);
326323
$this->omni->dataList($fieldsToUpdate, 'Fields to Update', 'text-emerald-500');
327324

328325
if (! empty($analysis['unmapped'])) {
329-
$this->omni->dataList($analysis['unmapped'], 'Unmapped Fields', 'text-rose-500');
326+
$this->omni->dataList($this->nestDotNotation($analysis['unmapped']), 'Unmapped Fields', 'text-rose-500');
330327
}
331328

332329
return 'CREATE_TEMP';
@@ -338,7 +335,7 @@ private function validateWithExistingTemp(bool $originalExists): string|false
338335

339336
if (! $originalExists) {
340337
$this->omni->warning('Original missing, temp has '.$this->tempCount.' docs');
341-
$this->omni->info('Resuming from danger zone temp is source of truth');
338+
$this->omni->info('Resuming from danger zone - temp is source of truth');
342339
$this->originalCount = $this->tempCount;
343340

344341
return 'CREATE_ORIGINAL';
@@ -347,27 +344,27 @@ private function validateWithExistingTemp(bool $originalExists): string|false
347344
$this->originalCount = $this->countDocs($this->indexName);
348345

349346
if ($this->tempCount === 0) {
350-
$this->omni->info('Empty temp from previous run dropping, starting fresh');
347+
$this->omni->info('Empty temp from previous run - dropping, starting fresh');
351348
$this->schema->dropIfExists($this->tempIndexName);
352349

353350
return 'CREATE_TEMP';
354351
}
355352

356353
if ($this->countsMatch($this->originalCount, $this->tempCount)) {
357-
$this->omni->info('Temp has matching data ('.$this->tempCount.' docs) resuming at verify');
354+
$this->omni->info('Temp has matching data ('.$this->tempCount.' docs) - resuming at verify');
358355

359356
return 'VERIFY_TEMP';
360357
}
361358

362-
$this->omni->info('Partial temp ('.$this->tempCount.'/'.$this->originalCount.') dropping, starting fresh');
359+
$this->omni->info('Partial temp ('.$this->tempCount.'/'.$this->originalCount.') - dropping, starting fresh');
363360
$this->schema->dropIfExists($this->tempIndexName);
364361

365362
return 'CREATE_TEMP';
366363
}
367364

368365
private function handleEmptyIndex(): string|false
369366
{
370-
$this->omni->warning('Index has 0 records just drop and recreate');
367+
$this->omni->warning('Index has 0 records - just drop and recreate');
371368

372369
if (! $this->option('force') && ! $this->promptYesNo('Drop and recreate with new mapping?')) {
373370
$this->newLine();
@@ -427,7 +424,7 @@ private function copyToTemp(): bool
427424
}
428425

429426
$this->tempCount = (int) ($result['created'] ?? 0);
430-
$this->omni->success('Copy complete '.$this->tempCount.' docs created');
427+
$this->omni->success('Copy complete - '.$this->tempCount.' docs created');
431428

432429
return true;
433430
} catch (Exception $e) {
@@ -480,7 +477,7 @@ private function dropOriginal(): bool
480477
return true;
481478
} catch (Exception $e) {
482479
$this->omni->statusError('Failed to drop original', $e->getMessage());
483-
$this->omni->info('Safe state original still exists. Drop temp manually if needed.');
480+
$this->omni->info('Safe state - original still exists. Drop temp manually if needed.');
484481
$this->newLine();
485482

486483
return false;
@@ -496,7 +493,7 @@ private function createOriginal(): bool
496493
$this->omni->divider('Phase 6: Create Original (New Mapping)');
497494

498495
if ($this->schema->hasTable($this->indexName)) {
499-
$this->omni->info('Partial original exists dropping before recreate');
496+
$this->omni->info('Partial original exists - dropping before recreate');
500497
try {
501498
$this->schema->drop($this->indexName);
502499
sleep(1);
@@ -536,7 +533,7 @@ private function copyBack(): bool
536533
$failures = $result['failures'] ?? [];
537534
if (empty($failures)) {
538535
$this->finalCount = (int) ($result['created'] ?? 0);
539-
$this->omni->success('Copy back complete '.$this->finalCount.' docs');
536+
$this->omni->success('Copy back complete - '.$this->finalCount.' docs');
540537

541538
return true;
542539
}
@@ -576,7 +573,7 @@ private function verifyFinal(): bool
576573
return true;
577574
}
578575

579-
$this->omni->warning('Final count mismatch ('.$this->finalCount.' vs '.$this->tempCount.') running catch-up');
576+
$this->omni->warning('Final count mismatch ('.$this->finalCount.' vs '.$this->tempCount.') - running catch-up');
580577

581578
try {
582579
$result = $this->schema->reindex($this->tempIndexName, $this->indexName);
@@ -591,7 +588,7 @@ private function verifyFinal(): bool
591588
$this->omni->warning('Catch-up failed: '.$e->getMessage());
592589
}
593590

594-
$this->critical('Final verification failed '.$this->finalCount.' vs '.$this->tempCount);
591+
$this->critical('Final verification failed - '.$this->finalCount.' vs '.$this->tempCount);
595592

596593
return false;
597594
}
@@ -651,7 +648,7 @@ private function countDocs(string $index): int
651648

652649
private function rollbackTemp(string $reason): void
653650
{
654-
$this->omni->warning('Rolling back dropping temp index');
651+
$this->omni->warning('Rolling back - dropping temp index');
655652
$this->omni->info('Reason: '.$reason);
656653

657654
try {
@@ -667,7 +664,7 @@ private function critical(string $message): void
667664
$this->omni->hrError();
668665
$this->omni->error('CRITICAL: '.$message);
669666
$this->omni->error('Data lives in temp index: '.$this->tempIndexName);
670-
$this->omni->error('DO NOT drop '.$this->tempIndexName.' it is the only complete copy');
667+
$this->omni->error('DO NOT drop '.$this->tempIndexName.' - it is the only complete copy');
671668
$this->omni->hrError();
672669
$this->newLine();
673670
}
@@ -678,51 +675,38 @@ private function showMappings(string $index): void
678675
return;
679676
}
680677

681-
$mapping = $this->schema->getFieldsMapping($index);
678+
$mapping = $this->schema->getMappings($index);
682679
$this->omni->dataList($mapping, $index.' mapping');
683680
}
684681

685682
private function mappingAnalysis(): array
686683
{
687-
$currentMapping = $this->schema->getFieldsMapping($this->indexName);
688-
689-
$blueprint = Helpers::getLaravelCompatabilityVersion() >= 12
690-
? new Blueprint($this->connection, $this->indexName)
691-
: new Blueprint($this->indexName); // @phpstan-ignore arguments.count
692-
($this->mappingDefinition)($blueprint);
684+
$currentMapping = $this->schema->getMappings($this->indexName);
685+
$desiredMapping = Sanitizer::flattenMappingProperties([
686+
'properties' => $this->schema->compileMapping($this->mappingDefinition),
687+
]);
693688

694-
$definedFields = [];
689+
$desiredFields = array_keys($desiredMapping);
695690
$mismatches = [];
696-
foreach ($blueprint->getAddedColumns() as $column) {
697-
$field = $column->name;
698-
$desiredType = $column->type;
699-
$definedFields[] = $field;
700-
$currentType = $currentMapping[$field] ?? null;
701-
702-
if ($currentType !== $desiredType) {
703-
$mismatches[$field] = [
704-
'current' => $currentType ?? 'missing',
705-
'desired' => $desiredType,
706-
];
707691

692+
foreach ($desiredMapping as $field => $desired) {
693+
$current = $currentMapping[$field] ?? null;
694+
if ($current === $desired) {
708695
continue;
709696
}
710697

711-
$subMismatch = $this->detectSubFieldMismatch($column, $field, $currentMapping);
712-
if ($subMismatch) {
713-
$mismatches[$field] = $subMismatch;
714-
}
698+
$mismatches[$field] = [
699+
'current' => ($current['type'] ?? null) ?? 'missing',
700+
'desired' => $desired['type'] ?? 'unknown',
701+
];
715702
}
716703

717704
$unmapped = [];
718-
foreach ($currentMapping as $field => $type) {
719-
if (in_array($field, $definedFields)) {
705+
foreach ($currentMapping as $field => $details) {
706+
if (isset($desiredMapping[$field])) {
720707
continue;
721708
}
722-
if ($this->isSubFieldOfDefined($field, $definedFields)) {
723-
continue;
724-
}
725-
$unmapped[$field] = $type;
709+
$unmapped[$field] = $details['type'] ?? 'object';
726710
}
727711

728712
return [
@@ -731,70 +715,37 @@ private function mappingAnalysis(): array
731715
];
732716
}
733717

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
718+
private function formatMismatchesForDisplay(array $mismatches): array
754719
{
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-
}
720+
$display = [];
721+
foreach ($mismatches as $field => $info) {
722+
$label = $info['current'].''.$info['desired'];
723+
if (empty($info['details'])) {
724+
$display[$field] = $label;
771725

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)) {
778726
continue;
779727
}
780-
$subName = substr($key, strlen($prefix));
781-
if (! str_contains($subName, '.')) {
782-
$subs[$subName] = $type;
783-
}
728+
$display[$field] = [$label => $info['details']];
784729
}
785730

786-
return $subs;
731+
return $display;
787732
}
788733

789-
private function isSubFieldOfDefined(string $field, array $definedFields): bool
734+
private function nestDotNotation(array $flat): array
790735
{
791-
foreach ($definedFields as $defined) {
792-
if (str_starts_with($field, $defined.'.')) {
793-
return true;
736+
$nested = [];
737+
foreach ($flat as $key => $value) {
738+
$parts = explode('.', $key);
739+
if (count($parts) === 1) {
740+
$nested[$key] = $value;
741+
742+
continue;
794743
}
744+
$parent = array_shift($parts);
745+
$nested[$parent][implode('.', $parts)] = $value;
795746
}
796747

797-
return false;
748+
return $nested;
798749
}
799750

800751
private function confirmSettings(): bool
@@ -842,7 +793,7 @@ private function editSettings(): void
842793
$this->maxRetries = (int) $retries;
843794
}
844795

845-
$this->omni->success('Settings updated tolerance: '.($this->tolerance * 100).'%, retries: '.$this->maxRetries);
796+
$this->omni->success('Settings updated - tolerance: '.($this->tolerance * 100).'%, retries: '.$this->maxRetries);
846797
}
847798

848799
private function confirmContinue(string $nextPhase): bool

src/Schema/Builder.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,17 @@ public function getMappings(string|array $table, $raw = false): array
288288
return Sanitizer::flattenMappingProperties($mappings);
289289
}
290290

291+
/**
292+
* Compile a Blueprint callback into its resulting ES mapping structure without creating the index.
293+
*/
294+
public function compileMapping(Closure $callback): array
295+
{
296+
$blueprint = $this->createBlueprint('_compile');
297+
$callback($blueprint);
298+
299+
return $this->connection->getSchemaGrammar()->compileMapping($blueprint);
300+
}
301+
291302
/**
292303
* Shows you the currently configured settings for one or more indices
293304
*/

src/Schema/Grammars/Grammar.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ public function compileUpdate(Blueprint $blueprint, Fluent $command, Connection
7070
};
7171
}
7272

73+
public function compileMapping(Blueprint $blueprint): array
74+
{
75+
return $this->getColumns($blueprint);
76+
}
77+
7378
protected function buildBody(Blueprint $blueprint): array
7479
{
7580
$body = [

tests/Models/ReIndexTarget.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ public static function mappingDefinition(Blueprint $index): void
2222
{
2323
$index->keyword('status');
2424
$index->text('name');
25+
$index->nested('tags')->properties(function (Blueprint $nested) {
26+
$nested->text('key', hasKeyword: true);
27+
$nested->text('value', hasKeyword: true);
28+
});
2529
$index->date('created_at');
2630
$index->date('updated_at');
2731
}

0 commit comments

Comments
 (0)