Vektordatenbank aktuell halten, ohne alles neu einzubetten
Eine Wissensdatenbank aus Webseiten aufzubauen ist ein Nachmittag. Sie aktuell zu halten ist die eigentliche Arbeit: Seiten ändern sich, Abschnitte kommen hinzu oder verschwinden, und Ihr Chatbot antwortet weiter mit dem Stand vom Tag des Imports. Dieser Leitfaden zeigt drei Bausteine, mit denen Sie nur erneuern, was sich tatsächlich geändert hat.
Kurz gesagt
- Jeder Chunk bekommt eine ID, die sich aus der Seite und seiner Position berechnet, keine zufällige. Ein neuer Import überschreibt dann, statt zu doppeln.
- Zu jedem Chunk gehört die ID seiner Seite. So löschen Sie alle Einträge einer Seite mit einem Filter.
- Ein Fingerabdruck (Hash) des Texts zeigt, was sich geändert hat. Nur das wird neu eingebettet.
Drei Fehler, die fast jede erste Pipeline hat
- Doppelte Einträge. Bekommt jeder Chunk beim Import eine zufällige ID, legt jeder neue Lauf alles noch einmal an. Die Suche liefert dann dreimal denselben Absatz, in drei Fassungen.
- Veraltete Einträge. Wird ein Abschnitt gestrichen, bleibt sein Chunk in der Datenbank und beantwortet weiter Fragen, mit Inhalten, die auf der Seite längst nicht mehr stehen.
- Unnötige Kosten. Wer jede Nacht alles neu einbettet, bezahlt für tausende unveränderte Abschnitte und wartet länger als nötig.
Baustein 1: Stabile IDs
Für die ID eines Chunks gibt es drei übliche Wege:
| ID aus … | Vorteil | Nachteil |
|---|---|---|
| Zufall (UUID4) | einfach | jeder Import doppelt |
| Hash des Inhalts | unveränderte Chunks behalten ihre ID | ein geänderter Chunk bekommt eine neue ID, der alte bleibt liegen |
| Adresse + Position (UUIDv5) | derselbe Abschnitt wird überschrieben | ein neuer Abschnitt oben verschiebt alle folgenden (siehe Grenzen) |
Wir empfehlen Adresse + Position, als UUID. Viele Datenbanken verlangen ohnehin eine UUID oder eine Zahl als ID, etwa Qdrant und Weaviate; eine UUID funktioniert in allen gängigen Systemen. Eine UUIDv5 ist keine Zufallszahl, sondern wird aus einem Text berechnet: gleicher Text, gleiche ID.
import uuid
from urllib.parse import urlsplit, urlunsplit, parse_qsl, urlencode
def normalisiere(url: str) -> str:
t = urlsplit(url)
host = t.netloc.lower().removeprefix("www.")
pfad = t.path.rstrip("/") or "/"
query = urlencode([(k, v) for k, v in parse_qsl(t.query) if not k.startswith("utm_")])
return urlunsplit(("https", host, pfad, query, "")) # ohne #Anker
def chunk_id(url: str, index: int) -> str:
return str(uuid.uuid5(uuid.NAMESPACE_URL, f"{normalisiere(url)}#{index}"))
Wichtig ist das Normalisieren: Ohne diesen Schritt bekommt dieselbe Seite neue IDs, nur weil sie einmal mit www., einmal mit
Schrägstrich am Ende oder mit einem utm_-Parameter verlinkt war. Andere Parameter bleiben stehen, denn ?seite=2 ist
wirklich eine andere Seite.
Baustein 2: Die Seite als Klammer
Speichern Sie in den Metadaten jedes Chunks eine ID der Seite, berechnet wie oben, nur ohne #index. Dann gilt:
- Seite gelöscht oder nicht mehr erreichbar: alle Einträge mit dieser Seiten-ID löschen, ein Aufruf.
- Seite kürzer geworden: die IDs ab der neuen Chunk-Anzahl löschen. Oder einfacher: alle Einträge der Seite löschen und neu schreiben, wenn sich viel geändert hat.
Baustein 3: Fingerabdruck statt Neuberechnung
Speichern Sie zu jedem Chunk einen Hash des Texts, den Sie einbetten. Beim nächsten Abruf vergleichen Sie: gleicher Hash, gleicher Vektor, nichts zu tun. Merken Sie sich die Vektoren zusätzlich nach Hash, kostet selbst eine verschobene Position nichts: Der Abschnitt steht unter einer anderen ID, aber sein Vektor liegt schon vor.
Ein Hash über das rohe HTML taugt dafür nicht. Zeitstempel, Tracking-Parameter und wechselnde Werbung ändern es bei fast jedem Abruf, auch wenn der Text gleich bleibt. Vergleichen Sie den bereinigten Text des Abschnitts.
Der Ablauf bei jeder Aktualisierung
- Seite abrufen und mit denselben Einstellungen in Chunks teilen wie beim ersten Import.
- Für jeden Chunk ID und Hash berechnen und mit dem gespeicherten Stand vergleichen.
- Nur neue und geänderte Hashes einbetten.
- Alle Chunks per Upsert unter ihrer ID schreiben.
- IDs löschen, die es im neuen Stand nicht mehr gibt.
- Abrufzeit in den Metadaten speichern, damit Ihre KI den Stand nennen kann.
Ändern Sie dagegen das Embedding-Modell oder die Chunk-Einstellungen, passt nichts mehr zusammen. Dann bauen Sie den Index neu auf, am besten in einer zweiten Sammlung, und schalten erst um, wenn sie fertig ist.
Die Schwäche positionsbasierter IDs, offen gesagt
Kommt oben auf der Seite ein Abschnitt hinzu, rutschen alle folgenden um eine Position nach hinten. Ihre IDs bleiben, bekommen aber neuen Inhalt. Das ist korrekt, solange Sie Schritt 4 und 5 ausführen; es bedeutet nur, dass viele Einträge neu geschrieben werden. Mit dem Hash-Vergleich aus Baustein 3 betten Sie trotzdem nur den einen neuen Abschnitt ein.
Wie oft prüfen?
So oft, wie sich die Quelle tatsächlich ändert: Preis- und Statusseiten täglich, Dokumentationen wöchentlich, Gesetzestexte und Archivartikel
selten. Auf Last-Modified- oder ETag-Angaben des Servers können Sie sich nicht verlassen; dynamische Seiten melden oft bei
jedem Abruf eine Änderung. Der Vergleich des bereinigten Texts ist zuverlässiger. Welche Quellen sich lohnen und in welchem Rhythmus, zeigt der
Leitfaden Datenstrategie.
Das Ragfeed-Format chunks liefert diese drei Bausteine mit: eine UUIDv5 aus normalisierter Adresse und Position,
die pageId und einen hash je Chunk, auf Wunsch mit eigenem Namensraum für mehrere Projekte
(Dokumentation). Die Überwachung Beta, ab Plan Fluss, meldet nach einer Prüfung
mit chunkDiff, welche IDs Sie neu schreiben und welche Sie löschen (chunkDiff).
Welche Chunk-Größe sich lohnt: Chunk-Größe für RAG.
Sehen Sie sich die IDs an Ihrer Seite an.
Die Chunk-Werkstatt zeigt jeden Datensatz mit ID, Hash und Metadaten, kostenlos und ohne Anmeldung.