Skip to content

fix(indexer): honor preset parsing_strategy, lazy pools, pymupdf markdown, parse timeout - #582

Merged
Ahmath-Gadji merged 7 commits into
refactor/hexagonalfrom
fix/preset-parsing-strategy
Jun 29, 2026
Merged

fix(indexer): honor preset parsing_strategy, lazy pools, pymupdf markdown, parse timeout#582
Ahmath-Gadji merged 7 commits into
refactor/hexagonalfrom
fix/preset-parsing-strategy

Conversation

@andyne13

@andyne13 andyne13 commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Makes the indexing pipeline honor a partition preset's parsing_strategy and hardens the PDF parse stage. Four related parser fixes (entangled in shared files, so one PR; #575 is a separate commit).

Tests: strategy dispatch, lazy pool creation, pymupdf markdown mode, parse-timeout error. Refs #569 #570 #571 #575.

Summary by CodeRabbit

  • New Features / Configuration
    • Added parse_timeout (default 3600s) to bound per-file parsing, with PARSE_TIMEOUT environment override support.
    • Preset PDF parsing strategies can now be selected per document, with defaults able to inherit the deployment-wide choice.
  • Bug Fixes
    • Parsing timeouts now report clearer timeout errors (including filename) and consistently mark parse failures.
    • Improved robustness of worker pool startup by lazily creating missing pools/actors.
    • PDF markdown parsing no longer extracts or inlines embedded images.
  • Tests
    • Added/expanded unit coverage for strategy routing, lazy pool creation, and parse timeout behavior.

andyne13 added 2 commits June 26, 2026 13:33
…timeout

- #569 honor a preset's parsing_strategy: ParserDispatcher.for_pdf_strategy()
  + a parser_factory wired into the pipeline, so pymupdf/docling are reachable
  per preset instead of always using the global default backend.
- #570 build pymupdf in markdown mode with embed_images=False: structured text
  for the chunker, no base64 bloat / Milvus gRPC overflow.
- #571 bound the parse stage with loader.parse_timeout (PARSE_TIMEOUT, default
  3600s) so a wedged parse fails that file instead of hanging indexing.

Tests cover strategy dispatch, pymupdf markdown mode and the parse-timeout error.
…ackends (#575)

A per-preset parsing_strategy can select a backend that isn't the global
default, which bootstrap never pre-warmed, so the loader's get-only
ray.get_actor failed with 'Failed to look up actor'. Create the pool on first
use via get_or_create_actor, with get_if_exists=True for race-safe concurrent
creation. Covers DoclingPool and MarkerPool.
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds a configurable parse timeout, per-preset PDF parser strategy routing, lazy Ray actor creation for Docling and Marker pools, and PyMuPDF markdown parsing that omits embedded images. Tests were added or updated for timeout handling, routing, caching, and loader acquisition.

Changes

Indexing parse and PDF routing

Layer / File(s) Summary
Parse timeout config
conf/config.yaml, openrag/core/config/indexation.py, openrag/core/config/loader.py, openrag/services/workers/stages/parse.py, tests/unit/services/workers/stages/test_parse.py, tests/unit/services/workers/test_indexer_pool.py
loader.parse_timeout is added, mapped from PARSE_TIMEOUT, and used when parse-stage timeouts are raised.
Parser strategy config
openrag/core/config/indexation_pipeline.py, openrag/api/routers/admin/presets.py, openrag/services/orchestrators/preset_service.py
PARSING_STRATEGIES is exported, the preset config field becomes optional, the admin options endpoint reads the shared strategy list, and the default indexation preset omits an explicit parsing strategy.
PDF routing and pipeline wiring
openrag/services/workers/indexer_pool.py, openrag/services/workers/parsers/parser_dispatcher.py, openrag/services/workers/pipeline_builder.py, tests/unit/services/workers/parsers/test_parser_dispatcher.py, tests/unit/services/workers/test_indexer_pool.py
ParserDispatcher.for_pdf_strategy() adds strategy-specific PDF wrappers, IndexerPool builds and passes parser factories and parse timeouts into the pipeline, and IndexingPipeline._select_parser only applies the factory when a parsing strategy is set.
Lazy Ray pool creation
openrag/services/workers/bootstrap.py, openrag/services/workers/parsers/docling_workers.py, openrag/services/workers/parsers/marker_workers.py, tests/unit/services/workers/parsers/test_pool_loaders.py
get_if_exists=True is used when bootstrapping Ray actors, and Docling and Marker loaders now obtain detached pools lazily; tests cover the acquisition path.
PyMuPDF markdown output
openrag/core/indexing/parsers/pdf/pymupdf.py, tests/unit/services/workers/parsers/test_parser_dispatcher.py
_extract_markdown() stops embedding images and returns empty image lists, and the parser tests assert the markdown-mode backend shape.

Sequence Diagram(s)

sequenceDiagram
  participant IndexerPool
  participant ParserDispatcher
  participant _PdfStrategyParser
  participant PDFBackend
  IndexerPool->>ParserDispatcher: for_pdf_strategy(strategy)
  ParserDispatcher-->>IndexerPool: parser_factory
  IndexerPool->>_PdfStrategyParser: parse(document)
  _PdfStrategyParser->>PDFBackend: parse PDF document
  _PdfStrategyParser-->>IndexerPool: parsed result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • linagora/openrag#355: The PyMuPDF markdown change affects ProcessedDocument.images, which overlaps with the loader path modified in that PR.
  • linagora/openrag#513: Both PRs change ParserDispatcher PDF routing behavior and related backend selection logic.
  • linagora/openrag#519: Both PRs update admin preset option plumbing for PDF parsing strategies in openrag/api/routers/admin/presets.py.

Suggested labels

feat, fix, refactor

Suggested reviewers

  • hedhoud
  • Ahmath-Gadji

Poem

A bunny hops through parse and time,
With strategy paths in tidy rhyme.
No pooled-up waits, no image haze,
Just markdown trails and timeout days.
🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is specific and accurately captures the main indexing changes: preset parsing strategy, lazy pools, PyMuPDF markdown, and parse timeout.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/preset-parsing-strategy

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 added the fix Fix issue label Jun 26, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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 `@openrag/core/config/indexation.py`:
- Around line 181-186: The parse_timeout config in the indexing settings should
be validated as strictly positive instead of accepting 0 or negative values.
Update the config model in the parse_timeout field definition so invalid values
are rejected at load time before reaching asyncio.wait_for, keeping the existing
ParseTimeout/indexation settings behavior but preventing immediate failures for
every file.

In `@openrag/services/workers/stages/parse.py`:
- Around line 42-48: The TimeoutError handling in parse() is incorrectly
rewrapping internal parser timeouts and can crash when timeout is None. Update
the try/except around run_with_optional_timeout so it only converts a
TimeoutError into the filename-based message when an actual timeout bound was
applied; if timeout is None, let the parser’s own TimeoutError propagate
unchanged. Use the parse() function and the run_with_optional_timeout call as
the key locations, and avoid formatting timeout with {timeout:g} unless timeout
is guaranteed to be numeric.

In `@tests/unit/services/workers/parsers/test_parser_dispatcher.py`:
- Around line 144-150: The test for ParserDispatcher’s pymupdf backend only
verifies _mode is markdown and misses the no-images contract. Update
test_pymupdf_backend_builds_in_markdown_mode_without_images to also assert the
parser is configured with embed_images disabled, or verify a parse result from
the pymupdf parser keeps images empty. Use ParserDispatcher and the
_get("pymupdf") setup to locate the relevant assertion area.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f37128dd-4a69-4d59-9d89-68c2989dafdc

📥 Commits

Reviewing files that changed from the base of the PR and between 3cd9957 and 8fdf4a6.

📒 Files selected for processing (14)
  • conf/config.yaml
  • openrag/core/config/indexation.py
  • openrag/core/config/loader.py
  • openrag/core/indexing/parsers/pdf/pymupdf.py
  • openrag/services/workers/bootstrap.py
  • openrag/services/workers/indexer_pool.py
  • openrag/services/workers/parsers/docling_workers.py
  • openrag/services/workers/parsers/marker_workers.py
  • openrag/services/workers/parsers/parser_dispatcher.py
  • openrag/services/workers/stages/parse.py
  • tests/unit/services/workers/parsers/test_parser_dispatcher.py
  • tests/unit/services/workers/parsers/test_pool_loaders.py
  • tests/unit/services/workers/stages/test_parse.py
  • tests/unit/services/workers/test_indexer_pool.py

Comment thread openrag/core/config/indexation.py Outdated
Comment thread openrag/services/workers/stages/parse.py
Comment thread tests/unit/services/workers/parsers/test_parser_dispatcher.py
@andyne13

Copy link
Copy Markdown
Contributor Author

Testing independently

Unit: uv run pytest tests/unit/services/workers/parsers/ tests/unit/services/workers/stages/test_parse.py tests/unit/services/workers/test_indexer_pool.py

Functional (API/logs, no admin UI needed):

andyne13 added 5 commits June 26, 2026 14:13
It feeds asyncio.wait_for, so 0/negative would fail every parse immediately
instead of disabling the bound. Reject at config load (CodeRabbit #582).
…s set

When timeout is None the parse stage applies no asyncio.wait_for, so a
TimeoutError can only be internal to the parser. Re-raise it as-is instead
of relabeling it 'parse timed out after {timeout}s' (which also crashed
formatting {timeout:g} on None). Test covers the timeout=None path.
Addresses CodeRabbit #582.
Strengthen the pymupdf test beyond _mode: build a PDF that contains an image
and assert _extract_markdown returns no ImageBlocks and inlines no base64
data URI — catches a regression that re-enables embed_images. (CodeRabbit #582)
…set-parsing-strategy

# Conflicts:
#	openrag/services/workers/bootstrap.py
…cing marker

The default indexation preset hardcoded parsing_strategy="marker", and the new
per-preset parser_factory routed every PDF through that strategy — overriding the
operator's global file_loaders.pdf (PDFLoader) choice. On a pymupdf-configured
deployment this forced marker, lazily spinning up the Marker Ray pool and loading
models inside the indexer actor; on a GPU-less/CPU runner the parse stage never
completes and indexing hangs.

Make parsing_strategy optional: None now means "inherit the global PDFLoader".
The default preset omits it (so it follows the deployment's configured backend);
named presets (legal/finance) keep their explicit marker opt-in. _select_parser
defers to the global dispatcher when no strategy is set.
@coderabbitai coderabbitai Bot added feat Add a new feature refactor labels Jun 26, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
openrag/core/config/indexation_pipeline.py (1)

20-30: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep PDF parsing strategy values in one placePARSING_STRATEGIES, the Literal[...], and ParserDispatcher._PDF_BACKEND_NAMES can drift independently. Derive one from the other or add an import-time assertion so the accepted values stay aligned.

🤖 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 `@openrag/core/config/indexation_pipeline.py` around lines 20 - 30, The PDF
parsing strategy values are duplicated across PARSING_STRATEGIES,
IndexationPipelineConfig.parsing_strategy, and
ParserDispatcher._PDF_BACKEND_NAMES, so they can drift out of sync. Make the
accepted strategy list come from a single source of truth by deriving the
Literal-backed config and dispatcher names from PARSING_STRATEGIES, or add an
import-time assertion that compares IndexationPipelineConfig and
ParserDispatcher against it. Use the symbols PARSING_STRATEGIES,
IndexationPipelineConfig, parsing_strategy, and
ParserDispatcher._PDF_BACKEND_NAMES to keep the values aligned.
🤖 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.

Nitpick comments:
In `@openrag/core/config/indexation_pipeline.py`:
- Around line 20-30: The PDF parsing strategy values are duplicated across
PARSING_STRATEGIES, IndexationPipelineConfig.parsing_strategy, and
ParserDispatcher._PDF_BACKEND_NAMES, so they can drift out of sync. Make the
accepted strategy list come from a single source of truth by deriving the
Literal-backed config and dispatcher names from PARSING_STRATEGIES, or add an
import-time assertion that compares IndexationPipelineConfig and
ParserDispatcher against it. Use the symbols PARSING_STRATEGIES,
IndexationPipelineConfig, parsing_strategy, and
ParserDispatcher._PDF_BACKEND_NAMES to keep the values aligned.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 57588816-6f6e-45e4-a041-06dadfd4344d

📥 Commits

Reviewing files that changed from the base of the PR and between 0c644ae and 8d5fdf0.

📒 Files selected for processing (4)
  • openrag/api/routers/admin/presets.py
  • openrag/core/config/indexation_pipeline.py
  • openrag/services/orchestrators/preset_service.py
  • openrag/services/workers/pipeline_builder.py

Comment on lines +67 to +68
pages = [(chunk.get("text") or "").strip() for chunk in chunks]
return pages, []

@Ahmath-Gadji Ahmath-Gadji Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The issue with this implementation is that we're dropping embedded images, even though this parser is capable of handling them. Do we really want to do that?
We can avoid embedding images inside the markdown, but preserve ImageBlocks.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since pymupdf image cropping quality has not been validated and compared to other advanced parsers like (docling & marker), we've decided to remove the embedded images until that validation.

@Ahmath-Gadji

Copy link
Copy Markdown
Collaborator

Found while testing this PR: docling parses run CPU-only on a GPU host

Exercising the new per-preset parsing_strategy routing (this PR makes docling reachable per preset, #569), I indexed a PDF into a docling-preset partition on a GPU host — and the parse ran on CPU. Symptom: a flood of

[W…] NNPACK.cpp:57 Could not initialize NNPACK! Reason: Unsupported hardware.

and slow parses. nvidia-smi confirmed it — ray::MarkerWorker was on the GPU, but there was no ray::DoclingWorker process on the device.

Root cause: _docling_num_gpus gated the worker's GPU reservation on torch.cuda.is_available(), which is evaluated inside the GPU-less DoclingPool actor (Ray sets CUDA_VISIBLE_DEVICES="" for a 0-GPU actor) → always FalseDoclingWorker got num_gpus=0 → CUDA hidden → docling's AcceleratorDevice.AUTO fell back to CPU. Marker avoids this by gating on ray.cluster_resources()["GPU"] (cluster-wide, visible from a GPU-less pool).

It's a pre-existing docling bug that this PR surfaces (docling was rarely reached before per-preset routing). Fixed separately in #584, which makes _docling_num_gpus mirror _marker_num_gpus.

@Ahmath-Gadji Ahmath-Gadji left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM.
I've tested it and it works

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

Labels

feat Add a new feature fix Fix issue refactor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants