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_document or batch_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_index pair on a layout element that references back into the single document.text string. 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:

  1. 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.
  2. Online vs. batch is a throughput decision. process_document is synchronous and right for interactive or low-volume use. batch_process_documents accepts 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).
  3. 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-element confidence field forward so downstream stages can filter low-quality extractions.
Loading diagram...

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 a LAYOUT_PARSER_PROCESSOR in the configured region and returns its resource name. The location (us or eu) is set at creation time and cannot be changed afterwards.
  • process_document_online: Synchronous path. Sends a single raw_document and returns the parsed Document protobuf 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. The field_mask is the most important knob for cost — requesting only text,pages.layout,pages.tables skips 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 in document.text and points into it with start_index/end_index segments.
  • parse_document_ai_response: Walks pages → paragraphs and pages → tables, emitting a flat list of {type, ...} dicts ready for chunking or indexing. The per-paragraph confidence flows 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

  1. Do use the Layout Parser processor (LAYOUT_PARSER_PROCESSOR) for ingestion pipelines so paragraphs, headings, and tables survive into downstream chunking.
  2. Do route high-volume workloads through batch_process_documents with GCS input and output configs, and set a field_mask (for example text,pages.layout,pages.tables) to skip extractions you do not need.
  3. Do propagate per-element confidence from paragraph.layout and cell layouts into your block records so downstream stages can filter or re-OCR low-confidence regions.

Don'ts

  1. Don't call process_document in a loop for high-volume ingestion — you will hit per-minute quotas and pay synchronous latency on every call; submit a batch operation instead.
  2. Don't copy the text field off each layout element directly — the API returns content via text anchors, so always dereference text_anchor.text_segments against document.text to get accurate spans.
  3. 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_key placeholder in lab scripts is the literal string student-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.

More free lessons in GenAI Data Pipelines

All free lessons in GenAI Data Engineering