Free lesson · GenAI Data Engineering
Use Google Document AI for managed OCR and layout parsing
Configure Document AI processors (Enterprise OCR, Layout Parser with Gemini 3 Flash) for high-volume document processing. Handle 200+ languages and handwritten text.
Course: GenAI Data Pipelines · Chapter 1 · Document Ingestion with VLMs
Free to read — no subscription required.
Introduction
When you need to ingest thousands of scanned PDFs, contracts, or invoices into a GenAI pipeline, hand-rolling OCR with Tesseract and a layout heuristic falls over fast — accuracy drifts on rotated scans, tables get flattened into runs of whitespace, and someone gets paged whenever the queue backs up. This lesson shows you how to use Google Document AI as a managed OCR and layout-parsing backend instead. By the end you will be able to create a Layout Parser processor programmatically, submit documents through both the online and batch APIs, and parse the response protobuf into structured blocks (paragraphs, tables, headings) ready to chunk and embed. The focus is on the integration shape that scales from one document to thousands without taking on OCR infrastructure of your own.
Key Terminology
- Processor: A configured Document AI model instance — for example a Layout Parser, OCR, or Form Parser processor — addressable by a resource name and invoked via
process_documentorbatch_process_documents. - Layout Parser processor: The processor type (
LAYOUT_PARSER_PROCESSOR) that returns hierarchical structure — paragraphs, headings, tables, and reading order — in addition to raw text. Preferred over plain OCR for ingestion pipelines. - Text anchor: The
start_index/end_indexpair on a layout element that references back into the singledocument.textstring. All structural elements (paragraphs, table cells) dereference text via anchors rather than carrying their own copy.
Concepts
Document AI exposes OCR and layout parsing as a Google-managed API, so the integration work is API plumbing, not model hosting. Three ideas drive the design of a production ingestion path:
- Pick the right processor for the job. Plain OCR returns flat text; Layout Parser returns paragraphs, headings, tables, and reading order. For RAG ingestion you almost always want Layout Parser — downstream chunking and metadata enrichment depend on the structure it preserves.
- Online vs. batch is a throughput decision.
process_documentis synchronous and right for interactive or low-volume use.batch_process_documentsaccepts GCS-resident inputs, writes JSON results back to GCS, and returns a long-running operation — use it whenever you are ingesting more than a few hundred documents (see Code Walkthrough). - The response is anchor-indexed, not self-contained. All text lives in
document.text; pages, paragraphs, and table cells reference slices of that string via text anchors. Your parser must dereference anchors to assemble usable blocks, and should carry the per-elementconfidencefield forward so downstream stages can filter low-quality extractions.
Code Walkthrough
Building on the concepts above, this walkthrough demonstrates the two halves of a Document AI integration: provisioning a processor and submitting documents (online or batch), then dereferencing text anchors to extract structured content from the response.
Provisioning a processor and submitting documents
Code snippetpython
1from google.cloud import documentai_v1 as documentai 2 3project_id = "your-gcp-project" 4location = "us" 5 6client = documentai.DocumentProcessorServiceClient() 7parent = client.common_location_path(project_id, location) 8 9def create_layout_parser() -> str: 10 """Create a Layout Parser processor and return its resource name.""" 11 processor = client.create_processor( 12 parent=parent, 13 processor=documentai.Processor( 14 display_name="ingestion-layout-parser", 15 type_="LAYOUT_PARSER_PROCESSOR", 16 ), 17 ) 18 return processor.name 19 20def process_document_online( 21 processor_name: str, 22 file_path: str, 23 mime_type: str = "application/pdf", 24) -> documentai.Document: 25 """Process a single document synchronously.""" 26 with open(file_path, "rb") as f: 27 content = f.read() 28 29 request = documentai.ProcessRequest( 30 name=processor_name, 31 raw_document=documentai.RawDocument(content=content, mime_type=mime_type), 32 ) 33 return client.process_document(request=request).document 34 35def batch_process_documents( 36 processor_name: str, 37 gcs_input_prefix: str, 38 gcs_output_prefix: str, 39) -> documentai.BatchProcessResponse: 40 """Submit a batch of documents from GCS for processing.""" 41 gcs_documents = documentai.GcsDocuments( 42 documents=[ 43 documentai.GcsDocument( 44 gcs_uri=f"{gcs_input_prefix}/doc_{i}.pdf", 45 mime_type="application/pdf", 46 ) 47 for i in range(100) 48 ] 49 ) 50 51 output_config = documentai.DocumentOutputConfig( 52 gcs_output_config=documentai.DocumentOutputConfig.GcsOutputConfig( 53 gcs_uri=gcs_output_prefix, 54 field_mask="text,pages.layout,pages.tables", 55 ), 56 ) 57 58 request = documentai.BatchProcessRequest( 59 name=processor_name, 60 input_documents=documentai.BatchDocumentsInputConfig(gcs_documents=gcs_documents), 61 document_output_config=output_config, 62 ) 63 64 operation = client.batch_process_documents(request=request) 65 return operation.result(timeout=3600)
create_layout_parser: Provisions aLAYOUT_PARSER_PROCESSORin the configured region and returns its resource name. Thelocation(usoreu) is set at creation time and cannot be changed afterwards.process_document_online: Synchronous path. Sends a singleraw_documentand returns the parsedDocumentprotobuf in one round-trip. Right for interactive or low-volume use.batch_process_documents: Async path. Reads inputs from a GCS prefix and writes per-document JSON results back to GCS via a long-running operation. Thefield_maskis the most important knob for cost — requesting onlytext,pages.layout,pages.tablesskips fields you do not consume.operation.result(timeout=3600)blocks until the batch finishes.
Parsing Document AI response structure
The Document AI response stores all text in a single document.text string, with structural elements referencing back via text anchors. Build extraction utilities that dereference these anchors:
Code snippetpython
1def extract_text_from_layout( 2 full_text: str, 3 layout: documentai.Document.Page.Layout, 4) -> str: 5 """Dereference text anchors to extract actual content.""" 6 parts = [] 7 for segment in layout.text_anchor.text_segments: 8 start = int(segment.start_index) 9 end = int(segment.end_index) 10 parts.append(full_text[start:end]) 11 return "".join(parts).strip() 12 13def parse_document_ai_response( 14 document: documentai.Document, 15) -> list[dict]: 16 """Extract structured content blocks from a Document AI response.""" 17 blocks: list[dict] = [] 18 19 for page in document.pages: 20 for paragraph in page.paragraphs: 21 blocks.append({ 22 "type": "paragraph", 23 "text": extract_text_from_layout(document.text, paragraph.layout), 24 "page": page.page_number, 25 "confidence": paragraph.layout.confidence, 26 }) 27 28 for table in page.tables: 29 headers = [ 30 extract_text_from_layout(document.text, cell.layout) 31 for row in table.header_rows 32 for cell in row.cells 33 ] 34 rows = [ 35 [extract_text_from_layout(document.text, cell.layout) for cell in row.cells] 36 for row in table.body_rows 37 ] 38 blocks.append({ 39 "type": "table", 40 "headers": headers, 41 "rows": rows, 42 "page": page.page_number, 43 }) 44 45 return blocks
extract_text_from_layout: The anchor-dereferencing helper. Every structural element shares this code path because Document AI stores all page text once indocument.textand points into it withstart_index/end_indexsegments.parse_document_ai_response: Walks pages → paragraphs and pages → tables, emitting a flat list of{type, ...}dicts ready for chunking or indexing. The per-paragraphconfidenceflows through so downstream stages can filter low-quality extractions.
You'll know it works when parse_document_ai_response() returns at least one {"type": "paragraph", "text": "..."} dict per input page, the text is non-empty, and confidence is above ~0.8 on clean scans.
Do's and Don'ts
Now that you have worked through the implementation, the practices below separate a durable approach from a fragile one.
Do's
- ✓Do use the Layout Parser processor (
LAYOUT_PARSER_PROCESSOR) for ingestion pipelines so paragraphs, headings, and tables survive into downstream chunking. - ✓Do route high-volume workloads through
batch_process_documentswith GCS input and output configs, and set afield_mask(for exampletext,pages.layout,pages.tables) to skip extractions you do not need. - ✓Do propagate per-element
confidencefromparagraph.layoutand cell layouts into your block records so downstream stages can filter or re-OCR low-confidence regions.
Don'ts
- ✗Don't call
process_documentin a loop for high-volume ingestion — you will hit per-minute quotas and pay synchronous latency on every call; submit a batch operation instead. - ✗Don't copy the
textfield off each layout element directly — the API returns content via text anchors, so always dereferencetext_anchor.text_segmentsagainstdocument.textto get accurate spans. - ✗Don't hardcode a real API key or service account secret into lab code — Document AI clients pick up Application Default Credentials, and the
api_keyplaceholder in lab scripts is the literal stringstudent-token.
This lesson is free to read. Its 3 hands-on labs — real code, in a cloud IDE — are part of the GenAI Data Engineering subscription.
From · cancel anytime
Listen to this lesson
Audio overviews of this lesson's labs and its chapter, from GenBodha Bytes.
- Document Ingestion with VLMsChapter overview20 min
More free lessons in GenAI Data Pipelines
- Ch 1Extract documents using Docling's unified multi-format parser
- Ch 1Use Google Document AI for managed OCR and layout parsingYou are here
- Ch 1Design a unified document model normalizing all extraction outputs
- Ch 1Build a routing system selecting the optimal extraction method
- Ch 2Implement content quality scoring with NeMo Curator filters
- Ch 3Build Anthropic's Contextual Retrieval pattern
- Ch 5Design multi-format storage strategies on GCS and PostgreSQL