Runbooks/ RAG Runbook Track A · Ingestion and chunking Deep readQuick learn
  1. 00 Start
  2. /
  3. 01 Chunking
  4. 02 Parsing
  5. 03 Identity
  6. 04 Access
  7. /
  8. 05 Models
  9. 06 Vectors
  10. 07 Limits
  11. 08 Model ops
  12. 09 Index I
  13. 10 Index II
  14. 11 Tuning
  15. 12 Capacity
  16. /
  17. 13 Sharding
  18. 14 Filtering
  19. 15 Hybrid
  20. /
  21. 16 Proof
Document 03 · Quick learn Boundaries, not text · Identity, not position · Tombstone, never orphan

One paragraphseventy-eight re-embeds

The part of RAG that separates a demo from a system: a million documents that change continuously, and an index that has to stay correct, cheap and available throughout.

“An outage is noticed in minutes. A stale orphan is noticed when somebody acts on a rescinded rule.”
78 of 80
chunks re-embedded by one insert
50×
the cost of getting boundaries wrong
85–90%
of IDs carry over in a normal edit
0 repairs
what a healthy reconciliation run returns
The update blast radiusone paragraph, and seventy-eight re-embeds

Somebody inserts one paragraph into a forty-page policy. How many chunks do you have to re-embed? The naive answer is one. The real answer, with the wrong design, is all of them — and the reason is not the text, it is the boundaries.

Offset boundaries

Cut every 100 tokens

By offset. 80 windows.

Insert 40 tokens at 250

Lands inside window #2.

Re-hash

78 of 80 hashes differ.

Every window below the edit contains shifted text

So the hashes genuinely differ, and 78 chunks are genuinely re-embedded — even though only one paragraph changed.

Hashing detects change. It does not prevent it.

Structural boundaries

Cut at sections

§3.1, §3.2, §3.3…

The same insert

Lands inside §3.2.

Re-hash

One hash differs.

The blast radius is bounded to one section

Because the cut points are decided by the document’s own structure, not by a running character count.

Hashing is then layered on top to suppress the writes — which is what it is actually good for
Worked50,000 documents × 80 chunks · 5% edited per hour → fixed-size: 200,000 re-embeds/hour · structural + hashing: ~3,750a fifty-fold difference, on the same corpus
Chunk identitythe single most expensive thing in this runbook to get wrong

A chunk ID must be derived from stable identity, not from position. Retrofitting it means a full reindex plus reconciling historical data that may no longer be reconcilable.

Before

Three sections. Positional IDs number them by order; identity IDs come from the section itself.

Insert §3.1a

Positional: every slot after the insert holds a different section.

Identity

One new ID appears. The other three are untouched.

Four sources of identity, and one to cross out

Source-system anchor

Use the ID the source already assigned — Confluence heading anchors, Notion block UUIDs, DITA element IDs.

Stable by design. Survives renames and moves
But: only exists where the source provides one
Derived · section path

hash(doc_id + “3.2 Maternity Leave” + ordinal)

Free, deterministic, no stored state
But: breaks on rename; depends on heading detection
Derived · content hash

hash(normalised_text)

Immune to renaming and reordering
But: any edit mints a new ID and tombstones the old — noisy on edit-heavy corpora
Assigned

A random UUID minted once at first ingest and stored.

Survives any change to heading or body, so citations and feedback stay valid
But: you now own matching logic, and matching can be wrong
Positional

doc_id#ordinal

Trivial — and it has all three failure modes.
Avoid
The highest-severity bug in RAG

Stale orphan chunks. The system does not error, does not degrade visibly, and answers confidently from retired content.

In a policy or compliance setting that is considerably worse than an outage — an outage is noticed within minutes, and this is noticed when somebody acts on a rescinded rule
Assigned IDs and re-ingest matchinga real algorithm with a real failure mode

When the source gives you nothing, your pipeline assigns identity at first ingest and stores it. On every later ingest you have to match freshly parsed, label-less sections back to those stored IDs.

1 · Exact hash match

Unchanged sections inherit their stored ID and skip embedding entirely.

2 · Heading match

The same heading under the same parent carries identity over even with a changed body.

3 · Similarity match

Embed the leftovers and compare. 0.91 against stored Maternity → inherit and re-embed.

4 · Leftovers

0.22 → no match. A new UUID, embedded fresh.

5 · Unclaimed

Any stored ID nobody claimed is tombstoned.

The discipline that keeps this correct

Greedy, highest-similarity first, and strictly one-to-one. A stored ID can be claimed exactly once. Without that rule, two new sections both inherit the same identity and your index is corrupted in a way that is very hard to detect — two rows with the same ID, one silently overwriting the other on every subsequent ingest.

And set the threshold deliberately

Too low and an unrelated new section inherits a stale identity along with its citations. Too high and every rewrite mints a new ID, which defeats the purpose.

Measure it: sample real edits, compute before/after similarity, compare against genuinely unrelated sections, and put the threshold in the gap. Around 0.8–0.9 is a common landing point
Boundary stabilitystable identity solves the naming; this solves the cascade
1 · Structural anchors

Cut on headings and paragraphs the author already marked.

Reach for this first — it is free and it is usually enough
2 · Content-defined boundaries

A rolling hash over a sliding window; cut where the hash hits a pattern.

The four words are still adjacent after an insert, so the hash is still the same and the cut is in the same place
3 · Ordinals scoped locally

An oversized paragraph is split internally, with ordinals scoped to it.

So any reshuffle stays local to that paragraph
“What if one paragraph is huge and becomes noisy?”

Two answers. First, an oversized paragraph is split internally with ordinals scoped to it. Second, and more importantly, you do not solve this with chunk size — you solve it with parent–child: embed small precise pieces and hand the surrounding paragraph to the model.

The ingest diff, and deletesreprocess everything; write only what changed

Parsing and hashing are cheap text processing; embedding is the expensive part, and the diff is what protects it.

New ID

Insert and embed.

Same ID, different hash

Re-embed.

Same ID, same hash

Skip — no embedding call at all.

Stored ID absent from the parse

Tombstone it, in the same transaction as the upserts.

The runtime pipeline around the diff

Change detection

Webhooks or CDC where the source offers them; fall back to polling with etag or last-modified.

Plus a nightly full crawl as a safety net, because webhooks silently drop events
Ordering

Partition the ingest queue by doc_id.

Two edits to the same document must not process concurrently. Different documents parallelise freely
Idempotency

Carry a monotonic doc_version on every event; discard anything older.

Retries and out-of-order delivery are guaranteed at scale
Atomicity

Tag chunks with doc_version and filter reads to the latest committed version — or accept a few hundred ms of mixed state.

A choice with a cost, not a best practice

Two guards, and the metric that catches what neither can

Guard 1

Hold if an update deletes more than 30–40% of a document’s chunks.

Catches a parser failure that returns nothing
Guard 2

Hold if ID overlap with the previous version is below ~70%.

A normal edit carries 85–90% of IDs over
The early-warning metric

Track ID overlap per source system, not just per document.

When a vendor changes their export format or you upgrade a parser, overlap drops across thousands of documents at once. That aggregate metric catches it in an hour — without it you find out from the reindex bill
Throughput and the embedding pipelinefour lines that settle the argument
Worked500k docs · 5%/day · 60% in a 3-hour window → 1.4 docs/s peak · 2.2 s work each → 12 workers with headroom · naive fixed-size: 8.05 s each → 36 workersand the source notes said “over 100”
Parsing dominates, not embedding

Roughly ninety per cent of the per-document time in a well-designed pipeline. So “the AI part is expensive” is usually wrong, and the parser tier from document 02 is what to optimise first.

The naive design breaks precisely here

Without the structural diff, each document embeds 80 chunks instead of two — six seconds of embedding rather than 150 ms, and the fleet goes from twelve workers to thirty-six.

That is the fifty-fold factor expressed as infrastructure rather than as an invoice

Running the embedder itself

Batching

Both APIs and local models are far more efficient per item on a batch.

Accumulate, then flush on size or a timeout
Rate limits

Treat the limit as a resource you schedule against, not an error you retry into.

When the queue grows faster than the limit allows, shed or defer
Retries

Exponential backoff with jitter, and a dead-letter queue after a bounded number of attempts.

A chunk that cannot be embedded must not block the document behind it
Idempotency

Re-embedding the same chunk twice must be harmless.

The write is an upsert keyed by chunk ID
Cost control

Meter embedding calls per document.

Alert on the ratio of calls to changed documents — a sudden rise is a boundary bug
Reconciliationthe job that turns a silent permanent error into a bounded one

Every event-driven pipeline drops events. Webhooks fail, retries expire, a deploy eats a queue, a source system has an outage.

1 · List the source

Its current inventory.

2 · Compare

Against your index’s inventory.

3 · Emit repair events

Into the normal pipeline.

4 · Record the count

As a metric.

The metric is the point

The repairs matter; the count matters more. A run that repairs zero documents means your event pipeline is healthy. A run that repairs four hundred means it is not — and you would never otherwise have known, because the safety net was quietly hiding the bug.

Alert on the discrepancy rate, not just on job failure
Scheduling it without melting the source

Full weekly, one seventh daily, and always after an incident.

The daily slice keeps the load flat and bounds the worst case to a week
Zero downtime, and what it costsordinary updates need no machinery at all

A vector database accepts writes while serving reads, and a query arriving mid-update sees either the old chunk or the new one — both valid, nobody notices. The hard case is a full rebuild: a new embedding model, or a changed chunking strategy.

1 · Build green alongside

Populated from the canonical store with the new model. Nobody reads it, so it can take hours.

2 · Dual-write

Live edits go to both — otherwise green is stale the moment the backfill finishes.

3 · Shadow read

Real queries hit both; only blue is served. Compare top-k overlap, gold-set recall, latency.

4 · Flip the alias

The application only ever points at an alias, never at an index.

5 · Keep blue warm

About a day. Instant rollback if quality drops, then retire.

Four migration patterns

Blue-green

Full second index, dual-write, shadow read, alias flip.

Cost: 2× storage during migration
Risk: lowest — instant rollback
Use: anything index-wide
Partial shadow index

Build green for one tenant or document type, compare, then expand.

Cost: small
Risk: low, but you validate on a slice that may not represent the whole
Use: validating a risky change cheaply
In-place rolling

Re-embed chunk by chunk into the same index.

Cost: 1× storage
Risk: high — the index temporarily mixes models, so distances are meaningless
Use: almost never for a model change
Dual-index read-merge

Query both, merge, retire the old once the new covers everything.

Cost: 2× query cost
Risk: medium — score comparability is not guaranteed
Use: adding a corpus rather than replacing one
The trap answer

“I would re-embed in place, chunk by chunk, so there is no extra storage.” It sounds efficient and it is usually wrong: during the migration your index holds vectors from two different models, distances between them are meaningless, and search quality is degraded for the entire duration in a way you cannot measure or bound.

Say why it is tempting and why you would reject it. That is a stronger answer than never mentioning it — it shows you considered the cheap option rather than reciting the expensive one
Answer it out loudthe ninety-second version, then five questions
“The thing that separates a demo from a system here is that documents change. If chunk boundaries depend on token offsets, inserting one paragraph shifts every cut point below it, so seventy-eight of eighty chunks re-embed even though the text did not change. Hashing detects that, it does not prevent it — what prevents it is cutting on structure so the blast radius is bounded to one section. Identity is the other half: IDs must come from what a section is, not where it sits — a source anchor if the system gives me one, otherwise a hash of the document ID plus the section path plus an ordinal scoped to that section. Then the ingest is a diff: new ID means embed, same ID and hash means skip, and any stored ID absent from the parse gets tombstoned. On a 50,000-document corpus at five per cent hourly churn that is 200,000 re-embeds an hour against about 3,750.”

Five questions, and the spine of each answer

Architect

You already hash chunks. Why does one paragraph re-embed everything?

  • Because hashing detects change, it does not prevent it
  • With offset boundaries, inserting text shifts every downstream cut point
  • So the content of every window genuinely differs, and the hashes genuinely differ
The fix is boundary stability — structural anchors, or content-defined boundaries with a rolling hash — with hashing layered on top to suppress the writes. 200,000 an hour becomes 3,750.
Architect

Walk me through what happens when a document is updated.

  • Parse to canonical form; compute a stable ID and a normalised content hash per section
  • Read the stored ID and hash set for that document, then diff
  • New ID → insert and embed. Same ID, different hash → re-embed. Same both → skip
  • Then subtract: any stored ID absent from the parse is a deleted section → tombstone
Bump the document version, and partition the queue by document ID so two edits to the same document cannot interleave.
Architect

How do you make sure a deleted paragraph stops being retrieved?

  • Diff the parsed ID set against the stored set and tombstone what is missing — in the same transaction as the upserts
  • Reconcile nightly against the source to catch dropped delete events
  • Alarm on chunk-count drift per document
Because stale orphans are the highest-severity failure mode in RAG: the system answers confidently from retired content and nothing looks broken.
Architect

How would you change the embedding model with no downtime?

  • Blue-green. Build green alongside from the canonical store; nobody reads it, so it can take hours
  • Dual-write live edits to both, or green is stale the moment the backfill ends
  • Shadow read: real queries hit both, only blue is served, compare offline
  • Flip the alias — the application only ever points at an alias
Keep blue warm about a day for instant rollback. And I would say why I rejected in-place rolling: it mixes two models in one index, so distances are meaningless for the whole duration.
Eng manager

How fast can we ingest, and how many workers do we need?

  • It is a capacity question in disguise, and it is four lines
  • 500k docs at 5%/day with 60% in a 3-hour window is 1.4 docs/s peak
  • At 2.2 s of work each, that is 12 workers with headroom
  • Naive fixed-size boundaries make it 8.05 s each — 36 workers
And parsing dominates, not embedding — about ninety per cent of per-document time. So “the AI part is expensive” is usually wrong, and the parser tier is what to optimise first.
Fast answersthe nine that come up most

QShould the content hash include metadata, or only the text?

Only the text that gets embedded, and normalise it first. Metadata changes — a new ACL tag, a corrected date — should update the row without re-embedding, because the vector would be identical. Never let a permission change trigger a GPU call.

QWhat happens to feedback and citations when a chunk is re-embedded?

They stay valid, because the identity did not move — that is precisely what stable identity buys. Also store the version at which feedback was given, so you can tell whether a thumbs-down refers to text that has since been rewritten.

QIs a nightly full crawl really necessary if we have webhooks?

Yes, and the reason is empirical: webhooks drop events during incidents, deploys and rate-limit episodes, and they do it silently. The crawl is not there to do the work — it is there to measure whether the work happened.

QHow large should the similarity-match threshold be?

Measure it rather than adopt a number. Sample real edits, compute before/after similarity, compare against genuinely unrelated sections, and put the threshold in the gap. 0.8–0.9 is a common landing point, but short boilerplate sections need a higher one.

QCan I avoid all of this by rebuilding the index nightly?

For a small corpus, genuinely yes — simpler, fewer failure modes, no diff to get wrong. It stops working when the rebuild no longer fits in the window, or when freshness needs to be minutes. Give the crossover explicitly.

QWhere does the version fence actually live?

Two places. On the write path, a document-level version record every write checks before committing. On the read path, a predicate filtering chunks to the latest committed version. It is a decision rather than a default because the read predicate is on every query, forever.

QDoes a tombstoned chunk still cost memory?

Yes — and this is the connection to capacity. The vector stays resident and its graph edges stay in place until compaction. On the reference stack, dead records are typically the second-largest line in the memory budget. Compaction policy is a bigger memory lever than any index parameter.

QWhat if the source has no change notification at all?

Poll with whatever cheap signal exists and reconcile more aggressively. The important part is to be explicit about the resulting freshness bound: “this source is polled hourly, so an edit is visible within an hour” is a design statement you can put in front of stakeholders.

QHow do I migrate to stable IDs from positional ones?

A full reindex behind blue-green — and accept that historical citations and feedback cannot be reliably remapped, because the whole problem is that the old IDs did not identify anything stable. Snapshot the old ID-to-text mapping first so existing bug reports stay interpretable.

Hashing detects change. Structure prevents it. If boundaries depend on offsets, one inserted paragraph re-embeds seventy-eight of eighty chunks and the hashes honestly agree that everything changed. Cut on structure and the blast radius is one section. Then give every chunk an identity derived from what it is rather than where it sits, diff on ingest, and tombstone anything the parse no longer produces — because stale orphan chunks are the highest-severity failure in RAG: nothing errors, nothing degrades visibly, and the system answers confidently from a rescinded rule.

offset boundaries · 200,000 re-embeds/hour
structural
Where this goes next

Questions to ask them

What fraction of the corpus changes in a day, and how quickly must an edit be visible?

Do the source systems provide stable anchors, or would we be assigning identity?

Is there a reconciliation job today, and what does its discrepancy count look like?

Has the embedding model ever been changed — and if so, how did that go?

What is the tolerance for a briefly inconsistent answer during a multi-chunk write?

← 02 · Parsing Read the full chapter → 04 · Access →