Where ingestion cost comes from
| Step | Cost driver |
|---|---|
| Parsing/OCR | Per page for cloud document services; CPU/GPU for self-hosted |
| Chunking | Cheap |
| Embedding | Per token (hosted) or GPU time (self-hosted) |
| Index writes | Vector store compute and storage |
| Enrichment (summaries, metadata extraction with LLMs) | Per token, often the most expensive |
Process only what changed
import hashlib
def content_hash(raw_bytes: bytes) -> str:
return hashlib.sha256(raw_bytes).hexdigest()
for doc in source.list_documents(modified_since=last_run):
h = content_hash(doc.read())
if state.get(doc.id) == h:
continue # unchanged content, skip parse/embed
chunks = parse_and_chunk(doc)
upsert(doc.id, embed(chunks)) # replace this document's chunks only
state[doc.id] = h
Also handle deletes: remove chunks for documents deleted at the source (see the resilient RAG pipelines post).
Chunk-level reuse
Hash each chunk's text too. When a document changes slightly, re-embed only new or changed chunks and keep the rest.
Skip waste
| Waste | Fix |
|---|---|
| Duplicate files in several folders | Deduplicate by content hash |
| Boilerplate (headers, footers, disclaimers) on every page | Strip before chunking |
| Irrelevant file types (images without text, archives) | Filter early |
| Old versions nobody should see | Index only current versions |
Choose parsing per document type
| Document | Approach |
|---|---|
| Born-digital PDFs, Office files | Text extraction (cheap); layout model only if tables matter |
| Scanned PDFs, images | OCR / layout model |
| Complex tables and forms | Layout model with table extraction |
| HTML, Markdown | Direct parsing |
Detect whether a PDF has a text layer before sending it to OCR.
Batch and size embedding calls
- Send many chunks per embedding request (within limits) to reduce overhead.
- Use batch APIs for big backfills where available.
- Consider smaller embedding dimensions (see the vector quantization post).
Measure
Track per run: documents seen, skipped, processed; pages OCR'd; embedding tokens; cost per processed document. A full re-index should be a rare, planned event (e.g. embedding model change).
Tip
Add a "skipped unchanged" counter to the ingestion dashboard. If it's near zero on a daily run over a mostly static library, change detection isn't working.
Checklist
- Document and chunk hashes stored
- Only changed documents processed; deletes handled
- Duplicates and boilerplate removed
- OCR only where there's no text layer
- Embedding calls batched
- Cost per document tracked