Keeping a RAG Index Up to Date with n8n: Upserts, Deletions and Resync
Published 1 August 2026 · 6 min read
Every RAG tutorial stops at the same place: documents are chunked, embedded, inserted into Supabase pgvector, the chatbot answers — the end. Except your documents are not frozen. The price list changes, the internal procedure gets rewritten, a Notion page is deleted. Three months later, your assistant confidently quotes a price that no longer exists, or blends two versions of the same document because chunks from both are sitting in the index. This guide tackles the problem initial ingestion ignores: detecting changes, cleanly replacing stale chunks, handling deletions, and resyncing without re-embedding the whole corpus every time.
The stakes are not cosmetic. The very point of RAG, as framed by Patrick Lewis and co-authors in the foundational paper Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks (NeurIPS 2020), is that the non-parametric memory — the document index — can be swapped or updated without retraining the model. An index you never update gives up exactly that advantage. And a study by Tu Vu and colleagues, FreshLLMs: Refreshing Large Language Models with Search Engine Augmentation (Findings of ACL 2024), quantified the phenomenon on their FreshQA benchmark: as soon as questions involve knowledge that evolves over time, model accuracy collapses, and access to up-to-date information is what restores it. A RAG pipeline wired to stale documents reproduces the exact flaw RAG was supposed to fix.
Why a RAG index degrades silently
Three distinct mechanisms rot an unmaintained index:
- Version duplicates: the Vector Store node in insert mode appends rows, it overwrites nothing. Re-ingesting a modified document without deleting the old one creates two competing sets of chunks — and vector search can happily return one chunk from each version in the same context window.
- Ghost documents: a file deleted from Google Drive or a page archived in Notion stays in pgvector forever. The assistant keeps citing it.
- Chunking drift: if you change your chunking strategy or your embedding model, new and old documents are no longer comparable — vector distances lose their coherence.
If you are starting from scratch, begin with our complete RAG guide with n8n and Supabase; everything below assumes a pgvector index already in place.
Detecting what changed: three levels of precision
1. Schedule Trigger + modification date
The simplest pattern: a periodic Schedule Trigger lists the source's documents and filters on modification date. Google Drive exposes modifiedTime, Notion last_edited_time, and a CMS or SQL database almost always has an updated_at field. The workflow keeps only documents modified since the last run (store that timestamp in a workflow variable, a Data Table or your tracking table — see below). It is robust, but latency equals the trigger interval; our Schedule Trigger guide covers cron configuration and timezone pitfalls.
2. Source webhooks
Google Drive and Notion can notify changes instead of waiting for the next polling run — n8n's Google Drive trigger watches file creations and updates, as described in our RAG on Google Drive guide. Latency drops to seconds, but a webhook can be lost (n8n instance down, workflow failing mid-run): always keep a scheduled resync as a safety net.
3. Content hash: the source of truth
Modification dates sometimes lie: a file re-saved unchanged, a cosmetic Notion property edit, and you re-embed for nothing. Hashing the extracted content settles it definitively:
// Code node — after extracting the document text
const crypto = require('crypto');
const hash = crypto.createHash('sha256')
.update($json.content)
.digest('hex');
return [{ json: { ...$json, content_hash: hash } }];
If the computed hash matches the one stored at the last ingestion, the document has not actually changed: skip the embedding. This single test is what makes synchronisation economically sane.
The delete-then-insert strategy, per document
Pgvector has no native notion of a "document upsert": the unit of insertion is the chunk, and a modified document does not produce the same number of chunks as before. Trying to match old chunks to new ones one by one is the classic trap: a sentence added at the top of a document shifts every chunk boundary, and your "chunk IDs" (doc-42-chunk-3…) no longer point to the same content. The only reliable strategy is atomic at the document level: delete all of the document's chunks, then re-insert the new version in full.
Prerequisite: every chunk must carry a stable identifier of its parent document in its metadata, set from the very first ingestion (Metadata field of the Default Data Loader):
{
"doc_id": "notion-1a2b3c4d",
"source": "notion",
"title": "Refund policy"
}
It is the exact same mechanism you use to filter by metadata at query time — here it targets the deletion instead. The update workflow then chains:
- Detection: document
notion-1a2b3c4dhas a hash different from the stored one. - Targeted deletion: a Postgres node (connected to Supabase, see our n8n–Supabase connection guide) runs the delete against the JSONB metadata column:
DELETE FROM documents
WHERE metadata->>'doc_id' = 'notion-1a2b3c4d';
- Re-insertion: the Supabase Vector Store node in Insert mode receives the full text, re-chunks it (same chunking settings as the initial ingestion) and inserts the new chunks with the same
doc_idmetadata. - Tracking update: the new hash replaces the old one in the tracking table.
The delete-then-insert order leaves a window of a few seconds during which the document is absent from the index — acceptable in almost every case, and far preferable to the opposite window where two versions coexist.
The tracking table: hashes, timestamps and deletions
A small SQL table in the same Supabase project centralises sync state:
CREATE TABLE rag_sync (
doc_id text PRIMARY KEY,
content_hash text NOT NULL,
last_synced_at timestamptz DEFAULT now()
);
It provides three services. First, the hash test before embedding (a simple SELECT per document). Second, handling deletions at the source, the blind spot of incremental sync: a document deleted from Drive or archived in Notion stops appearing in listings, so no "modified" event ever flags it. The fix: during the full resync, compare the list of doc_ids present in the source against those in rag_sync — any orphaned identifier triggers a DELETE in documents and in rag_sync. Third, last_synced_at doubles as a dashboard: a document that has not been resynced for months deserves a look.
Frequent incremental sync, periodic full resync
The two modes complement rather than compete:
- Incremental sync (webhook or hourly/daily Schedule Trigger, modified documents only) delivers day-to-day freshness at near-zero marginal cost.
- Full resync (weekly or monthly) re-lists the entire source, runs every document through the hash test — so it still only re-embeds what changed — and purges orphans. It catches lost webhooks and failed executions.
On the cost side, the leverage is almost entirely in the hash test: on a corpus of several thousand documents where a few dozen change per week, re-embedding everything on every run would multiply your embeddings API bill a hundredfold or more, with zero quality gain. After any significant corpus evolution, also take the time to evaluate your RAG's answer quality: it is the only way to verify that synchronisation produces the intended effect on the user side.
To start from a working base, our free Notion-to-vector-store sync workflow implements this pattern end to end; and the RAG Assistant Pack (€119) ships the full ingestion + sync + assistant stack, ready to adapt to your document source.
Key takeaways
A RAG index is not a deliverable, it is a system to maintain. What makes the difference: a stable doc_id in every chunk's metadata from the very first ingestion, the delete-then-insert strategy at document level (never chunk-to-chunk matching), a content hash stored in a tracking table so you only re-embed what actually changed, and the combination of frequent incremental sync plus periodic full resync to also handle deletions at the source. Without this, your assistant answers with six-month-old documentation — and it is exactly the kind of drift no user ever reports, because the answers stay fluent and confident.
FAQ
Frequently asked questions
Why can't I just re-insert a modified document into the vector store?
Because the Vector Store node in insert mode appends rows, it never replaces anything. Re-ingesting an already indexed document creates duplicates: the old and new versions coexist, and retrieval can return contradictory chunks from both. You must first delete the document's existing chunks (via its doc_id in metadata), then insert the new version.
Do I need to re-embed the whole document if only one section changed?
In practice, yes, at the document level: chunking depends on the full text, and a single edit can shift every chunk boundary. The profitable optimisation happens at the document level, not the chunk level: a content hash lets you re-embed only documents that actually changed and skip all the others.
How often should I run a full resync of the RAG index?
Incremental sync (documents modified since the last run) can run hourly or daily depending on the freshness you need. A weekly or monthly full resync acts as a safety net: it catches missed webhooks, documents deleted at the source and accumulated drift, at the cost of walking the entire source.
How do I handle a document deleted in Google Drive or Notion?
Incremental sync based on modification dates never sees it: a deleted document simply stops appearing in listings. Two approaches: periodically compare the list of doc_ids present in the source against your tracking table and delete the orphans from the vector store, or rely on a deletion webhook when the source provides one.
Bundle FlowKit Complet
€269