Add Docling corpus pipeline, retrieval evaluation and hybrid search with reranking
- prepare_corpus.py: split double-page scans (split_spreads.py), re-OCR with OCRmyPDF/Tesseract only where the existing text layer is worse, Docling layout extraction with heading hierarchy, QA report per PDF - docling_loader.py: cleanup (margin noise, split paragraphs, footnote marks, OCR comma/period repair), page mapping back to the original PDF, chunk headers with author, title and section - local_rag.py: bge-m3 embeddings, configurable chunking and loader, index settings check (index_info.json), hybrid/union BM25 search with Reciprocal Rank Fusion and bge-reranker-v2-m3 cross-encoder; query instruction text removed after it lowered recall - eval_retrieval.py: CLI options, same retrieval path as the chat, tolerant file name matching; goldsets and evaluation reports for the Kafka corpus - grobid_test.py, grobid_qa.py, extract_docling.py, compare_extractors.py: extractor tests and quality checks - .gitignore: corpus PDFs, vector stores and extractor output stay local - server.py: uses ./vectorstore_docling_hdr Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
PDFs mit Docling extrahieren, zum Vergleich mit GROBID und PyPDFLoader.
|
||||
|
||||
Vier Varianten:
|
||||
docling nutzt die vorhandene Textschicht des PDFs (bei Scans: deren OCR)
|
||||
docling_ocr erkennt jede Seite neu per OCR (Apple Vision über ocrmac, Deutsch),
|
||||
ignoriert also die mitgelieferte, oft schlechte OCR-Schicht.
|
||||
Achtung: lässt bei diesen Scans ganze Absätze aus.
|
||||
docling_tess wie docling_ocr, aber mit Tesseract und dem deutschen Modell
|
||||
aus tessdata/deu.traineddata (tessdata_best). Verliert ebenfalls Text:
|
||||
Doclings eigener OCR-Ablauf ist bei diesen Doppelseiten-Scans unzuverlässig.
|
||||
docling_ocrmypdf
|
||||
Arbeitsteilung: OCRmyPDF (Tesseract, deutsch, mit Geraderichten) legt
|
||||
eine neue Textschicht ins PDF (extract_out/ocrmypdf/), Docling liest
|
||||
diese dann ohne eigene OCR und erkennt die Struktur.
|
||||
|
||||
Läuft in einer eigenen Umgebung, weil Docling PyTorch und Layout-Modelle mitbringt:
|
||||
conda activate docling
|
||||
python extract_docling.py # Variante docling_tess, Titel aus TITLES
|
||||
python extract_docling.py docling # andere Variante
|
||||
|
||||
Ausgabe: extract_out/<variante>/<pdf>.json (verlustfrei, für grobid_qa.py)
|
||||
und .md (zum Lesen). Vorhandene Ergebnisse werden übersprungen.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from docling.datamodel.base_models import InputFormat
|
||||
from docling.datamodel.pipeline_options import (
|
||||
OcrMacOptions, OcrMode, PdfPipelineOptions, TesseractCliOcrOptions)
|
||||
from docling.document_converter import DocumentConverter, PdfFormatOption
|
||||
|
||||
PDF_DIR = Path("data")
|
||||
OUT_ROOT = Path("extract_out")
|
||||
TESSDATA = Path("tessdata").resolve() # enthält deu.traineddata
|
||||
# Docling prüft die verfügbaren Tesseract-Sprachen ohne Rücksicht auf `path`,
|
||||
# deshalb das Modellverzeichnis auch über die Umgebungsvariable bekanntmachen.
|
||||
os.environ["TESSDATA_PREFIX"] = str(TESSDATA)
|
||||
VARIANTS = ("docling", "docling_ocr", "docling_tess", "docling_ocrmypdf")
|
||||
OCRMYPDF_DIR = OUT_ROOT / "ocrmypdf" # neu erkannte PDFs für docling_ocrmypdf
|
||||
# Auswahl für den Vergleich: Dateinamen beginnen mit diesen Präfixen
|
||||
TITLES = [
|
||||
"Beissner,", # Aufsatz, viele Fußnoten
|
||||
"Fiechter,", # GROBID: alles ein Absatz
|
||||
"Trost,", # sehr schlechte OCR-Schicht
|
||||
"Harzer,", # GROBID: Fließtext als Abbildung
|
||||
"Binder,_Hartmut_KafkaHandbuch_Band2_AText", # großes Buch
|
||||
]
|
||||
|
||||
|
||||
def converter(variant: str) -> DocumentConverter:
|
||||
opts = PdfPipelineOptions()
|
||||
opts.do_table_structure = True
|
||||
if variant == "docling_ocr":
|
||||
opts.do_ocr = True
|
||||
opts.ocr_options = OcrMacOptions(mode=OcrMode.FULL_PAGE, lang=["de-DE"])
|
||||
elif variant == "docling_tess":
|
||||
opts.do_ocr = True
|
||||
opts.ocr_options = TesseractCliOcrOptions(mode=OcrMode.FULL_PAGE, lang=["deu"], path=str(TESSDATA))
|
||||
else: # docling, docling_ocrmypdf: Textschicht lesen
|
||||
opts.do_ocr = False
|
||||
return DocumentConverter(format_options={InputFormat.PDF: PdfFormatOption(pipeline_options=opts)})
|
||||
|
||||
|
||||
def ocrmypdf(pdf: Path) -> Path:
|
||||
"""Legt mit OCRmyPDF eine neue deutsche Textschicht an (überspringt Vorhandenes)."""
|
||||
OCRMYPDF_DIR.mkdir(parents=True, exist_ok=True)
|
||||
target = OCRMYPDF_DIR / pdf.name
|
||||
if not target.exists():
|
||||
subprocess.run(
|
||||
[str(Path(sys.executable).parent / "ocrmypdf"), "--force-ocr", "-l", "deu", "--deskew",
|
||||
"--output-type", "pdf", "--jobs", "4", "--quiet", str(pdf), str(target)],
|
||||
check=True)
|
||||
return target
|
||||
|
||||
|
||||
def main():
|
||||
variant = sys.argv[1] if len(sys.argv) > 1 else "docling_tess"
|
||||
if variant not in VARIANTS:
|
||||
sys.exit(f"Variante muss eine von {', '.join(VARIANTS)} sein")
|
||||
out_dir = OUT_ROOT / variant
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
pdfs = [p for p in sorted(PDF_DIR.glob("*.pdf")) if any(p.name.startswith(t) for t in TITLES)]
|
||||
conv = converter(variant)
|
||||
for i, pdf in enumerate(pdfs, 1):
|
||||
target = out_dir / f"{pdf.stem}.json"
|
||||
if target.exists():
|
||||
print(f"[{i}/{len(pdfs)}] {pdf.name}: vorhanden, übersprungen", flush=True)
|
||||
continue
|
||||
print(f"[{i}/{len(pdfs)}] {pdf.name} ...", end=" ", flush=True)
|
||||
t0 = time.time()
|
||||
try:
|
||||
source = ocrmypdf(pdf) if variant == "docling_ocrmypdf" else pdf
|
||||
doc = conv.convert(source).document
|
||||
except Exception as e: # ein kaputtes PDF soll den Lauf nicht abbrechen
|
||||
print(f"FEHLER: {e}", flush=True)
|
||||
continue
|
||||
doc.save_as_json(target)
|
||||
doc.save_as_markdown(out_dir / f"{pdf.stem}.md")
|
||||
print(f"fertig ({time.time() - t0:.0f} s)", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user