Volltextsuche ohne Suchserver: was auf Shared Hosting trägt und wo es aufhört

Volltextsuche ohne Suchserver: was auf Shared Hosting trägt und wo es aufhört

Wie baut man eine intelligente Suche für tausende Termine auf einfachem Shared Hosting? Erfahren Sie, wie normalisierte Datenbankspalten, clevere Fallback-Strategien und eine strikte Sichtbarkeitslogik dafür sorgen, dass Eltern und Trainer trotz Tippfehlern oder fehlender Umlaute immer genau das finden, was sie suchen.

Dennis Schwenker-Sanders 4 Min. Lesezeit

Eine Mutter tippt "Turnen" und erwartet das Kinderturnen. Ein Vater tippt "maerchen", weil sein Handy keine Umlaute vorschlägt, und will die Märchenstunde. Ein Trainer tippt "U12" und sucht das "U 12 Training". Für Lokido, eine Familienplattform für Termine im Nordwesten, brauchte ich eine Suche, die mit solchen Eingaben umgehen kann. Die Plattform läuft auf gewöhnlichem Shared Hosting. Ein Suchserver war keine Option.

Die Anforderungen

Gesucht wird über Termine, Terminreihen, Vereinsgesuche und Anbieter mit eigener Seite. Die Datenmenge liegt im Bereich einiger tausend Datensätze. Die harte Bedingung kam aus einer anderen Ecke: Termine in Moderation oder außerhalb ihres Sichtbarkeitsfensters antworten auf ihrer Detailseite mit 404, damit niemand von außen erkennt, dass es sie gibt. Die Suche durfte diese Regel nicht über Treffer, Trefferzahlen oder Vorschläge unterlaufen.

Der Aufbau

Die Grundlage sind normalisierte Suchspalten direkt an den Doctrine-Entities. Beim Speichern wird der Text kleingeschrieben, Umlaute werden aufgelöst und Sonderzeichen entfernt. Gesucht wird mit LIKE über diese Spalten, mehrteilige Eingaben werden in Wörter zerlegt und einzeln geprüft. Damit findet "maerchen" wie "marchen" dieselbe Märchenstunde.

Darüber sitzt ein Such-Service mit einer eigenen Schnittstelle. Er rankt nach Relevanz und Datum und prüft die Sichtbarkeit mit genau derselben Logik, die auch Listen und Zähler verwenden. Ein Architekturtest stellt sicher, dass kein zweiter Zählpfad entsteht. Das ist der Teil, den ich bei jedem ähnlichen Projekt wieder so bauen würde.

Findet die Suche nichts, versucht sie es in festen Schritten weiter und sagt auf der Ergebnisseite, welchen Schritt sie gegangen ist:

  1. jedes Wort einzeln statt aller zusammen
  2. Einzahl statt Mehrzahl
  3. ein Korrekturvorschlag bei Tippfehlern
  4. gesetzte Filter lösen
  5. den ganzen Landkreis statt nur der Stadt

Der letzte Schritt landet dort, wo die Plattform ohnehin gegliedert ist, auf Landkreisebene. Wie so ein Landkreis aussieht, zeigt zum Beispiel der Hub für den Landkreis Oldenburg.

Für wiederkehrende Anlässe wie Laternenumzüge oder Weihnachtsmärkte gibt es feste Themenrouten. Die erlaubten Slugs stehen mit ihren Suchbegriffen in der Bundle-Konfiguration als YAML. Alles, was dort nicht steht, antwortet mit 404. Es gibt dafür keine Entity und keine Admin-Oberfläche, weil sich die Liste zweimal im Jahr ändert.

Die Vorschläge beim Tippen kommen über einen kleinen JSON-Endpunkt und einen Stimulus-Controller im Kopfbereich. Ohne JavaScript führt der Link auf eine normale Suchseite. Ausgewertet werden nur Suchbegriffe als Tageszähler, ohne Personenbezug. Eingaben, die nach E-Mail-Adresse oder langer Ziffernfolge aussehen, werden verworfen, bevor sie gezählt werden.

Was teurer war als geplant

Die erste Normalisierung hat Einzelzeichen-Wörter zerlegt. Aus "U 12" wurden die Fragmente "u" und "12", aus "E-Jugend" wurden "e" und "jugend", und bei "Rock 'n' Roll" ging das "n" verloren. Die Fragmente trafen fast alles und damit nichts Brauchbares. Die Lösung waren zusätzliche Verbund-Varianten, die solche Einzelzeichen mit dem Nachbarwort zusammenziehen.

Direkt danach kam der zweite Fehler. Die Verbünde entstanden auch über Feldgrenzen hinweg, also aus dem letzten Wort des Titels und dem ersten Wort der Beschreibung. Das erzeugte Treffer für Wörter, die in keinem der beiden Felder standen. Die Normalisierung musste deshalb je Feld getrennt laufen. Beide Korrekturen zusammen haben etwa so viel Zeit gekostet wie das ursprüngliche Fundament.

Aus der Praxis

Was ich daraus mitnehme: Testdaten für eine Suche sollten nicht aus sauberen Titeln bestehen, sondern aus dem, was Vereine tatsächlich eintragen. Die Fehler oben wären mit echten Titeln aus der Datenbank am ersten Tag aufgefallen.

Was davon übertragbar ist

Der Ansatz passt für Symfony-Anwendungen mit einigen hundert bis wenigen tausend durchsuchbaren Datensätzen auf Hosting ohne eigene Dienste. Das trifft auf viele Verzeichnisse, kleinere Shop-Kataloge und Veranstaltungskalender zu. Zwei Teile lassen sich auch einzeln übernehmen: die Regel, dass Sichtbarkeit und Zählung genau eine Implementierung haben und ein Test das absichert, und eine Null-Treffer-Kette, die dem Nutzer sagt, was sie getan hat, statt eine leere Seite zu zeigen.

Wo der Weg aufhört

Sobald die Datenmenge in den Bereich mehrerer zehntausend Datensätze wächst oder die Relevanz mehr braucht als Treffer auf einzelne Wörter, etwa Phrasen, Wortnähe oder gewichtete Synonyme, tragen LIKE-Abfragen über normalisierte Spalten nicht mehr. Dann ist ein echter Index nötig. Weil der Such-Service hinter einer Schnittstelle liegt, bleibt dieser Wechsel ein Austausch der Implementierung und kein Umbau der Anwendung.

Die Suche läuft jetzt seit ein paar Tagen. Als Nächstes schaue ich mir an, welche Begriffe ohne Treffer bleiben, weil das die ehrlichste Liste dessen ist, was in den Daten noch fehlt.

Häufige Fragen

Warum nicht einfach MariaDB FULLTEXT?

FULLTEXT arbeitet mit Mindestwortlängen und Stoppwörtern. Genau die kurzen Fragmente, um die es hier ging, fallen dabei heraus: "U 12", "E-Jugend", Umlaut-Varianten. Die Normalisierung hätte ich trotzdem gebraucht. Bei der Datenmenge war LIKE über vorbereitete Spalten der kleinere Umbau.

Wie verhindert man, dass die Suche unsichtbare Inhalte verrät?

Indem Treffer, Trefferzahlen und Vorschläge durch dieselbe Sichtbarkeitsprüfung laufen wie Listen und Detailseiten. Eine zweite, "nur schnell für die Suche" geschriebene Bedingung ist der Moment, in dem Entwürfe oder moderierte Inhalte über eine Trefferzahl auffallen. Ein Architekturtest prüft, dass es nur einen Zählpfad gibt.

Braucht die Null-Treffer-Kette nicht viele zusätzliche Abfragen?

Nur, wenn die erste Abfrage leer ist. Die Kette bricht beim ersten Schritt ab, der Treffer liefert. Bei den meisten Suchen läuft also genau eine Abfrage.

Lässt sich das später auf einen echten Suchdienst umstellen?

Ja, wenn der Such-Service eine eigene Schnittstelle hat und Controller nur mit ihr sprechen. Dann tauscht man die Implementierung und behält Normalisierung, Sichtbarkeitsregel und Null-Treffer-Kette als fachliche Logik.

Artikel teilen: