Loading...

Algolia durch eine In-Memory-Suche ersetzen in Next.js 14

2. Juni 2026
Inhaltsverzeichnis

Der befriedigendste Commit unserer Migration weg von Contentful war der, der nur Dinge gelöscht hat. Raus flogen der Algolia-Client, die Sync-Route, die jeden veröffentlichten Beitrag an einen gehosteten Index schob, und vier Umgebungsvariablen, die ich nicht länger geheim halten musste. Die On-Site-Suche von cloudapp.dev steckt jetzt in einer einzigen Datei, ohne Dienst dahinter — und ich möchte die Überlegung dahinter durchgehen, samt dem einen Teil, der wirklich Nachdenken brauchte.

Nicht Algolia war das Problem — unsere Größe war es

Ganz offen: Algolia ist gute Software, und bei einem großen Katalog würde ich wieder dazu greifen. Aber unser "Katalog" sind rund 220 Blogposts. Trotzdem musste jede Veröffentlichung an einen externen Index verteilt werden — das bedeutet einen Sync-Job, einen Webhook und eine leise, wiederkehrende Sorge: ist der Index wirklich auf dem Stand des Live-Inhalts, oder fehlt still und heimlich ein Beitrag in der Suche? Wir pflegten eine zweite Kopie unserer Inhalte und bezahlten dafür — um einen Datensatz zu durchsuchen, der bequem in den Arbeitsspeicher passt.

Beim Umstieg von Contentful auf ein selbstgehostetes Headless-CMS gab ein Detail den Ausschlag: der Adapter hält ohnehin alles im Speicher.

Der Store liegt schon im Speicher

Beim ersten Request an eine Server-Instanz lädt der Adapter den ganzen Space — jeden Record, jedes Asset, jeden Tag, rund 8 MB JSON — und memoisiert ihn für die Lebensdauer dieser Instanz. Eine kleine Randnotiz, die sich als wichtig erwies: dieser Payload ist groß genug, dass Next.js ihn schlicht nicht in seinen Data-Cache aufnimmt (im Log steht freundlich items over 2MB can not be cached). Was passt, denn ich wollte nie eine zweite Cache-Schicht — der In-Memory-Store ist der Cache. Suche darüber ist kein I/O-Problem. Es ist ein String-Match über ein Array, das wir bereits halten:

Der ehrliche Preis ist der erste Request an eine kalte Instanz, der diesen ~8-MB-Fetch bezahlt; danach ist der Store warm und jede Suche — und jedes Tag-Cloud-Rendering — quasi gratis. Für eine Seite, die ein paar Mal pro Woche deployt, ist das ein Tausch, den ich jederzeit eingehe.

Die Bewertung ist bewusst langweilig

Jeder Beitrag wird einmal in einen kleingeschriebenen haystack zusammengeführt — Titel, Beschreibung, Text und Tags aneinandergehängt. Das Ranking tut dann das Naheliegende: gewichte einen Treffer danach, in welchem Feld er landet, und verlange, dass jeder Begriff irgendwo vorkommt — so grenzt eine Zwei-Wort-Suche die Ergebnisse ein, statt sie auszuweiten.

Die Gewichte sind mit keiner Wissenschaft justiert — ein Titel-Treffer (8) soll einen Body-Treffer (2) schlicht schlagen, denn wenn deine Suchbegriffe im Titel stehen, ist dieser Beitrag fast sicher gemeint. Derselbe In-Memory-Durchlauf speist auch die Tag-Cloud der Startseite: computeFacets() zählt nur die Tags über dieselben Beiträge, sodass Suche und Facetten nie uneins sein können, was existiert.

Der einzige Teil, über den ich wirklich nachdenken musste

Die Suche selbst waren die einfachen zwanzig Zeilen. Worüber ich saß, war sicherzustellen, dass der Index niemals lügt. Also memoisiere ich den gebauten Index auf einer WeakMap, deren Schlüssel das Store-Objekt selbst ist. Wenn ein Content-Webhook feuert und wir den Store via resetStore() austauschen, wird das alte Store-Objekt unerreichbar — und sein WeakMap-Eintrag geht mit ihm, eingesammelt vom GC. Kein resetSearchIndex(), an das man denken müsste, keine TTL zum Raten, kein Weg, auf dem der Index vom beschriebenen Inhalt abweichen kann. Er kann seine Quelle schlicht nicht überleben. Genau diese Eigenschaft war der Punkt; der Token-Scan war fast nebensächlich.

Wofür ich weiterhin für Algolia zahlen würde

Fairerweise zum Trade-off: das funktioniert nur, weil der Datensatz klein und das Matching einfach ist. Keine Tippfehlertoleranz, keine Synonyme, kein Stemming, keine Relevanz-Feinabstimmung im großen Maßstab. Wer Millionen Dokumente indiziert oder Fuzzy-Ranking braucht, bei dem verdient eine gehostete Engine jeden Cent. Aber für ein paar hundert Beiträge, bei denen Leute meist einen Framework-Namen tippen, reicht exaktes Teilstring-Matching mit einer Handvoll Feldgewichten völlig — und "reicht völlig", das ich in einer Datei von oben bis unten lesen kann, schlägt "ausgefeilt", das ich betreiben muss.

Das Beste an der Änderung war der Diff: fast alles rot. SDK, Sync-Route und Env-Variablen sind weg, und Suchergebnisse wie die Tag-Cloud der Startseite kommen jetzt aus demselben Store-Snapshot, aus dem die restliche Seite ohnehin rendert. Eine Abhängigkeit zu löschen, der man entwachsen ist, gehört zu den leiseren Freuden dieses Jobs.