Skip to content

fix(docling): thread page_from/page_to into Docling Serve request - #17451

Open
Elyytscha wants to merge 2 commits into
infiniflow:mainfrom
Elyytscha:fix/docling-page-range
Open

fix(docling): thread page_from/page_to into Docling Serve request#17451
Elyytscha wants to merge 2 commits into
infiniflow:mainfrom
Elyytscha:fix/docling-page-range

Conversation

@Elyytscha

Copy link
Copy Markdown

Summary

rag/app/naive.py::by_docling accepts from_page/to_page and then calls
DoclingParser.parse_pdf() without them, and DoclingParser never sets
options.page_range on the docling-serve payload. So when RAGFlow splits a PDF
into task_page_size tasks, every task converts the whole document and
RAGFlow slices the result locally. The Page(145~157) prefix in the progress log
is RAGFlow's own task label, not what Docling was asked to convert.

Every sibling backend already forwards its range — by_deepdoc
(naive.py:119), by_mineru (naive.py:175-176, since #16957) and
by_mistral_ocr (naive.py:394-395). Docling is the one that drops it. This is
the Docling half of the same bug fixed for MinerU in #16957 (issues #16586,
#16971).

Repro steps

  1. Point RAGFlow at an external Docling Serve with DOCLING_SERVER_URL.
  2. Create a dataset with layout_recognize = "Docling".
  3. Upload a PDF longer than task_page_size (default 12) pages, so RAGFlow
    creates more than one task.
  4. Inspect the outgoing /v1/convert/source request, or the Docling Serve logs.

Every task posts the whole document with no page_range, and every task returns
the whole document's content.

Expected

Each task converts only its own pages. Docling Serve already supports this:
ConvertDocumentsOptions.page_range (docling.datamodel.service.options),
documented as "Only convert a range of pages. The page number starts at 1."
RAGFlow's from_page/to_page are 0-based with to_page exclusive (Python
slice stop); Docling's page_range is 1-based with both bounds inclusive.

No page-number rebasing is needed, unlike MinerU: Docling converts only the
requested pages but still numbers them from the start of the document
(docling/pipeline/base_pipeline.py builds Page(page_no=i + 1) from the
full-document index), so positions and citations stay correct. DoclingParser
was evidently written expecting a ranged response — it already keeps
page_from/page_to and offsets result page numbers by them
(docling_parser.py:212,286); only the wiring was missing.

Fix

  • Thread page_from/page_to through by_doclingparse_pdf
    _parse_pdf_remote in rag/app/naive.py and
    deepdoc/parser/docling_parser.py, clamping to_page to
    MAXIMUM_PAGE_NUMBER the way by_mineru does.
  • Translate the range once in DoclingParser._resolve_page_range, which returns
    None when the caller did not narrow the range, so an unsplit document sends
    byte-for-byte the request it sent before. It also returns None for a
    degenerate range: Docling's _validate_page_range rejects end < start, which
    would otherwise 422 every payload variant and turn a no-op task into a hard
    RuntimeError.
  • Set options.page_range on all four payload variants the fallback chain can
    reach (chunked/standard × v1/v1alpha).
  • Set page_range on the local DocumentConverter.convert() path too.
  • Log the resolved range, mirroring the MinerU fix.

self.page_from/self.page_to are deliberately left alone: the local path still
renders the whole document in __images__, and Docling returns absolute page
numbers, so _make_line_tag, crop and cropout_docling_table keep the page
arithmetic they have today.

Out of scope, mentioned for whoever hits them next: by_opendataloader and
by_tcadp have the same gap, DoclingParser.__init__'s hardcoded
request_timeout: int = 600 has no env override, and the local path still
renders every page of the document per task.

This overlaps the Docling part of #16857, which also fixes OpenDataLoader wiring
and MinerU and has been open since 13 July; its MinerU half now conflicts with
the merged #16957. This PR is the Docling-only slice, rebased onto the
page_from/page_to convention that #16957 established, so it can land
independently. Happy to close this in favour of that PR if it is revived.

Tests

Three regression tests in
test/unit_test/deepdoc/parser/test_docling_parser_remote.py, mirroring the ones
#16957 added for MinerU:

  • the conversion table for _resolve_page_range, including the un-narrowed and
    degenerate cases;
  • a task owning pages 145-157 puts "page_range": [145, 157] on every payload
    the fallback chain sends (both chunked attempts and the standard fallback);
  • a whole-document task sends no page_range at all.
uv run pytest test/unit_test/deepdoc/parser/test_docling_parser_remote.py

11 passed (8 existing + 3 new). ruff format --check clean on the three touched
files.

Verified in production on a 2383-page / 22.6 MB PDF against Docling Serve v1.21.0
with a vLLM-backed vlm pipeline: each task drops from 2383 pages / ~1432s —
which always hit RAGFlow's hardcoded 600s client timeout, so the ingest could
never finish — to 13 pages / ~10s, and Docling Serve stops being OOMKilled at
16 GiB.

Fixes #17450

by_docling accepted from_page/to_page and called DoclingParser.parse_pdf()
without them, and DoclingParser never set options.page_range on the payload.
Every task of a page-split PDF therefore converted the whole document and
RAGFlow sliced the result locally, so the work was repeated once per task and
each request was as slow and as memory-hungry as the entire file. The sibling
backends already forward their range: by_deepdoc, by_mineru (infiniflow#16957) and
by_mistral_ocr.

Thread page_from/page_to through by_docling -> parse_pdf -> _parse_pdf_remote,
and translate them once in _resolve_page_range: RAGFlow is 0-based with page_to
exclusive, Docling is 1-based with both bounds inclusive. The helper returns
None when the caller did not narrow the range, so an unsplit document sends the
request it sent before, and for a degenerate range, which Docling rejects with
end < start. page_range is set on all four payload variants the fallback chain
can reach, and on the local DocumentConverter.convert() path.

No page-number rebasing is needed, unlike MinerU: Docling converts only the
requested pages but still numbers them from the start of the document, so
page_from/page_to are deliberately left at their defaults and the existing
crop/_make_line_tag/cropout_docling_table arithmetic is unchanged.
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. 🐞 bug Something isn't working, pull request that fix bug. labels Jul 27, 2026
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 895cf9f1-528b-44d0-908d-7a9d79222bd0

📥 Commits

Reviewing files that changed from the base of the PR and between f8bd1fd and e3d6c63.

📒 Files selected for processing (2)
  • deepdoc/parser/docling_parser.py
  • test/unit_test/deepdoc/parser/test_docling_parser_remote.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • deepdoc/parser/docling_parser.py

📝 Walkthrough

Walkthrough

Docling parsing now accepts RAGFlow page bounds, translates them to Docling’s page convention, and forwards them through local conversion and remote fallback payloads. The Docling application caller now supplies task page bounds, with tests covering ranged and full-document requests.

Changes

Docling page-range handling

Layer / File(s) Summary
Range contract and local conversion
deepdoc/parser/docling_parser.py, rag/app/naive.py, test/unit_test/deepdoc/parser/test_docling_parser_remote.py
parse_pdf resolves zero-based exclusive bounds, limits local rendering and conversion to the selected pages, rebases line geometry, and by_docling forwards task bounds. Tests verify translation and local behavior.
Remote payload propagation
deepdoc/parser/docling_parser.py, test/unit_test/deepdoc/parser/test_docling_parser_remote.py
Remote standard and chunking payload variants include options.page_range during fallback attempts, while full-document requests omit it.
Page-range test support
test/unit_test/deepdoc/parser/test_docling_parser_remote.py
Test doubles and capture helpers validate rendered windows, conversion arguments, geometry offsets, and every remote fallback payload.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested labels: 🧪 test

Suggested reviewers: yurii214, euvre

Poem

A bunny hopped through pages bright,
And trimmed the PDF just right.
Zero starts became one-based lore,
Remote hops now skip the extra chore.
“No whole book!” the rabbit sings.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: forwarding page ranges into Docling Serve.
Description check ✅ Passed The PR description fits the template's Summary section and provides clear context, repro steps, fix details, and tests.
Linked Issues check ✅ Passed The changes forward task page ranges to Docling Serve and limit local conversion, matching #17450's objective.
Out of Scope Changes check ✅ Passed No clearly unrelated code changes stand out; the local-window and test updates support the Docling range fix.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
deepdoc/parser/docling_parser.py (1)

140-142: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove compatibility-history wording from the new range documentation.

  • deepdoc/parser/docling_parser.py#L140-L142: describe None as the full-document/degenerate-range behavior without referring to earlier requests.
  • deepdoc/parser/docling_parser.py#L454-L456: state that full-document requests omit page_range, without “byte-for-byte” history.
  • test/unit_test/deepdoc/parser/test_docling_parser_remote.py#L213-L215: describe the asserted omission behavior directly.

As per coding guidelines, “do not add compatibility wording to comments or documentation.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deepdoc/parser/docling_parser.py` around lines 140 - 142, Remove
compatibility-history wording from the range documentation and test description:
in deepdoc/parser/docling_parser.py lines 140-142, describe None as the
full-document or degenerate-range behavior; in deepdoc/parser/docling_parser.py
lines 454-456, state directly that full-document requests omit page_range; and
in test/unit_test/deepdoc/parser/test_docling_parser_remote.py lines 213-215,
describe the asserted omission behavior directly without referring to prior
requests or byte-for-byte compatibility.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@deepdoc/parser/docling_parser.py`:
- Line 655: Update the local-path image rendering call in __images__() to pass
page_range[0] - 1 and page_range[1] when a page range is selected, while
preserving default full-document bounds otherwise. Keep conv.convert() aligned
with the same range, ensure self.page_from reflects the narrowed image set, and
add a local-path test covering page-range rendering and alignment.
- Around line 134-148: Update _resolve_page_range to clamp page_from to the
valid nonnegative source range and page_to to MAXIMUM_PAGE_NUMBER before
converting to Docling’s 1-based inclusive bounds. Preserve the None behavior for
an unrestricted or empty range, and add assertions confirming the returned start
and end are valid 1-based bounds with the end not preceding the start.

---

Nitpick comments:
In `@deepdoc/parser/docling_parser.py`:
- Around line 140-142: Remove compatibility-history wording from the range
documentation and test description: in deepdoc/parser/docling_parser.py lines
140-142, describe None as the full-document or degenerate-range behavior; in
deepdoc/parser/docling_parser.py lines 454-456, state directly that
full-document requests omit page_range; and in
test/unit_test/deepdoc/parser/test_docling_parser_remote.py lines 213-215,
describe the asserted omission behavior directly without referring to prior
requests or byte-for-byte compatibility.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 315ab300-bbcd-4ace-9d03-14f9387ec8a3

📥 Commits

Reviewing files that changed from the base of the PR and between 8b793ee and f8bd1fd.

📒 Files selected for processing (3)
  • deepdoc/parser/docling_parser.py
  • rag/app/naive.py
  • test/unit_test/deepdoc/parser/test_docling_parser_remote.py

Comment thread deepdoc/parser/docling_parser.py Outdated
Comment thread deepdoc/parser/docling_parser.py
Follow-up to review feedback on the page-range change.

_resolve_page_range trusted its inputs, so a negative page_from produced a
0-based start, which Docling rejects outright. Clamp both bounds before
translating, since parse_pdf is public and its callers are not required to
pre-clamp.

The local path narrowed conv.convert() but still rasterised the whole document
in __images__, so a page-split task kept the full-document CPU and memory cost
and self.page_from did not describe the images it had rendered. Render the same
window the conversion covers.

That makes page_from non-zero for the first time, which _make_line_tag was not
written for: it indexed page_images with Docling's absolute page number, while
crop() adds page_from back when it turns a tag into a position. Emit
window-relative page numbers instead, matching what cropout_docling_table
already indexes by, so a tag and the page it names stay aligned.
@yingfeng yingfeng added the ci Continue Integration label Jul 28, 2026
@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.65%. Comparing base (8b793ee) to head (e3d6c63).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #17451      +/-   ##
==========================================
- Coverage   94.56%   90.65%   -3.91%     
==========================================
  Files          10       10              
  Lines         717      717              
  Branches      118      118              
==========================================
- Hits          678      650      -28     
- Misses         25       39      +14     
- Partials       14       28      +14     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐞 bug Something isn't working, pull request that fix bug. ci Continue Integration size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Docling parser ignores the task page range and converts the whole PDF on every task

2 participants