← Alle Beiträge

Shopware 6: „index_not_found_exception: no such index [sw6_product_…]“ alle fünf Minuten im Log

Shopware 6: „index_not_found_exception: no such index [sw6_product_…]“ alle fünf Minuten im Log

Im Storefront fällt nichts auf, die Suche liefert Treffer, aber das Log läuft voll. Alle fünf Minuten derselbe Eintrag, im elasticsearch-Channel und noch einmal als „Scheduled task failed with“. In den Produktionsdaten, die wir uns angesehen haben, kam ein einzelner Shop so auf mehrere tausend identische Meldungen, bevor jemand nachgesehen hat.

Die Fehlermeldung

no such index [sw6_product_1700000000]

Scheduled task failed with: no such index [sw6_product_1700000000]
{"error":"[object] (OpenSearch\\Exception\\NotFoundHttpException(code: 0):
index_not_found_exception: no such index [sw6_product_1700000000] at
/var/www/shop/vendor/opensearch-project/opensearch-php/src/OpenSearch/Exception/HttpExceptionFactory.php:26)",
"scheduledTask":"shopware.elasticsearch.create.alias"}

Je nach Client und Setup taucht dieselbe Meldung auch als roher JSON-Body auf ("type":"index_not_found_exception","reason":"no such index [...]","status":404). Die Ursache ist dieselbe.

Wichtig ist der Indexname. Da steht nicht sw6_product, also der Alias, den das Storefront abfragt, sondern ein Name mit Zeitstempel. Shopware sucht also gezielt nach einem bestimmten Index, den es nicht mehr gibt.

Was dahintersteckt

Wenn du bin/console es:index startest, legt Shopware einen neuen Index mit Zeitstempel an, zum Beispiel sw6_product_1700000000, und schreibt dazu eine Zeile in die Tabelle elasticsearch_index_task: Indexname, Alias und doc_count, also die Zahl der noch ausstehenden Dokumente. Die Indexing-Messages laufen über die Queue und zählen doc_count herunter. Der Alias sw6_product zeigt währenddessen weiter auf den alten Index, deshalb sieht im Storefront niemand den halbfertigen Stand.

Den Wechsel übernimmt der Scheduled Task shopware.elasticsearch.create.alias, standardmäßig alle fünf Minuten. Der CreateAliasTaskHandler geht jede Zeile in elasticsearch_index_task durch und macht als Erstes das hier, ohne vorher zu prüfen, ob der Index existiert:

foreach ($indices as $row) {
    $index = $row['index'];
    $count = (int) $row['doc_count'];

    $this->client->indices()->refresh(['index' => $index]);

    if ($count > 0) {
        continue;
    }
    // Alias umhängen, dann:
    // DELETE FROM elasticsearch_index_task WHERE id = :id
}

Ist der Index weg, schlägt refresh() mit 404 fehl. Die Exception bricht die Schleife ab, das DELETE kommt nie dran, und die Zeile bleibt stehen. Fünf Minuten später das Gleiche. Der Task ist mit shouldRescheduleOnFailure() = true gebaut, er gibt also nie auf. Dieser Code ist in 6.5.8, 6.6.10 und 6.7.0 identisch, wir haben die Tags auf GitHub verglichen.

Unangenehm wird es, wenn noch weitere Zeilen in der Tabelle stehen, etwa ein neuer Index für Kategorien oder Hersteller. Die kommen nach dem Abbruch nicht mehr dran, ihr Alias wird nie umgehängt. Das Storefront arbeitet dann still mit veralteten Daten weiter.

Bleibt die Frage, wer den Index gelöscht hat. Drei Wege sehen wir in der Praxis:

  1. es:index:cleanup während eines laufenden Reindex. Der ElasticsearchOutdatedIndexDetector hält jeden Index ohne Alias für veraltet. Ein Index, der gerade befüllt wird, hat aber noch keinen Alias. Läuft der Cleanup per Cronjob oder Deploy-Skript, während die Queue noch arbeitet, löscht er genau den neuen Index.
  2. Ein geteilter Cluster mit gleichem Präfix. Der Detector sucht per Wildcard nach <präfix>_product_*. Laufen Staging und Live (oder zwei Shops) gegen denselben OpenSearch-Cluster und beide mit dem Standard-Präfix, dann löscht ein es:reset oder es:index:cleanup auf Staging auch die Indizes von Live.
  3. Der Cluster hat seine Daten verloren. Typisch: ein OpenSearch-Container ohne persistentes Volume oder ein Snapshot-Restore auf einen älteren Stand. Die Tabelle in MySQL weiß davon nichts.

Prüfen, ob es wirklich das ist

Zuerst die Tabelle ansehen:

SELECT entity, `index`, alias, doc_count, created_at
FROM elasticsearch_index_task;

Dann den Cluster fragen, welche Indizes und Aliase tatsächlich existieren (Host und Präfix an dein Setup anpassen):

curl -s 'http://opensearch:9200/_cat/indices/sw6_*?v&s=index'
curl -s 'http://opensearch:9200/_cat/aliases/sw6_*?v'

Taucht der Index aus der Fehlermeldung in elasticsearch_index_task auf, aber nicht in _cat/indices, ist es dieser Fall. bin/console es:status hilft hier nur bedingt: Es liest dieselbe Tabelle und meldet je nach doc_count „in progress“ oder „completed“, ob der Index existiert, prüft es nicht.

Die Lösung

Wenn der Alias sw6_product noch auf einen intakten älteren Index zeigt, funktioniert das Storefront. Dann reicht es, die verwaiste Zeile zu entfernen. Datenbank vorher sichern:

DELETE FROM elasticsearch_index_task
WHERE `index` = 'sw6_product_1700000000';

Danach den Alias-Task einmal von Hand laufen lassen, damit eventuell blockierte Zeilen für andere Entitäten abgearbeitet werden:

bin/console es:create:alias

Der alte Index ist damit aber auch veraltet. Für einen frischen Stand neu indexieren, wenn du nur Produkte brauchst, mit --only:

bin/console es:index --only=product
bin/console messenger:consume async --time-limit=300
bin/console es:create:alias

Mit --no-queue läuft die Indexierung direkt im Prozess statt über die Queue. Bei großen Katalogen lieber auf der Kommandozeile mit genug memory_limit als über einen Cronjob mit kurzem Timeout.

Ist gar kein brauchbarer Index mehr da, oder sind die Aliase im Cluster durcheinander, ist der saubere Weg ein kompletter Reset. es:reset löscht alle Shopware-Indizes mit deinem Präfix, leert elasticsearch_index_task und entfernt die wartenden ElasticsearchIndexingMessages aus messenger_messages:

bin/console es:reset
bin/console es:index
bin/console messenger:consume async --time-limit=600
bin/console es:create:alias

Bis der neue Index steht, hat das Storefront keine Suche aus OpenSearch. Ob es dann auf MySQL zurückfällt oder einen Fehler zeigt, hängt an SHOPWARE_ES_THROW_EXCEPTION. Steht das auf 1, mach den Reset nicht zur Hauptverkehrszeit.

Damit es nicht wieder passiert

Das Löschen der Zeile behebt das Symptom. Wenn die Ursache Nummer 1 oder 2 ist, kommt der Fehler beim nächsten Reindex zurück.

  • es:index:cleanup erst laufen lassen, wenn elasticsearch_index_task leer ist. In einem Deploy-Skript lässt sich das mit einer einfachen SQL-Abfrage absichern, bevor der Cleanup startet.
  • Jede Umgebung auf einem geteilten Cluster bekommt ihr eigenes Präfix:
# .env.local auf Staging
SHOPWARE_ES_INDEX_PREFIX=sw6_staging
SHOPWARE_ADMIN_ES_INDEX_PREFIX=sw6_admin_staging

Nach einer Präfix-Änderung ist ein vollständiges es:index fällig, die alten Indizes werden nicht umbenannt.

  • Bei selbst betriebenem OpenSearch im Container: Datenverzeichnis auf ein Volume legen. Sonst ist nach jedem Neustart alles weg, und die Tabelle zeigt auf Indizes, die es nicht mehr gibt.

Ein offenes Issue zu diesem Verhalten haben wir im Shopware-Repository nicht gefunden. Eine Existenzprüfung vor refresh() würde den Loop beenden. Wer das upstream angehen will, findet die Stelle in src/Elasticsearch/Framework/Indexing/CreateAliasTaskHandler.php.

Diese Meldung tut niemandem weh, bis der nächste Reindex nicht mehr live geht und Produkte in der Suche fehlen. ShopSignal zählt Fehler und Warnungen pro Shop, macht die Logs per Volltextsuche durchsuchbar und behält den Zustand des Elasticsearch-Clusters im Blick. Mit einer Alarmregel auf die Fehlerrate erfährst du am ersten Tag davon und nicht erst nach ein paar tausend Einträgen.