Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/gibro/igm-bildungsprogramm/archive/refs/heads/main.zipEigenständiges WordPress-Plugin für Seminar- und Veranstaltungsprogramme: durchsuchbare Seminarliste mit Filterleiste, Online-Anmeldung mit automatischem Mail-Versand, CSV-Import und Marketing-Kacheln. Entwickelt für das Bildungsprogramm der IG Metall, aber ohne externe Abhängigkeiten und damit auf jeder WordPress-Installation einsetzbar – es werden keine Page-Builder- oder Formular-Plugins benötigt.
Inhalt
- Funktionen
- Voraussetzungen
- Installation
- Einrichtung
- Shortcodes
- Benachrichtigungen
- Verfügbarkeits-Ampel
- Kampagnen-Auswertung
- Die Detailseiten
- Ausbildungsreihen
- Der Import-Lauf
- Kosten und Orte ab Programm 2027
- Anmeldevarianten
- Anmeldeformulare
- Die drei Haken am Seminar
- Datenqualität
- Die Seminarliste im Backend
- Die Bearbeiten-Maske
- Eigene Datenfelder
- Datenpflege: Export und Umzug
- Abgleich mehrerer Websites
- Datenmodell
- Anpassung
- Aufbau des Codes
- Lizenz
Funktionen
-
Seminare als eigener Beitragstyp (
bi_seminar) – gepflegt mit der WordPress-eigenen Editier-Oberfläche, inklusive Start-/Enddatum, Seminarnummer, Plätzen, Kosten und Buchungsstatus. -
Online-Seminare als zweiter Beitragstyp (
bi_online) – eigenes Feldset (Untertitel, Referent*innen, Veranstalter*in, Webinar-Tool, Anmelde- und Teilnahme-Link), aber dieselben Taxonomien und dieselbe Darstellung im Frontend. Im Backend stehen sie gemeinsam mit den Präsenz-Seminaren unter „Seminare"; das Auswahlfeld Seminarform grenzt bei Bedarf ein. -
Such- & Filterleiste per Shortcode: Volltextsuche mit Autovervollständigung, Filter-Chips mit Mehrfachauswahl (echtes ODER), Datumsbereich mit Kalender, Live-Trefferzähler. Der Chip Seminarform trennt Präsenz- und Online-Seminare. Die Volltextsuche liest alle Datenfelder – Titel, Beschreibung, Themen, Seminarort, Referent*innen, Ansprechpartner*in, Seminarnummer, den Namen der Ausbildungsreihe und die Begriffe aller Filter-Chips. Alles zusammen steht in einer eigenen Indextabelle mit
FULLTEXT-Index. Wer mag, verknüpft mitODER,NICHT,"Wortgruppen"und Klammern — ohne Zutun bleibt es beim UND. -
Online-Anmeldung als vierstufiger Formular-Wizard; Anmeldungen werden in der Datenbank gespeichert und sind im Backend einsehbar.
-
Benachrichtigungen: beliebig viele Mails pro Anmeldung – an Teilnehmer*innen, die zuständige Geschäftsstelle (per PLZ-Zuordnung), das Bildungszentrum oder feste Adressen; mit Bedingungen und Platzhaltern.
-
CSV-Import für Seminare mit frei zuordenbaren Spalten (Mapping-Schritt mit Auto-Erkennung) – je ein eigener Tab für Präsenz- und Online-Seminare.
-
CSV-Import für PLZ → Geschäftsstelle (eigene Tabelle, schneller Lookup).
-
Marketing-Kacheln: Bild-/Text-Teaser, die auf die Suche mit vorbefüllten Filtern verlinken – inklusive Backend-Builder mit Live-Vorschau.
-
Verfügbarkeits-Ampel: auf der Detailseite steht neben dem Termin, ob er noch frei ist (Verfügbar / Fast ausgebucht / Warteliste möglich). Die Angabe wird nachts mit der Seminarsuche auf
igmetall.deabgeglichen und über die Seminarnummer zugeordnet. -
Kampagnen-Auswertung: eigene Links für Newsletter und Mailings, die den Weg vom Klick über die Seminaransicht bis zur abgeschickten Anmeldung nachvollziehbar machen.
-
Mini-Dashboard im Backend mit Kennzahlen, Warnhinweisen (z. B. fehlende E-Mail-Adressen) und Einrichtungs-Anleitung.
-
Abgleich mehrerer Websites – Sprockhövel und Berlin pflegen ihre Seminare selbst, bildung.igmetall.de holt sie ab. Kein iframe: Jede Website führt ihren eigenen Bestand und findet in der Suche nur ihn.
Voraussetzungen
| Version | |
|---|---|
| WordPress | ≥ 5.8 |
| PHP | ≥ 7.4 |
Keine weiteren Plugin-Abhängigkeiten. Der Datums-Kalender (flatpickr) wird mitgeliefert.
Installation
Aus dem Release (empfohlen):
- Aktuelles Release als ZIP herunterladen.
- Im WP-Adminbereich unter Plugins → Installieren → Plugin hochladen die
ZIP-Datei hochladen – oder den entpackten Ordner
igm-bildungsprogrammnachwp-content/plugins/kopieren. - Plugin unter Plugins aktivieren.
Aus dem Repository:
cd wp-content/plugins
git clone https://github.com/<user>/igm-bildungsprogramm.git
Beim Aktivieren werden die Tabellen wp_bi_plz, wp_bi_anmeldungen,
wp_bi_kampagnen und wp_bi_events angelegt, der Beitragstyp registriert und
drei Standard-Benachrichtigungen erstellt. Deaktivieren und Löschen entfernt keine Daten.
Einrichtung
Alle Schritte finden sich auch im Backend unter Bildungsprogramm → Übersicht.
- PLZ importieren – Einstellungen → Tab „PLZ-Import": CSV mit den Spalten
PLZ,Geschäftsstelle,E-Mail(Trennzeichen,oder;wird automatisch erkannt). - Seminare importieren – Einstellungen → Tab „Seminar-Import": CSV
hochladen, im zweiten Schritt jede Spalte einem Feld zuordnen. Der Import
läuft anschließend in Häppchen mit Fortschrittsbalken und Zähler. Mehrfach-Felder
(Zielgruppe, Freistellung) dürfen mehrere Werte je Zelle enthalten, getrennt
mit
|oder,. Alternativ lassen sich Seminare komplett von Hand anlegen. - Bildungszentren-Mails – unter Seminare → Bildungszentrum je Eintrag eine E-Mail-Adresse hinterlegen (für den Trigger „Mail an Bildungszentrum").
- Benachrichtigungen prüfen und anpassen – gleichnamiger Menüpunkt.
- Such-Seite anlegen mit
[bi_seminarsuche]. - Anmelde-Seite anlegen mit
[bi_anmeldung]. - Unter Einstellungen → Seiten die beiden Seiten zuordnen (oder die automatische Erkennung nutzen) und die Anmeldevarianten konfigurieren.
- Optional: unter Kampagnen je Newsletter einen Link anlegen, wenn die Wirkung von Mailings ausgewertet werden soll.
Shortcodes
| Shortcode | Zweck | Attribute |
|---|---|---|
[bi_seminarsuche] |
Such-/Filterleiste + Ergebnisliste | anmeldung_url, per_page (Vorgabe, Standard 20), pro_seite (wählbare Stufen, Standard 10\|20\|50\|100, nein = keine Wahl), programm, form (praesenz/online) |
[bi_suchmaske] |
Nur die Suchmaske, ohne Ergebnisliste – Button springt auf die Übersicht | ziel_url, button, titel, kicker, hinweis, programm, form |
[bi_anmeldung] |
Anmeldeformular | seminar="ID" für ein festes Seminar, reihe="ID" für eine Sammelanmeldung |
[bi_kachel] |
Marketing-Kachel (Teaser mit Filter-Link) | siehe unten |
[bi_kacheln] |
Optionaler Grid-Container für mehrere Kacheln | spalten (2/3/4, Standard 3) |
[bi_reihen] |
Kachelübersicht aller Ausbildungsreihen | titel, overline, subline, anzahl |
[bi_seminarsuche] – Such- und Filterleiste
Zeigt die Filterleiste (Volltextsuche, Seminarform, Programm, Bildungszentrum,
Themenfeld, Zielgruppe, Freistellung, Zeitraum) mit der Ergebnisliste. Gefiltert
wird über GET-Parameter (?thema=…&ort=…, Mehrfachwerte pipe-getrennt) –
Filterseiten sind damit verlinkbar und bookmarkfähig. Angezeigt werden nur
kommende, sichtbare Seminare, sortiert nach Startdatum.
Wie viele Seminare auf eine Seite kommen (seit 1.124.0)
Zwischen der Filterleiste und der Trefferliste steht rechtsbündig die Wahl „Seminare je Seite" (10 · 20 · 50 · 100). Die Redaktion kann das nicht für beide Seiten zugleich richtig entscheiden: Wer sucht, will eine kurze Liste; wer stöbert, will scrollen statt klicken. Also entscheidet es, wer davorsitzt.
Sie steht über der Liste, weil sie zu dem gehört, was gleich kommt – unter der Liste hätte man erst zwanzig Treffer durchgescrollt, um zu erfahren, dass es auch fünfzig auf einmal gegeben hätte. Rechtsbündig, weil dort schon die Trefferzahl der Filterleiste steht; links bleibt der Rand für die roten Balken der Trefferzeilen frei. Das Blättern bleibt unverändert mittig unter der Liste.
[bi_seminarsuche] Vorgabe 20, Stufen 10|20|50|100
[bi_seminarsuche per_page="50"] Seite startet mit 50 – 50 steht mit zur Wahl
[bi_seminarsuche pro_seite="10|25|50"] eigene Stufen
[bi_seminarsuche pro_seite="nein"] keine Wahl anbieten, per_page gilt fest
Drei Entscheidungen stecken darin:
- Links, kein Auswahlfeld mit JavaScript. Jede Stufe ist eine eigene
Adresse (
?bi_pro_seite=50) – teilbar, als Lesezeichen brauchbar, und sie funktioniert auch dort, wo das Skript der Filterleiste nicht ankommt (Page-Builder, eingebetteter Rahmen). Die Vorgabe der Seite kommt ohne Parameter aus, damit die Adresse beim Zurückstellen wieder sauber ist. - Nur die angebotenen Stufen gelten. Der Wunsch steht in der Adresse und
ist damit öffentlich;
?bi_pro_seite=5000wäre eine Einladung, den Server rechnen zu lassen, und jede beliebige Zahl wäre zusätzlich ein eigener Eintrag im Seiten-Cache. Die Vorgabe ausper_pagereiht sich immer mit ein, sonst ließe sich der Ausgangszustand nicht wiederherstellen. - Die Wahl verschwindet, wo sie folgenlos wäre – bei
pro_seite="nein", wenn nur eine Stufe übrig bleibt, und wenn ohnehin alle Treffer auf die kleinste Stufe passen. Ein Bedienelement, das nichts ändert, ist keine Freundlichkeit.
Die gewählte Länge hängt an der Adresse und bleibt deshalb beim Blättern und beim Filtern erhalten. Auch „Alle zurücksetzen" in der Filterleiste lässt sie stehen: Das ist eine Aussage über die Ansicht, nicht darüber, welche Seminare gesucht werden. Ein Wechsel der Länge beginnt wieder auf Seite eins – Seite 7 von 40 hat bei 100 Treffern je Seite keine Entsprechung.
Im Einbettungsmodus (?bi_embed=1) bleibt es zusätzlich bei der alten
Freiheit: Dort nennt die Adresse weiterhin jede Zahl zwischen 3 und 50, weil
das fremde Redaktionssystem oft nur ein Adressfeld anbietet und bestehende
Rahmen nicht umspringen sollen.
Geprüft wird das in tests/test-pro-seite.php (ohne WordPress lauffähig).
Warum die Seite schnell lädt (seit 1.96.0)
Die Seminarsuche brauchte auf dem echten Bestand (2.400 Seminare, 51.000 Meta-Zeilen) rund 500 ms, davon 95 % in der Datenbank — und fast alles in drei Abfragen à ~120 ms: die Facetten-Basis, die Trefferliste und die Gesamtzahl.
Schuld war eine einzige Bedingung: „Auf der Website anzeigen". Als
meta_query geschrieben ist sie ein ODER („Wert ist 1 oder es gibt gar keine
Zeile"), und ein ODER zwingt WordPress, den meta_key aus der ON-Bedingung zu
nehmen und postmeta zweimal zusätzlich anzuhängen — einmal davon ganz ohne
Einschränkung. Aus 2.400 Seminaren wurden so über 40.000 Zwischenzeilen, die ein
GROUP BY danach wieder einsammelte.
Dieselbe Aussage als korrelierte Unterabfrage — „es gibt keine Zeile
_bi_anzeigen mit einem anderen Wert als 1" — kostet nichts, weil sie über den
post_id-Index läuft:
| Abfrage | vorher | nachher |
|---|---|---|
| Facetten-Basis | 121 ms | 4 ms |
| Trefferliste | 125 ms | 7 ms |
| Gesamtzahl | 124 ms | 4 ms |
Gemessen an derselben Datenbank, mit identischem Ergebnis (1777 Treffer) und
identischer Reihenfolge. Dazu holt die Gesamtzahl nicht mehr alle IDs
(nopaging) und sortiert sie auch nicht mehr nach post_date — gebraucht wird
die Zahl, also liest sie found_posts aus einer Abfrage über eine Zeile.
Benutzt wird das über BI_CPT::sichtbar_arg(), das ein Abfrage-Argument
(bi_sichtbar) setzt; BI_CPT::sichtbar_where() hängt die Unterabfrage an.
Das alte Fragment BI_CPT::visible_clause() bleibt bestehen und ist für kleine
Abfragen — zwei Dutzend Reihen, eine Kachel — völlig in Ordnung.
Eine Falle steckt darin.
get_posts()setztsuppress_filtersauf true, und dann läuftposts_wherenicht: Die Einschränkung fiele still weg, die Liste zeigte ein paar Einträge zu viel, und niemand suchte danach. Deshalb meldetBI_CPT::sichtbar_pruefen()diesen Fall über_doing_it_wrong(), statt ihn hinzunehmen. Die Gleichwertigkeit beider Formulierungen steht als Wahrheitstafel intests/test-sichtbarkeit.php.
Welche Chips erscheinen, stellt man unter Einstellungen → Tab
„Such-Filterleiste" je Filter mit einem Haken ein. Gedacht ist das für Filter,
die nur zeitweise gebraucht werden: Der Chip Programm (?programm=2027) ist
im Auslieferungszustand aus und wird für den Übergang eingeschaltet, in dem zwei
Programmjahre nebeneinander buchbar sind – danach wieder aus.
Ein abgeschalteter Filter verschwindet nur aus der Leiste; steht sein Wert in der Adresse, filtert er weiterhin. Sonst würden bestehende Links und Marketing-Kacheln in dem Moment still eine größere Treffermenge zeigen, in dem jemand den Haken entfernt.
Eigene Chips („Schnellzugriff"): die Adresse einer Suche
Unter Einstellungen → Such-Filterleiste → Eigene Chips steht je Zeile ein Name
und die Adresse einer Suche. Ein Klick auf den Chip setzt genau deren Filter,
ohne die Seite zu wechseln; ein zweiter Klick nimmt sie wieder zurück. Aus der
Adresse zählt nur der Filterteil — die Seite, utm_… und paged fallen weg
(BI_Filter::url_params).
Die Volltextsuche gehört dazu, samt Operatoren. Steht beim Kopieren ein
Suchbegriff im Feld, hängt er als q=… mit in der Adresse:
| In der Adresszeile | Was der Chip sucht |
|---|---|
?q=%22BR+kompakt%3A%22 |
"BR kompakt:" — genau diese Wortfolge |
?q=bonn+ODER+berlin |
bonn ODER berlin |
?q=arbeitsrecht+-online |
arbeitsrecht -online |
?q=%28bonn+%7C+berlin%29+jav |
(bonn \| berlin) jav |
Kopiert wird die Adresse so, wie sie im Browser steht — kodiert. Die Zeile unter dem Eingabefeld zeigt, was daraus gelesen wurde, und wie viele Seminare der Chip gerade träfe.
Behoben in 1.126.1. Bis 1.126.0 lief die eingegebene Adresse beim Speichern durch
sanitize_text_field()— und das entfernt jede Prozentfolge. Aus?q=%22BR+kompakt%3A%22wurde damit?q=BR+kompakt(die Anführungszeichen der Wortgruppe verschwanden) und aus?ort=Sprockh%C3%B6velein?ort=Sprockhvel. Geprüft wurde beim Speichern die rohe Eingabe — deshalb sah die Zeile unter dem Feld noch richtig aus und erst nach dem nächsten Öffnen falsch. Gespeichert wird jetzt überBI_Filter::url_saeubern(): kaputtes UTF-8, Steuerzeichen, HTML und Länge fliegen raus, die Kodierung bleibt.Ein vorhandener Chip mit kodierten Zeichen muss einmal neu eingefügt werden — was beim Speichern verlorenging, steht nirgends mehr.
Dass es so lange unbemerkt blieb, lag an der Attrappe im Test: Sie war
trim(strip_tags(…))und damit gutmütiger als WordPress. Eine Attrappe, die mehr durchlässt als das Original, prüft nichts — sie bestätigt nur. Sie bildet die Prozentfolgen-Falle jetzt nach.
Geprüft in tests/test-filterleiste-chips.php.
Seiten-Cache: das Plugin leert ihn selbst (seit 1.110.0)
Ein Seiten-Cache liefert fertiges HTML aus, ohne PHP zu starten — auf einer gemessenen Installation der Unterschied zwischen rund einer Sekunde und rund fünfzig Millisekunden. Er weiß aber nichts davon, wann sich der Seminarbestand ändert. Ohne Zutun zeigte die Liste nach einem CSV-Import stundenlang den alten Stand, und die Trefferzahl in der Filterleiste („1.200 buchbare Seminare") stünde eingefroren im HTML. Ein Cache, der falsche Zahlen ausliefert, ist schlimmer als kein Cache — beim langsamen Aufruf sieht man wenigstens, was stimmt.
Das Plugin leert ihn deshalb bei zwei Anlässen:
- Daten geändert — Seminar gespeichert, importiert, massenbearbeitet, gelöscht, Begriffe zugeordnet. Anders als beim Suchindex zählt hier jedes Meta-Feld: „Ausgebucht", „Freie Plätze" und das Startdatum tragen keine Wörter und stehen deshalb nicht im Suchindex — die Seite ändern sie trotzdem.
- Der Tag hat gewechselt. Buchbar heißt „Startdatum ab heute", und das wird beim Rendern ausgewertet. Eine um 23:50 gespeicherte Seite zeigte um 00:10 noch den Termin von gestern. Deshalb läuft kurz nach Mitternacht ein Leeren, ohne dass sich irgendetwas geändert haben muss.
Einmal je Aufruf, höchstens einmal je Minute. Ein Import setzt zwanzig
Meta-Felder je Zeile und legt hundert Zeilen je Häppchen an; würde jede Änderung
sofort leeren, liefen tausende Löschläufe über das Cache-Verzeichnis — und der
Cache wäre dauerhaft leer, also wirkungslos. Während des Aufrufs wird deshalb nur
vorgemerkt, am Ende einmal geleert. Kommt in der Sperrfrist noch etwas, bleibt es
als offen stehen und wird nachgeholt — von jedem beliebigen Aufruf, nicht
nur von WP-Cron. Sonst bliebe auf einer Installation mit DISABLE_WP_CRON genau
der letzte Import-Stand ungeleert.
Erkannt werden WP Super Cache, WP Rocket, W3 Total Cache, WP Fastest Cache, Cache Enabler, LiteSpeed Cache, SG Optimizer, Kinsta und WP Engine — an ihrer Funktion bzw. Klasse, nicht am Plugin-Namen, damit es auch bei zwei parallel laufenden greift. Ist keiner da, passiert schlicht nichts. Für alles andere:
add_action( 'bi_cache_leeren', function () { /* eigenen Cache leeren */ } );
Nicht dabei ist Autoptimize: Das ist kein Seiten-Cache, sondern ein Asset-Optimierer. Seinen Zwischenspeicher bei jeder Seminaränderung zu leeren, hieße CSS und JS ohne Grund neu bauen zu lassen.
Unter Einstellungen → Such-Filterleiste steht unten, welcher Cache erkannt wurde und wann zuletzt geleert wurde, dazu ein Knopf Cache jetzt leeren (der geht an der Sperrfrist vorbei — wer ihn drückt, will jetzt leeren).
Wichtig bei der Einrichtung des Cache-Plugins: Adressen mit Parametern (
?q=,?ort=,?thema=…) müssen vom Caching ausgenommen bleiben. Sonst bekämen alle Besucher dieselbe Trefferliste, egal was sie filtern. Die Blätterung ist davon nicht betroffen —/seminare/page/2/ist ein Pfad und darf gecacht werden.
Geprüft in tests/test-cache-purge.php.
Volltextsuche: Wörter statt Wortfolge, mit Tippfehler-Korrektur
Gesucht wird Wort für Wort und mit UND verknüpft (seit 1.87.0):
arbeitsrecht grundlagen findet „Grundlagen des Arbeitsrechts", die Reihenfolge
spielt keine Rolle. Vorher wurde die ganze Eingabe als eine Zeichenkette
verglichen — schon zwei Wörter in der falschen Reihenfolge fanden nichts.
Seit 1.126.0 lässt sich das UND überschreiben — siehe den nächsten Abschnitt.
Boolesche Operatoren (seit 1.126.0)
Das UND bleibt die Vorgabe — wer nichts weiter tut, sucht wie vorher. Wer mehr braucht, schreibt es hin:
| Eingabe | Bedeutung |
|---|---|
arbeitsrecht bonn |
beide Wörter müssen vorkommen (wie bisher) |
bonn ODER berlin |
eines von beiden genügt — auch OR, auch | |
arbeitsrecht UND bonn |
dasselbe wie ohne UND — auch AND, auch & |
NICHT online |
ohne dieses Wort — auch NOT, auch -online |
"neue Betriebsräte" |
genau diese Wortfolge — auch „…" aus dem Programmheft |
(bonn ODER berlin) jav |
Klammern fassen zusammen |
Vorrang wie überall: NICHT bindet stärker als UND, UND stärker als ODER.
a ODER b c heißt also a ODER (b UND c).
Unter dem Suchfeld steht der aufklappbare Hinweis „Suche verfeinern" mit denselben Beispielen. Zugeklappt ist er eine Zeile: Wer die Operatoren nicht braucht, soll sie nicht wegsehen müssen.
Vier Entscheidungen dahinter:
- Der Bindestrich ist nur am Anfang eines Stücks ein Ausschluss. Mitten im
Wort ist er ein Bindestrich — sonst würde aus
E-Learningein „alles ohne Learning" und aus der SeminarnummerLO-1234eine Suche ohne die 1234. Ein Gedankenstrich mit Leerzeichen ringsum (Grundlagen – Teil 1) ist gar kein Operator und fällt weg. - Klein geschrieben ist ein Angebot, GROSS geschrieben eine Ansage. „nicht",
„und" und „oder" sind auch gewöhnliches Deutsch: Nicht nur reden ist ein
Seminartitel, keine Ausschlussbedingung. Findet eine Anfrage mit
kleingeschriebenen Operatorwörtern nichts, wird sie deshalb ein zweites Mal
wortwörtlich gelesen — ohne Hinweis über der Liste, denn es steht nichts
anderes darin, als eingetippt wurde; nur die Verknüpfung war eine andere.
Bei
NICHTin Großbuchstaben und bei jedem Zeichen (-,",|,&, Klammern) passiert das nicht: Wer so schreibt, meint es, und eine leere Liste ist dann die richtige Antwort. Sonst zeigtearbeitsrecht NICHT onlineam Ende genau die Online-Seminare, die jemand gerade ausgeschlossen hat. - Was
MATCHnicht kann, geht sofort zuLIKE.BOOLEAN MODEkennt kein UND innerhalb einer ODER-Gruppe ((a b) ODER c), keinen Ausschluss einer ganzen Gruppe (NICHT (a ODER b)) und liefert bei einer Anfrage aus lauter Ausschlüssen die leere Menge statt „alles außer". Solche Bäume gehen deshalb von vornherein überLIKE— das bildet jeden Baum ab. Die Antwort wäre sonst nicht langsam, sondern falsch. Entschieden wird das inBI_Suche::modus_fuer(), einmal je Suche, vor der ersten Abfrage. - Es bleibt EINE Unterabfrage. Egal wie verschachtelt die Eingabe ist —
aus dem Baum wird ein einziges
EXISTSaufwp_bi_suchindex.
Die Tippfehler-Korrektur lässt Operatoren stehen und behält das Vorzeichen:
aus -arbeitsercht wird -Arbeitsrecht, nicht Arbeitsrecht. Auch die
Autovervollständigung ergänzt nur das angefangene Wort und rührt weder
Operatorwörter noch ein gesetztes - an.
Die Operatoren tragen auch über die Adresse: ?q=%22BR+kompakt%3A%22 ist eine
gültige Suche und damit als eigener Chip speicherbar — siehe
Eigene Chips.
Geprüft in tests/test-suche-boolean.php. Ausschalten lassen sich die
Operatorwörter über den Filter bi_suche_operatoren (die Zeichen -, " und
die Klammern bleiben davon unberührt).
Über alle Datenfelder (seit 1.107.0)
Bis 1.106.3 verglich die Suche nur den Titel. Alles andere, was ein Seminar
ausmacht, war unsichtbar: Wer Sprockhövel eintippte, bekam nichts — obwohl
dreihundert Seminare dort stattfinden. Wer sich an ein Stichwort aus den Themen
erinnerte, an die Referentin oder an die Ansprechpartner*in aus dem Telefonat,
suchte vergeblich.
Jedes Suchwort wird jetzt gegen alles gehalten:
| Wo | Was |
|---|---|
| Titel | post_title |
| Beschreibung und Themen im Seminar | ohne HTML, siehe unten |
| alle Meta-Felder | Seminarnummer, Untertitel, Seminarort, Referent*innen, Ansprechpartner*in und deren Mailadresse, Kosten-Hinweis, Webinar-Tool, Anmelde- und Online-Link, „Teil | Reihe" — und jedes selbst angelegte Feld (_bix_…) |
| alle Taxonomien | Bildungszentrum, Themenfeld, Zielgruppe, Freistellung, Programmjahr |
Alles zusammen steht seit 1.109.0 in einer Zeile je Seminar in der Tabelle
wp_bi_suchindex — siehe Der Suchindex.
Vier Entscheidungen dahinter:
- Das UND gilt zwischen den Wörtern, das ODER zwischen den Feldern.
sprockhövel betriebsratfindet ein Seminar, das in Sprockhövel stattfindet und „Betriebsrat" im Titel trägt. Beide Wörter im selben Feld zu verlangen wäre unbrauchbar — niemand tippt so. (Das UND zwischen den Wörtern ist seit 1.126.0 nur noch die Vorgabe, siehe oben; das ODER zwischen den Feldern gilt unverändert immer.) - Zahlen, Daten und Haken bleiben draußen. Ja/Nein, Datum, Uhrzeit, Zahl und
Betrag tragen keine Wörter. Wären sie dabei, fände
1jedes Seminar mit irgendeinem gesetzten Haken und12jeden Dezember-Termin, jeden Termin am 12. und jeden Preis mit einer 12 darin. Für genau diese Felder gibt es den Chip Zeitraum. Die Liste steht inBI_Suche::keine_wortfelder(). - Ein eigenes Feld ist von allein durchsuchbar. Die Liste der Meta-Schlüssel
kommt aus der Feld-Registry beider Seminarformen
(
BI_Suche::meta_keys()) — ein in der Datenpflege angelegtes Textfeld muss nirgends nachgetragen werden. Filter:bi_suche_meta_keys,bi_suche_taxonomien. - Eine einzige Unterabfrage, egal wie viele Wörter. Bis 1.108.0 stand je
Suchwort eine Unterabfrage über dreißig
postmeta-Schlüssel und eine über die drei Term-Tabellen in der Bedingung — sechs Wörter waren zwölf Unterabfragen, und keine davon konnte einen Index benutzen. Das kostete auf dem Bestand Sekunden. Seit 1.109.0 ist es einEXISTSaufwp_bi_suchindex.
Was die Leiste als Chip anbietet, ändert sich dadurch nicht: Filterbare Facetten bleiben Taxonomien. Neu ist nur, dass man ihre Begriffe auch tippen kann, statt zweimal zu klicken.
Der Suchindex (seit 1.109.0)
Neben jedem Seminar steht eine Zeile in wp_bi_suchindex: Titel,
Beschreibung, alle Meta-Werte, alle Begriffe — in einem Feld, ohne HTML, mit
einem FULLTEXT-Index darauf.
Warum eine eigene Tabelle. Bis 1.108.0 suchte die Bedingung je Wort mit zwei
Unterabfragen: eine über postmeta (dreißig Schlüssel, meta_value LIKE '%wort%') und eine über die drei Term-Tabellen. Sechs Wörter waren zwölf
Unterabfragen, und keine davon konnte einen Index benutzen — LIKE '%…%' kann
das nie. Gemessen auf 20.000 Indexzeilen:
| Weg | eine selektive Suche |
|---|---|
MATCH … AGAINST auf wp_bi_suchindex |
1 ms |
LIKE auf wp_bi_suchindex |
58 ms |
die alten EXISTS über postmeta |
98 ms |
Zwei Wege, weil Deutsch zusammensetzt. MATCH findet Wortanfänge:
betriebs findet „Betriebsrat". Es findet aber nicht mitten im Wort — rat
findet kein „Betriebsrat", und im Deutschen ist das kein Sonderfall, sondern der
Alltag. Deshalb läuft die Suche erst mit MATCH, und nur wenn das nichts
findet, noch einmal mit LIKE auf derselben Tabelle. Der teure Weg wird also nur
bezahlt, wenn der billige nichts bringt — dieselbe Regel wie bei der
Tippfehler-Korrektur.
Von vornherein mit LIKE gesucht wird, wenn ein Wort kürzer als drei Zeichen
ist. Das ist keine Feinheit: InnoDB nimmt kürzere Wörter gar nicht erst in den
Index auf (innodb_ft_min_token_size), und ein Pflichtwort ohne Indexeintrag
lässt MATCH die gesamte Abfrage leer zurückgeben. Aus BR im Betrieb würde
also nicht etwa eine ungenauere Suche, sondern gar keine. Dasselbe gilt für
E-Learning — zerlegt sind das „E" und „Learning".
Lässt sich der FULLTEXT-Index nicht anlegen (alte MySQL-Version, MyISAM), merkt
das Plugin sich das und sucht durchgehend mit LIKE. Langsamer als MATCH, aber
immer noch um Größenordnungen schneller als die alten Unterabfragen.
Kein rohes HTML
Die Beschreibung (post_content) und die Themen im Seminar (_bi_themen) sind
HTML. Roh mit LIKE verglichen war das in beide Richtungen falsch:
classtraf jeden formatierten Absatz,httpsjeden Link,strongjede fette Stelle. Treffer, die mit dem Inhalt nichts zu tun haben.Betriebs<strong>rat</strong>enthält kein „Betriebsrat". Das Seminar war für die Suche danach unsichtbar, obwohl das Wort auf der Seite steht.
Beim Entkleiden (BI_Suche::text_entkleiden()) gilt deshalb eine Unterscheidung,
die sich nicht von selbst versteht:
| HTML | wird zu | warum |
|---|---|---|
Betriebs<strong>rat</strong> |
Betriebsrat |
Auszeichnungen im Wort (b, strong, em, a, span …) verschwinden spurlos |
<li>Kündigung</li><li>Abmahnung</li> |
Kündigung Abmahnung |
jedes andere Tag hinterlässt ein Leerzeichen |
<p class="hinweis">Kosten</p> |
Kosten |
Attribute werden nie zu Wörtern |
Arbeit & Recht |
Arbeit & Recht |
Entities werden aufgelöst, sonst stünde „amp" im Index |
| `` | (nichts) | dort steht Code, kein Text |
Umlaute stehen doppelt drin
Zu jedem Wort mit Umlaut kommt die aufgelöste Schreibweise dazu — „Betriebsräte" bringt „betriebsraete" mit. Die Datenbank hält „ä" und „a" für gleich, „ae" und „ä" aber nicht; wer ohne Umlauttaste tippt, fände das Seminar sonst erst über die Tippfehler-Korrektur, also nach einer zweiten Abfrage und mit einem Hinweis über der Liste. So trifft schon die erste.
Wie der Index aktuell bleibt
Beim Speichern, bei jeder Änderung an einem beteiligten Feld und beim Setzen von
Begriffen — das deckt auch CSV-Import und Massenbearbeitung ab, die beide an
save_post vorbeiarbeiten. Gesammelt wird während des Aufrufs, geschrieben
einmal am Ende: Ein Import setzt zwanzig Meta-Felder je Zeile und würde die
Zeile sonst zwanzigmal neu bauen.
Für den Bestand läuft ein Nachbau in Häppchen (200 Zeilen), angestoßen bei jedem Aufruf im Backend und zusätzlich von einem Cron-Ereignis. Je Aufruf laufen so viele Häppchen, wie in drei Sekunden passen: Solange der Index leer ist, findet die Suche gar nichts, und ein einzelnes Häppchen je Aufruf hieße bei zweitausend Seminaren zehn Klicks im Backend, bis die Suche wieder trägt. Kein Knopf, den jemand drücken muss — und keine Abhängigkeit von WP-Cron allein, das auf vielen Installationen abgeschaltet ist. Solange etwas fehlt, steht der Stand als Hinweis im Backend:
Bildungsprogramm: Der Suchindex wird aufgebaut – 1.200 von etwa 5.400 Einträgen. Bis er fertig ist, findet die Volltextsuche nicht alle Seminare.
Geprüft in tests/test-suche-vorschlag.php und tests/test-suche-nummer.php.
Eine Suche, nicht vier (seit 1.109.0)
Ein Aufruf mit ?q=… fuhr bis 1.108.0 drei bis vier Mal dieselbe teure
Bedingung:
- eine Probeabfrage, nur um zu wissen, ob es Treffer gibt
- die Facettenzähler (
count_base_ids, mitnopagingüber alle Treffer-IDs) - die eigentliche Liste
- und griff die Tippfehler-Korrektur, noch einmal Nummer 1
Jetzt wird zuerst die Liste geholt — ob sie leer ist, sagt found_posts von
selbst — und die Facetten rechnen danach mit dem Text und dem Suchweg, mit
dem am Ende tatsächlich gesucht wurde. Dazu merkt sich count_base_ids() sein
Ergebnis für die Dauer eines Aufrufs; dieselbe Frage kommt je Seite mehrfach
(Gesamtzahl, je Facette mit eigener Auswahl, Kopfzahl der Suchmaske).
Autovervollständigung (seit 1.108.0)
Ab dem dritten Zeichen schlägt das Suchfeld etwas vor, nach einer Denkpause von 300 ms. Drei Arten, weil es drei Arten von Absicht gibt:
| Gruppe | Beispiel | ein Klick bewirkt |
|---|---|---|
| Suchbegriffe | betrieb → „Betriebsrat" |
das angefangene Wort wird zu Ende geschrieben und gesucht |
| Filter | sprock → „Bildungszentrum Sprockhövel" |
setzt den Chip, nicht den Volltext |
| Seminare | der Treffer selbst, mit Datum und Ort | führt direkt auf die Detailseite |
Entschieden ist dabei:
-
Die Suche läuft weiterhin erst mit Enter. Die Vorschläge sind ein Angebot, keine Suche beim Tippen. Wer sie ignoriert und Enter drückt, bekommt genau das, was im Feld steht.
-
Erst verfeinern, dann treffen — außer bei einer Seminarnummer. Wer eine Nummer abtippt, will genau ein Seminar und nichts sonst; dann stehen die Seminare oben und sonst nichts.
-
Nur Wortanfänge.
arbeitschlägt „Arbeitsrecht" vor, nicht „Sozialarbeit". Ein Vorschlag, der irgendwo in der Mitte passt, wirkt wie ein Fehler. -
Ein Filtervorschlag liefert den Wert, den die Leiste kennt. Bildungszentrum und Zielgruppe fasst die Leiste zu Gruppen zusammen („Berlin-Pichelssee" gehört zu „Bildungszentrum Berlin"); ein Vorschlag mit dem rohen Begriff führte auf einen Chip, den es gar nicht gibt. Verglichen wird deshalb gegen die fertigen Optionen der Leiste.
-
Das angefangene Wort wird ersetzt, der Rest bleibt. Aus
arbeitsrecht sprock+ Klick auf das Bildungszentrum wirdq=arbeitsrechtplus Chip — nicht beides doppelt. -
Bedienbar ohne Maus. ↓ ↑ wandern durch die Liste (und wieder heraus zur eigenen Eingabe), Enter übernimmt, Esc schließt. Das Feld ist eine
comboboxmitaria-activedescendant. -
Über JSON geht Text, kein HTML (seit 1.109.1).
get_the_title()schickt den Titel durchwptexturize(); aus"Ent-rüstet euch!"wird dabei„Ent-rüstet euch!“und aus einem Gedankenstrich–. Für HTML ist das richtig — aber das JavaScript maskiert beim Einsetzen selbst, und dann stand&#8211;in der Liste und der Browser zeigte die Zeichenfolge an. Anzeigetexte werden deshalb serverseitig aufgelöst. Filterwerte nicht: Ein Wert wandert in die Adresszeile und wird gegen den Begriffsnamen gehalten, so wie er in der Datenbank steht. Dasselbe gilt für die Beschriftungen der Filter-Chips (Arbeit & Recht→ „Arbeit & Recht"). -
Seminarvorschläge erst ab vier Zeichen. Suchbegriffe und Filter kommen aus zwischengespeicherten Listen und kosten praktisch nichts; ein Seminarvorschlag ist dagegen eine echte Abfrage gegen den Bestand — bei drei Buchstaben mit einer Trefferliste, die noch nichts über die Absicht verrät.
Technisch: eine AJAX-Aktion bi_suche_vorschlag (auch für Gäste, rein lesend),
entprellt um 300 ms, laufende Anfragen werden abgebrochen. Zwischengespeichert
wird zweimal — die Facettenliste eine Stunde (sie hängt nur am Bestand) und jede
fertige Antwort fünf Minuten. Ohne das liefe bei jedem Tastendruck eine
Zählabfrage über den ganzen Bestand; und jede Anfrage kostet auf dem Server
einen vollen WordPress-Start, ganz gleich wie billig die Abfrage dahinter ist.
Die Seminarnummer findet ihr Seminar (seit 1.95.0)
Die Seminarnummer steht im gedruckten Programm, in jeder Bestätigungsmail und
auf jedem Anmeldebogen. Wer sie in das Suchfeld tippt — B00027030,
SE14822 —, landet auf dem Seminar. Vorher verglich die Suche nur den Titel:
Die Nummer, mit der am Telefon und in der Geschäftsstelle gearbeitet wird, fand
als Einzige nichts.
- Nur wenn die Eingabe nach einer Nummer aussieht wird die Nummer überhaupt
abgefragt: ein Wort, mindestens vier Zeichen, mindestens eine Ziffer darin.
Sonst zöge eine Suche nach
2026alle Nummern mit einer 26 darin herein, und die Trefferliste wäre nicht mehr die Antwort auf die Frage. - Teilstücke reichen.
B0002findetB00027030— beim Abtippen aus dem Heft bricht es sonst am letzten Zeichen ab. - Neben dem Titel, nicht statt seiner. Gefunden wird, was im Titel passt oder die Nummer trägt. Alle übrigen Filter gelten unverändert weiter.
- Keine Tippfehler-Korrektur auf Nummern. Findet eine Nummer nichts, ist das kein Buchstabendreher, sondern eine Nummer ohne Seminar. Sie zu einem ähnlich geschriebenen Titelwort zu verbiegen, beantwortete eine Frage, die niemand gestellt hat.
Was die Suche im Frontend nicht zeigt, zeigt sie auch mit Nummer nicht: Die Ergebnisliste kennt nur veröffentlichte, sichtbare Seminare ab heute. Die Nummer eines vergangenen oder ausgeblendeten Termins bleibt deshalb ohne Treffer — im Backend findet dieselbe Nummer ihn weiterhin.
Nicht zu verwechseln mit dem Parameter ?nr=…: Der filtert exakt auf eine
oder mehrere Nummern (pipe-getrennt) und ist für verlinkte Listen und
Marketing-Kacheln gedacht. Das Suchfeld vergleicht dagegen auch Teilstücke.
Geprüft in tests/test-suche-nummer.php.
Findet die Suche nichts, wird einmal korrigiert. Jedes Wort der Eingabe wird mit dem Wortschatz aller Seminartitel verglichen (Levenshtein-Abstand); liegt ein Wort nah genug an einem bekannten, wird damit erneut gesucht:
Keine Treffer für Arbeitsercht. Gezeigt werden Treffer für Arbeitsrechts.
Vier Entscheidungen dahinter:
- Nur bei null Treffern. Solange die Suche etwas findet, wird nichts geraten. Ein Wort, das im Wortschatz steht, ist richtig geschrieben — auch wenn es einem anderen ähnlich sieht.
- Nie stillschweigend. Der Hinweis über der Liste nennt beides: was eingetippt wurde und wonach gesucht wird. Eine Suche, die heimlich etwas anderes sucht, verwirrt mehr, als sie hilft — man sieht Ergebnisse und weiß nicht, wieso.
- Umlaute sind der halbe Nutzen.
betriebsraetefindet „Betriebsräte". Die Datenbank hält „ae" und „ä" nicht für dasselbe; der häufigste „Tippfehler" ist gar keiner, sondern eine Tastatur ohne Umlauttaste. Deshalb steht im Wortschatz zu jedem normalisierten Wort auch die Originalschreibweise — gesucht wird mit der. - Lieber zaghaft als falsch. Die Toleranz wächst mit der Wortlänge (1 Fehler
bis 5 Zeichen, 2 bis 9, sonst 3), Wörter unter vier Zeichen bleiben unangetastet,
und ein Wortanfang wie
bilanzwird nicht zu „Bilanzanalyse" gemacht — er findet ohnehin. Bringt auch die Korrektur nichts, steht in der Meldung wieder das, was eingetippt wurde.
Der Wortschatz kommt seit 1.108.0 aus drei Quellen: den Titeln aller
veröffentlichten Seminare und Reihen, den Begriffen aller Taxonomien
(Bildungszentrum, Themenfeld, Zielgruppe, Freistellung, Programm) und einer
Handvoll Begriffsfelder — Seminarort, Untertitel, Referent*innen,
Ansprechpartner*in, „Teil | Reihe". Damit wird auch sprockhövl zu
„Sprockhövel" korrigiert, was vorher nicht ging: Der Ort steht in keinem Titel.
Begriffe zählen dabei mit ihrer Verwendung: Ein Bildungszentrum steht einmal in der Term-Tabelle, hängt aber an dreihundert Seminaren. Ohne Gewicht verlöre „Sprockhövel" jeden Gleichstand gegen ein beliebiges Titelwort.
Bewusst nicht aus dem Fließtext. Beschreibung und Themen bestehen zu großen
Teilen aus „werden", „einer", „Teilnehmerinnen". Im Wortschatz verdrängten diese
Wörter die Begriffe — die Häufigkeit entscheidet ja bei gleichem Abstand —, und
jede Korrektur führe gegen das nächstbeste Füllwort. Ein Wortschatz ist eine
Begriffsliste, kein Textkorpus. Die Liste steht in
BI_Suche::wortschatz_felder(), Filter bi_suche_wortschatz_felder.
Er liegt in einem Transient und wird verworfen, sobald ein Seminar oder ein
Begriff gespeichert oder gelöscht wird. Die Korrektur gilt für die Ergebnisliste
([bi_seminarsuche]) — die eigenständige Suchmaske zählt weiter die Eingabe
wörtlich, weil sie bei j
This README is longer than the copy stored here. Read the rest on GitHub →