A subset of the PDFs came through as gibberish and nobody looked
In short
Unreadable extractions never raise an error. Garbled text embeds, indexes and sits in the corpus retrieving for nothing, so the only defence is a legibility screen run on every document before it is embedded: character-class ratios, a dictionary hit rate, an empty-page count and a token-length distribution, with thresholds that quarantine for human sampling rather than silently drop.
Key takeaways
- Bad extraction is silent. Nothing errors, nothing retries, and the document is simply never retrieved.
- Four cheap numbers catch most of it: character classes, dictionary hits, empty pages, token lengths.
- Ligature loss is the subtle one. Confirm becomes conrm, field becomes eld, and the page still looks fine.
- Quarantine rather than reject. A failing document needs a human decision, not a silent drop.
- A clean legibility score does not mean readable order. Multi-column read-across passes every check.
Nothing in the pipeline is going to tell you. A page that extracted as a stream of replacement characters is a perfectly valid string, so it chunks, it embeds, it indexes, and it occupies a row that will never be returned for any query anyone asks. The run is green. The document count matches. The only symptom is a document that a user swears is in the system and that the assistant behaves as though it has never seen — and by the time somebody reports that, the corpus has been growing for 3 months.
This is not a rare edge. Any collection assembled over a decade contains scans without a text layer, files exported from a system that subset its fonts badly, forms with permission flags set, and pages someone photographed sideways. The proportion is usually somewhere between 2 and 15 percent, and the only way to know your number is to measure it, because none of those documents will complain.
The four numbers a legibility screen computes
Compute these per document and per page, store them alongside the extraction, and you have converted an invisible failure into a sortable column. All 4 are cheap enough to run on every file in the corpus.
- Character-class ratios. The share of extracted characters that are letters, digits, whitespace, punctuation and everything else. Ordinary prose is dominated by letters and whitespace; a broken encoding shows a large share in the last bucket, including replacement characters and control codes.
- Dictionary hit rate. Split on whitespace, strip punctuation, and count what proportion of tokens appear in a word list for the document's language. Clean business prose sits high even with jargon; encoding damage collapses it, because the damaged text is not words at all.
- Empty and near-empty pages. Count pages yielding fewer than about 50 characters. A scanned page with no text layer yields close to zero, so a document that is 40 pages long and 38 near-empty pages is an image-only scan, not a short document.
- Token-length distribution. Mean and maximum length of whitespace-delimited tokens. Encoding failures produce very long runs with no spaces; aggressive de-hyphenation produces the opposite. Both are visible as a distribution that does not look like language.
The dictionary check needs one refinement or it will flag half your corpus. Technical documents are full of part numbers, chemical names and internal acronyms that are correctly extracted and absent from any word list, so exclude tokens containing digits before computing the hit rate, and calibrate the threshold on a sample of documents you have read yourself rather than on a published figure.
Thresholds that flag for a human rather than decide alone
A screen that rejects documents on its own will throw away legitimate content — a table-heavy appendix scores badly on every one of these measures and is perfectly readable. So the threshold's job is to route to review, and the number that matters is how much review it generates.
- Set thresholds from your own corpus, not from a blog. Compute the 4 numbers across everything you already have, look at the distribution, and put the line where the tail visibly separates from the body.
- Read the flagged sample before trusting the line. Twenty flagged documents opened by hand tells you whether the screen is finding damage or finding tables, and no amount of tuning substitutes for that half hour.
- Track the flag proportion over time as its own signal. A jump from 3 to 9 percent in one week means a source changed how it exports, which is a different and more urgent problem than the historical backlog.
- Keep the scores even for documents that pass. When somebody later reports a missing answer, the first useful question is what this document scored, and that is only answerable if you stored it.
An unreadable chunk is the quietest defect in the stack. It indexes without complaint, retrieves for nothing, and leaves no line in any log.
Five ways a PDF turns into noise
| Failure | What the text looks like | Caught by |
|---|---|---|
| Image-only page, no text layer | Empty, or a handful of characters from a stamped header | Empty-page count. The most reliable signal of the 4 |
| Subset font with no usable character map | Letters mapped to the wrong glyphs — text of the right shape and length that is not words | Dictionary hit rate near zero while character classes look normal |
| Ligature and diacritic loss | Real sentences with letters missing inside common words | Dictionary hit rate, but only against a tight threshold |
| Rotated or skewed scan through recognition | Fragmented lines, stray single characters, plausible words in impossible sequences | Token-length distribution and a low dictionary hit rate together |
| Permission-restricted or damaged file | Empty output, or a partial first page, with the error swallowed upstream | Empty-page count, plus an explicit extraction-status field |
The last row is worth handling separately, because it is not really an extraction problem — it is an error that a permissive parser converted into an empty string. If the extractor returns a status, persist it; if it throws, record the failure against the document rather than logging it and moving on. A pipeline that reports success while quietly producing nothing is the pattern behind most silent corpus gaps, and it is worth alerting on directly, as set out in what to alert on in an ingest pipeline.
Where the screen belongs, and why not later
Run it between extraction and chunking. Before chunking, the unit is a document and the decision is simple — index, quarantine, or send for recognition. After chunking, the damage is distributed across dozens of rows with no shared handle, and after embedding you have paid for vectors that will never be retrieved and taken on the job of deleting them cleanly.
Placing it there also gives the pipeline a natural fork. Documents that fail on empty pages go to optical recognition; documents that fail on dictionary hits with healthy character classes usually need a different extraction library rather than recognition; documents that fail on character classes alone often extract correctly on a second pass with a different tool. That fork is worth building before the corpus is large, alongside the format-normalisation work in normalising mixed formats before indexing.
One consequence to plan for: recognition is the slow, expensive path, and a backlog of quarantined scans released into it at once will saturate whatever runs it. That is the same capacity shape described in an ingest queue that backs up every Monday, and the answer is the same — meter the release rather than buying a larger machine.
What quarantine has to mean in practice
Quarantine is only useful if it is visible. A held document that nobody can see is indistinguishable from a lost one, and the whole point of the screen was to stop losing documents silently.
- A queue somebody owns, with the scores, the first page rendered and the extracted text side by side. Deciding takes seconds when you can see both; it is impossible from a score alone.
- Three outcomes per document: re-extract with a named tool, send for recognition, or accept as-is with a reason. The third exists because some documents genuinely are tables of numbers.
- A count on a dashboard, not a log line. The number of quarantined documents and the age of the oldest are the 2 figures that tell you whether the process is working.
- An answer for users. If a document is held, a search for it should say so rather than returning nothing, because a user who is told the file is being processed does not file a bug.
That queue is a small, unglamorous piece of software, and the decision about whether it deserves to exist as a real tool rather than a shared spreadsheet is a recurring one — the case is argued in whether a small internal tool is worth owning. It is also the kind of thing worth including in a first release rather than deferring, which is how we scope product and MVP builds that depend on a document corpus.
Quarantine also has a cost that has to be declared. A document held for review is a document not answerable, and if anyone has promised how current the corpus will be, the holding time counts against that promise. Work out where it lands against the commitment described in what a freshness commitment actually binds you to before the queue grows.
What a clean score still does not prove
The screen measures whether the characters are language. It says nothing about whether the language is in the right order, and that is the failure it structurally cannot see. A 2-column page read straight across produces real words, real sentences fragments and a perfect dictionary hit rate, while the actual meaning is shredded — the first half of every line from the left column glued to the first half of every line from the right. Catching that needs a layout check or a human reading a sample, and a sample is cheaper.
Two further things it does not settle. Re-extracting the corpus after fixing a parser is a change the pipeline will probably not notice, because the source file is untouched and change detection is keyed to the file — the same blind spot described in an updated document that still serves old text, and the reason an extractor version belongs in whatever signals a re-index. And a corpus can be perfectly legible and still contradict itself, which is two copies of one document and an assistant that believes both. Both sit in data readiness and pipelines, part of the engineering library.
Frequently asked questions
Short answers to the follow-ups this page tends to raise.
How do I find out which documents failed to parse in my pipeline?
Score every extracted document on 4 cheap measures and sort by the worst: character-class ratios, dictionary hit rate on tokens without digits, count of pages yielding under about 50 characters, and token-length distribution. Failed extractions rarely raise an error, so a log search will not find them — the failures are valid strings that happen not to be language. Store the scores alongside the extraction so the question is answerable later without a re-run.
Why does extracted PDF text have missing letters inside ordinary words?
Because ligature glyphs were dropped rather than mapped back to their component letters, so confirm becomes conrm and field becomes eld. It comes from the font encoding in the source file rather than from your extractor's settings, and it survives every visual check because the page still reads as English. A dictionary hit rate compared against the corpus median is the practical detector.
Should a document that fails the legibility screen be dropped?
No — quarantine it and put it in a queue a person can see. Table-heavy appendices, forms and number-dense reports score badly on every legibility measure and are perfectly usable, so an automatic reject deletes good content. The screen's job is to route to a decision: re-extract with a different tool, send for optical recognition, or accept with a recorded reason.
Can a legibility screen tell me the text is in the right order?
No, and this is its main blind spot. A multi-column page read straight across yields real words with a perfect dictionary hit rate while the meaning is destroyed, because lines from 2 columns have been interleaved. Detecting that needs layout-aware extraction or a human reading a sample of pages, and reading 20 pages is the cheaper of the two.
- document extraction
- ingest quality
- ocr
- corpus hygiene
The work behind this page
Builds from our portfolio that this page draws on.
ScanQueue
An AI radiology worklist that flags suspected critical findings on incoming CT, MR and X-ray studies and orders every read by acuity and SLA — so the sickest patient is read first, not FIFO.
Healthcare AIAskVault
An AI internal knowledge-search platform that answers employee questions from your own docs — grounded in citations, with knowledge gaps surfaced and deflection tracked.
Productivity AIRead next
- Two copies of the same document, and the assistant believes bothAn exact duplicate is harmless. A near-duplicate agrees with the original everywhere except the one clause that changed, and both copies rank convincingly.diagnostic
- The same customer appears three times under three spellingsTotals that disagree and an assistant that says a customer has two accounts are the same defect: nothing in the chain ever decided that two records describe one company.diagnostic
- Someone updated the document and the system still serves the old textThe indexing job is rarely the culprit. A timestamp that never moved, a hash over the wrong bytes, or an edit the extractor discards all leave the pipeline correctly deciding there is nothing to do.diagnostic
- A freshness SLA is a promise about the worst case, not the averageFreshness is the age of the data behind an answer when it is served, held under a stated ceiling. Latency is how fast a run finishes, and the two can disagree by a week.definition
- An upstream field changed and the pipeline carried on regardlessA renamed source field does not raise an error. It returns nothing, coalesces to an empty string, and quietly hollows out every record ingested since — until someone plots completeness by day.diagnostic
- Backfill: the word that hides four different jobsFirst historical load, gap repair, transform-change reprocessing and full rebuild are all called backfill. They share a shape and nothing else, including risk.definition
Working on something in this space?
Tell us where you are in a sentence or two. We'll tell you honestly whether we're the right team, and what a sensible first slice of the work looks like.
Start the conversation