From source system to corpus

Extraction is the bottleneck, not the embedding call - because the content still lives inside someone else's system.

This lesson is about the step before a file exists on your disk at all - getting it out of a wiki, a helpdesk, a CRM, or a shared drive that was never built to be read by a pipeline. Parsing documents before you chunk them, next, is about what happens once that file is in hand. Skip this step, and no amount of layout-aware parsing helps, because the document never arrives.

Getting bytes out is a different job than parsing them

A PDF sitting in a folder is the easy case. Something already put it there, and your job is only to read it correctly. Most of a real corpus does not arrive that way. It lives inside a running system - a wiki with a permissions model, a ticketing tool with a rate-limited API, a CRM that only shows you records through its UI or a paid export - and getting content out means talking to that system on its terms, not yours.

Each kind of source system fails in its own way, and the failure has nothing to do with file format.

Source systemHow you get content outWhat actually breaks
Wiki (Confluence, Notion-style)Paginated REST API, page tree walkNested spaces, permission-scoped pages, renamed paths breaking cached IDs
Helpdesk / ticketingREST API, often rate-limited per minuteRate limits, attachments fetched separately from ticket text
CRMREST API or a scheduled exportFields hidden behind record-level permissions, export schemas that change without notice
Shared driveSync client or storage APIAuth tokens that expire mid-crawl, folders that are actually shortcuts to other folders
Internal databaseDirect query or a read replicaNo API at all - someone has to grant access and agree on a query

None of these problems show up in a parsing library, because parsing runs after extraction. If your retrieval quality looks bad and the corpus is missing whole documents rather than mangling the ones it has, look here first, not at chunk size.

Buy the connector, hand-roll only the gap

Once you accept extraction as its own problem, the next question is who solves it: a connector someone else maintains, or a script you write and own.

A connector is a piece of software built specifically to pull data out of one kind of system, maintained by someone who watches that system's API for changes. Airbyte and Fivetran are the general-purpose examples; Unstructured and similar libraries ship source connectors for wikis, drives and ticketing tools aimed specifically at feeding an LLM pipeline. Using one means someone else has already solved pagination, retry-on-429, and incremental fetch for that exact system, and keeps solving it as the system's API drifts.

A hand-rolled script is code you write against the source's API directly. It is faster to start - no new dependency, no configuration format to learn - and it can do exactly the narrow thing you need and nothing else. It is also entirely your job to keep working when the source system changes anything.

Maintained connectorHand-rolled script
Time to first working pullSlower - setup and configurationFaster - straight to the API
Handles pagination and rate limitsUsually, out of the boxOnly what you write
Survives the source's API changingSomeone else's job to fixYour job, discovered when it silently stops working
Best fitCommon systems, ongoing syncOne narrow, stable endpoint nobody else needs
Ongoing costA dependency to manageCode to maintain forever

The right default is to reach for a connector first and hand-roll only the gap it does not cover - a niche internal tool, a one-off migration, an endpoint too narrow for a general connector to be worth adopting. Where a script is genuinely the right call, write it as if someone else has to run it without you: log every failure, never let a partial fetch look like a successful one, and never hard-code something about the source's structure - a folder path, a fixed set of IDs - that the source is free to change without telling you.

import hashlib
import time

import requests

def extract_pages(base_url: str, api_token: str, since_cursor: str | None = None):
    """Pull every page from a wiki-style API, one batch at a time.

    Real extraction code, whether hand-rolled or inside a connector,
    has to handle three things a parser never sees: pagination, rate
    limits, and detecting which pages actually changed.
    """
    headers = {"Authorization": f"Bearer {api_token}"}
    cursor = since_cursor
    documents = []

    while True:
        resp = requests.get(
            f"{base_url}/pages",
            headers=headers,
            params={"cursor": cursor, "limit": 100},
            timeout=30,
        )
        if resp.status_code == 429:
            wait = int(resp.headers.get("Retry-After", 5))
            time.sleep(wait)
            continue
        resp.raise_for_status()

        batch = resp.json()
        for page in batch["results"]:
            content_hash = hashlib.sha256(page["body"].encode()).hexdigest()
            documents.append({
                "source_id": page["id"],
                "title": page["title"],
                "body": page["body"],
                "content_hash": content_hash,
                "source_url": page["url"],
            })

        cursor = batch.get("next_cursor")
        if not cursor:
            break

    return documents

Notice what the function returns: not just text, but a content_hash and a source_url alongside it. That is not incidental - it is the beginning of a schema every document needs before it is ever chunked: a stable id, a content hash, and a source url decided at the point of extraction, and it is cheapest to capture right here rather than reconstructed later from a corpus that has already forgotten where anything came from.

Where the ingestion budget actually goes

Teams that budget an ingestion project usually put the biggest number next to embedding, because that is the line item with a per-token price attached to it. In practice the money and the time go somewhere else.

TaskEffort, in most real pipelines
Building and maintaining connectors, one per source systemLarge, ongoing
Handling auth, pagination, rate limits and retriesLarge, ongoing
Normalising formats before parsingModerate, one-time per source type
Embedding the resulting chunksSmall, predictable, scales with corpus size
Writing vectors to the storeSmall, predictable

Embedding a chunk costs the same whether that chunk came from a PDF, a wiki page, or a support ticket - it is one uniform API call regardless of source. Extraction has none of that uniformity. Every source system has its own auth model, its own pagination scheme, its own way of telling you a record changed, and its own way of silently going quiet when something upstream shifts. That variability, not the per-token price of an embedding call, is what actually consumes an ingestion team's time.

The embedding call looks the same for every document. Getting the document is different every single time - that is where the budget goes.

This is worth saying plainly before you scope a project, because it changes what you plan for. A four-source corpus is not one extraction problem times four - it is four different extraction problems, each with its own way of breaking, and each needing someone to notice when it does.

WHAT YOU TAKE AWAY

  1. Treat getting content out of the source system as its own problem, separate from parsing the file.
  2. Reach for a maintained connector before you write a scraper against someone else's API.
  3. Handle pagination, auth expiry and rate limits as real code paths, not edge cases.
  4. Store a content hash per document at extraction time, before anything else touches it.
  5. Plan an ingestion budget assuming extraction costs more than embedding, not the other way round.

RECALL NO SCROLLING BACK

00 / 05 answered

  1. QUESTION 01

    You already fixed layout-aware parsing for your PDFs. Retrieval is still missing entire documents. What is the most likely cause?

  2. QUESTION 02

    A hand-rolled extraction script silently stops pulling new pages from a wiki after a folder gets renamed. What does that tell you?

  3. QUESTION 03

    When is hand-rolling extraction the right call instead of a connector?

  4. QUESTION 04

    Why does extraction usually cost more engineering time than the embedding step?

  5. QUESTION 05

    You store a content hash for each document the moment you extract it. What does that buy you later?