Volltextsuche in SQLite mit FTS5 – schneller und besser als LIKE '%wort%
LIKE '%suchwort%' kann keinen Index nutzen und liest bei jeder Suche die ganze Tabelle. FTS5 ist in SQLite eingebaut, findet auch Wortanfänge und sortiert die Treffer nach Relevanz – ohne Elasticsearch daneben.
Eine Suche über Blogbeiträge, Notizen oder Produktbeschreibungen fängt fast immer so an:
SELECT * FROM beitraege
WHERE titel LIKE '%rechnung%' OR text LIKE '%rechnung%';Das funktioniert und ist bei zweihundert Zeilen völlig in Ordnung. Zwei Dinge stören trotzdem: Ein Index hilft hier nie (das führende % macht ihn unbrauchbar, es wird immer die ganze Tabelle gelesen), und die Treffer kommen in beliebiger Reihenfolge — ein Beitrag, in dem das Wort einmal am Rand vorkommt, steht gleichauf mit einem, der davon handelt.
SQLite bringt für genau das ein eigenes Modul mit. Ohne Zusatzinstallation.
Die Suchtabelle anlegen
CREATE VIRTUAL TABLE beitraege_fts USING fts5(
titel,
text,
content = 'beitraege', -- Inhalt bleibt in der Originaltabelle
content_rowid = 'id'
);content= ist der Teil, den ich immer setze: Damit speichert FTS5 den Text nicht noch einmal, sondern nur den Suchindex. Deine Datenbank wächst dadurch deutlich weniger.
Den Index aktuell halten
Weil der Inhalt in der Originaltabelle liegt, muss FTS5 über Änderungen informiert werden. Das erledigen drei Trigger, die man einmal schreibt und dann vergisst:
CREATE TRIGGER beitraege_ai AFTER INSERT ON beitraege BEGIN
INSERT INTO beitraege_fts(rowid, titel, text) VALUES (new.id, new.titel, new.text);
END;
CREATE TRIGGER beitraege_ad AFTER DELETE ON beitraege BEGIN
INSERT INTO beitraege_fts(beitraege_fts, rowid, titel, text)
VALUES ('delete', old.id, old.titel, old.text);
END;
CREATE TRIGGER beitraege_au AFTER UPDATE ON beitraege BEGIN
INSERT INTO beitraege_fts(beitraege_fts, rowid, titel, text)
VALUES ('delete', old.id, old.titel, old.text);
INSERT INTO beitraege_fts(rowid, titel, text) VALUES (new.id, new.titel, new.text);
END;Das Löschen sieht seltsam aus — man schreibt 'delete' in die Tabelle hinein — ist aber genau die vorgesehene Schreibweise.
Für den bestehenden Bestand einmalig:
INSERT INTO beitraege_fts(beitraege_fts) VALUES ('rebuild');Suchen
SELECT b.id, b.titel
FROM beitraege_fts f
JOIN beitraege b ON b.id = f.rowid
WHERE beitraege_fts MATCH 'rechnung'
ORDER BY rank; -- die besten Treffer zuerstORDER BY rank ist der Punkt, an dem FTS5 LIKE deutlich überlegen ist: Ein Beitrag, in dem das Wort im Titel und mehrfach im Text steht, landet oben.
Die Suchsprache kann noch mehr:
MATCH 'rechnung*' -- Wortanfang: rechnung, rechnungen, rechnungsnummer
MATCH 'rechnung AND 2026' -- beides muss vorkommen
MATCH 'titel : rechnung' -- nur in der Titel-Spalte
MATCH '"offene rechnung"' -- exakt diese WortfolgeUnd die Fundstelle anzeigen
SELECT snippet(beitraege_fts, 1, '<mark>', '</mark>', '…', 12) AS auszug
FROM beitraege_fts WHERE beitraege_fts MATCH 'rechnung' ORDER BY rank;snippet liefert den Textausschnitt rund um den Treffer, mit markiertem Suchwort. Das ist die Sorte Detail, für die man sonst eine halbe Stunde Anwendungscode schreibt.
Wann es sich lohnt
Unter ein paar tausend Zeilen ist LIKE völlig ausreichend, und die drei Trigger wären nur Ballast. Sobald aber eine echte Suchfunktion für Nutzer entsteht — mit Relevanz, Wortanfängen und hervorgehobenen Fundstellen — ist FTS5 der Punkt, an dem man sich einen ganzen zusätzlichen Suchserver spart.
Du überlegst, ob dein Projekt eine richtige Suche braucht? Frag mich, ich sage dir ehrlich, ab wann sich der Aufwand lohnt.
Quellen
Du brauchst mehr als ein Snippet?
Ich entwickle Android-Apps in Kotlin und moderne Websites für Selbstständige und kleine Unternehmen — von der ersten Idee bis zum Release.
Projekt anfragen →Verwandte Snippets
Keyset-Pagination – warum LIMIT ... OFFSET auf Seite 500 kriecht
OFFSET 10000 heißt für die Datenbank: zehntausend Zeilen lesen und wegwerfen. Und wenn zwischendurch jemand etwas einfügt, sieht der Nutzer beim Blättern denselben Eintrag zweimal. Beides löst dieselbe Umstellung.
JSON in SQLite abfragen – json_extract und der ->>-Operator
Ein JSON-Feld in der Datenbank ist bequem, bis man danach filtern will. SQLite kann direkt hineinschauen – und mit einem Index auf dem richtigen Ausdruck ist das sogar schnell.
WITH RECURSIVE – Kategoriebäume und Kommentar-Threads in einer einzigen Abfrage
Kategorien mit Unterkategorien, Kommentare mit Antworten, Ordner in Ordnern: Die meisten holen sich so etwas mit einer Schleife und einer Abfrage pro Ebene. Eine rekursive CTE holt den ganzen Baum in einem Rutsch – inklusive Tiefe und Pfad.