Warum ich Search API + Facet API durch 150 Zeilen eigenes Modul ersetzt habe
Ein Refactoring nach Jahren des Update-Frusts
Sechs Module für 16 Produkte: jahrelang hat mich die Such- und Filter-Architektur einer kleinen Manufaktur-Website bei jedem CMS-Update zur Verzweiflung gebracht. Heute habe ich sie durch ein eigenes Modul mit 150 Zeilen Code ersetzt — und dabei mehr gelernt, als in den vielen gescheiterten Update-Versuchen davor zusammen.
Eine kleine Vorgeschichte
Die Website einer handwerklichen Specksteinofen-Manufaktur — 16 Produkte, ein paar Inhaltsseiten, ein Kontaktformular. Ich habe sie ursprünglich mit Drupal 8 aufgesetzt. Damals schien das die zukunftssichere Wahl. Doch je länger das Projekt lief, desto klarer wurde: Drupal 8 (und alles, was danach kam) ist für eine handwerkliche Manufaktur-Website eine Nummer zu gross. Composer, Twig, regelmässige API-Brüche, Major-Versions-Migrationen alle paar Jahre — das ist Overhead, den ein Handwerksbetrieb nicht bezahlen will.
Also haben wir die Site auf Backdrop CMS umgestellt — den Community-Fork, der den Geist von Drupal 7 weiterführt: stabile API, kleinere Update-Schritte, ohne Composer-Zwang. Wartung statt Disruption.
Soweit das Versprechen. Und es wurde grösstenteils auch eingehalten — bis auf eine Komponente, die mich über die Jahre immer wieder eingeholt hat: das Such- und Filter-System der Produkt-Übersicht.
Die Update-Falle
Die Filter der Produkt-Übersicht stützten sich auf den klassischen Drupal-Such-Stack, der nach Backdrop migriert worden war:
- Search API — abstrahierte Such-API
- Search API Database Service — DB-Backend für den Index
- Search API Views — Integration mit Views
- Search API Facetapi — Bindeglied
- Facet API — die eigentlichen Filter
- Entity Plus — zusätzliches Hilfs-Modul als Pflichtabhängigkeit
Sechs Module, ein engmaschiges Abhängigkeitsgeflecht. Funktional in Ordnung, aber massiv überdimensioniert für 16 Produkte — diese Werkzeuge sind für Suchportale mit zehntausenden Inhalten konzipiert.
Und mit jedem Backdrop-Update kam dasselbe Drama: ich startete den Update-Versuch, die Filter brachen, die Produkt-Seite zeigte SQL-Fehler, gelegentlich quittierte die ganze Site mit HTTP 500. Jedes Mal landete ich beim selben Schluss: Rollback, weiter laufen lassen wie bisher, Update verschieben.
So vergingen die Monate. Die Site blieb auf Backdrop 1.24.1 vom Januar 2025, während längst 1.34.0 verfügbar war. Auf dem Hostpoint-Server lief inzwischen PHP 8.3, während die alten Such-Module noch aus der PHP-7-Ära stammten und mit Strict Typing nicht klarkamen.
Der Tag, an dem ich es nochmal versuchte
Heute habe ich mir vorgenommen: jetzt oder gar nicht.
Erste Strategie: die alten Module updaten, Bugs patchen, durchziehen. Klassischer Whack-a-Mole.
TypeError: SelectQuery::fields(): Argument #2 ($fields)
must be of type array, null given
Eine alte Drupal-Konvention — entity_load_multiple($type, FALSE) für „lade alle Entities" — knallt unter PHP 8 mit strict typing. Ich patchte die Stelle in search_api_views.views.inc. Es crashte an der nächsten Stelle in rules.module. Ich patchte die. Es crashte in entity_plus. Ich patchte dort eine zentrale Bridge — und übersah, dass diese Änderung an anderer Stelle die Semantik kippte. Auf einmal lud Backdrop keine Indizes mehr statt aller.
Nach gut einer Stunde Whack-a-Mole standen vier Patches in vier verschiedenen Dateien, eine geänderte View-Konfiguration — und mir dämmerte, was ich rückblickend schon Jahre früher hätte erkennen sollen:
Ich verteidige hier eine Architektur, die für meine Anforderung nie passend war.
Der Schritt zurück
16 Produkte. Eine Hand voll Filter-Tags. Keine Volltextsuche. Keine komplexen Facetten-Hierarchien.
Was ich wirklich brauchte:
- Eine Liste aller Produkte
- Eine Checkbox-Liste mit Tags zum Filtern
- Anzahl-Anzeige pro Tag
- Klick auf Tag = sofortige Filterung
Das ist eine Standard-View mit exposed Filter. Genau wofür Backdrop Views konzipiert ist — ganz ohne Search-API-Ebene dazwischen.
Die neue Architektur
Die Produkt-Übersicht wurde komplett neu gebaut:
1. Eine reguläre Views-Konfiguration
Statt base_table: search_api_index_produkt_index (einer virtuellen Tabelle des Such-Index) jetzt einfach base_table: node mit Filter type = produkte und status = 1. Klassische Views-API. Keine Magie.
2. Ein kleines eigenes Modul: speckstein_filters
Das Modul macht genau drei Dinge:
htdocs/modules/speckstein_filters/
├── speckstein_filters.info (4 Zeilen)
├── speckstein_filters.module (~100 Zeilen PHP)
└── speckstein_filters.css (~50 Zeilen)
a) Ein Block, der die Tags rendert — direkt aus der Datenbank, mit Count-Aggregation:
$sql = "
SELECT t.tid, t.name, COUNT(DISTINCT n.nid) AS cnt
FROM {taxonomy_term_data} t
INNER JOIN {field_data_field_tags} f ON f.field_tags_tid = t.tid
INNER JOIN {node} n ON n.nid = f.entity_id
AND n.type = 'produkte' AND n.status = 1
WHERE t.vocabulary = 'tags'
GROUP BY t.tid, t.name
HAVING cnt > 0
ORDER BY t.weight, t.name
";
Eine SQL-Abfrage. Tags ohne zugeordnete Produkte fliegen automatisch raus. Counts sind kontextuell korrekt.
b) HTML-Rendering der Checkboxes — manuell, ohne Form-API-Magie:
<input type="checkbox" name="tags[]" value="11"
class="form-checkbox">
<label>antik <span class="speckstein-filter-count">(5)</span></label>
Das name="tags[]" Format ist exakt, was die Backdrop-View als URL-Parameter erwartet. Filter funktioniert via ?tags[]=11 — keine zusätzliche Logik nötig.
c) Auto-Submit per Mini-JavaScript:
$form.find('input[type=checkbox]').on('change', function () {
$form.submit();
});
$form.find('.speckstein-filters-submit').hide();
Klick auf Checkbox = sofort gefiltert. Der „Anwenden"-Button bleibt als Fallback ohne JavaScript erhalten (Progressive Enhancement).
d) Sichtbarkeits-Steuerung — Filter erscheint nur auf der Produkte-Seite:
if (current_path() !== 'node/16') {
return NULL;
}
Eine Zeile. Keine Block-Visibility-Konfiguration im Admin nötig.
Der Vergleich
| Vorher | Nachher | |
| Verwendete Module | 6 (+ Abhängigkeiten) | 1 (eigenes) |
| Codezeilen für Filter-Logik | mehrere Zehntausend | ~150 |
| Update-Abhängigkeit | hoch | keine |
| PHP-Patches | 4 in fremdem Code | 0 |
| Datenbank-Tabellen | 9 zusätzlich | 0 |
| Anpassbarkeit | umständlich via Admin | direkt im Code |
| Klick-zu-Filter | Klick + „Anwenden" | nur Klick |
Was ich daraus mitnehme
Kontext schlägt Vollständigkeit. Search API + Facetapi sind grossartige Werkzeuge — für die Probleme, die sie lösen. Für eine handwerkliche Manufaktur-Website mit 16 Produkten sind sie die berühmte Kanone auf den Spatz.
Eigener Code ist nicht schlechter als fremder Code. 150 Zeilen, die genau das tun was ich brauche, sind in Summe wartungsärmer als ein Stack aus sechs Modulen, von denen jeder Update-Zyklus zur Stolperfalle wird.
Backdrop-Bordmittel reichen erstaunlich oft. Backdrop Views kann mehr als man denkt, sobald man sich von der Idee verabschiedet, dass „echte" Filter Search-API brauchen.
Patches sind Schulden. Jeder Patch in fremdem Code muss bei jedem Update wieder eingespielt werden. Vier Patches in fremden Modulen sind nicht günstiger als ein eigenes Modul — sie sind teurer.
Vor allem aber: das Refactoring hätte ich Jahre früher angehen sollen. Die vielen gescheiterten Update-Versuche zusammengerechnet kosteten weit mehr Zeit als die Umstellung selbst — und mit jedem aufgeschobenen Update wurde die Lücke zwischen Live-Stand und aktueller Version grösser, die Migration heikler. Manchmal ist die schmerzhafteste Erkenntnis am Ende eines Tages auch die wertvollste.
Das Ergebnis
Backdrop 1.34.0 — endlich auf aktuellem Stand. 32 Module aktualisiert, sechs Module deinstalliert, neun Datenbanktabellen gedroppt. Die Website lädt schneller (weniger Module = weniger Boot-Overhead, kein Search-Index mehr im Hintergrund). Die nächste CMS-Aktualisierung wird ein Routine-Vorgang statt eines Abenteuers.
Manchmal ist der saubere Weg, das Werkzeug nicht zu reparieren — sondern festzustellen, dass man das falsche Werkzeug hatte. Und dann den Mut zum Schnitt zu finden.