<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>DBOS |</title><link>https://aretascodes.dev/de/tags/dbos/</link><atom:link href="https://aretascodes.dev/de/tags/dbos/index.xml" rel="self" type="application/rss+xml"/><description>DBOS</description><generator>HugoBlox Kit (https://hugoblox.com)</generator><language>de-DE</language><lastBuildDate>Fri, 12 Jun 2026 00:00:00 +0000</lastBuildDate><image><url>https://aretascodes.dev/media/icon_hu_2ab4f4763b27c75b.png</url><title>DBOS</title><link>https://aretascodes.dev/de/tags/dbos/</link></image><item><title>CogniVault Backend erklärt, Teil 2 · Von der Datei zum durchsuchbaren Wissen</title><link>https://aretascodes.dev/de/blog/backend-explained-ingestion/</link><pubDate>Fri, 12 Jun 2026 00:00:00 +0000</pubDate><guid>https://aretascodes.dev/de/blog/backend-explained-ingestion/</guid><description>
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;Alle Abkürzungen werden im Anhang am Ende der Seite vollständig erklärt.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Ein LLM kann dein PDF nicht &amp;ldquo;öffnen&amp;rdquo;. Dieser Satz überrascht viele Neulinge, also lass uns das kurz sacken lassen: Wenn du in CogniVault mit deinen Dokumenten chattest, fasst das Modell die Originaldateien nie an. Es muss etwas &lt;em&gt;zwischen&lt;/em&gt; &amp;ldquo;Ich habe eine Datei in den Browser gezogen&amp;rdquo; und &amp;ldquo;Die KI hat mir gerade Seite 47 zitiert&amp;rdquo; passieren.&lt;/p&gt;
&lt;p&gt;Dieses Etwas nennt man &lt;strong&gt;Ingestion&lt;/strong&gt; (Datenaufnahme), und darum geht es in diesem Teil. In
haben wir das große Ganze skizziert; heute zoomen wir in einen bestimmten Bereich – das Fließband, das Dateien in durchsuchbares Wissen verwandelt.&lt;/p&gt;
&lt;h2 id="das-fließband"&gt;Das Fließband&lt;/h2&gt;
&lt;p&gt;Stell dir die Ingestion wie ein Fließband mit vier Stationen vor:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Extrahieren:&lt;/strong&gt; Den Text aus jeder Datei herausholen – auch aus gescannten.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chunken (Zerlegen):&lt;/strong&gt; Den Text in Stücke zerschneiden, die klein genug sind, um in einen Prompt zu passen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Embedden (Einbetten):&lt;/strong&gt; Jeden Chunk in einen Vektor (eine Liste von Zahlen, die seine Bedeutung einfängt) verwandeln, damit ähnliche Ideen im Vektorraum nah beieinander landen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Speichern:&lt;/strong&gt; Vektoren und Metadaten so ablegen, dass sie später durchsucht werden können.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="mermaid"&gt;flowchart TD
A["Upload&lt;br/&gt;POST /upload&lt;br/&gt;gespeichert in docs/"] --&gt; B
subgraph WF["DBOS dauerhafter Workflow"]
B["Schritt 1&lt;br/&gt;Welche Dateien haben sich geändert?&lt;br/&gt;SHA-256 Fingerabdrücke"] --&gt; C["Schritt 2&lt;br/&gt;Text extrahieren&lt;br/&gt;pro Format + OCR-Fallback"]
C --&gt; D["Chunking&lt;br/&gt;1000 Zeichen, 100 Überlappung"]
D --&gt; E["Schritt 3&lt;br/&gt;Embedden&lt;br/&gt;embeddinggemma, 5er-Batches"]
E --&gt; F["Schritt 4&lt;br/&gt;Speichern&lt;br/&gt;FAISS Index + Metadaten JSON"]
end
F --&gt; G["In-Memory Index neu laden&lt;br/&gt;sofort durchsuchbar"]
&lt;/div&gt;
&lt;p&gt;Ziemlich simpel. Die spannende Ingenieursarbeit steckt in den Fehlerfällen – fangen wir also damit an.&lt;/p&gt;
&lt;h2 id="das-kassenbuch-der-fabrik-warum-die-pipeline-keine-arbeit-verlieren-darf"&gt;Das Kassenbuch der Fabrik: Warum die Pipeline keine Arbeit verlieren darf&lt;/h2&gt;
&lt;p&gt;Das Embedden einer großen Bibliothek dauert Minuten. Was passiert, wenn dein Laptop bei Seite 800 eines 1.000-seitigen Handbuchs in den Ruhezustand geht? Bei einem einfachen Python-Skript fängt alles wieder bei Seite 1 an.&lt;/p&gt;
&lt;p&gt;CogniVault schreibt die Pipeline stattdessen als einen &lt;strong&gt;dauerhaften DBOS-Workflow&lt;/strong&gt;. Stell dir eine Fabrik vor, in der jede Station einen dauerhaften Stempel in ein Kassenbuch drückt, sobald sie eine Kiste fertiggestellt hat. Fällt der Strom aus, baut niemand fertige Kisten neu zusammen – die Arbeiter lesen das Buch und machen beim ersten ungestempelten Eintrag weiter.&lt;/p&gt;
&lt;p&gt;DBOS ist dieses Buch, und PostgreSQL ist das Papier, auf dem es geschrieben steht. Jede Station der Pipeline ist ein mit Checkpoints versehener Schritt; nach einem Neustart liefern abgeschlossene Schritte sofort ihre aufgezeichneten Ergebnisse zurück und die Ausführung geht beim ersten unfertigen Schritt weiter. Ein fehlgeschlagener Embedding-Batch wird einfach nochmal probiert.&lt;/p&gt;
&lt;p&gt;Das ist auch der Mechanismus hinter der Live-Fortschrittsanzeige in der UI: Der Start einer Ingestion liefert eine &lt;code&gt;workflow_id&lt;/code&gt; zurück, und das Frontend fragt regelmäßig einen Status-Endpoint ab, der meldet, welche Schritte abgeschlossen sind, welche laufen und welche noch warten.&lt;/p&gt;
&lt;p&gt;Ich habe einen ausführlichen Deep-Dive über diesen Mechanismus geschrieben – inklusive dessen, was passiert, wenn du den Prozess mitten in der Ingestion mit &lt;code&gt;kill -9&lt;/code&gt; beendest – in
.&lt;/p&gt;
&lt;h2 id="fingerabdrücke-kein-blindflug-sha-256-änderungserkennung"&gt;Fingerabdrücke, kein Blindflug: SHA-256 Änderungserkennung&lt;/h2&gt;
&lt;p&gt;Deine komplette Bibliothek jedes Mal neu zu embedden, wenn du eine einzige Datei hinzufügst, wäre Verschwendung. Bevor also irgendwelche Arbeit passiert, berechnet die Pipeline für jede Datei einen &lt;strong&gt;SHA-256 Hash&lt;/strong&gt; (einen Inhalts-Fingerabdruck – ändere ein Zeichen in der Datei, und der Fingerabdruck ändert sich komplett) und vergleicht ihn mit dem Fingerabdruck, der bei den vorhandenen Chunks der Datei gespeichert ist:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Noch nie gesehen&lt;/strong&gt; → einlesen (ingest).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fingerabdruck hat sich geändert&lt;/strong&gt; → die alten Chunks werden &lt;em&gt;soft-gelöscht&lt;/em&gt; und die Datei wird neu eingelesen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fingerabdruck identisch&lt;/strong&gt; → komplett überspringen.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Warum &amp;ldquo;soft&amp;rdquo;-gelöscht? Weil der FAISS-Indextyp, den CogniVault nutzt, keine einzelnen Vektoren entfernen kann. Veraltete Chunks werden in den Metadaten einfach als &lt;code&gt;deleted: true&lt;/code&gt; markiert; ihre Vektoren bleiben im Index, aber jede Suche filtert sie heraus. Das ist eine ehrliche, langweilige Lösung – und sie korrumpiert niemals den Index.&lt;/p&gt;
&lt;h2 id="jedes-format-kriegt-eine-sonderbehandlung"&gt;Jedes Format kriegt eine Sonderbehandlung&lt;/h2&gt;
&lt;p&gt;Hier ist ein Detail, das eine Demo von einem Produkt unterscheidet. Eine naive Pipeline extrahiert einfach &amp;ldquo;den ganzen Text&amp;rdquo; und macht dann Feierabend. Bei CogniVault bekommt jedes Format einen Extractor, der genau die &lt;em&gt;Struktur&lt;/em&gt; beibehält, die das Retrieval später braucht:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Strategie&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PDF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Seite für Seite, wobei die Seitenzahlen behalten werden (diese werden später zu Quellenangaben). Jede Seite mit weniger als 50 Zeichen gilt als gescannt und wird an die OCR geschickt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gescannte Seite&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Die Seite wird mit etwa 144 dpi als Bild gerendert, dann extrahiert Tesseract OCR (Optical Character Recognition – Text aus Bildern auslesen) die Wörter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Markdown&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Wird an Überschriften aufgeteilt; jeder Abschnitts-Chunk bekommt einen Breadcrumb-Präfix wie &lt;code&gt;[Section: Intro &amp;gt; Setup]&lt;/code&gt;, damit sein Embedding die Dokumentenhierarchie in sich trägt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CSV&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Zeilen werden in 20er-Gruppen gechunkt – und &lt;em&gt;jeder&lt;/em&gt; Chunk bekommt die Kopfzeile vorangestellt, sodass das Modell immer die Spaltennamen kennt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Excel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Gleiches Zeilen-Gruppen-Prinzip pro Arbeitsblatt, mit dem Präfix &lt;code&gt;[Sheet: name]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PowerPoint&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ein Chunk pro Folie&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Word&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Absätze plus Tabellenzellen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Webseiten&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Werden bei Bedarf abgerufen und auf sauberen Artikeltext reduziert – geschützt durch einen SSRF-Guard (Schutz vor Server-Side Request Forgery: der Server weigert sich, private oder interne Adressen abzurufen)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Frag dich mal, warum das CSV-Detail wichtig ist. Wenn Chunk 14 eines Spreadsheets einfach nur zwanzig nackte Zahlenreihen enthält, wird keine Suche ihn jemals mit der Frage &amp;ldquo;Wie hoch war das Q3-Budget?&amp;rdquo; in Verbindung bringen. Stellst du die Kopfzeile voran, &lt;em&gt;weiß&lt;/em&gt; der Chunk, dass er Budget-Spalten enthält. Struktur ist der Treibstoff fürs Retrieval.&lt;/p&gt;
&lt;h2 id="chunking-1000-zeichen-mit-100-zeichen-sicherheitsnetz-überlappung"&gt;Chunking: 1.000 Zeichen mit 100 Zeichen Sicherheitsnetz (Überlappung)&lt;/h2&gt;
&lt;p&gt;Lange Texte werden in Stücke von etwa 1.000 Zeichen zerlegt, wobei sich benachbarte Stücke um 100 Zeichen überlappen. Diese Überlappung ist eine Versicherung: Ein Satz, der genau an der Chunk-Grenze zerschnitten wird, taucht in einem der beiden Nachbarn immer noch als Ganzes auf, sodass keine Idee in die Lücke zwischen den Chunks fällt.&lt;/p&gt;
&lt;h2 id="embedden-und-speichern"&gt;Embedden und Speichern&lt;/h2&gt;
&lt;p&gt;Chunks werden von &lt;code&gt;embeddinggemma&lt;/code&gt; (via Ollama) in 5er-Batches embedded – jeder Chunk wird zu einem Vektor. Die Vektoren werden normalisiert und an einen FAISS-Index angehängt; daneben hält eine JSON-Datei für jeden Chunk den Quelldateinamen, die Seitenzahl, die Kategorie, den Fingerabdruck und den eigentlichen Text fest. Der Index speichert die Zahlen; das JSON speichert die Bedeutung.&lt;/p&gt;
&lt;p&gt;Eine Designentscheidung, die man für Anfänger hervorheben sollte: Das hier ist ein &lt;strong&gt;exakter&lt;/strong&gt; Index, kein approximativer. Viele Vektor-Datenbanken nutzen ANN (Approximate Nearest Neighbour)-Abkürzungen, die bei riesiger Skalierung ein wenig Genauigkeit gegen Geschwindigkeit tauschen. Im Maßstab einer persönlichen Bibliothek brauchst du diesen Kompromiss nicht – CogniVault prüft jeden Vektor bei jeder Suche und ist trotzdem schnell.&lt;/p&gt;
&lt;h2 id="die-gesamte-reise-von-anfang-bis-ende"&gt;Die gesamte Reise, von Anfang bis Ende&lt;/h2&gt;
&lt;div class="mermaid"&gt;%%{init: {'sequence': {'actorFontSize': 28, 'messageFontSize': 24, 'loopTextFontSize': 22, 'noteFontSize': 22}}}%%
sequenceDiagram
actor U as Du
participant F as Frontend
participant B as FastAPI
participant W as DBOS Workflow
participant O as Ollama (embeddinggemma)
participant V as FAISS + Metadaten
U-&gt;&gt;F: Drag and Drop einer Datei, Kategorie wählen
F-&gt;&gt;B: POST /upload
B-&gt;&gt;B: Typ und Größe validieren, in docs/ speichern
F-&gt;&gt;B: POST /ingest
B-&gt;&gt;W: Dauerhaften Workflow starten
B--&gt;&gt;F: workflow_id
loop Status abfragen
F-&gt;&gt;B: GET /ingest/status/{workflow_id}
B--&gt;&gt;F: Schrittliste (steuert die Fortschrittsanzeige)
end
W-&gt;&gt;W: SHA-256 Änderungserkennung
W-&gt;&gt;W: Text extrahieren (pro Format, OCR falls gescannt)
W-&gt;&gt;W: Chunking (1000 Zeichen / 100 Überlappung)
W-&gt;&gt;O: Embedden in 5er-Batches
O--&gt;&gt;W: Vektoren
W-&gt;&gt;V: Vektoren + Metadaten anhängen
B--&gt;&gt;F: SUCCESS — Index neu geladen
F--&gt;&gt;U: "Wissens-Sync abgeschlossen"
&lt;/div&gt;
&lt;h2 id="fazit"&gt;Fazit&lt;/h2&gt;
&lt;p&gt;Bei der Ingestion entscheidet sich meistens die eigentliche RAG-Qualität – lange bevor irgendwelches clevere Prompting ins Spiel kommt. Beibehaltene Seitenzahlen, Header, die in jeden Spreadsheet-Chunk kopiert werden, gerettete Scans durch OCR, und ein Kassenbuch, das das Ganze absturzsicher macht: Nichts davon ist glamourös, aber alles davon zeigt sich später in Form von Antworten, die die richtige Seite zitieren.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id="anhang-abkürzungen-in-diesem-post"&gt;Anhang: Abkürzungen in diesem Post&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Abkürzung&lt;/th&gt;
&lt;th&gt;Volle Form&lt;/th&gt;
&lt;th&gt;Bedeutung&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LLM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Large Language Model&lt;/td&gt;
&lt;td&gt;Ein neuronales Netz, trainiert mit riesigen Textmengen, das Sprache lesen und erzeugen kann&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DBOS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Database-Oriented Operating System&lt;/td&gt;
&lt;td&gt;Die Bibliothek, die Workflow-Schritte in PostgreSQL sichert, damit abgestürzte Jobs fortgesetzt werden können&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SHA-256&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Secure Hash Algorithm, 256-bit&lt;/td&gt;
&lt;td&gt;Ein Inhalts-Fingerabdruck – ändere ein Byte einer Datei und der Hash ändert sich komplett&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OCR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Optical Character Recognition&lt;/td&gt;
&lt;td&gt;Text aus Bildern lesen – der Rettungsweg für gescannte PDF-Seiten&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SSRF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Server-Side Request Forgery&lt;/td&gt;
&lt;td&gt;Ein Angriff, bei dem ein Server ausgetrickst wird, interne URLs abzurufen; der URL-Importer blockiert dies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FAISS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Facebook AI Similarity Search&lt;/td&gt;
&lt;td&gt;Der Vektor-Index, an den die Embeddings angehängt werden&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ANN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Approximate Nearest Neighbour&lt;/td&gt;
&lt;td&gt;Die Genauigkeit-gegen-Geschwindigkeit-Abkürzung, die CogniVault absichtlich &lt;em&gt;nicht&lt;/em&gt; nimmt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;dpi&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dots Per Inch&lt;/td&gt;
&lt;td&gt;Bildauflösung – gescannte Seiten werden vor der OCR mit ca. 144 dpi gerendert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JSON&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JavaScript Object Notation&lt;/td&gt;
&lt;td&gt;Das Format der Chunk-Metadaten-Datei neben dem FAISS-Index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PDF / CSV&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Portable Document Format / Comma-Separated Values&lt;/td&gt;
&lt;td&gt;Zwei der acht+ unterstützten Dateiformate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Application Programming Interface&lt;/td&gt;
&lt;td&gt;Die Endpoints (&lt;code&gt;/upload&lt;/code&gt;, &lt;code&gt;/ingest&lt;/code&gt;, &lt;code&gt;/ingest/status/…&lt;/code&gt;), die den Ablauf steuern&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Als Nächstes:&lt;/strong&gt;
— hybrides Retrieval, der 6-Tools-Agent und der 2-Phasen-Stream, der zeigt, wie das Modell denkt, bevor es antwortet.&lt;/p&gt;</description></item><item><title>Teil 2 · CogniVault Architektur: Dauerhafte Ingestion mit DBOS</title><link>https://aretascodes.dev/de/blog/cognivault-ingestion-pipeline/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://aretascodes.dev/de/blog/cognivault-ingestion-pipeline/</guid><description>
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;Alle Abkürzungen werden im Anhang am Ende der Seite ausführlich erklärt.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In einem einfachen lokalen KI-Setup ist das Hinzufügen von Dokumenten zu deiner Datenbank normalerweise nur ein simples Python-Skript. Du öffnest ein PDF, zerhackst den Text in Chunks, verwandelst diese Chunks in Mathe (Embeddings) und speicherst sie.&lt;/p&gt;
&lt;p&gt;Das funktioniert super für ein fünfseitiges Essay. Aber was passiert, wenn du ein 1.000-seitiges technisches Handbuch einliest (Ingestion) und dein Laptop bei Seite 800 in den Ruhemodus geht?&lt;/p&gt;
&lt;p&gt;Das Skript stirbt. Wenn du deinen Laptop aufweckst, musst du wieder bei Seite 1 anfangen und verschwendest so Zeit und Rechenleistung. Ein einfaches Skript reichte für CogniVault nicht aus. Wir brauchten einen &lt;strong&gt;Durable Workflow&lt;/strong&gt; (dauerhaften Workflow).&lt;/p&gt;
&lt;h2 id="das-fabrikbuch-dbos"&gt;Das Fabrikbuch (DBOS)&lt;/h2&gt;
&lt;p&gt;Stell dir die Daten-Ingestion wie ein Fließband in einer Fabrik vor. Wenn der Strom ausfällt, sollten die Arbeiter nicht jedes Produkt von Grund auf neu bauen müssen. Sie sollten einfach in ein permanentes Kassenbuch (Ledger) schauen, genau sehen, welche Kiste sie gerade gepackt haben, als das Licht ausging, und dort weitermachen.&lt;/p&gt;
&lt;p&gt;CogniVault verwendet ein Framework namens &lt;strong&gt;DBOS (Database-Oriented Operating System)&lt;/strong&gt;, das von einer PostgreSQL-Datenbank gestützt wird, um als dieses Buch zu fungieren.&lt;/p&gt;
&lt;p&gt;Jeder Schritt des Ingestion-Prozesses protokolliert seinen Abschluss in Postgres. Wenn der Server mittendrin abstürzt, passiert im Moment nichts Dramatisches — die Magie entfaltet sich beim Neustart: DBOS liest das Buch, sieht, welche Schritte bereits abgeschlossen sind, spielt die aufgezeichneten Ergebnisse sofort ab und macht beim ersten unvollendeten Schritt weiter.&lt;/p&gt;
&lt;p&gt;Eine wichtige Grenze: Postgres enthält &lt;strong&gt;nur das Buch&lt;/strong&gt; — welche Schritte gelaufen sind und was sie zurückgegeben haben. Deine Dokumente, Chunks und Vektoren leben dort nie. Sie wandern in einen FAISS-Index plus eine JSON-Metadaten-Datei auf der Festplatte.&lt;/p&gt;
&lt;h2 id="sha-256-hashing-der-idempotenz-trick"&gt;SHA-256 Hashing: Der Idempotenz-Trick&lt;/h2&gt;
&lt;p&gt;Das System muss auch bei erneuten Uploads clever sein. Wenn du einen Tippfehler in einem riesigen Dokument behebst und es noch einmal hochlädst, willst du nicht, dass das System 10 Minuten verschwendet, um das Ganze neu einzubetten (re-embedding).&lt;/p&gt;
&lt;p&gt;CogniVault erreicht &lt;strong&gt;Idempotenz&lt;/strong&gt; (die Fähigkeit, dieselbe Operation mehrmals auszuführen, ohne das Ergebnis nach der ersten Anwendung zu verändern) mit dem allerersten Schritt des Workflows: Es scannt den &lt;code&gt;docs/&lt;/code&gt;-Ordner und generiert einen &lt;strong&gt;SHA-256-Hash&lt;/strong&gt; (einen einzigartigen digitalen Fingerabdruck) für jede Datei.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Wenn der Hash neu ist, wird die Datei verarbeitet.&lt;/li&gt;
&lt;li&gt;Wenn sich der Hash geändert hat (weil du die Datei bearbeitet hast), löscht es die alten Text-Chunks per &amp;ldquo;Soft-Delete&amp;rdquo; und bettet nur die neue Version neu ein.&lt;/li&gt;
&lt;li&gt;Wenn der Hash identisch ist, überspringt es die Datei komplett.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hier können wir sehen, wie das logisch abläuft:&lt;/p&gt;
&lt;div class="mermaid"&gt;graph TD
Raw[📄 Hochgeladenes Dokument] --&gt; DBOS[🐘 DBOS Workflow startet]
subgraph DauerhaftePipeline ["Dauerhafte Ingestion-Pipeline"]
DBOS --&gt;|Schritt 1| Hash{Hash-Prüfung SHA-256}
Hash --&gt;|Unverändert| Skip[Verarbeitung überspringen]
Hash --&gt;|Neu / Geändert| Extract[✂️ Schritt 2: Text pro Dokument extrahieren]
Extract --&gt; Chunk[Chunking: 1000 Zeichen, 100 Überlappung]
Chunk --&gt;|Schritt 3, 5er-Batches| Embed[🔢 embeddinggemma Embeddings]
Embed --&gt;|Schritt 4| Save[(💾 FAISS Index + Metadaten JSON)]
end
Save --&gt;|Workflow abgeschlossen| Done[✅ Bereit für die Suche]
&lt;/div&gt;
&lt;p&gt;(Ein Detail für die Neugierigen: Die per Checkpoint gesicherten &lt;em&gt;Schritte&lt;/em&gt; sind der Scan, die Extraktion pro Dokument, jeder Embedding-Batch und das Speichern. Das Chunking dazwischen ist schnelle, reine Python-Arbeit, also läuft es einfach als Teil des Workflow-Körpers erneut — es mit einem Checkpoint zu versehen, würde mehr kosten, als es neu zu machen.)&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id="was-kommt-als-nächstes"&gt;Was kommt als Nächstes?&lt;/h3&gt;
&lt;p&gt;Indem wir die Ingestion-Pipeline in DBOS verpacken, verwandelt sich das System von einem anfälligen Skript in eine robuste Zustandsmaschine (State Machine) auf Produktionsniveau.&lt;/p&gt;
&lt;p&gt;Jetzt, da unsere Daten sicher eingelesen sind, wie deployen wir diese gesamte Pipeline, ohne die GPU unseres Laptops zum Schmelzen zu bringen?
&lt;strong&gt;Lies Teil 3: Warum wir Ollama nicht in Docker packen&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Du kannst die DBOS-Implementierung auch direkt in der Datei &lt;code&gt;backend/services/ingest.py&lt;/code&gt; im
erkunden.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="anhang-abkürzungen-in-diesem-beitrag"&gt;Anhang: Abkürzungen in diesem Beitrag&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Abbreviation&lt;/th&gt;
&lt;th&gt;Full form&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DBOS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Database-Oriented Operating System&lt;/td&gt;
&lt;td&gt;Eine Bibliothek, die Workflow-Schritte in einer Datenbank sichert, sodass abgestürzte Jobs fortgesetzt statt neu gestartet werden&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SHA-256&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Secure Hash Algorithm, 256-bit&lt;/td&gt;
&lt;td&gt;Eine Fingerabdruck-Funktion: Jede Datei wird auf einen einzigartigen 64-Zeichen-Hash abgebildet; änderst du ein Byte, ändert sich der Hash komplett&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PDF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Portable Document Format&lt;/td&gt;
&lt;td&gt;Das Dokumentenformat, dessen Text (und Scans) die Pipeline extrahiert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FAISS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Facebook AI Similarity Search&lt;/td&gt;
&lt;td&gt;Metas Vektorsuch-Bibliothek — wo die Embeddings tatsächlich leben&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JSON&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JavaScript Object Notation&lt;/td&gt;
&lt;td&gt;Das Textformat, das für die Chunk-Metadaten-Datei neben dem FAISS-Index verwendet wird&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Artificial Intelligence&lt;/td&gt;
&lt;td&gt;Software, die Aufgaben ausführt, für die normalerweise menschliche Intelligenz erforderlich ist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GPU&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Graphics Processing Unit&lt;/td&gt;
&lt;td&gt;Die Hardware, die lokale Modell-Inferenz schnell macht — das Thema von Teil 3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description></item><item><title>Teil 4 · Crash-Resumable Ingestion: DBOS, SHA-256 und wie man ein kill -9 überlebt</title><link>https://aretascodes.dev/de/blog/crash-resumable-ingestion-dbos/</link><pubDate>Tue, 05 May 2026 00:00:00 +0000</pubDate><guid>https://aretascodes.dev/de/blog/crash-resumable-ingestion-dbos/</guid><description>
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;Teil einer Serie über den Bau von
. Zuvor:
.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;Alle Abkürzungen werden im Anhang am Ende der Seite ausführlich erklärt.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Es gibt zwei Dinge, die deine RAG-Ingestion-Pipeline auf keinen Fall tun sollte:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Ein 200-seitiges PDF neu einbetten, weil du einen Tippfehler auf Seite 12 korrigiert hast.&lt;/li&gt;
&lt;li&gt;Ihren Fortschritt verlieren, wenn du auf halber Strecke den Laptop zuklappst.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Das Erste verschwendet Zeit und Rechenressourcen. Das Zweite führt zu Misstrauen in das System. Beides hat denselben Ursprung: Die Ingestion wird wie eine Fire-and-Forget-Funktion behandelt, obwohl sie eigentlich eine lang laufende Pipeline ist, deren Zwischenzustände es wert sind, erhalten zu bleiben.&lt;/p&gt;
&lt;p&gt;CogniVault behandelt Ingestion als einen &lt;strong&gt;Durable Workflow&lt;/strong&gt;. Genauer gesagt als einen
-Workflow, der in Postgres mit Checkpoints versehen ist und Content-Hashing für inkrementelle Arbeit nutzt. In diesem Beitrag schauen wir uns beides an.&lt;/p&gt;
&lt;h2 id="die-pipeline"&gt;Die Pipeline&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1. Scan docs/ → SHA-256 hash per file
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── New file → queue for embedding
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── Changed file → soft-delete old chunks, re-embed
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── Unchanged → skip (idempotent)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2. Extract text → per-format extractor (PDF/OCR, DOCX, PPTX, XLSX, MD, CSV, TXT, HTML)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3. Chunk → RecursiveCharacterTextSplitter (1000 chars, 100 overlap)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;4. Embed → embeddinggemma via Ollama, batches of 5
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;5. Save → append to FAISS IndexFlatIP + JSON metadata on disk
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Die rechenintensiven Stufen laufen als DBOS-Schritte innerhalb eines übergeordneten Workflows und sind alle mit Checkpoints versehen: Wenn der Prozess zwischen den Schritten stirbt, macht der nächste Start genau beim letzten abgeschlossenen Schritt weiter.&lt;/p&gt;
&lt;h2 id="sha-256-als-einzige-quelle-der-wahrheit"&gt;SHA-256 als einzige Quelle der Wahrheit&lt;/h2&gt;
&lt;p&gt;Der naive Ansatz ist, die Ingestion anhand des Dateinamens zu verfolgen. Das geht genau dann schief, wenn jemand eine Datei direkt bearbeitet. Der Dateiname ist derselbe; der Inhalt nicht. Der Vector-Store schleppt dann klammheimlich veraltete Chunks mit sich herum.&lt;/p&gt;
&lt;p&gt;Die Lösung ist inhaltsadressiert: Hashe die Datei-Bytes und speichere den Hash zusammen mit den Chunks. Bei jedem Ingestion-Durchlauf passiert Folgendes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;current_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;stored_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chunk_metadata_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;file_hash&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;stored_hash&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;schedule_ingest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# new file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;stored_hash&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;current_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;skip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# unchanged&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;soft_delete_chunks_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# changed&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;schedule_ingest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Das verleiht der Ingestion eine &lt;strong&gt;idempotente&lt;/strong&gt; Eigenschaft, die Gold wert ist: Die Pipeline zweimal hintereinander laufen zu lassen, bewirkt beim zweiten Mal fast nichts. Das ist nicht nur eine Optimierung — erst dadurch wird der nächste Abschnitt überhaupt möglich.&lt;/p&gt;
&lt;h2 id="dbos-workflows"&gt;DBOS-Workflows&lt;/h2&gt;
&lt;p&gt;
ist eine Python-Bibliothek, die normale Funktionen in Checkpoint-basierte Workflows verwandelt, die von Postgres gestützt werden. Das Modell ist kinderleicht: Dekoriere eine Funktion mit &lt;code&gt;@DBOS.workflow()&lt;/code&gt;, markiere jeden lang laufenden Aufruf darin als &lt;code&gt;@DBOS.step()&lt;/code&gt;, und DBOS speichert während der Ausführung für jeden Schritt Input, Output und Status in Postgres.&lt;/p&gt;
&lt;p&gt;Wenn der Workflow abstürzt — Prozess gekillt, OS-Reboot, Abbruch der Postgres-Verbindung — sieht der nächste Start, dass ein unvollendeter Workflow mit derselben ID existiert, spielt die &lt;em&gt;aufgezeichneten&lt;/em&gt; Schritt-Outputs aus Postgres ab (ohne sie neu auszuführen) und macht beim ersten unvollständigen Schritt weiter.&lt;/p&gt;
&lt;p&gt;Hier ist die eigentliche Schrittstruktur (leicht vereinfacht aus &lt;code&gt;backend/services/ingest.py&lt;/code&gt;):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@DBOS.workflow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ingest_workflow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;filenames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;list_document_files&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# @DBOS.step — scan + hash check&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;filenames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;process_single_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# @DBOS.step — extract text, one file each&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# plain Python — fast, re-runs freely&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;batches_of_5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;embed_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# @DBOS.step — the slow one, retried on failure&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;save_vector_store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# @DBOS.step — append to FAISS + metadata&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Die Granularität von &lt;code&gt;@DBOS.step&lt;/code&gt; entspricht der Granularität der Crash-Recovery und wurde bewusst so gewählt. Die Extraktion ist ein Schritt &lt;strong&gt;pro Datei&lt;/strong&gt;, sodass bei einem Absturz während Datei 9 von 10 die ersten acht nicht neu gelesen werden. Embedding ist ein Schritt &lt;strong&gt;pro Batch von fünf Chunks&lt;/strong&gt;, und zwar aus einem bestimmten Grund: &lt;strong&gt;&lt;code&gt;embed_batch&lt;/code&gt; ist der langsame Part.&lt;/strong&gt; Wenn der Laptop während der Embeddings den Geist aufgibt, setzen wir den Embedding-Loop beim fehlgeschlagenen Batch fort, nicht bei der PDF-Extraktion.&lt;/p&gt;
&lt;p&gt;Fällt dir auf, was &lt;em&gt;kein&lt;/em&gt; Schritt ist? Das Chunking. Text aufzuteilen ist schnelle, reine Python-Arbeit — es mit Checkpoints zu versehen, würde mehr Buchhaltung im Ledger kosten, als es bei einer Fortsetzung einfach neu zu machen.&lt;/p&gt;
&lt;p&gt;In der Batch-Größe verbirgt sich noch ein kleiner Trick. DBOS speichert den Output jedes Schritts in Postgres, und &lt;code&gt;embed_batch&lt;/code&gt; gibt seine Vektoren zurück — also enthält jeder Ledger-Eintrag Float-Werte für fünf Embeddings. Kleine Batches halten jeden Checkpoint-Datensatz klein und jeden erneuten Versuch (Retry) günstig. Ein riesiger &amp;ldquo;Bette alles ein&amp;rdquo;-Schritt würde eine riesige Ledger-Zeile und null Resume-Granularität bedeuten.&lt;/p&gt;
&lt;h2 id="die-format-extraktoren"&gt;Die Format-Extraktoren&lt;/h2&gt;
&lt;p&gt;Schritt 2 (&lt;code&gt;process_single_document&lt;/code&gt;) ist eine Weiche basierend auf der Dateiendung. Jeder Extraktor ist klein und einleuchtend; die interessanten Entscheidungen liegen in der Chunking-Strategie, die jeder nachgelagert füttert.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Chunking note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PDF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pypdf&lt;/code&gt; Seite für Seite; &lt;code&gt;pytesseract&lt;/code&gt; OCR-Fallback für Bild-Seiten&lt;/td&gt;
&lt;td&gt;Rekursiver Splitter, 1000/100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DOCX&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;python-docx&lt;/code&gt; (Absätze + Tabellenzeilen als Text verbunden)&lt;/td&gt;
&lt;td&gt;Rekursiver Splitter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PPTX&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;python-pptx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ein Chunk pro Folie (Titel + Body-Text)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;XLSX&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;openpyxl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Header + 20-Zeilen-Batches, pro Arbeitsblatt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MD&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MarkdownHeaderTextSplitter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ein Chunk pro H1/H2/H3-Abschnitt, Breadcrumbs davor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CSV&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manueller Reader&lt;/td&gt;
&lt;td&gt;Header-Zeile + 20-Zeilen-Batches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TXT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rohes UTF-8 Lesen&lt;/td&gt;
&lt;td&gt;Rekursiver Splitter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTML&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;trafilatura&lt;/code&gt; sauberer Text&lt;/td&gt;
&lt;td&gt;Rekursiver Splitter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Der OCR-Fallback ist es wert, kurz innezuhalten. PDFs gibt es in zwei Ausführungen: solche mit einer echten Textebene und solche, die im Grunde nur gescannte Bilder in einem PDF-Kostüm sind. &lt;code&gt;pypdf&lt;/code&gt; liefert für die zweite Sorte &lt;em&gt;nichts Brauchbares&lt;/em&gt; zurück, wirft aber auch keinen Fehler — es gibt einfach leere Strings zurück. Ohne ein Fallback lügt dich dein &amp;ldquo;Ingestion erfolgreich&amp;rdquo;-Log an.&lt;/p&gt;
&lt;p&gt;Der Detektor ist eine Heuristik: Wenn &lt;code&gt;pypdf&lt;/code&gt; weniger als 50 Zeichen für eine Seite zurückgibt, leite die Seite durch &lt;code&gt;pymupdf&lt;/code&gt; → &lt;code&gt;Pillow&lt;/code&gt; → &lt;code&gt;pytesseract&lt;/code&gt; OCR. Langsamer, aber es produziert immerhin Text. Der Schwellenwert ist so eingestellt, dass er sensibel genug ist, um gescannte Seiten abzufangen, ohne legitimerweise kurze Seiten (wie ein Kapitel-Deckblatt oder ein Impressum) zu bestrafen.&lt;/p&gt;
&lt;h2 id="soft-delete-nicht-hard-delete"&gt;Soft Delete, nicht Hard Delete&lt;/h2&gt;
&lt;p&gt;Wenn sich eine Datei ändert und wir sie neu einlesen, müssen die alten Chunks weg. Es ist verlockend, sie physisch aus dem FAISS-Index zu entfernen, aber FAISS &lt;code&gt;IndexFlatIP&lt;/code&gt; unterstützt kein effizientes Löschen — du müsstest ihn neu aufbauen.&lt;/p&gt;
&lt;p&gt;Stattdessen &lt;strong&gt;Soft Delete&lt;/strong&gt;: Bei geänderten Dateien werden die alten Chunks in den Metadaten mit einem &lt;code&gt;deleted: true&lt;/code&gt;-Flag markiert; neue Chunks werden ohne Flag angehängt. Bei einer Suchanfrage wird nach diesem Flag gefiltert, sodass veraltete Vektoren völlig harmlos im Index liegen bleiben. Wenn sich jemals genug totes Gewicht ansammelt, ist das Ventil offensichtlich — bau den Index nur mit aktiven Chunks neu auf —, aber in der Praxis habe ich das noch nie gebraucht.&lt;/p&gt;
&lt;p&gt;Das ist dasselbe Muster, das die meisten Append-only-Systeme verwenden. Es passt natürlich perfekt zum Content-Hashing — Markieren-und-Anhängen ist viel billiger als Entfernen-und-Neubauen. Eine Feinheit dabei: Der Keyword-Index muss mitziehen. CogniVaults &lt;code&gt;VectorDB.delete_by_source()&lt;/code&gt; setzt die Flags &lt;strong&gt;und baut BM25 neu auf&lt;/strong&gt;, und zwar über die verbleibenden aktiven Chunks, sodass sich die beiden Retriever nie uneinig darüber sind, was eigentlich existiert.&lt;/p&gt;
&lt;h2 id="was-der-user-sieht"&gt;Was der User sieht&lt;/h2&gt;
&lt;p&gt;Das Starten einer Ingestion (&lt;code&gt;POST /ingest&lt;/code&gt;) liefert eine &lt;code&gt;workflow_id&lt;/code&gt; zurück, und das Frontend fragt regelmäßig &lt;code&gt;GET /ingest/status/{workflow_id}&lt;/code&gt; ab, um eine Live-Timeline der Workflow-Schritte zu zeichnen — Scannen, Extraktion pro Datei (&amp;ldquo;Lese Seiten… 3 von 21&amp;rdquo;), Einbetten (&amp;ldquo;Kalibriere Batch 4 von 12&amp;rdquo;), Speichern. Wenn der User den Tab mitten in der Ingestion schließt, fünf Minuten später wiederkommt und ihn neu öffnet — der Workflow ist im Hintergrund sowieso fertig gelaufen. Der nächste Aufruf von &lt;code&gt;GET /api/vault/stats&lt;/code&gt; spiegelt die neue Chunk-Anzahl wider. Kein &amp;ldquo;Klicken zum Fortsetzen&amp;rdquo;-Button, kein manueller Recovery-Tanz.&lt;/p&gt;
&lt;p&gt;Als ich das erste Mal mitten im Einbetten den Deckel zugeklappt habe und dann beim Aufwecken zusehen konnte, wie der Workflow sich den nächsten Schritt geschnappt und einfach weitergemacht hat, war ich, ehrlich gesagt, ein bisschen stolz. Das ist genau die Eigenschaft, die ich wollte, und das mit überraschend wenig Code.&lt;/p&gt;
&lt;h2 id="fallstricke-und-randfälle"&gt;Fallstricke und Randfälle&lt;/h2&gt;
&lt;p&gt;Ein paar Dinge, die ich auf die harte Tour lernen musste:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mach &lt;code&gt;embed_batch&lt;/code&gt; nicht zu groß.&lt;/strong&gt; Ollama ist nicht besonders gut im Umgang mit Backpressure. Batches von 5 sind ein Sweetspot für &lt;code&gt;embeddinggemma&lt;/code&gt; auf einer Maschine mit 16 GB RAM — größere Batches bleiben am Speicher hängen, kleinere verschwenden Overhead für die Round-Trips. (Und wie oben erwähnt: Die Batch-Größe bestimmt gleichzeitig die Größe deines Checkpoint-Datensatzes.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sei vorsichtig beim Löschen von Dateien.&lt;/strong&gt; Soft-gelöschte Chunks müssen auch aus dem Korpus von BM25 verschwinden, sonst liefert die Keyword-Suche weiterhin Text, den die Dense Search (Vektorsuche) gar nicht mehr sieht. Wenn du BM25 innerhalb von &lt;code&gt;delete_by_source()&lt;/code&gt; neu aufbaust, bleiben die beiden im Gleichschritt.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OCR ist langsam.&lt;/strong&gt; Ein 50-seitiger Scan kann eine Minute oder länger dauern. Mach diese Wartezeit für den User sichtbar, sonst denken sie, das System hat sich aufgehängt.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="fazit"&gt;Fazit&lt;/h2&gt;
&lt;p&gt;Durable Workflows sind nicht nur etwas für verteilte Systeme. Eine lokale App für einen einzelnen Nutzer profitiert davon auf &lt;em&gt;genau die gleiche Weise&lt;/em&gt;: inkrementelle Arbeit, Crash-Recovery, idempotente Retries. DBOS macht die Einstiegskosten dafür extrem niedrig — dekoriere deine Funktion, lass Postgres lokal laufen, und du bekommst eine Pipeline, die das Zuklappen des Laptops, OS-Updates und dein eigenes &lt;code&gt;Ctrl-C&lt;/code&gt; überlebt.&lt;/p&gt;
&lt;p&gt;In Kombination mit inhaltsadressiertem Hashing ist die Ingestion nicht länger etwas, das du meidest, aus Angst, 20 Minuten warten zu müssen. Es wird zu etwas, das du einfach neu startest, wann immer du Lust dazu hast — denn ein Neustart kostet nichts, wenn sich nichts geändert hat.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="anhang-abkürzungen-in-diesem-beitrag"&gt;Anhang: Abkürzungen in diesem Beitrag&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Abbreviation&lt;/th&gt;
&lt;th&gt;Full form&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DBOS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Database-Oriented Operating System&lt;/td&gt;
&lt;td&gt;Eine Bibliothek, die Workflow-Schritte in Postgres sichert, sodass abgestürzte Jobs fortgesetzt statt neu gestartet werden&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SHA-256&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Secure Hash Algorithm, 256-bit&lt;/td&gt;
&lt;td&gt;Ein Content-Fingerabdruck: Änderst du ein Byte einer Datei, ändert sich der Hash komplett&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RAG&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Retrieval-Augmented Generation&lt;/td&gt;
&lt;td&gt;Rufe zuerst relevante Passagen aus deinen eigenen Dokumenten ab; lass das Modell daraus antworten&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OCR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Optical Character Recognition&lt;/td&gt;
&lt;td&gt;Das Umwandeln von Bildern von Text (gescannte Seiten) in maschinenlesbaren Text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FAISS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Facebook AI Similarity Search&lt;/td&gt;
&lt;td&gt;Der Vektorindex, an den die Embeddings angehängt werden&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IP&lt;/strong&gt; (in &lt;code&gt;IndexFlatIP&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Inner Product&lt;/td&gt;
&lt;td&gt;FAISS&amp;rsquo;s Ähnlichkeitsmaß; entspricht der Cosinus-Ähnlichkeit bei normalisierten Vektoren&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;BM25&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Best Match 25&lt;/td&gt;
&lt;td&gt;Der Keyword-Index, der beim Löschen mit FAISS im Gleichschritt bleiben muss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PDF / DOCX / PPTX / XLSX / MD / CSV / TXT / HTML&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Portable Document Format / Word / PowerPoint / Excel / Markdown / Comma-Separated Values / plain text / HyperText Markup Language&lt;/td&gt;
&lt;td&gt;Die Formate, die von den entsprechenden Extraktoren verarbeitet werden&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JSON&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JavaScript Object Notation&lt;/td&gt;
&lt;td&gt;Das Format der Chunk-Metadaten-Datei neben dem FAISS-Index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UTF-8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unicode Transformation Format, 8-bit&lt;/td&gt;
&lt;td&gt;Die Textkodierung, die beim Lesen von Klartextdateien verwendet wird&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Operating System&lt;/td&gt;
&lt;td&gt;Das, was mitten in der Ingestion unter dir neu startet&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Als Nächstes:&lt;/strong&gt;
— was passiert, wenn Gemma 4 enthusiastisch &lt;code&gt;{&amp;quot;questions&amp;quot;: [{&amp;quot;text&amp;quot;: &amp;quot;...&amp;quot;},}]&lt;/code&gt; zurückgibt.&lt;/p&gt;</description></item></channel></rss>