Navigation überspringen

Von unstrukturierten Dokumenten zu strukturierten Daten: Wie Spring AI und Tesseract OCR die Datenextraktion vereinfachen

Rechnungen, Teilelisten, gescannte Formulare – überall stecken wertvolle Daten in unstrukturierten Dokumenten. Wir haben getestet, wie sich mit Spring AI und GenAI-Modellen strukturierte JSON-Daten aus PDFs und Bildern extrahieren lassen. Das Ergebnis: produktionsreif und überraschend einfach umzusetzen. 

Wissen
Spring, AI, KI, Technologie

Die Idee ist simpel: Dokumente mit unstrukturierten Daten hochladen, strukturierte JSON-Antworten zurückbekommen. Im Hintergrund arbeitet eine GenAI-gestützte ETL-Pipeline, die mit Spring AI realisiert wird.  

Wir werten JPEGs, PNGs und PDF-Dateien aus, in denen irgendwelche Daten aufgelistet sind. Im Ergebnis wollen wir Daten extrahieren, die sich mithilfe von JSON Schema beschreiben lassen. Dabei nutzen wir Spring AI, um auf das Modell zuzugreifen. Den Code zu unserem Experiment findet ihr auf GitHub unter dem Namen TL;DR (Tool for Linguistic Document Reading). 

Wir implementieren das Ganze in vier Schritten: 

Unsere Anforderungen im Überblick

Für das Projekt haben wir uns klare Rahmenbedingungen gesetzt: 

  • Binäre Datenquellen: JPEGs, PNGs und PDFs mit Bildinformationen sollen verarbeitet werden
  • Spring AI als Framework: Wir nutzen Spring AIs ETL-Pipeline-Design für eine saubere Architektur 
  • Lokale OCR mit Tesseract: Die Texterkennung läuft lokal, um Token-Kosten bei ChatGPT zu vermeiden 
  • OpenAI-Modell: Wir greifen auf ein Modell aus der Holisticon-Organisation zu 
  • REST-Schnittstelle: Per POST-Request ein Dokument hochladen, strukturiertes JSON zurückbekommen 

Spring AI und Tesseract 

Wir verwenden Spring AI. Warum? Spring AI ist unsere erste Wahl, weil es: 

  • eine einheitliche API über verschiedene Modellanbieter bietet 
  • Auto-Konfiguration für Beans, Prompts und Output-Converter mitbringt 
  • sich nahtlos in bestehende Spring-Boot-Architekturen integriert 

Tesseract übernimmt die OCR, weil es: 

  • einfach einzubinden ist 
  • keine Cloud-Kosten verursacht 
  • sehr stabile Ergebnisse für gedruckte Texte liefert 

Praktische Learnings aus der OCR-Integration 

  • Pre-Processing lohnt sich: Mit ImageMagick könnt ihr durch Entzerren und Kontrast-Anpassung deutlich bessere Ergebnisse erzielen 
  • Garbage in, Garbage out: Schlechte OCR-Ergebnisse führen direkt zu schlechter AI-Qualität 
  • komplexe Layouts: Für Tabellen und mehrspaltige Layouts könnte ein Upgrade auf Tesseract LSTM oder PaddleOCR sinnvoll sein 

Im Ergebnis entsteht eine einfache, aber erstaunlich leistungsfähige ETL-Pipeline – also eine klassische „Extract, Transform, Load“-Kette, diesmal mit AI im Transform-Schritt.

Konfiguration in 3 Schritten 

Neben einigen anderen benötigen wir diese drei Dependencies in unserer pom.xml: 

Listing1: Maven Dependencies  

<dependency> 

            <groupId>org.springframework.boot</groupId> 

            <artifactId>spring-boot-starter-web</artifactId> 

        </dependency> 

        <dependency> 

            <groupId>org.springframework.ai</groupId> 

            <artifactId>spring-ai-starter-model-openai</artifactId> 

        </dependency> 

        <dependency> 

            <groupId>org.springframework.ai</groupId> 

            <artifactId>spring-ai-tika-document-reader</artifactId> 

        </dependency>

In unserer application.yml fügen wir diese Konfigurationen hinzu: 

Listing 2: application yaml

tldr: 

  prompt-template: | 

    You are a data extraction assistant. 

     

    With this request we send a json schema definition. The schema defines an array of objects. The objects in the array 

    have required fields. 

     

    Try to extract the required fields from the given text. 

     

    The given text is an unstructured document that was obtained through ocr from a scanned jpeg document. 

     

    Return the extracted data in JSON format according to the provided JSON schema.  

     

    If a field is missing, set its value to null. 

    Input text: 

      {{document}} 

 

spring: 

  config: 

    import: optional:file:.env[.properties] 

  ai: 

    openai: 

      api-key: ${OPENAI_API_KEY} 

      organization-id: ${OPENAI_ORG_ID} 

      project-id: ${OPENAI_PROJECT_ID} 

      chat: 

        options: 

          model: ${OPENAI_MODEL:gpt-5-mini} 

          temperature: 1 

          reasoning-effort: ${OPENAI_MODEL_REASONING_EFFORT:minimal}

Wir hinterlegen unseren API-Key. Optional können noch eine Organization ID und eine Project ID hinterlegt werden. 

Wichtige Parameter erklärt: 

  • Model: Mit der Property spring.ai.openai.chat.options.model wählen wir das gewünschte Modell aus. Wir nutzen gpt-5-mini als Default 
  • Temperature: Steuert die "Kreativität" des Modells (0,0-0,3 = deterministisch, 0,5-0,7 = ausgewogen, höher = kreativer, etwas unvorhersehbarer). In unserem Anwendungsfall hatte dieser Wert allerdings keinen messbaren Einfluss. 
  • Reasoning Effort: Relevant für GPT-5-Modelle, steuert den Rechenaufwand des Modells für Antworten. Wir setzen ihn auf minimal 

Implementierung 

Mit dieser Konfiguration aus der application.yaml erhalten wir eine Spring Bean, die wir überall in unserer Application injizieren können und mit der wir Prompts an das GenAI-Modell schicken können. 

In Listing 3 trägt diese Bean den Namen chatModel.  

Was passiert hier? 

  • In Zeile 35 schicken wir einen Prompt an das Modell.  
  • Der Prompt wird in Zeile 34 zusammengebaut. Er enthält zum einen unsere natürlich sprachliche Frage mit dem per OCR extrahierten Text aus der Bilddatei.  
  • Weiterhin enthält der Prompt die Konfiguration des gewünschten Rückgabeformats. In unserem Fall ist dies eine JSON Schema definition (siehe Listing 4). 

Listing 3: ChatModel  

@RequiredArgsConstructor 

@Slf4j 

@Service 

public class AiModelOutAdaper implements AiModelOutPort { 

 

    private final @NonNull OpenAiChatModel chatModel; 

    private final @NonNull BeanOutputConverter<PartList> partListBeanOutputConverter; 

    private final @NonNull ApplicationProperties applicationProperties; 

 

    /** 

     * Extracts structured data from the given file content using an AI model and a specified JSON schema. 

     * 

     * @param fileContent the content of the file to extract data from 

     * @param jsonSchema  the JSON schema to format the response 

     * @return an Optional containing the structured data as a String, or an empty Optional if an error occurs 

     */ 

    @Override 

    public Optional<String> extractStructuredData(String fileContent, String jsonSchema) { 

 

        final String promptTemplate = applicationProperties.getPromptTemplate(); 

 

        final String preparedPrompt = promptTemplate.replace("{{document}}", fileContent); 

 

        log.info("AiModelOutAdaper prepared prompt {}", preparedPrompt); 

 

        try { 

            final OpenAiChatOptions options = OpenAiChatOptions.builder() 

                    .responseFormat(ResponseFormat.builder() 

                            .type(ResponseFormat.Type.JSON_SCHEMA) 

                            .jsonSchema(jsonSchema) 

                            .build()) 

                    .build(); 

 

            final Prompt prompt = new Prompt(new UserMessage(preparedPrompt), options); 

            final Generation chatResponse = chatModel.call(prompt).getResult(); 

 

            return Optional.of(chatResponse) 

                    .map(Generation::getOutput) 

                    .map(AbstractMessage::getText); 

        } catch (final NonTransientAiException e) { 

            log.error("AiModelOutAdaper error calling chat model", e); 

            return Optional.empty(); 

        } 

    } 

}

JSON Schema: Die Geheimwaffe für strukturierte Daten 

Das JSON Schema definiert exakt, welche Struktur die Antwort haben soll. 

Listing 4: JSON Schema  

{ 

  "$schema": "https://json-schema.org/draft/2020-12/schema", 

  "title": "PartList", 

  "type": "object", 

  "properties": { 

    "parts": { 

      "type": "array", 

      "items": { 

        "type": "object", 

        "properties": { 

          "partNumber": { 

            "type": "string", 

            "description": "The unique identifier of the part" 

          }, 

          "manufacturer": { 

            "type": "string", 

            "description": "The name of the manufacturer" 

          }, 

          "serialNumber": { 

            "type": "string", 

            "description": "The serial number of the part" 

          } 

        }, 

        "required": [ 

          "partNumber", 

          "manufacturer", 

          "serialNumber" 

        ], 

        "additionalProperties": false 

      } 

    } 

  }, 

  "required": [ 

    "parts" 

  ], 

  "additionalProperties": false 

}

Das Schema zwingt das Modell, sich an die Struktur zu halten – ein Game Changer für die Zuverlässigkeit. 

Fazit: Was wir gelernt haben 

Das Projekt war zu 80 % klassisches Software-Design einer Spring-Anwendung – wir haben versucht, dem hexagonalen Ansatz zu folgen. Die AI-Integration selbst war überraschend unkompliziert. 

Unsere wichtigsten Erkenntnisse 

  1. OCR ist der Engpass. 
    Gute Texterkennung bringt mehr als jede Prompt-Optimierung. 
  2. JSON Schema wirkt Wunder. 
    Es zwingt das Modell, sich an Struktur zu halten. 
  3. Spring AI macht AI-Integrationen produktionsreif. 
    Die Spring-Konventionen (Beans, Fehlerbehandlung, Properties) funktionieren einfach. 
  4. Temperatur und Reasoning-Effort 
    haben in unserem Fall kaum Einfluss auf die Extraktionsqualität. 

Der komplette Source Code ist hier zu finden: https://github.com/holisticon/tl-dr 

Ausblick: Wohin könnte die Reise gehen? 

Für die Weiterentwicklung haben wir uns folgende Ideen überlegt: 

Technische Verbesserungen:

  • Experimente mit anderen Modellen (Claude, Mistral) 
  • Alternative OCR-Libraries als Alternative zu Tesseract testen 
  • Dokumententypen definieren, das umfasst etwa:  
    • Art des Dokuments 
    • Definition der auszulesenden Daten 
    • Speziell an diesen Dokumententyp angepasster Prompt 
  • Kostentransparenz durch Token-Tracking 
  • Automatische Validierung ausgelesenen Daten, z.B. gegen eine vorhandene Teile-Datenbank

User Experience: 

  • ein User Interface, das die ausgelesenen Daten zusammen mit dem gescannten Dokument anzeigt. Anwender*innen könnten hier nochmal "gegenlesen"  
  • darauffolgende Korrekturmöglichkeiten für Anwender*innen 

Betrieb: 

  • Ein Betriebsmodell entwickeln, also beschreiben, wie man TL;DR in einem Kubernetes-Cluster betreiben kann. Fragen wären insbesondere: 
    • Zugriff auf das Gen AI-Modell aus dem Cluster heraus. 
    • Installation von Tesseract oder anderen OCR-Tools im Cluster. 

Spring AI macht die Integration von GenAI-Modellen in bestehende Spring-Architekturen erstaunlich unkompliziert – probiert es aus und lasst euch von den Ergebnissen überraschen! 

Autor

Jan Weinschenker

Lego-Fan, Angler, Chili-Anbauer und Langstreckenläufer. Aufbauer mit Ausdauer. Web- und Performance-Enthusiast. Liebt Reactive Programming und hält manchmal Vorträge auf Konferenzen.

Über uns

Holisticon in 76 Worten

Holisticon ist die Boutique unter den IT-Beratungen für digitale Transformation. Ganzheitlich und individuell: Wir denken Technologie, Strategie und Organisation konsequent zusammen. Mit unserem Strategize-Ansatz schließen wir zudem die Lücke zwischen Strategie und Umsetzung. So schaffen wir die Grundlage für zukunftssichere, innovative Geschäftsmodelle in der digitalen Welt.

Über 70 Mitarbeitende an den Standorten Hamburg, Hannover und München begleiten Unternehmen aus verschiedensten Branchen erfolgreich durch die Transformation.

Holisticon ist Partner führender Technologiemarken und Mitglied der internationalen Nexer Group.