iOS-FTS5-Suche prüfen: Tokenisierung, Abfragen und Indexabgleich
Ein Kunde ändert den Titel eines Tickets. In der Liste erscheint der neue Titel, doch die Offline-Suche findet weiterhin den alten Begriff. Solche Fehler bleiben leicht unbemerkt, wenn ein Studio seine Builds auf wechselnden Cloud-Macs erstellt: Das Projekt lässt sich kompilieren und die Datenbank öffnen, aber der Volltextindex ist nicht mehr mit der Fachtabelle synchron. Für die Abnahme vor der Übergabe eignen sich feste Testdaten, mit denen sich das vereinbarte Suchverhalten prüfen lässt. Tests mit produktiven Daten können ergänzende Beobachtungen liefern, taugen aber nicht als alleiniger Maßstab, weil ihre Ergebnisse kaum zuverlässig reproduzierbar sind.
Zuerst die tatsächlich verwendete SQLite-Version prüfen
Im Terminal des Cloud-Macs sqlite3 :memory: 'SELECT sqlite_version();' ausführen und die für den Test verwendete Version festhalten. Anschließend versuchen, eine virtuelle FTS5-Tabelle anzulegen. Schlägt das fehl, den Test abbrechen: Fehlende Suchergebnisse dürfen dann nicht als Tokenisierungsproblem interpretiert werden.
Das Kommandozeilenwerkzeug prüft nur die Testumgebung. Ein iOS-Projekt kann gegen die Systembibliothek gelinkt sein oder eine eigene SQLite-Version mitbringen. Deshalb muss dieselbe Anweisung zum Anlegen der Tabelle auch über die tatsächliche Datenbankverbindung der App ausgeführt werden. Diese Prüfung gehört in das Test-Target und muss auf dem Zielsystem laufen. Reine SQL-Tests lassen sich zunächst schnell auf dem Mac weiterentwickeln, ersetzen aber keine Ausführung auf der mobilen Plattform.
Tokenisierung und Abfragesemantik festlegen
Zunächst entscheiden, ob die Suche „wörtliche Phrasen“ oder eine „erweiterte Suchsyntax“ unterstützen soll. Ersteres eignet sich für ein gewöhnliches Ticketsuchfeld, Letzteres für Werkzeuge, die ausdrücklich boolesche Ausdrücke unterstützen. Beide Verhaltensweisen sollten nicht ohne entsprechenden Hinweis in einem einzigen Eingabefeld vermischt werden.
unicode61 remove_diacritics 2 kann gängige Akzentzeichen lateinischer Buchstaben behandeln, ist aber kein universeller Tokenizer für Chinesisch. Die Testdaten müssen mindestens Groß- und Kleinschreibung, Akzente, Bindestriche, Anführungszeichen und zusammenhängenden chinesischen Text abdecken. Erfolgreiche englische Tests sind kein Beleg dafür, dass sich im Chinesischen nach beliebigen Wörtern suchen lässt.
SQL-Parameterbindung und das Escaping für MATCH sind zwei getrennte Ebenen. Die Parameterbindung verhindert, dass eine Eingabe Teil der SQL-Struktur wird. Sie deaktiviert jedoch nicht die eigene Abfragesyntax von MATCH. Für eine wörtliche Phrase zunächst alle enthaltenen doppelten Anführungszeichen verdoppeln und anschließend die gesamte Eingabe in doppelte Anführungszeichen einschließen:
func literalFTSQuery(_ text: String) -> String? {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
return "\"" + trimmed.replacingOccurrences(of: "\"", with: "\"\"") + "\""
}
Den Rückgabewert an WHERE ticket_fts MATCH ? binden, statt SQL per Zeichenkettenverkettung aufzubauen. Auch eine „wörtliche“ Suche durchläuft hier die Tokenisierung und ist daher keine bytegenaue Teilstringsuche. Bei leerer Eingabe direkt eine leere Ergebnismenge oder die im Produkt festgelegte Standardliste zurückgeben.
Die Regeln für Suchtreffer gehören zum Produktvertrag. Präfix-, Phrasen- und Teilstringsuche sind nicht austauschbar. Syntaxfehler dürfen erst recht nicht stillschweigend in „keine Treffer“ umgewandelt werden.
Externen Inhaltsindex mit Triggern synchronisieren
Eine externe Inhaltstabelle eignet sich für Projekte, die bereits eine Fachtabelle besitzen und deren Text nicht doppelt speichern möchten. Sie ist nicht geeignet, wenn die Implementierung darauf setzt, dass FTS5 Änderungen an der Fachtabelle automatisch überwacht. content deklariert lediglich die Zuordnung; Einfügen, Löschen und Aktualisieren müssen weiterhin synchronisiert werden.
Das folgende Beispiel lässt sich direkt im Terminal ausführen. Sämtliche Daten bleiben im Arbeitsspeicher. Sobald eine Assertion fehlschlägt, endet der Befehl mit einem von null verschiedenen Exitstatus:
sqlite3 -bail :memory: <<'SQL'
CREATE TABLE ticket(id INTEGER PRIMARY KEY, title TEXT NOT NULL);
CREATE VIRTUAL TABLE ticket_fts USING fts5(
title,
content='ticket',
content_rowid='id',
tokenize='unicode61 remove_diacritics 2'
);
CREATE TRIGGER ticket_ai AFTER INSERT ON ticket BEGIN
INSERT INTO ticket_fts(rowid, title) VALUES(new.id, new.title);
END;
CREATE TRIGGER ticket_ad AFTER DELETE ON ticket BEGIN
INSERT INTO ticket_fts(ticket_fts, rowid, title)
VALUES('delete', old.id, old.title);
END;
CREATE TRIGGER ticket_au AFTER UPDATE ON ticket BEGIN
INSERT INTO ticket_fts(ticket_fts, rowid, title)
VALUES('delete', old.id, old.title);
INSERT INTO ticket_fts(rowid, title) VALUES(new.id, new.title);
END;
CREATE TEMP TABLE assertion(ok INTEGER NOT NULL CHECK(ok = 1));
INSERT INTO ticket VALUES(1, 'Café delivery');
INSERT INTO assertion
SELECT count(*) = 1 FROM ticket_fts
WHERE ticket_fts MATCH '"cafe"';
UPDATE ticket SET title = 'Invoice approved' WHERE id = 1;
INSERT INTO assertion
SELECT count(*) = 0 FROM ticket_fts
WHERE ticket_fts MATCH '"cafe"';
INSERT INTO assertion
SELECT count(*) = 1 FROM ticket_fts
WHERE ticket_fts MATCH '"invoice"';
BEGIN;
DELETE FROM ticket WHERE id = 1;
INSERT INTO assertion
SELECT count(*) = 0 FROM ticket_fts
WHERE ticket_fts MATCH '"invoice"';
ROLLBACK;
INSERT INTO assertion
SELECT count(*) = 1 FROM ticket_fts
WHERE ticket_fts MATCH '"invoice"';
DELETE FROM ticket WHERE id = 1;
INSERT INTO assertion
SELECT count(*) = 0 FROM ticket_fts
WHERE ticket_fts MATCH '"invoice"';
SQL
Zum Löschen eines Indexeintrags muss der alte Text übergeben werden. Deshalb entfernt der Update-Trigger zunächst den alten Eintrag und fügt danach den neuen ein. Die Änderung an den Fachdaten und die Ausführung des Triggers liegen in derselben Transaktion. Nach einem Rollback müssen Fachtabelle und Index gemeinsam wiederhergestellt sein. Bleibt das Beispiel ohne Fehlermeldung, ist der Test bestanden; eine manuelle Kontrolle von Tabellen im Terminal ist nicht nötig.
Auch Bestandsdaten und Sortierung abnehmen
Wurde zuerst die Fachtabelle angelegt und der Index erst später ergänzt, werden vorhandene Datensätze durch das Einrichten der Trigger nicht automatisch nachindexiert. Während der Migration INSERT INTO ticket_fts(ticket_fts) VALUES('rebuild'); ausführen, wobei Schreibzugriffe in diesem Zeitraum kontrolliert sein müssen. Anschließend mit festen Datensätzen aus der Zeit vor der Migration prüfen, ob die erwarteten Treffer erscheinen. Der Neuaufbau gehört zur Migration und darf nicht bei jedem Öffnen der Suchansicht laufen.
Nicht mit SELECT count(*) FROM ticket_fts prüfen, ob der Index vollständig ist: Bei einer externen Inhaltstabelle können Abfragen ohne MATCH direkt auf die Inhaltstabelle zugreifen. Stattdessen sind Assertions mit echten Suchabfragen erforderlich, die getrennt bestätigen, dass der alte Begriff nicht mehr und der neue Begriff nun gefunden wird.
Bei gleichem Rang zusätzlich stabil nach dem fachlichen Primärschlüssel sortieren, etwa mit ORDER BY rank, rowid. Testdaten für die Paginierung müssen mehrere Treffer mit gleichem Rang enthalten. Andernfalls können wechselnde Reihenfolgen bei wiederholter Ausführung fälschlich als fehlende Datensätze diagnostiziert werden.
Fehlerfälle in die Übergabeprüfung aufnehmen
Bei jeder Übergabe SQLite-Version, Parameter zum Anlegen der Tabellen, Version der Testdaten und Namen fehlgeschlagener Testfälle dokumentieren. Echte Suchtexte von Kunden dürfen nicht gespeichert werden. Die Tests um leere Zeichenketten, reine Satzzeichen, überlange Eingaben und doppelte Anführungszeichen ergänzen. Eingaben oberhalb der im Produkt festgelegten Längengrenze ausdrücklich ablehnen, statt sich auf Datenbankfehler als Auffanglösung zu verlassen.
Bei der Abnahme einzeln prüfen: Eine neu angelegte und eine migrierte Datenbank liefern dieselben Ergebnisse; nach einer Umbenennung trifft der alte Begriff nicht mehr; nach dem Löschen bleiben keine Treffer zurück; ein Transaktions-Rollback stellt die Ergebnisse wieder her; und das Verhalten bei chinesischem Text entspricht der Beschreibung. Zum Schluss denselben Datensatz im iOS-Test-Target ausführen. So wird beim Wechsel des Cloud-Macs oder bei der Übergabe eines Kundenprojekts das Suchverhalten selbst geprüft – nicht ein zufällig noch korrekter Index auf einem bestimmten Rechner.
Häufig gestellte Fragen
Beweist FTS5 im sqlite3-Werkzeug des Mac die Unterstützung in iOS?
Nein. App und Werkzeug können unterschiedliche SQLite-Bibliotheken verwenden. Erstellen Sie über die echte App-Verbindung eine temporäre FTS5-Tabelle und prüfen Sie die Testdaten auf dem Zielsystem.
Zerlegt unicode61 chinesische Sätze in Wörter?
Eine allgemeine chinesische Wortsegmentierung bietet es nicht. Prüfen Sie echte Texte und verwenden Sie bei Wortsuche eine zusätzliche, für Indexierung und Abfragen identische Segmentierung.
Mietdauer passend zum Projektplan wählen
Ideal für kurzfristige Builds, zusätzliche Kapazität in der Release-Woche und getrennte Umgebungen für mehrere Kundenprojekte. Wenn Ihr Team dauerhaft ausgelastet ist und bereits eigene Geräte nutzt, vergleichen Sie Kauf und Miete, bevor Sie sich entscheiden.
VMSSD bietet Ihnen dedizierte physische Mac mini – keine virtuellen Maschinen. Wählen Sie eine Mietdauer von einem Tag, einer Woche, einem Monat oder einem Quartal. Ohne Verlängerung endet die Miete automatisch. Verfügbarkeit von Modellen und Standorten sehen Sie aktuell in der Konsole.