Claris FileMaker Pro

seen from Singapore
seen from Australia
seen from Mexico
seen from Ireland
seen from United States
seen from India

seen from Switzerland
seen from Mexico

seen from United States

seen from Malaysia
seen from China
seen from Netherlands
seen from United States

seen from United States

seen from United States

seen from France
seen from Canada
seen from Mexico

seen from United States

seen from United States
Claris FileMaker Pro
Jessie Maple, a bacteriologist who took up filmmaking in the 1970s, became the first Black woman to join the camera operators union in New York and went on to direct trailblazing independent films. Maple turned first to journalism and then to film, working as a camera operator and a documentarian before releasing her first feature, “Will” (1981), a family drama that she made for less than $12,000. Jessie Maple passed away at her home on 30 May 2023, in Atlanta. She was 86.
seit 1985
Die Post und das Konto
1985: Ich eröffne ein Konto bei der Post und kann jetzt mit der Postcard vom Automaten Geld beziehen. Besonders faszinierend: Auch aus Automaten in Spanien lässt sich mit derselben PIN (vierstellige Zahl) Bargeld holen, allerdings keine Schweizer Franken, sondern spanische Peseten.
1987: Ich komme aus dem Ausland zurück und lebe in einer Wohngemeinschaft. Ich kann mich vage daran erinnern, dass ich damals ein Konto bei der Zürcher Kantonalbank eröffnet habe, um die Miete überweisen zu können. Aber ich kann mich nicht an das Vorgehen erinnern, das Molinarius und Tilman Otter im Techniktagebuch beschrieben haben. Ich habe nie auf der Post Geld bar abgehoben und auf einer Bank wieder eingezahlt oder umgekehrt. Das einzige Geld, das ich der Post bar einzahle, ist das Fünflibergeld und das geht immer auf mein eigenes Konto. Für wiederkehrende Zahlungen richte ich Daueraufträge ein; einmalige Zahlungen überweise ich per Zahlungsauftrag. Vor dem Online-Zeitalter geht das so: Man entnimmt allen Rechnungen die beiliegenden Einzahlungsscheine, trennt die Girobelege ab, füllt noch nicht eingedruckte Felder manuell aus, zählt alles zusammen, schreibt die Summe auf einen Zahlungsauftrag und steckt alle Belege zusammen mit dem Zahlungsauftrag in ein vorfrankiertes Couvert. Manchmal ist in den Vermischten Meldungen zu lesen, dass solche Couverts aus den Briefkästen gefischt und die Belege ausgetauscht werden. Ich selbst hatte mit dem Verfahren nie Probleme.
1995: Ich arbeite in einem alternativen Lokalradio, das über die Beiträge von rund 2'000 Vereinsmitgliedern finanziert wird. Wir stellen den Zahlungsverkehr um von „grünen“ Einzahlungsscheinen auf „blaue“. In diesem Zusammenhang höre ich zum ersten Mal des Kürzel „ESR“ (= Einzahlungsschein mit Referenznummer). ESR steht für eine algorithmisch generierte Nummer auf dem Girobeleg des Einzahlungsscheins. Dank dieser Nummer lässt sich der Zahlungsverkehr automatisieren. Im Fall des Lokalradios müssen die Einzahlungen der Mitglieder nicht mehr manuell erfasst werden. Die Debitorenbuchhaltung wird massiv vereinfacht. Wir drucken nun individualisierte Einzahlungsscheine aus und erhalten von der Post regelmässig Daten-Files, die wir in unsere Mitgliederverwaltung (eine 4D-Anwendung für Apple) einlesen können. Wer bezahlt hat und wer noch nicht, lässt sich so ohne viel händische Arbeit kontrollieren. Probleme gibt es nur, wenn der Drucker die Referenznummer nicht sauber in das dafür vorgesehene Feld druckt.
ca. 1999: Aus Widerwillen gegenüber dem Bankenwesen löse ich mein Bankkonto auf und regle meinen gesamten Verkehr über das Konto der Postfinance. An Probleme beim Überweisen auf Bankkonten kann ich mich nicht erinnern. Das löst alles der ESR. Die Zahlungen gehen auf das Postkonto der jeweiligen Bank und von dort auf das Bankkonto der Rechnungsstellerin (siehe Abbildung).
2004: Ein Freund hat eine Möglichkeit (eine Filemaker-Anwendung für Apple) entwickelt, mit der sich so genannte EZAG-Files erstellen lassen. EZAG steht für „Elektronischer Zahlungsauftrag“. Ich erfasse meine Zahlungen offline, exportiere das EZAG-File, logge mich im Yellownet, dem Portal der Postfinance, ein und lade dort das EZAG-File hoch. Damit erspare ich mir viel Ärger, den unstabile Internetverbindungen verursachen können, wenn man gerade dabei ist seine Zahlungen zu erfassen. Um mich im Yellownet einloggen zu können, muss ich meine Yellownet-Nummer eingeben, ein Passwort sowie ein Zahl aus einer Abstreichliste (siehe Abbildung).
Irgendwann wird dieses System umgestellt. Alle Postfinance-Kunden erhalten ein gelbes Kästchen, in das man seine Postcard einschieben kann. Beim Einloggen gebe ich immer noch meine Yellownet-Nummer und ein Passwort ein, dann erhalte ich eine Nummer, die ich in das Kästchen eingeben muss, worauf das Kästchen das Passwort meiner Postcard einfordert, und mir dann eine Nummer ausgibt, die mir im Yellownet die Türe zu meinem Konto öffnet.
2017: Ich habe arbeitgeberwechselbedingt keinen File-Maker mehr auf meinem Rechner und erfasse Zahlungen fortan online. Die Verbindungen sind inzwischen so stabil, dass das kein Problem mehr ist. Die App der Postfinance auf dem klugen Telefon nutze ich nur, um hin und wieder meinen Kontostand abzurufen.
(Franziska Nyffenegger)
Whether you need a simple composition retyped or a sermon transcribed, I will accommodate you to the specifics provided in accordance with the services offered.
Zu faul für Parameterketten: FileMaker-Scripte per Mail debuggen
Serverseitige Scripte haben ein nerviges Problem: Man kann sie nicht im Debugger anhalten oder sehen. Kein Breakpoint, kein Data Viewer, kein Schritt für Schritt. Läuft etwas schief, sitzt man da und tippt sich im schlimmsten Fall durch eine ganze Kette von Sub-Scripts, schleift Rückgabewerte über Script-Ergebnis und Parameter durch und versucht irgendwie an die Variableninhalte zu kommen, die an genau dieser einen Stelle wichtig waren. Irgendwann war ich zu faul dafür. Und Faulheit ist bekanntlich die Mutter eleganter Lösungen. Die Idee: FileMaker kann aus jedem Script heraus eine Mail verschicken, serverseitig per SMTP, ganz ohne Client. Also schicke ich mir den Zustand einfach selbst zu. Gefundene Treffermengen, Variableninhalte, Zwischenergebnisse, genau an der relevanten Stelle direkt ins Postfach. Kein Durchschleifen, kein Rückgabewert-Tetris. Im Betreff der Script-Name plus Zeitstempel, im Body die Werte, die mich interessieren. Damit habe ich pro Lauf eine durchsuchbare, sortierbare Spur, quasi ein Logfile, das sich selbst zustellt. Bei mehreren Scripts, die parallel oder hintereinander laufen, sehe ich sofort, welches wann wo stand, ohne mir vorher eine aufwändige Logging-Tabelle gebaut zu haben. Ist es elegant? Nein. Funktioniert es? Absolut. Und manchmal ist die schnell hingebaute Lösung genau die, die einem den Nachmittag rettet. Ein paar Dinge, die man trotzdem im Kopf behalten sollte: In Schleifen nicht pro Durchlauf eine Mail rausjagen, sondern Werte sammeln und am Ende einmal senden, sonst bremst man das Script aus oder flutet das eigene Postfach. Keine echten Kundendaten in die Debug-Mails packen. Und am Ende nicht vergessen das ganze wieder zu entfernen.
FileMaker-Skripte ohne Tastenkombinationen-Gedächtnis: ai2fm + Raycast
# FileMaker-Skripte ohne Tastenkombinationen-Gedächtnis: ai2fm + Raycast Wer regelmäßig FileMaker-Skripte zwischen FileMaker und VS Code hin- und herschiebt, kennt das Tool **ai2fm**: Es übersetzt FileMakers kryptisches XML-Clipboard in lesbaren, zeilenorientierten Text (`.fmscript`) und wieder zurück. Genau ein Step pro Zeile, Calculations inline, saubere Einrückung. Endlich Skripte, die man *lesen* kann. Das Einzige, was mich gestört hat: die Shortcuts. `⌥⌘T`, `⌥⌘F`, `⌥⌘G`, `⌥⌘V`, wer merkt sich das schon zu den ganzen anderen Shortcuts? ## Die Lösung: Raycast Statt mir vier Tastenkombinationen einzuprägen, habe ich mir zwei kleine **Raycast Script Commands** gebaut. Jetzt tippe ich in Raycast einfach: - **„ai2fm FM to Text"** – FileMaker-Skript ⌘C, Befehl auslösen, fertig: der lesbare Text erscheint in VS Code. - **„ai2fm Text to FM"** – Datei bearbeiten, Befehl auslösen, in FileMaker ⌘V. Kein Auswendiglernen mehr. Ich tippe ein paar Buchstaben, Enter, es läuft. Wer Raycast (oder ein ähnliches Launcher-Tool) eh schon offen hat, integriert sich das nahtlos in den Workflow. ## Das eigentlich Coole: man bekommt Feedback Mein Lieblingsdetail: Sobald der Befehl durchläuft und das Ergebnis in der Zwischenablage liegt, meldet sich Raycast mit einem kurzen HUD-Hinweis: > ✅ Text → FM: Clipboard bereit – jetzt in FileMaker ⌘V Klingt nach Kleinigkeit, ist aber Gold wert. Man weiß **sofort**, dass die Übersetzung geklappt hat und der Inhalt in der Zwischenablage liegt, kein Rätselraten, ob man jetzt einfügen kann oder nicht. Genau diese kleine Rückmeldung macht den Unterschied zwischen „funktioniert hoffentlich" und „funktioniert, weiter geht's". ## Fazit **ai2fm** macht FileMaker-Skripte lesbar und versionierbar, ein wirklich starkes System. Und mit **Raycast** als Auslöser braucht man sich keine Shortcuts zu merken und bekommt obendrein eine saubere Erfolgs-Rückmeldung beim Kopieren in die Zwischenablage. Für alle, die mit FileMaker und VS Code arbeiten: klare Empfehlung.
# Volltext-Suche über 100 000 Mails- und sie reagiert in 50 Millisekunden
Wie wir den klassischen FileMaker-WebViewer-Suchpatterns einen modernen Such-Backend untergeschoben haben, ohne die FileMaker-Datei selbst zu berühren. Mit einer SQLite-Datenbank, die schon im System lag. Mit einer Eingebauten von SQLite, die kaum jemand kennt. Und mit einer 2-Stunden-Implementierung, die jetzt für Datenbestände von 100 000 bis 10 Millionen Mails ausreicht, bei gleichbleibend sub-100-ms-Reaktion auf jeden Tastendruck.* --- ## Das Problem Im Mail-System eines Kunden lagen nach dem großen Legacy-Import knapp 96 000 Mails. Das vorhandene Suchpattern war ein bewährter FileMaker-WebViewer-Trick: Eine **gespeicherte Berechnung** pro Datensatz baut ein HTML-Schnipsel (`
…
`), eine zweite **ungespeicherte Berechnung** verknüpft alle Schnipsel zu einer großen HTML-Seite mit Suchfeld, der WebViewer rendert sie als `data:`-URL, JavaScript filtert clientseitig per `data-search`-Attribut. Bei 5 000 Datensätzen ist das **magisch**: man tippt, die Liste filtert sich sofort, kein Server-Roundtrip, kein Lag. Bei 10 000 noch ok. Bei 92 000 stirbt es. Konkrete Zahlen aus der Diagnose: - `Liste()` über 96 000 Datensätze: **10–30 Minuten** pro Aufruf (Oder FileMaker gibt auf) - Generiertes HTML: **~75 MB** als String - `data:`-URL: bei manchen WebKit-Versionen über 64 MB einfach gekappt - 92 000 DOM-Knoten + JS `querySelectorAll` pro Tastendruck: **mehrere hundert Millisekunden** zähes Laggen Kurz: Das Pattern skaliert auf 5–10 k Datensätze. Bei 92 k bricht es. ## Die Optionen Drei klassische Wege: **A — Vorfilter.** Statt alle Mails zu zeigen, immer nur eine sinnvolle Teilmenge (letzte 90 Tage, Mails eines Kontakts). Reduziert auf <2 000 Datensätze, dann funktioniert das alte Pattern wieder. Pragmatisch, aber zwingt den User in eine starre Vorfilter-Logik. **B — Klassische FileMaker-Suche.** Suchfeld → ExecuteSQL → Ergebnismenge in Listenansicht. Skaliert auf Millionen, verliert aber das schöne Live-Filter-Gefühl. **C — Server-side-Index.** Ein eigener Such-Endpoint im PHP-Backend, der gegen eine speziell optimierte Such-Datenbank fragt, JSON zurückliefert, vom WebViewer per Fetch konsumiert. Skaliert auf Millionen, behält Live-Filter, kostet aber ein paar Stunden Aufbau. Variante C wäre normalerweise Cadillac-Engineering. In dem Moment, als der Kunde fragte „haben wir nicht schon eine SQLite im System?", wurde sie zur Anderthalb-Stunden-Lösung. ## Der entscheidende Aha-Moment Der PHP-Mailclient hatte schon eine SQLite-Datei. Genauer: `mailclient/data/links.sqlite`, eingerichtet für die kleine Verknüpfungstabelle `mail_links`, die Mail-Message-IDs auf FileMaker-Entitäten (Kontakte, Projekte, Konten) abbildet. PDO-Treiber konfiguriert, `db()`-Helper definiert, Schema-Initialisierung idempotent. Alles da. Und dann der zweite Aha: **SQLite hat seit Version 3.9 einen eingebauten Volltext-Index namens FTS5.** Eine virtuelle Tabelle, die Token-basierte Volltext-Suche über Millionen Datensätze in <50 ms macht. Genau das, was Apple Spotlight nutzt. Kostenlos, ohne Plugin, ohne externen Index-Service. Ein schneller Check, ob FTS5 in der PHP-SQLite-Installation kompiliert ist: ```php $pdo = new PDO("sqlite::memory:"); $pdo->exec("CREATE VIRTUAL TABLE t USING fts5(x)"); $pdo->exec("INSERT INTO t VALUES ('hallo welt'), ('foo bar')"); $r = $pdo->query("SELECT x FROM t WHERE t MATCH 'hallo'")->fetchAll(PDO::FETCH_COLUMN); // → ['hallo welt'] ``` Ein „FTS5 verfügbar: ja" als Antwort, und das Projekt war von „2 Tage Aufwand" auf „2 Stunden" geschrumpft. ## Die Architektur ``` FileMaker WebViewer PHP-Mailclient SQLite +-------------------+ +-------------------+ +----------------+ | + JS | fetch(q=...) | /api/search_mails | SQL | mails_fts | | rendert Top 100 | <------------> | sucht im FTS5 | <-----> | (92k Mails) | +-------------------+ JSON +-------------------+ +----------------+ Tastendruck ^ | bei jedem neuen DS | /api/index_mail (von FM aufgerufen) ``` Drei neue PHP-Endpoints, eine FTS5-Tabelle, ein Python-Initial-Lauf, eine neue FileMaker-Berechnung mit ein paar Dutzend Zeilen JavaScript. Mehr nicht. ## Die FTS5-Tabelle ```sql CREATE VIRTUAL TABLE mails_fts USING fts5( pk_mail_id UNINDEXED, message_id UNINDEXED, betreff, body, absender, empfaenger, datum UNINDEXED, id_contacts UNINDEXED, id_account UNINDEXED, z_eingang UNINDEXED, thread_root_id UNINDEXED, tokenize = 'unicode61 remove_diacritics 2' ); ``` Zwei Details lohnen Erklärung: **`UNINDEXED`-Spalten** werden gespeichert, aber nicht in den Volltext-Index aufgenommen. Sie sind als reine Lookup- und Filter-Felder gedacht. So bleibt der Index kompakt: nur das, was wirklich durchsuchbar sein muss (`betreff`, `body`, `absender`, `empfaenger`), trifft den Tokenizer. **`remove_diacritics 2`** im Tokenizer ist der versteckte Held: er normalisiert alle Akzente und Umlaute. Eine Suche nach „muller" findet „Müller", „Mueller" und „Müller". Bei einem multilingualen Mail-Archiv mit deutschen, französischen, türkischen Namen ist das nicht Nice-to-have, sondern Funktionalität. ## Der Initial-Lauf Bei 96 000 Mails wäre ein Datensatz-für-Datensatz-Index-Update über die FileMaker-Skript-Engine vermutlich ein Stundenprozess. Python machte es in **15 Sekunden**: ```python import sqlite3, csv conn = sqlite3.connect('mailclient/data/links.sqlite') conn.execute("PRAGMA journal_mode = WAL") conn.execute("PRAGMA synchronous = NORMAL") for csv_file in [mailausgang_csv, maileingang_csv]: batch = [] with open(csv_file, encoding='utf-8') as f: for row in csv.DictReader(f): batch.append(( row['Message_ID'], row['Betreff'], row['Body_Plain'], row['Absender_mailadresse'], row['Empfaenger'], # … )) if len(batch) >= 1000: conn.executemany("INSERT INTO mails_fts (...) VALUES (?,?,...)", batch) conn.commit() batch = [] ``` 6 100 Datensätze pro Sekunde mit Batch-Inserts und WAL-Mode. Das Ergebnis: 91 935 Mails im Index, Datei-Größe der SQLite ~250 MB, Initial-Investment zum Aufbauen einmalig 15 Sekunden, danach für immer verfügbar. ## Der Such-Endpoint PHP, gut 80 Zeilen Code. Der Kern: ```php $sql = " SELECT pk_mail_id, message_id, betreff, absender, empfaenger, datum, id_contacts, id_account, z_eingang, thread_root_id, snippet(mails_fts, 3, '', '', '…', 12) AS snippet FROM mails_fts WHERE mails_fts MATCH ? ORDER BY rank LIMIT 100 "; ``` `snippet()` ist eine eingebaute FTS5-Funktion: sie nimmt das Body-Feld (Index 3), markiert die Treffer-Worte mit ``-Tags und schneidet 12 Tokens links und rechts ringsum raus. Das Ergebnis sieht aus wie Gmail-Suchergebnisse: man tippt „heiratsurkunde", sieht ein paar Zeilen Body-Kontext mit gelb hervorgehobenem Treffer. `ORDER BY rank` sortiert nach FTS5-internem Relevanz-Algorithmus (BM25). Die häufigste/beste Übereinstimmung erscheint oben. Kein Tuning, kein TF-IDF-Selbstbau nötig. Etwas mehr Sorgfalt verlangt das Mapping von **Benutzer-Eingaben in FTS5-Query-Syntax**: ```php $tokens = preg_split('/\s+/', $userInput); $clean = []; foreach ($tokens as $t) { $safe = preg_replace('/["\\\\:()*]/', '', $t); if (preg_match('/[@.\-]/', $safe)) { $clean[] = '"' . $safe . '"'; // E-Mail-Adresse als Phrase } else { $clean[] = $safe . '*'; // Prefix-Match für "mueller" → "muellermann" } } $ftsQuery = implode(' ', $clean); ``` Drei Regeln: gefährliche Sonderzeichen rausnehmen, Token mit `@`/`.` als Phrasen (sonst zerlegt FTS5 sie an Punkten), normale Wörter als Prefix-Match mit Stern (damit „mueller" sofort matcht, sobald 7 Zeichen getippt sind, ohne dass der User ENTER drücken muss). ## Das Frontend Im WebViewer: ```javascript let timer = null; function suche(q) { clearTimeout(timer); timer = setTimeout(() => doFetch(q), 200); } function doFetch(q) { if (!q || q.trim().length < 2) { document.getElementById('liste').innerHTML = '
Mindestens 2 Zeichen…
'; return; } fetch(`${API}?q=${encodeURIComponent(q)}&limit=100&token=${TOKEN}`) .then(r => r.json()) .then(d => render(d)); } ``` Zwei Details: **Debouncing mit 200 ms.** Tippt der User „mueller", wird nicht für jeden Buchstaben ein Request abgesetzt. Erst wenn 200 ms keine neue Eingabe kommt, geht der Fetch los. Das schont den Server und ist auf modernen Maschinen subjektiv noch immer „sofort". **Race-Condition-Schutz mit `lastReq`-ID.** Wenn der User schnell tippt und der erste Request langsamer zurückkommt als der zweite, würde die Liste sonst kurz die alte Antwort zeigen. Über einen lokalen Zähler werden veraltete Antworten verworfen. ## Die CORS-Falle Beim ersten Test im WebViewer: nichts. „Load failed". Ein typisches Browser-Mysterium, das eine Stunde Diagnose verschlingt, wenn man's nicht kennt. Erklärung: Der WebViewer lädt seine HTML-Seite per `data:`-URL. Aus Browser-Sicht hat eine `data:`-URL als Origin den Wert `null`. Macht der eingebettete JavaScript-Code dann einen `fetch()` an einen HTTP-Server, prüft der Browser CORS (Cross-Origin Resource Sharing). Liefert der Server keine `Access-Control-Allow-Origin: *`-Header, wird die Antwort blockiert — der `fetch()` erhält einen generischen „Load failed"-Fehler ohne nähere Erklärung. Fix: zwei Zeilen im `json_response()`-Helper: ```php header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); ``` Sofort lief alles. Eine dieser Sachen, die man einmal gelernt hat und nie wieder vergisst, aber bis dahin Stunden kosten kann. ## Performance — die echten Zahlen Über 96 935 Mails, gemessen direkt am Server, dann inklusive HTTP-Roundtrip aus dem WebViewer: | Such-Typ | SQL-Zeit | HTTP-Roundtrip | |---|---:|---:| | Volltext „mueller*" | 4 ms | 59 ms | | Volltext „heiratsurkunde" | 4 ms | 55 ms | | Volltext + Filter (z_eingang=1) | 5 ms | 34 ms | | Phrasen-Suche „aicher.com" | 4 ms | 42 ms | Der HTTP-Roundtrip ist dabei der Löwenanteil. Die eigentliche FTS5-Query liegt **unter 10 ms** — auch bei Millionen Datensätzen würde sich daran wenig ändern. Vergleich mit dem alten Pattern: | | Liste-basierter WebViewer | FTS5-Backend | |---|---|---| | Initiales Rendering | 10–30 m | <100 ms | | Tastendruck-Reaktion | 100–500 ms (DOM-Filter) | 50–100 ms (HTTP) | | HTML-Größe initial | ~75 MB | ~1 KB pro Request | | Skalierungsgrenze | ~10 000 DS | 10 Mio+ | | Body-Volltext-Suche | begrenzt, kein Stemming | voll, mit Diacritics-Normalisierung | | Sortierung | nach Anlegen | nach Relevanz (BM25) | | Snippet-Hervorhebung | nein | ja, mit `` | | Sonderzeichen/Umlaut-Toleranz | nein | ja | ## Wartung Damit der Index nicht veraltet, muss er bei jeder neuen oder geänderten Mail aktualisiert werden: - **Mails per IMAP abholen**: das FileMaker-Skript ruft nach jedem neuen Datensatz `Mail indizieren` auf, der schickt den DS per POST an `/api/index_mail.php` - **Mails senden**: macht der PHP-Endpoint selbst nach erfolgreichem Versand - **Manuelle Änderungen** in FileMaker (Betreff editiert, Body angepasst): per Skript-Trigger OnRecordCommit → `Mail indizieren` Bei Inkonsistenzen genügt ein Reset: Python-Script neu laufen lassen, leert den Index, baut ihn neu auf. 15 Sekunden. ## Bilanz Was am Ende steht: - **Live-Volltext-Suche über 96 935 Mails** mit <100 ms Antwortzeit - **Sub-50-ms-Performance auf der DB-Ebene** — würde auch bei 10 Mio Datensätzen noch unter 100 ms bleiben - **Snippet-Hervorhebung** mit Kontext-Wörtern um den Treffer herum - **Umlaut- und Akzent-tolerant**: „muller" findet „Müller" - **Filter-Kombinationen**: Volltext + Kontakt + Richtung + Thread (über zusätzliche URL-Parameter) - **Skaliert** auf jeden Datenbestand, der realistisch in FileMaker liegt - **Wartung minimal**: PHP-Endpoint indiziert neue Mails automatisch, Python-Script setzt bei Bedarf alles zurück Das Pattern ist breit übertragbar. Überall dort, wo FileMaker mit großen Datenmengen Live-Suche bieten soll — Kontakte, Aufgaben, Dokumente, Logs — ist FTS5 in einer Helper-SQLite die richtige Antwort. Die FM-Datei selbst bleibt unangetastet, die Suche läuft daneben, der WebViewer ist die Brücke. ## Was als Nächstes käme Drei mögliche Erweiterungen, die diese Lösung von „funktional" zu „brillant" heben würden: 1. **Server-side Pagination**: aktuell 100 Treffer pro Anfrage. Bei einer „1000 Treffer für muller" könnte „Mehr laden"-Button mit `offset`-Parameter ergänzt werden. 2. **Faceted Search**: Aggregations-Endpoint, der für eine Such-Query liefert „X Treffer in Inbox, Y in Sent, Z bei Kontakt A". Bietet sofortige Verfeinerungs-Optionen. 3. **Synonyms-Wörterbuch**: für Geschäftsbegriffe Synonyme definieren („Rechnung" matcht auch „Invoice"). FTS5 unterstützt das über Custom-Tokenizer. Aber: nichts davon braucht es heute. Sub-100-ms-Volltextsuche über 96 000 Mails reicht für die nächsten Jahre. Das Pattern ist offen, falls je gebraucht. --- **Stack:** - SQLite 3.x mit FTS5 (in praktisch jeder modernen PHP-Installation enthalten) - PHP 8.x, PDO-SQLite-Treiber - FileMaker WebViewer mit `data:`-URL + JavaScript-Fetch - Python 3 für den einmaligen Initial-Index-Lauf **Code-Umfang nach Implementierung:** - `lib/helper.php`: +40 Zeilen (FTS5-Tabelle + `index_mail()`-Helper) - `api/search_mails.php`: ~100 Zeilen (komplett neu) - `api/index_mail.php`: ~40 Zeilen (komplett neu) - `/tmp/initial_index_mails.py`: ~80 Zeilen (einmalig) - FileMaker-Berechnung `wv_html_search_fts`: ~80 Zeilen JavaScript+HTML in einer FM-Calc - FileMaker-Skript `Mail indizieren`: ~12 Steps
Wie wir ein 50-GB-Mailarchiv aus einem 12 Jahre alten FileMaker-System gerettet haben
# Wie wir ein 50-GB-Mailarchiv aus einem 12 Jahre alten FileMaker-System gerettet haben *Eine Detektivgeschichte über CSV-Exporte ohne Header, drei verschiedene Encoding-Katastrophen, IDs aus drei Tabellen die alle „Kontakt" hießen und am Ende doch nicht das Gleiche meinten — und darüber, was eine moderne KI in einer solchen Migration konkret leistet. Was ein Senior-FileMaker-Entwickler in zwei Wochen allein hingekriegt hätte, war im Tandem in drei Tagen erledigt. Inklusive funktionierendem Threading.* --- ## Der Ausgangspunkt: ein Archiv ohne Karte Bei einem Kunden lag eine FileMaker-Datenbank mit knapp **50 GB Mailarchiv** auf der Platte. Aufgebaut über zwölf Jahre, durchgewuchert mit einem Plugin namens „Dacons MailIT", erweitert mit eigenen Korrespondenz-Tabellen, irgendwann nicht mehr richtig gepflegt. Der ursprüngliche Entwickler war nicht mehr greifbar. Das produktive System lief auf einer alten FileMaker-Version, auf die niemand mehr Zugriff hatte. Das einzige, was noch verfügbar war: zwei CSV-Exporte. Diese wurden aus einer anderen Perspektive der Altdatenbank exportiert. Feldnamen völlig unbekannt und aufgrund der Bezeichner nicht erklärbar. - **`Mailausgang.csv`** — 426 MB, 45 787 Datensätze, **72 Spalten ohne Header** - **`MailEingang.csv`** — **3.5 GB**, ca. 9.5 Millionen CSV-Zeilen, **70 Spalten ohne Header** Was sich nach „ein paar Mails importieren" anhörte, entpuppte sich als datenarchäologisches Projekt. Was folgt, ist die Geschichte, wie wir aus diesen rohen Exporten am Ende ~93 000 Mails mit funktionierendem Threading in der neuen Datenbank hatten — und warum eine KI in diesem Workflow den Unterschied zwischen drei Tagen und drei Wochen macht. ## Erster Blick — und der erste Schock Beide CSVs hatten **keinen Header**. Das heißt: 72 bzw. 70 Spalten von Werten, ohne dass jemand sagen konnte, was jede einzelne bedeutet. Manche Spalten waren immer leer. Manche enthielten die gleiche Konstante in allen Zeilen. Manche enthielten 2 KB lange „Wertelisten" aus dem alten FM-Schema, die irgendwie ins Datenfeld geraten waren. Beispiel eines einzelnen Datensatzes: ``` "","","","","","","1","verlag degener & co\x0bmanfred\x0bHerr manfred musterman\x0b…", "","unser zeichen: m-m","","45787","45787","16-3314","","Correspondence_Number","", "OfferteLohnabrechnungKostenvoranschlagProjektAuftragsbestätigung…", "45787","1","verlag degen & co\x0b…","","","16-3314 nachlassache [erblasser]", … ``` `\x0b` ist ein **Vertical Tab**, in FileMaker ein interner Zeilenumbruch innerhalb eines Feldes. Die Adresse `verlag degener & co\x0bmanfred\x0bHerr manfred dreiss\x0bam brühl 9\x0b91610 insingen\x0bdeutschland` ist also eigentlich eine mehrzeilige Anschrift, die nur platt nebeneinander geschrieben aussieht. Und der Datensatz-Trenner zwischen Zeilen war kein `\n` und auch kein `\r\n`, sondern **`\r` allein** — Mac-Klassik-CR, wie in den Neunzigern. Standard-Tools wie `wc -l` lieferten daher für eine 426-MB-Datei „0 Zeilen". Erfreulich. ## Detektivarbeit Phase 1 — die Spalten erschließen Hier kommt die erste Stelle, an der eine KI dramatisch beschleunigt. Statt sich durch 72 Spalten manuell durchzuarbeiten und für jede zu raten, was sie sein könnte, lief eine Python-Stichprobe über 500 Datensätze: - Wie viele Werte sind in jeder Spalte gefüllt? - Wie viele unique? - Sehen die Werte aus wie Zahlen, Datums, E-Mail-Adressen, freier Text? - Welche Spalten haben eindeutige Werte (PK-Kandidaten)? - Welche haben sich wiederholende kleine Zahlen (FK-Kandidaten)? Das Ergebnis war ein systematischer Steckbrief pro Spalte. Aus dem ließ sich rückschließen: ``` [14] Aktenzeichen (Projektcode): "16-3314" [26] Versanddatum: "13/04/2016" [40] Mail-PK (eindeutig 17–244) [44] FK-Kandidat (nur 1 oder 2) [45] FK-Kandidat (62 unique, Range 1–60) ← id_kontakt? [53] Empfänger-Mail: "[email protected]" [59] Eindeutig pro DS, 2–3314 ← könnte id_customer sein [64] Betreff [65/67] Body (Plain) ``` Der Aha-Moment kam mit einer einfachen Hypothese: **Hängt Spalte [59] mit dem Aktenzeichen zusammen?** Aktenzeichen `16-3314` und Spalte [59] `3314` — das war ein verdächtiger Zufall. Test über 10 Stichproben: ``` DS 1: Aktenz. "16-3314" → [59] = "3314" ✓ MATCH DS 2: Aktenz. "16-0002" → [59] = "2" ✓ MATCH DS 3: Aktenz. "16-0003" → [59] = "3" ✓ MATCH … ``` Bingo. Das Aktenzeichen ist `JJ-CCCCC`, wobei `JJ` das Jahr und `CCCCC` die **Customer-ID** ist. In zwei Minuten geknackt, was in der alten Dokumentation nicht stand und niemand mehr wusste. ## Detektivarbeit Phase 2 — die Encoding-Hölle Eine frühere Version des Exports (`mailausgang_clean.csv`, von einer anderen Migration übrig geblieben) sah auf den ersten Blick gut aus — 14 saubere Spalten mit Header. Aber dann das hier: ``` "Mit freundlichen Gr√ºssen Jörg Muster Universit√§t Bern" ``` Das ist **Mojibake**: UTF-8-Bytes, die als MacRoman gelesen und wieder als UTF-8 gespeichert wurden. Aus `ü` (UTF-8 `\xc3\xbc`) wird beim Doppel-Encoding `√º`. Aus `ä` wird `√§`, aus `ß` wird `√ü`. Die Reverse-Operation: ```python "Gr√ºssen".encode('mac_roman').decode('utf-8') # → 'Grüssen' ``` Diese eine Zeile — `encode('mac_roman').decode('utf-8')` — hat 41 133 Datensätze gerettet. Sie ist nicht trivial: man muss wissen, dass alte FileMaker-Versionen auf Macs intern MacRoman als Charset benutzten, dass aber die meisten Tools heute UTF-8 erwarten. Das Reverse-Pattern ist Sachkunde, kein Lehrbuchwissen. Ein Entwickler, der nicht regelmäßig mit alten Mac-Daten arbeitet, kann da Stunden bis Tage drauf verlieren. Aber das war nur die erste Encoding-Falle. Die nächste war versteckter. ## Die BOM-Falle Die clean.csv ließ sich nicht parsen. `csv.DictReader` lieferte Datensätze, aber das Feld `Mail_ID` war immer leer. Ein Hexdump auf die ersten Bytes: ``` \xef\xbb\xbf M a i l _ I D , I D _ C O N T A C T ,… ``` Da waren drei Bytes vor dem Header, die der CSV-Reader nicht erkannte: **UTF-8 BOM** (Byte Order Mark). Die erste Header-Spalte hieß also nicht `Mail_ID`, sondern ` Mail_ID`. Jeder Lookup auf `row['Mail_ID']` lief ins Leere. Fix: `open(file, encoding='utf-8-sig')` — das `-sig` frisst das BOM automatisch. ## Die Modifikationsdatum-Falle Beim ersten Import-Test der MailEingang-Mails zeigten alle Mails Empfangsdatum **2026** — also in der Zukunft. Eine Mail von Hiltrud Jacob, die laut Inhalt aus 2016 stammte, hatte als `Empfangen_ts` den Wert „2026-01-04 20:26:17". Schaute man genauer hin: Es gab in der CSV zwei verschiedene Zeitstempel-Felder. Feld [4] mit „12/04/2016" — Original-Empfangsdatum. Feld [62] mit „04/01/2026 20:26:17" — das **letzte Modifikationsdatum** in MailIT, also der letzte Touch (z.B. ein „Mail als gelesen markiert" oder eine spätere Bearbeitung). Der erste Konverter-Lauf nutzte [62]. Falsch. Hätte vermutlich ein menschlicher Entwickler bei der ersten Stichprobe auch entdeckt, aber: man muss wissen, dass MailIT zwei verschiedene Datumsfelder pflegt. Auch das ist Sachkunde, die durch systematische Stichproben („zeig mir 3 echte Mails, lass mich sehen was wo steht") schneller aufgedeckt wird als durch manuelles Klicken. ## Die ID-Brücke, die keine war Beide CSV-Exporte hatten Kontakt-IDs. Aber sie meinten verschiedene Tabellen. | | Mailausgang | MailEingang | |---|---|---| | Spalte | [45] `ID_CONTACT` | [8] `id_kontakt` | | Wertebereich | 2 000 – 15 000 | 1 – 146 | | Verweist auf | Korrespondenz-Tabelle | (vermutlich) MailIT-Adressbuch | Die naheliegende Annahme — beide IDs zeigen auf dieselbe zentrale Kontakttabelle — war falsch. Die Wertebereiche überlappten sich nicht einmal. Eine ID 51 in MailEingang ist nicht „derselbe Kontakt" wie ID 51 in Mailausgang. **Es waren schlicht zwei verschiedene Adressbücher.** Daraus folgte: Eine direkte ID-Brücke zwischen Eingang und Ausgang gab es nicht. Aber: Die **E-Mail-Adresse** des Gegenübers war in beiden Archiven vorhanden. Wenn man die als Brücke nahm — kombiniert mit dem normalisierten Betreff (also Betreff ohne „Re:", „Aw:", „Fwd:") — bekam man eine heuristische Cross-Archive-Verbindung. ## Die 9.5-Millionen-Zeilen-Datei mit den 47.000 Mails Die MailEingang.csv war 3.5 GB groß. Bei den ersten Stichproben sah es so aus, als wären nur etwa 1 % der Zeilen echte Mails (mit Header), der Rest „Müll-Datensätze" mit nur Betreff und Datum. Hochrechnung auf 47 000 Zeilen ergab ~560 echte Mails. Aber ein vollständiger Stream-Durchlauf zeigte was ganz anderes: ``` Gesamt: 9 536 233 Zeilen, davon 47 678 echte Mails (0.50 %) ``` Die Datei hat nicht 47 000 Zeilen, sondern **9.5 Millionen**. FileMaker hat beim Export pro Datensatz offenbar mehrere Hilfs-Zeilen produziert (vermutlich für Wertelisten oder Such-Indizes), und die echten Mails sind über die ganze Datei verstreut. Hier eine wichtige technische Notiz: Python kann das in **35 Sekunden** durchstreamen, wenn man weiß, wie: ```python csv.field_size_limit(sys.maxsize) with open(path, 'r', encoding='utf-8', errors='replace', newline='') as f: for row in csv.reader(f): if len(row) > 41 and row[41].strip(): # echte Mail ... ``` Drei Details, die zwingend nötig sind: 1. `csv.field_size_limit(sys.maxsize)` — sonst wirft Python einen `FieldOverflow` bei den 23-KB-Headers 2. `errors='replace'` — sonst stürzt der Reader an einem einzelnen kaputten Byte ab 3. `newline=''` — sonst übernimmt Python die Zeilenumbruch-Logik und macht aus `\r\n` ein `\n`, was die FM-internen `\r` zerstört Das sind drei Zeilen, die zusammen vier Stunden Debugging ersparen. ## Threading — die schönste Disziplin Das eigentliche Geschmacks-Ziel des Kunden war ursprünglich nicht der Mail-Import, sondern **Threading**: Mails, die thematisch zusammengehören, sollen als Konversation gruppiert sichtbar werden. Das ist im RFC 5322 sauber gelöst — Mail-Header `Message-ID`, `In-Reply-To`, `References` ergeben einen klaren Baum. Aber: - **Mailausgang.csv hatte keine Mail-Header.** Keine Message-ID, kein In-Reply-To, nichts. Die alte Korrespondenz-Datenbank hatte das nie gespeichert. - **MailEingang.csv hatte echte Header in Feld [41]** — und davon hatten 60 % einen In-Reply-To, 62 % References. Daraus folgten drei verschiedene Threading-Strategien: **1. Mailausgang — heuristisch über Kontakt + Betreff** Jede Mail-Gruppe mit demselben `id_contacts` und demselben normalisierten Betreff wurde zu einem Thread. Älteste Mail = Wurzel. → 8 800 Threads, 27 800 Mails gruppiert (63 %). **2. MailEingang — echtes RFC-Threading** Per `In_Reply_To` rekursiv die Wurzel suchen, mit Memoization für Performance. → 2 600 Threads, 6 100 Mails gruppiert (13 %, weil viele Vorgänger im anderen Archiv liegen). **3. Cross-Archive-Threading** — die Königsdisziplin Gruppen-Key: `(Gegenüber-E-Mail-Adresse, normalisierter Betreff)`. Damit verbinden sich die ausgehenden Mails des Kunden mit den eingehenden Antworten, obwohl die ausgehenden nur synthetische Message-IDs haben. → 11 000 Threads, **69 300 Mails in Threads (75 % aller Legacy-Daten)**. Ein konkretes Beispiel aus dem Top-Thread: ``` Kontakt: [email protected] Betreff: "heiratsurkunde" 921 Mails über 10 Jahre (2016–2025) 4 ausgehend, 917 eingehend ``` Eine zehn Jahre lange Geschäftskorrespondenz mit einer Notarin/Standesbeamtin, die jetzt erstmals als zusammenhängender Thread sichtbar ist. ## Die FileMaker-Falle, die niemand sieht In FileMaker gibt es eine kleine Checkbox in den Auto-Eingabe-Optionen eines Feldes: **„Vorhandenen Feldwert nicht ersetzen (während des Imports oder beim Setzen über Skript)"**. Sie ist standardmäßig aktiv. Das klingt harmlos. Aber: 1. Datensatz wird neu angelegt → Auto-Eingabe-Berechnung läuft einmal mit leerem Quellfeld → ergibt leeren Wert 2. Skript setzt nachträglich `Message_ID` → Auto-Eingabe **läuft nicht mehr**, weil der Wert (leer) „schon da war" 3. `Message_ID_norm` bleibt dauerhaft leer 4. Threading-Match per `Message_ID_norm` greift nie Diese Falle hat im Verlauf des Projekts zu mehreren Stunden Fehlersuche geführt — beim ersten Test wurden alle frisch gesendeten Mails beim nächsten Abruf wieder als neu importiert, weil der Match nicht griff. Die Lösung ist trivial (Häkchen weg), aber das Wissen darüber, dass dieses Häkchen das ist, was schiefgeht, ist nichts, was in der Standard-Dokumentation auftaucht. Es ist tribal knowledge der FileMaker-Community. ## Was die KI hier konkret leistet Ehrliche Bilanz nach drei Tagen: **Was der Mensch lieferte:** - Den Domain-Kontext (Kunden-Geschäftslogik, was sind FK-Tabellen, wo soll das Ergebnis hin) - Alle FileMaker-Entscheidungen (Schema, Beziehungsgrafik, Skripte, Layouts) - Die Hypothesen (z.B. „könnte die Conversation eine Obertabelle sein?") - Den Pragmatismus („das reicht so, Mojibake-Reste fixen wir später") **Was die KI lieferte:** - Pattern-Erkennung in unstrukturierten CSVs **in Sekunden**, nicht Stunden - Encoding-Sachkunde (MacRoman-Roundtrip, BOM-Stripping, FileMaker-spezifische Steuerzeichen) - Python-Skripte **on-the-fly** für Einmal-Aufgaben (Streaming, Header-Parsing, Cross-Reference) - Cross-Reference-Analysen zwischen den Archiven (welche IDs überlappen?) - Drei verschiedene Threading-Strategien sauber implementiert - Iteratives Debugging mit klaren Tests („zeig mir die erste echte Mail, schau ob Datum stimmt") - Doku & Blogpost am Ende — bei laufender Erinnerung an alle Sackgassen und Aha-Momente Wäre der Entwickler ohne KI rangegangen: Ich schätze konservativ **5–10 Arbeitstage**, mit deutlich mehr Frustrations-Spitzen. Insbesondere die Encoding-Forensik (MacRoman-Mojibake erkennen) und die statistische Spalten-Identifikation (welche Spalte ist welche FK?) sind ohne Tools mit großer NLP- und Pattern-Recognition-Erfahrung mühsam. Mit KI: drei Tage, von denen die letzten beiden hauptsächlich für Tests und Feintuning draufgingen. ## Was am Ende stand | | Anzahl | |---|---:| | Mailausgang-Datensätze importiert | 44 257 | | MailEingang-Datensätze importiert | 47 678 | | **Gesamt-Mails im neuen System** | **91 935** | | Davon mit FK-Zuordnung (Kontakt + Customer) | ~86 000 | | **Mails in Konversations-Threads** | **69 300 (75 %)** | | Cross-Archive-Threads (Ausgang ↔ Eingang verbunden) | 3 054 | | Verlorene Datensätze | 0 | | Manuelle Klicks im Migrationsprozess | ~30 | Plus die Sicherheit, dass künftige Mails (per IMAP über die neue PHP-Mail-Pipeline) mit echtem RFC-Threading reinkommen und mit dem Legacy-Archiv kohärent zusammenleben. ## Lessons Learned **1. Bei alten Datenexporten gibt es immer mehr Encoding-Probleme als man denkt.** UTF-8-BOM, MacRoman-Mojibake, Windows-1252-Mojibake, FM-interne Vertical Tabs — das sind nicht „Exoten", das sind die Regel. Wer mit Legacy-Daten arbeitet, sollte die Reverse-Patterns parat haben oder eine KI dabei haben, die sie kennt. **2. „Streamingfähig denken" gewinnt.** 3.5 GB CSV in 35 Sekunden durchstreamen statt 30 Minuten zu warten ist eine Frage von drei richtig gesetzten Python-Parametern. Wer das nicht weiß, optimiert tagelang an einer Bulk-Load-Lösung herum. **3. Heuristische Brücken sind oft besser als perfekte Spezifikation.** Es gab keinen klaren ID-Mapping zwischen den Archiven. Die Lösung war nicht, eine perfekte Brücke zu rekonstruieren, sondern eine pragmatische heuristische über E-Mail-Adresse und Betreff. Sie ist nicht 100 % korrekt, aber sie macht aus 37 % Threading-Quote 75 %. Ein „gut genug" zur richtigen Zeit ist mehr wert als ein „perfekt" nie. **4. Iteratives Vorgehen mit kleinen Test-Imports rettet Stunden.** Test mit 100 Datensätzen, prüfen, justieren, dann erst die 47 000. Wer beim ersten Versuch alle 47 000 importiert und dann merkt, das Empfangsdatum ist Modifikationsdatum, hat richtig schlechte Laune. **5. KI ist im Datenmigrations-Kontext ein massiver Hebel.** Nicht weil sie irgendwas Magisches macht, sondern weil sie drei Dinge zusammenbringt, die selten zusammenkommen: Pattern-Recognition über große Datenmengen, breite Encoding/Format/Tool-Sachkunde, und die Geduld, dieselbe stupide Frage zum 47. Mal zu beantworten („zeig mir das Feld nochmal"). --- **Stack:** - Python 3 (csv, re, base64) — Streaming-Konverter - Git/Dropbox — Datenversionierung - FileMaker 22 — Zieldatenbank - PHP-Mailclient — eigene Pipeline für laufende Mails (siehe separater Blog-Post zur Plugin-Migration) **Zeitaufwand:** - Tag 1: PHP-Migration MailIT → eigener Mailclient - Tag 2: Threading-Schema in FileMaker - Tag 3: Legacy-Import (das hier beschriebene) **Datenmenge:** - 50 GB FileMaker-Originaldatei (nicht zugänglich) - 3.9 GB CSV-Exporte (zugänglich) - 161 + 345 MB FM-importbereite CSVs (am Ende) - 91 935 Mails in der neuen Datenbank