Ein FULLTEXT-Index zwingt MySQL bei JEDEM ADD COLUMN auf ALGORITHM=COPY. Auf press_releases (>1 GB) bedeutet das einen vollständigen Rebuild, während dessen kein Schreibzugriff möglich ist — im August hat genau das die Seite zum Stehen gebracht, weil die wartenden Requests PHP-FPM-Worker und DB-Verbindungen belegten. Die durchsuchbaren Felder liegen jetzt gespiegelt in `press_release_search`, die die Indizes trägt. Ihr Schema ist per Konstruktion stabil und wird nie erweitert; neue Felder gehören nach press_releases, wo sie ohne Volltext-Index Millisekunden kosten. Synchron hält sie ein Observer (alle Schreibzugriffe laufen über Eloquent, auch der Importer), reparieren lässt sie sich mit press-releases:reindex-search. Der Umstieg ist über mehrere Migrationen geteilt und schaltet erst um, wenn press_releases seine Indizes verloren hat — bis dahin sucht die Anwendung unverändert weiter. Die Drop-Migration verweigert den Dienst, solange der Suchindex unvollständig ist, und verwirft die gecachte Umschalt-Prüfung: Stünde dort noch der alte Stand, setzte der Rückfallpfad sein MATCH() auf einen soeben entfernten Index ab (MySQL-Fehler 1191) und jede Suche endete im 500er. Dazu die betriebliche Absicherung: - DB_LOCK_WAIT_TIMEOUT (Default 10 s) statt MySQLs einem Jahr. Requests scheitern bei einer laufenden Schemaänderung nach Sekunden, statt sich aufzustauen, bis keine Verbindung mehr frei ist. - Sessions und Cache nach Redis, damit eine blockierte MySQL-Tabelle nicht die gesamte Anwendung mitreißt. - db:online-alter als Notausgang (pt-online-schema-change), falls eine der kleinen FULLTEXT-Tabellen doch einmal groß wird. Percona-Toolkit ist bewusst nicht Teil des Standard-Setups. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
53 lines
1.8 KiB
PHP
53 lines
1.8 KiB
PHP
<?php
|
|
|
|
namespace App\Observers;
|
|
|
|
use App\Models\PressRelease;
|
|
use App\Models\PressReleaseSearchIndex;
|
|
|
|
/**
|
|
* Hält die Suchtabelle synchron zur Pressemitteilung.
|
|
*
|
|
* Der Importer und die Anwendung schreiben durchgängig über Eloquent
|
|
* (`updateOrCreate`, `->save()`), deshalb greift der Observer auch bei
|
|
* Importen. Wer die Tabelle künftig per Query-Builder in Massen ändert, muss
|
|
* anschließend `php artisan press-releases:reindex-search` laufen lassen —
|
|
* Model-Events greifen dort nicht.
|
|
*/
|
|
class PressReleaseSearchObserver
|
|
{
|
|
public function created(PressRelease $pressRelease): void
|
|
{
|
|
PressReleaseSearchIndex::syncFor($pressRelease);
|
|
}
|
|
|
|
/**
|
|
* Nur neu indizieren, wenn sich wirklich ein durchsuchbares Feld geändert
|
|
* hat — der Textkörper macht den Großteil der Datenmenge aus, ihn bei
|
|
* jedem Statuswechsel neu zu schreiben wäre reine Verschwendung.
|
|
*
|
|
* Bewusst `updated` statt `saved` mit einer `wasRecentlyCreated`-Abfrage:
|
|
* Dieses Kennzeichen bleibt für die Lebensdauer der Instanz gesetzt, ein
|
|
* späteres Update auf demselben Objekt hätte den Index also trotzdem
|
|
* jedes Mal neu geschrieben.
|
|
*/
|
|
public function updated(PressRelease $pressRelease): void
|
|
{
|
|
if (! $pressRelease->wasChanged(PressReleaseSearchIndex::SourceColumns)) {
|
|
return;
|
|
}
|
|
|
|
PressReleaseSearchIndex::syncFor($pressRelease);
|
|
}
|
|
|
|
/**
|
|
* Hartes Löschen räumt der Fremdschlüssel (cascadeOnDelete) mit ab; ein
|
|
* Soft Delete lässt die Index-Zeile bewusst stehen, weil die Suche
|
|
* ohnehin über `press_releases` filtert und gelöschte PMs dort schon
|
|
* ausgeschlossen sind.
|
|
*/
|
|
public function forceDeleted(PressRelease $pressRelease): void
|
|
{
|
|
PressReleaseSearchIndex::query()->whereKey($pressRelease->getKey())->delete();
|
|
}
|
|
}
|