2025-01-21 20:52:28 +08:00
#
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
#
2024-08-15 09:17:36 +08:00
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
2025-01-21 20:52:28 +08:00
2024-11-14 17:13:48 +08:00
import logging
2025-03-13 18:48:32 +08:00
import re
2025-10-17 18:46:47 +08:00
import os
2025-03-13 18:48:32 +08:00
from functools import reduce
2024-08-15 09:17:36 +08:00
from io import BytesIO
from timeit import default_timer as timer
2025-03-13 18:48:32 +08:00
from docx import Document
2025-08-14 12:14:03 +08:00
from docx . opc . pkgreader import _SerializedRelationships , _SerializedRelationship
2026-01-07 15:08:17 +08:00
from docx . table import Table as DocxTable
from docx . text . paragraph import Paragraph
2025-08-14 12:14:03 +08:00
from docx . opc . oxml import parse_xml
2025-07-15 13:03:01 +08:00
from markdown import markdown
2025-03-13 18:48:32 +08:00
from PIL import Image
2025-11-25 19:54:06 +08:00
from common . token_utils import num_tokens_from_string
2025-03-13 18:48:32 +08:00
Fix: Remove hardcoded page limits causing parsing failures on large PDFs (>300 pages) (#14382)
### What problem does this PR solve?
Fixes #14196
## Problem
When using DeepDOC to parse large PDFs (over 1000 pages), the parser
silently truncated processing at 300 pages due to a hardcoded default
`page_to=299` in `RAGFlowPdfParser.__images__()`. This caused:
- **Errors** on pages beyond the limit
- **Poor image quality** as the parser attempted to compensate with
missing page data
- **Inconsistent chunk splitting** between full PDF imports and partial
imports
Additionally, the codebase scattered magic numbers (`299`, `600`,
`10000`, `100000`, `100000000`, `10000000000`, `10**9`) across 22 files
as sentinel values for "parse all pages", making future maintenance
error-prone.
## Root Cause
```python
# deepdoc/parser/pdf_parser.py (before)
def __images__(self, fnm, zoomin=3, page_from=0, page_to=299, callback=None):
# Only the first 300 pages were rendered; everything beyond was silently dropped
```
While most callers in `rag/app/*.py` correctly passed `to_page=100000`,
the base class `RAGFlowPdfParser.__call__()` and `parse_into_bboxes()`
invoked `__images__` **without** forwarding `page_from`/`page_to`,
falling back to the restrictive default of 299.
## Solution
### 1. Define constants in `common/constants.py`
```python
MAXIMUM_PAGE_NUMBER = 100000 # Used by the parsing layer
MAXIMUM_TASK_PAGE_NUMBER = MAXIMUM_PAGE_NUMBER * 1000 # Used by the task/DB layer
```
### 2. Replace all hardcoded sentinel values
| Layer | Files Changed | Old Values | New Value |
|---|---|---|---|
| **Deepdoc parsers** | `pdf_parser.py`, `mineru_parser.py`,
`docling_parser.py`, `opendataloader_parser.py`, `paddleocr_parser.py`,
`docx_parser.py` | `299`, `600`, `10**9`, `100000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Chunk parsers** | `naive.py`, `book.py`, `qa.py`, `one.py`,
`manual.py`, `paper.py`, `presentation.py`, `laws.py`, `resume.py`,
`email.py`, `table.py` | `100000`, `10000`, `10000000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Task/DB layer** | `db_models.py`, `task_service.py`,
`document_service.py`, `file_service.py` | `100000000` |
`MAXIMUM_TASK_PAGE_NUMBER` |
### 3. Fix `parse_into_bboxes()` missing parameters
Added `from_page`/`to_page` parameters to `parse_into_bboxes()` so that
the `rag/flow/parser/parser.py` DeepDOC path no longer falls back to the
restrictive default.
## Files Changed (22)
- `common/constants.py`
- `deepdoc/parser/pdf_parser.py`
- `deepdoc/parser/mineru_parser.py`
- `deepdoc/parser/docling_parser.py`
- `deepdoc/parser/opendataloader_parser.py`
- `deepdoc/parser/paddleocr_parser.py`
- `deepdoc/parser/docx_parser.py`
- `rag/app/naive.py`
- `rag/app/book.py`
- `rag/app/qa.py`
- `rag/app/one.py`
- `rag/app/manual.py`
- `rag/app/paper.py`
- `rag/app/presentation.py`
- `rag/app/laws.py`
- `rag/app/resume.py`
- `rag/app/email.py`
- `rag/app/table.py`
- `api/db/db_models.py`
- `api/db/services/task_service.py`
- `api/db/services/document_service.py`
- `api/db/services/file_service.py`
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
---------
Signed-off-by: noob <yixiao121314@outlook.com>
2026-04-27 06:57:20 +00:00
from common . constants import LLMType , MAXIMUM_PAGE_NUMBER
2025-03-18 14:52:20 +08:00
from api . db . services . llm_service import LLMBundle
2026-03-05 17:27:17 +08:00
from api . db . joint_services . tenant_model_service import get_model_config_by_type_and_name , get_tenant_default_model_by_type
2025-11-03 19:26:09 +08:00
from rag . utils . file_utils import extract_embed_file , extract_links_from_pdf , extract_links_from_docx , extract_html
2026-03-17 15:14:06 +03:00
from deepdoc . parser import DocxParser , EpubParser , ExcelParser , HtmlParser , JsonParser , MarkdownElementExtractor , MarkdownParser , PdfParser , TxtParser
2026-01-09 17:48:45 +08:00
from deepdoc . parser . figure_parser import VisionFigureParser , vision_figure_parser_docx_wrapper_naive , vision_figure_parser_pdf_wrapper
2025-03-20 10:48:38 +08:00
from deepdoc . parser . pdf_parser import PlainParser , VisionParser
2025-10-23 19:44:25 +08:00
from deepdoc . parser . docling_parser import DoclingParser
2025-10-27 15:14:58 +08:00
from deepdoc . parser . tcadp_parser import TCADPParser
2026-01-27 12:43:01 +08:00
from common . float_utils import normalize_overlapped_percent
2025-12-17 19:48:24 +08:00
from common . parser_config_utils import normalize_layout_recognizer
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
from common . text_utils import normalize_arabic_presentation_forms
2026-01-09 17:48:45 +08:00
from rag . nlp import (
concat_img ,
find_codec ,
naive_merge ,
naive_merge_with_images ,
naive_merge_docx ,
rag_tokenizer ,
tokenize_chunks ,
doc_tokenize_chunks_with_images ,
tokenize_table ,
append_context2table_image4pdf ,
tokenize_chunks_with_images ,
) # noqa: F401
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
def _normalize_section_text_for_rtl_presentation_forms ( sections ) :
if not sections :
return sections
normalized_sections = [ ]
for section in sections :
if isinstance ( section , tuple ) :
if not section :
normalized_sections . append ( section )
continue
text = section [ 0 ]
normalized_text = normalize_arabic_presentation_forms ( text )
normalized_sections . append ( ( normalized_text , * section [ 1 : ] ) )
continue
if isinstance ( section , list ) :
if not section :
normalized_sections . append ( section )
continue
text = section [ 0 ]
normalized_text = normalize_arabic_presentation_forms ( text )
normalized_sections . append ( [ normalized_text , * section [ 1 : ] ] )
continue
normalized_sections . append ( normalize_arabic_presentation_forms ( section ) )
return normalized_sections
Fix: Remove hardcoded page limits causing parsing failures on large PDFs (>300 pages) (#14382)
### What problem does this PR solve?
Fixes #14196
## Problem
When using DeepDOC to parse large PDFs (over 1000 pages), the parser
silently truncated processing at 300 pages due to a hardcoded default
`page_to=299` in `RAGFlowPdfParser.__images__()`. This caused:
- **Errors** on pages beyond the limit
- **Poor image quality** as the parser attempted to compensate with
missing page data
- **Inconsistent chunk splitting** between full PDF imports and partial
imports
Additionally, the codebase scattered magic numbers (`299`, `600`,
`10000`, `100000`, `100000000`, `10000000000`, `10**9`) across 22 files
as sentinel values for "parse all pages", making future maintenance
error-prone.
## Root Cause
```python
# deepdoc/parser/pdf_parser.py (before)
def __images__(self, fnm, zoomin=3, page_from=0, page_to=299, callback=None):
# Only the first 300 pages were rendered; everything beyond was silently dropped
```
While most callers in `rag/app/*.py` correctly passed `to_page=100000`,
the base class `RAGFlowPdfParser.__call__()` and `parse_into_bboxes()`
invoked `__images__` **without** forwarding `page_from`/`page_to`,
falling back to the restrictive default of 299.
## Solution
### 1. Define constants in `common/constants.py`
```python
MAXIMUM_PAGE_NUMBER = 100000 # Used by the parsing layer
MAXIMUM_TASK_PAGE_NUMBER = MAXIMUM_PAGE_NUMBER * 1000 # Used by the task/DB layer
```
### 2. Replace all hardcoded sentinel values
| Layer | Files Changed | Old Values | New Value |
|---|---|---|---|
| **Deepdoc parsers** | `pdf_parser.py`, `mineru_parser.py`,
`docling_parser.py`, `opendataloader_parser.py`, `paddleocr_parser.py`,
`docx_parser.py` | `299`, `600`, `10**9`, `100000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Chunk parsers** | `naive.py`, `book.py`, `qa.py`, `one.py`,
`manual.py`, `paper.py`, `presentation.py`, `laws.py`, `resume.py`,
`email.py`, `table.py` | `100000`, `10000`, `10000000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Task/DB layer** | `db_models.py`, `task_service.py`,
`document_service.py`, `file_service.py` | `100000000` |
`MAXIMUM_TASK_PAGE_NUMBER` |
### 3. Fix `parse_into_bboxes()` missing parameters
Added `from_page`/`to_page` parameters to `parse_into_bboxes()` so that
the `rag/flow/parser/parser.py` DeepDOC path no longer falls back to the
restrictive default.
## Files Changed (22)
- `common/constants.py`
- `deepdoc/parser/pdf_parser.py`
- `deepdoc/parser/mineru_parser.py`
- `deepdoc/parser/docling_parser.py`
- `deepdoc/parser/opendataloader_parser.py`
- `deepdoc/parser/paddleocr_parser.py`
- `deepdoc/parser/docx_parser.py`
- `rag/app/naive.py`
- `rag/app/book.py`
- `rag/app/qa.py`
- `rag/app/one.py`
- `rag/app/manual.py`
- `rag/app/paper.py`
- `rag/app/presentation.py`
- `rag/app/laws.py`
- `rag/app/resume.py`
- `rag/app/email.py`
- `rag/app/table.py`
- `api/db/db_models.py`
- `api/db/services/task_service.py`
- `api/db/services/document_service.py`
- `api/db/services/file_service.py`
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
---------
Signed-off-by: noob <yixiao121314@outlook.com>
2026-04-27 06:57:20 +00:00
def by_deepdoc ( filename , binary = None , from_page = 0 , to_page = MAXIMUM_PAGE_NUMBER , lang = " Chinese " , callback = None , pdf_cls = None , * * kwargs ) :
2025-11-05 13:00:42 +08:00
callback = callback
binary = binary
pdf_parser = pdf_cls ( ) if pdf_cls else Pdf ( )
2026-01-09 17:48:45 +08:00
sections , tables = pdf_parser ( filename if not binary else binary , from_page = from_page , to_page = to_page , callback = callback )
2025-11-06 15:20:35 +08:00
2026-01-05 09:55:43 +08:00
tables = vision_figure_parser_pdf_wrapper (
tbls = tables ,
sections = sections ,
callback = callback ,
* * kwargs ,
)
2025-11-05 13:00:42 +08:00
return sections , tables , pdf_parser
2025-12-17 19:48:24 +08:00
def by_mineru (
2026-01-09 17:48:45 +08:00
filename ,
binary = None ,
from_page = 0 ,
Fix: Remove hardcoded page limits causing parsing failures on large PDFs (>300 pages) (#14382)
### What problem does this PR solve?
Fixes #14196
## Problem
When using DeepDOC to parse large PDFs (over 1000 pages), the parser
silently truncated processing at 300 pages due to a hardcoded default
`page_to=299` in `RAGFlowPdfParser.__images__()`. This caused:
- **Errors** on pages beyond the limit
- **Poor image quality** as the parser attempted to compensate with
missing page data
- **Inconsistent chunk splitting** between full PDF imports and partial
imports
Additionally, the codebase scattered magic numbers (`299`, `600`,
`10000`, `100000`, `100000000`, `10000000000`, `10**9`) across 22 files
as sentinel values for "parse all pages", making future maintenance
error-prone.
## Root Cause
```python
# deepdoc/parser/pdf_parser.py (before)
def __images__(self, fnm, zoomin=3, page_from=0, page_to=299, callback=None):
# Only the first 300 pages were rendered; everything beyond was silently dropped
```
While most callers in `rag/app/*.py` correctly passed `to_page=100000`,
the base class `RAGFlowPdfParser.__call__()` and `parse_into_bboxes()`
invoked `__images__` **without** forwarding `page_from`/`page_to`,
falling back to the restrictive default of 299.
## Solution
### 1. Define constants in `common/constants.py`
```python
MAXIMUM_PAGE_NUMBER = 100000 # Used by the parsing layer
MAXIMUM_TASK_PAGE_NUMBER = MAXIMUM_PAGE_NUMBER * 1000 # Used by the task/DB layer
```
### 2. Replace all hardcoded sentinel values
| Layer | Files Changed | Old Values | New Value |
|---|---|---|---|
| **Deepdoc parsers** | `pdf_parser.py`, `mineru_parser.py`,
`docling_parser.py`, `opendataloader_parser.py`, `paddleocr_parser.py`,
`docx_parser.py` | `299`, `600`, `10**9`, `100000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Chunk parsers** | `naive.py`, `book.py`, `qa.py`, `one.py`,
`manual.py`, `paper.py`, `presentation.py`, `laws.py`, `resume.py`,
`email.py`, `table.py` | `100000`, `10000`, `10000000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Task/DB layer** | `db_models.py`, `task_service.py`,
`document_service.py`, `file_service.py` | `100000000` |
`MAXIMUM_TASK_PAGE_NUMBER` |
### 3. Fix `parse_into_bboxes()` missing parameters
Added `from_page`/`to_page` parameters to `parse_into_bboxes()` so that
the `rag/flow/parser/parser.py` DeepDOC path no longer falls back to the
restrictive default.
## Files Changed (22)
- `common/constants.py`
- `deepdoc/parser/pdf_parser.py`
- `deepdoc/parser/mineru_parser.py`
- `deepdoc/parser/docling_parser.py`
- `deepdoc/parser/opendataloader_parser.py`
- `deepdoc/parser/paddleocr_parser.py`
- `deepdoc/parser/docx_parser.py`
- `rag/app/naive.py`
- `rag/app/book.py`
- `rag/app/qa.py`
- `rag/app/one.py`
- `rag/app/manual.py`
- `rag/app/paper.py`
- `rag/app/presentation.py`
- `rag/app/laws.py`
- `rag/app/resume.py`
- `rag/app/email.py`
- `rag/app/table.py`
- `api/db/db_models.py`
- `api/db/services/task_service.py`
- `api/db/services/document_service.py`
- `api/db/services/file_service.py`
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
---------
Signed-off-by: noob <yixiao121314@outlook.com>
2026-04-27 06:57:20 +00:00
to_page = MAXIMUM_PAGE_NUMBER ,
2026-01-09 17:48:45 +08:00
lang = " Chinese " ,
callback = None ,
pdf_cls = None ,
parse_method : str = " raw " ,
mineru_llm_name : str | None = None ,
tenant_id : str | None = None ,
* * kwargs ,
2025-12-17 19:48:24 +08:00
) :
2025-12-09 18:54:14 +08:00
pdf_parser = None
if tenant_id :
if not mineru_llm_name :
try :
from api . db . services . tenant_llm_service import TenantLLMService
env_name = TenantLLMService . ensure_mineru_from_env ( tenant_id )
2025-12-11 17:33:12 +08:00
candidates = TenantLLMService . query ( tenant_id = tenant_id , llm_factory = " MinerU " , model_type = LLMType . OCR )
2025-12-09 18:54:14 +08:00
if candidates :
mineru_llm_name = candidates [ 0 ] . llm_name
elif env_name :
mineru_llm_name = env_name
except Exception as e : # best-effort fallback
logging . warning ( f " fallback to env mineru: { e } " )
if mineru_llm_name :
try :
2026-03-05 17:27:17 +08:00
ocr_model_config = get_model_config_by_type_and_name ( tenant_id , LLMType . OCR , mineru_llm_name )
ocr_model = LLMBundle ( tenant_id = tenant_id , model_config = ocr_model_config , lang = lang )
2025-12-09 18:54:14 +08:00
pdf_parser = ocr_model . mdl
sections , tables = pdf_parser . parse_pdf (
filepath = filename ,
binary = binary ,
callback = callback ,
parse_method = parse_method ,
2025-12-16 07:15:25 +02:00
lang = lang ,
2025-12-17 19:48:24 +08:00
* * kwargs ,
2025-12-09 18:54:14 +08:00
)
return sections , tables , pdf_parser
except Exception as e :
logging . error ( f " Failed to parse pdf via LLMBundle MinerU ( { mineru_llm_name } ): { e } " )
2025-11-05 13:00:42 +08:00
2025-12-09 18:54:14 +08:00
if callback :
callback ( - 1 , " MinerU not found. " )
return None , None , None
2025-11-05 13:00:42 +08:00
2025-12-16 07:15:25 +02:00
Fix: Remove hardcoded page limits causing parsing failures on large PDFs (>300 pages) (#14382)
### What problem does this PR solve?
Fixes #14196
## Problem
When using DeepDOC to parse large PDFs (over 1000 pages), the parser
silently truncated processing at 300 pages due to a hardcoded default
`page_to=299` in `RAGFlowPdfParser.__images__()`. This caused:
- **Errors** on pages beyond the limit
- **Poor image quality** as the parser attempted to compensate with
missing page data
- **Inconsistent chunk splitting** between full PDF imports and partial
imports
Additionally, the codebase scattered magic numbers (`299`, `600`,
`10000`, `100000`, `100000000`, `10000000000`, `10**9`) across 22 files
as sentinel values for "parse all pages", making future maintenance
error-prone.
## Root Cause
```python
# deepdoc/parser/pdf_parser.py (before)
def __images__(self, fnm, zoomin=3, page_from=0, page_to=299, callback=None):
# Only the first 300 pages were rendered; everything beyond was silently dropped
```
While most callers in `rag/app/*.py` correctly passed `to_page=100000`,
the base class `RAGFlowPdfParser.__call__()` and `parse_into_bboxes()`
invoked `__images__` **without** forwarding `page_from`/`page_to`,
falling back to the restrictive default of 299.
## Solution
### 1. Define constants in `common/constants.py`
```python
MAXIMUM_PAGE_NUMBER = 100000 # Used by the parsing layer
MAXIMUM_TASK_PAGE_NUMBER = MAXIMUM_PAGE_NUMBER * 1000 # Used by the task/DB layer
```
### 2. Replace all hardcoded sentinel values
| Layer | Files Changed | Old Values | New Value |
|---|---|---|---|
| **Deepdoc parsers** | `pdf_parser.py`, `mineru_parser.py`,
`docling_parser.py`, `opendataloader_parser.py`, `paddleocr_parser.py`,
`docx_parser.py` | `299`, `600`, `10**9`, `100000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Chunk parsers** | `naive.py`, `book.py`, `qa.py`, `one.py`,
`manual.py`, `paper.py`, `presentation.py`, `laws.py`, `resume.py`,
`email.py`, `table.py` | `100000`, `10000`, `10000000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Task/DB layer** | `db_models.py`, `task_service.py`,
`document_service.py`, `file_service.py` | `100000000` |
`MAXIMUM_TASK_PAGE_NUMBER` |
### 3. Fix `parse_into_bboxes()` missing parameters
Added `from_page`/`to_page` parameters to `parse_into_bboxes()` so that
the `rag/flow/parser/parser.py` DeepDOC path no longer falls back to the
restrictive default.
## Files Changed (22)
- `common/constants.py`
- `deepdoc/parser/pdf_parser.py`
- `deepdoc/parser/mineru_parser.py`
- `deepdoc/parser/docling_parser.py`
- `deepdoc/parser/opendataloader_parser.py`
- `deepdoc/parser/paddleocr_parser.py`
- `deepdoc/parser/docx_parser.py`
- `rag/app/naive.py`
- `rag/app/book.py`
- `rag/app/qa.py`
- `rag/app/one.py`
- `rag/app/manual.py`
- `rag/app/paper.py`
- `rag/app/presentation.py`
- `rag/app/laws.py`
- `rag/app/resume.py`
- `rag/app/email.py`
- `rag/app/table.py`
- `api/db/db_models.py`
- `api/db/services/task_service.py`
- `api/db/services/document_service.py`
- `api/db/services/file_service.py`
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
---------
Signed-off-by: noob <yixiao121314@outlook.com>
2026-04-27 06:57:20 +00:00
def by_docling ( filename , binary = None , from_page = 0 , to_page = MAXIMUM_PAGE_NUMBER , lang = " Chinese " , callback = None , pdf_cls = None , * * kwargs ) :
2025-11-05 13:00:42 +08:00
pdf_parser = DoclingParser ( )
2025-11-20 19:07:17 +08:00
parse_method = kwargs . get ( " parse_method " , " raw " )
2025-11-05 13:00:42 +08:00
if not pdf_parser . check_installation ( ) :
2026-03-12 18:09:03 +09:00
if callback :
callback ( - 1 , " Docling not found. " )
2025-11-10 15:08:24 +08:00
return None , None , pdf_parser
2025-11-05 13:00:42 +08:00
sections , tables = pdf_parser . parse_pdf (
filepath = filename ,
binary = binary ,
callback = callback ,
2026-03-12 18:09:03 +09:00
output_dir = os . environ . get ( " DOCLING_OUTPUT_DIR " , " " ) ,
delete_output = bool ( int ( os . environ . get ( " DOCLING_DELETE_OUTPUT " , 1 ) ) ) ,
docling_server_url = os . environ . get ( " DOCLING_SERVER_URL " , " " ) ,
2026-01-09 17:48:45 +08:00
parse_method = parse_method ,
2025-11-05 13:00:42 +08:00
)
return sections , tables , pdf_parser
Feat: add OpenDataLoader PDF parser backend (#14058) (#14097)
### What problem does this PR solve?
Closes #14058.
RAGFlow supports multiple PDF parsing backends (DeepDOC, MinerU,
Docling, TCADP, PaddleOCR). This PR adds **OpenDataLoader**
([opendataloader-project/opendataloader-pdf](https://github.com/opendataloader-project/opendataloader-pdf))
as a new optional backend, giving users a deterministic, local-first
alternative with competitive table extraction accuracy.
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
- [x] Documentation Update
---
### Changes
#### Backend
- `deepdoc/parser/opendataloader_parser.py` — new `OpenDataLoaderParser`
class inheriting `RAGFlowPdfParser`. Implements `check_installation()`
(guards Python package + Java 11+ runtime), `parse_pdf()` with
JSON-first extraction (heading/paragraph/table/list/image/formula) and
Markdown fallback, position-tag generation compatible with the shared
`@@page\tx0\tx1\ty0\ty1##` format, and temp-dir lifecycle with cleanup.
- `rag/app/naive.py` — new `by_opendataloader()` wrapper, registered in
`PARSERS` dict, added to `chunk_token_num=0` override list.
- `rag/flow/parser/parser.py` — `"opendataloader"` branch in the
pipeline PDF handler + check validation list.
#### Infrastructure
- `docker/entrypoint.sh` — `ensure_opendataloader()` function: opt-in
via `USE_OPENDATALOADER=true`, skips gracefully if Java is not on PATH.
#### Frontend
- `web/src/components/layout-recognize-form-field.tsx` —
`OpenDataLoader` added to `ParseDocumentType` enum and parser dropdown.
Cascades automatically to the pipeline editor's Parser component.
#### Docs
- `docs/guides/dataset/select_pdf_parser.md` — added OpenDataLoader
entry and full env-var reference.
---
### Environment variables
| Variable | Default | Description |
|---|---|---|
| `USE_OPENDATALOADER` | `false` | Set `true` to install
`opendataloader-pdf` on container startup |
| `OPENDATALOADER_VERSION` | latest | Pin the PyPI release (e.g.
`==2.2.1`) |
| `OPENDATALOADER_HYBRID` | _(unset)_ | Enable hybrid AI mode (e.g.
`docling-fast`) |
| `OPENDATALOADER_IMAGE_OUTPUT` | _(unset)_ | `off` / `embedded` /
`external` |
| `OPENDATALOADER_OUTPUT_DIR` | _(tmp)_ | Persistent output dir; temp
dir used + cleaned if unset |
| `OPENDATALOADER_DELETE_OUTPUT` | `1` | `0` to retain intermediate
files for debugging |
| `OPENDATALOADER_SANITIZE` | _(unset)_ | `1` to filter prompt-injection
patterns from output |
---
### Dependencies
- **Runtime**: `opendataloader-pdf` (PyPI, Apache 2.0) — opt-in, not
added to `pyproject.toml` core deps. Installed by
`ensure_opendataloader()` at container startup when
`USE_OPENDATALOADER=true`.
- **System**: Java 11+ on PATH (JVM is the underlying engine). The
installer skips with a warning if `java` is not found.
---
### How to test
**Standalone parser:**
```bash
source .venv/bin/activate
uv pip install opendataloader-pdf
python3 -c "
import sys; sys.path.insert(0, '.')
from deepdoc.parser.opendataloader_parser import OpenDataLoaderParser
p = OpenDataLoaderParser()
print('available:', p.check_installation())
s, t = p.parse_pdf('path/to/test.pdf', parse_method='pipeline')
print(f'sections={len(s)} tables={len(t)}')
"
```
### Benchmark vs Docling
```
file parser secs sections tables
----------------------------------------------------------------------
text-heavy.pdf docling 45.29 148 10
text-heavy.pdf opendataloader 3.14 559 0
table-heavy.pdf docling 7.05 76 3
table-heavy.pdf opendataloader 3.71 90 0
complex.pdf docling 42.67 114 8
complex.pdf opendataloader 3.51 180 0
```
2026-04-24 18:33:02 +02:00
def by_opendataloader (
filename ,
binary = None ,
from_page = 0 ,
Fix: Remove hardcoded page limits causing parsing failures on large PDFs (>300 pages) (#14382)
### What problem does this PR solve?
Fixes #14196
## Problem
When using DeepDOC to parse large PDFs (over 1000 pages), the parser
silently truncated processing at 300 pages due to a hardcoded default
`page_to=299` in `RAGFlowPdfParser.__images__()`. This caused:
- **Errors** on pages beyond the limit
- **Poor image quality** as the parser attempted to compensate with
missing page data
- **Inconsistent chunk splitting** between full PDF imports and partial
imports
Additionally, the codebase scattered magic numbers (`299`, `600`,
`10000`, `100000`, `100000000`, `10000000000`, `10**9`) across 22 files
as sentinel values for "parse all pages", making future maintenance
error-prone.
## Root Cause
```python
# deepdoc/parser/pdf_parser.py (before)
def __images__(self, fnm, zoomin=3, page_from=0, page_to=299, callback=None):
# Only the first 300 pages were rendered; everything beyond was silently dropped
```
While most callers in `rag/app/*.py` correctly passed `to_page=100000`,
the base class `RAGFlowPdfParser.__call__()` and `parse_into_bboxes()`
invoked `__images__` **without** forwarding `page_from`/`page_to`,
falling back to the restrictive default of 299.
## Solution
### 1. Define constants in `common/constants.py`
```python
MAXIMUM_PAGE_NUMBER = 100000 # Used by the parsing layer
MAXIMUM_TASK_PAGE_NUMBER = MAXIMUM_PAGE_NUMBER * 1000 # Used by the task/DB layer
```
### 2. Replace all hardcoded sentinel values
| Layer | Files Changed | Old Values | New Value |
|---|---|---|---|
| **Deepdoc parsers** | `pdf_parser.py`, `mineru_parser.py`,
`docling_parser.py`, `opendataloader_parser.py`, `paddleocr_parser.py`,
`docx_parser.py` | `299`, `600`, `10**9`, `100000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Chunk parsers** | `naive.py`, `book.py`, `qa.py`, `one.py`,
`manual.py`, `paper.py`, `presentation.py`, `laws.py`, `resume.py`,
`email.py`, `table.py` | `100000`, `10000`, `10000000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Task/DB layer** | `db_models.py`, `task_service.py`,
`document_service.py`, `file_service.py` | `100000000` |
`MAXIMUM_TASK_PAGE_NUMBER` |
### 3. Fix `parse_into_bboxes()` missing parameters
Added `from_page`/`to_page` parameters to `parse_into_bboxes()` so that
the `rag/flow/parser/parser.py` DeepDOC path no longer falls back to the
restrictive default.
## Files Changed (22)
- `common/constants.py`
- `deepdoc/parser/pdf_parser.py`
- `deepdoc/parser/mineru_parser.py`
- `deepdoc/parser/docling_parser.py`
- `deepdoc/parser/opendataloader_parser.py`
- `deepdoc/parser/paddleocr_parser.py`
- `deepdoc/parser/docx_parser.py`
- `rag/app/naive.py`
- `rag/app/book.py`
- `rag/app/qa.py`
- `rag/app/one.py`
- `rag/app/manual.py`
- `rag/app/paper.py`
- `rag/app/presentation.py`
- `rag/app/laws.py`
- `rag/app/resume.py`
- `rag/app/email.py`
- `rag/app/table.py`
- `api/db/db_models.py`
- `api/db/services/task_service.py`
- `api/db/services/document_service.py`
- `api/db/services/file_service.py`
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
---------
Signed-off-by: noob <yixiao121314@outlook.com>
2026-04-27 06:57:20 +00:00
to_page = MAXIMUM_PAGE_NUMBER ,
Feat: add OpenDataLoader PDF parser backend (#14058) (#14097)
### What problem does this PR solve?
Closes #14058.
RAGFlow supports multiple PDF parsing backends (DeepDOC, MinerU,
Docling, TCADP, PaddleOCR). This PR adds **OpenDataLoader**
([opendataloader-project/opendataloader-pdf](https://github.com/opendataloader-project/opendataloader-pdf))
as a new optional backend, giving users a deterministic, local-first
alternative with competitive table extraction accuracy.
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
- [x] Documentation Update
---
### Changes
#### Backend
- `deepdoc/parser/opendataloader_parser.py` — new `OpenDataLoaderParser`
class inheriting `RAGFlowPdfParser`. Implements `check_installation()`
(guards Python package + Java 11+ runtime), `parse_pdf()` with
JSON-first extraction (heading/paragraph/table/list/image/formula) and
Markdown fallback, position-tag generation compatible with the shared
`@@page\tx0\tx1\ty0\ty1##` format, and temp-dir lifecycle with cleanup.
- `rag/app/naive.py` — new `by_opendataloader()` wrapper, registered in
`PARSERS` dict, added to `chunk_token_num=0` override list.
- `rag/flow/parser/parser.py` — `"opendataloader"` branch in the
pipeline PDF handler + check validation list.
#### Infrastructure
- `docker/entrypoint.sh` — `ensure_opendataloader()` function: opt-in
via `USE_OPENDATALOADER=true`, skips gracefully if Java is not on PATH.
#### Frontend
- `web/src/components/layout-recognize-form-field.tsx` —
`OpenDataLoader` added to `ParseDocumentType` enum and parser dropdown.
Cascades automatically to the pipeline editor's Parser component.
#### Docs
- `docs/guides/dataset/select_pdf_parser.md` — added OpenDataLoader
entry and full env-var reference.
---
### Environment variables
| Variable | Default | Description |
|---|---|---|
| `USE_OPENDATALOADER` | `false` | Set `true` to install
`opendataloader-pdf` on container startup |
| `OPENDATALOADER_VERSION` | latest | Pin the PyPI release (e.g.
`==2.2.1`) |
| `OPENDATALOADER_HYBRID` | _(unset)_ | Enable hybrid AI mode (e.g.
`docling-fast`) |
| `OPENDATALOADER_IMAGE_OUTPUT` | _(unset)_ | `off` / `embedded` /
`external` |
| `OPENDATALOADER_OUTPUT_DIR` | _(tmp)_ | Persistent output dir; temp
dir used + cleaned if unset |
| `OPENDATALOADER_DELETE_OUTPUT` | `1` | `0` to retain intermediate
files for debugging |
| `OPENDATALOADER_SANITIZE` | _(unset)_ | `1` to filter prompt-injection
patterns from output |
---
### Dependencies
- **Runtime**: `opendataloader-pdf` (PyPI, Apache 2.0) — opt-in, not
added to `pyproject.toml` core deps. Installed by
`ensure_opendataloader()` at container startup when
`USE_OPENDATALOADER=true`.
- **System**: Java 11+ on PATH (JVM is the underlying engine). The
installer skips with a warning if `java` is not found.
---
### How to test
**Standalone parser:**
```bash
source .venv/bin/activate
uv pip install opendataloader-pdf
python3 -c "
import sys; sys.path.insert(0, '.')
from deepdoc.parser.opendataloader_parser import OpenDataLoaderParser
p = OpenDataLoaderParser()
print('available:', p.check_installation())
s, t = p.parse_pdf('path/to/test.pdf', parse_method='pipeline')
print(f'sections={len(s)} tables={len(t)}')
"
```
### Benchmark vs Docling
```
file parser secs sections tables
----------------------------------------------------------------------
text-heavy.pdf docling 45.29 148 10
text-heavy.pdf opendataloader 3.14 559 0
table-heavy.pdf docling 7.05 76 3
table-heavy.pdf opendataloader 3.71 90 0
complex.pdf docling 42.67 114 8
complex.pdf opendataloader 3.51 180 0
```
2026-04-24 18:33:02 +02:00
lang = " Chinese " ,
callback = None ,
pdf_cls = None ,
parse_method : str = " raw " ,
opendataloader_llm_name : str | None = None ,
tenant_id : str | None = None ,
* * kwargs ,
) :
if tenant_id :
if not opendataloader_llm_name :
try :
from api . db . services . tenant_llm_service import TenantLLMService
env_name = TenantLLMService . ensure_opendataloader_from_env ( tenant_id )
candidates = TenantLLMService . query ( tenant_id = tenant_id , llm_factory = " OpenDataLoader " , model_type = LLMType . OCR )
if candidates :
opendataloader_llm_name = candidates [ 0 ] . llm_name
elif env_name :
opendataloader_llm_name = env_name
except Exception as e :
logging . warning ( f " fallback to env opendataloader: { e } " )
if opendataloader_llm_name :
try :
ocr_model_config = get_model_config_by_type_and_name ( tenant_id , LLMType . OCR , opendataloader_llm_name )
ocr_model = LLMBundle ( tenant_id = tenant_id , model_config = ocr_model_config , lang = lang )
pdf_parser = ocr_model . mdl
sections , tables = pdf_parser . parse_pdf (
filepath = filename ,
binary = binary ,
callback = callback ,
parse_method = parse_method ,
* * kwargs ,
)
return sections , tables , pdf_parser
except Exception as e :
logging . error ( f " Failed to parse pdf via LLMBundle OpenDataLoader ( { opendataloader_llm_name } ): { e } " )
if callback :
callback ( - 1 , " OpenDataLoader not found. " )
return None , None , None
Fix: Remove hardcoded page limits causing parsing failures on large PDFs (>300 pages) (#14382)
### What problem does this PR solve?
Fixes #14196
## Problem
When using DeepDOC to parse large PDFs (over 1000 pages), the parser
silently truncated processing at 300 pages due to a hardcoded default
`page_to=299` in `RAGFlowPdfParser.__images__()`. This caused:
- **Errors** on pages beyond the limit
- **Poor image quality** as the parser attempted to compensate with
missing page data
- **Inconsistent chunk splitting** between full PDF imports and partial
imports
Additionally, the codebase scattered magic numbers (`299`, `600`,
`10000`, `100000`, `100000000`, `10000000000`, `10**9`) across 22 files
as sentinel values for "parse all pages", making future maintenance
error-prone.
## Root Cause
```python
# deepdoc/parser/pdf_parser.py (before)
def __images__(self, fnm, zoomin=3, page_from=0, page_to=299, callback=None):
# Only the first 300 pages were rendered; everything beyond was silently dropped
```
While most callers in `rag/app/*.py` correctly passed `to_page=100000`,
the base class `RAGFlowPdfParser.__call__()` and `parse_into_bboxes()`
invoked `__images__` **without** forwarding `page_from`/`page_to`,
falling back to the restrictive default of 299.
## Solution
### 1. Define constants in `common/constants.py`
```python
MAXIMUM_PAGE_NUMBER = 100000 # Used by the parsing layer
MAXIMUM_TASK_PAGE_NUMBER = MAXIMUM_PAGE_NUMBER * 1000 # Used by the task/DB layer
```
### 2. Replace all hardcoded sentinel values
| Layer | Files Changed | Old Values | New Value |
|---|---|---|---|
| **Deepdoc parsers** | `pdf_parser.py`, `mineru_parser.py`,
`docling_parser.py`, `opendataloader_parser.py`, `paddleocr_parser.py`,
`docx_parser.py` | `299`, `600`, `10**9`, `100000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Chunk parsers** | `naive.py`, `book.py`, `qa.py`, `one.py`,
`manual.py`, `paper.py`, `presentation.py`, `laws.py`, `resume.py`,
`email.py`, `table.py` | `100000`, `10000`, `10000000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Task/DB layer** | `db_models.py`, `task_service.py`,
`document_service.py`, `file_service.py` | `100000000` |
`MAXIMUM_TASK_PAGE_NUMBER` |
### 3. Fix `parse_into_bboxes()` missing parameters
Added `from_page`/`to_page` parameters to `parse_into_bboxes()` so that
the `rag/flow/parser/parser.py` DeepDOC path no longer falls back to the
restrictive default.
## Files Changed (22)
- `common/constants.py`
- `deepdoc/parser/pdf_parser.py`
- `deepdoc/parser/mineru_parser.py`
- `deepdoc/parser/docling_parser.py`
- `deepdoc/parser/opendataloader_parser.py`
- `deepdoc/parser/paddleocr_parser.py`
- `deepdoc/parser/docx_parser.py`
- `rag/app/naive.py`
- `rag/app/book.py`
- `rag/app/qa.py`
- `rag/app/one.py`
- `rag/app/manual.py`
- `rag/app/paper.py`
- `rag/app/presentation.py`
- `rag/app/laws.py`
- `rag/app/resume.py`
- `rag/app/email.py`
- `rag/app/table.py`
- `api/db/db_models.py`
- `api/db/services/task_service.py`
- `api/db/services/document_service.py`
- `api/db/services/file_service.py`
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
---------
Signed-off-by: noob <yixiao121314@outlook.com>
2026-04-27 06:57:20 +00:00
def by_tcadp ( filename , binary = None , from_page = 0 , to_page = MAXIMUM_PAGE_NUMBER , lang = " Chinese " , callback = None , pdf_cls = None , * * kwargs ) :
2025-11-05 13:00:42 +08:00
tcadp_parser = TCADPParser ( )
if not tcadp_parser . check_installation ( ) :
callback ( - 1 , " TCADP parser not available. Please check Tencent Cloud API configuration. " )
2025-11-10 15:08:24 +08:00
return None , None , tcadp_parser
2025-11-05 13:00:42 +08:00
2026-01-09 17:48:45 +08:00
sections , tables = tcadp_parser . parse_pdf ( filepath = filename , binary = binary , callback = callback , output_dir = os . environ . get ( " TCADP_OUTPUT_DIR " , " " ) , file_type = " PDF " )
2025-11-05 13:00:42 +08:00
return sections , tables , tcadp_parser
2026-01-09 17:48:45 +08:00
def by_paddleocr (
filename ,
binary = None ,
from_page = 0 ,
Fix: Remove hardcoded page limits causing parsing failures on large PDFs (>300 pages) (#14382)
### What problem does this PR solve?
Fixes #14196
## Problem
When using DeepDOC to parse large PDFs (over 1000 pages), the parser
silently truncated processing at 300 pages due to a hardcoded default
`page_to=299` in `RAGFlowPdfParser.__images__()`. This caused:
- **Errors** on pages beyond the limit
- **Poor image quality** as the parser attempted to compensate with
missing page data
- **Inconsistent chunk splitting** between full PDF imports and partial
imports
Additionally, the codebase scattered magic numbers (`299`, `600`,
`10000`, `100000`, `100000000`, `10000000000`, `10**9`) across 22 files
as sentinel values for "parse all pages", making future maintenance
error-prone.
## Root Cause
```python
# deepdoc/parser/pdf_parser.py (before)
def __images__(self, fnm, zoomin=3, page_from=0, page_to=299, callback=None):
# Only the first 300 pages were rendered; everything beyond was silently dropped
```
While most callers in `rag/app/*.py` correctly passed `to_page=100000`,
the base class `RAGFlowPdfParser.__call__()` and `parse_into_bboxes()`
invoked `__images__` **without** forwarding `page_from`/`page_to`,
falling back to the restrictive default of 299.
## Solution
### 1. Define constants in `common/constants.py`
```python
MAXIMUM_PAGE_NUMBER = 100000 # Used by the parsing layer
MAXIMUM_TASK_PAGE_NUMBER = MAXIMUM_PAGE_NUMBER * 1000 # Used by the task/DB layer
```
### 2. Replace all hardcoded sentinel values
| Layer | Files Changed | Old Values | New Value |
|---|---|---|---|
| **Deepdoc parsers** | `pdf_parser.py`, `mineru_parser.py`,
`docling_parser.py`, `opendataloader_parser.py`, `paddleocr_parser.py`,
`docx_parser.py` | `299`, `600`, `10**9`, `100000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Chunk parsers** | `naive.py`, `book.py`, `qa.py`, `one.py`,
`manual.py`, `paper.py`, `presentation.py`, `laws.py`, `resume.py`,
`email.py`, `table.py` | `100000`, `10000`, `10000000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Task/DB layer** | `db_models.py`, `task_service.py`,
`document_service.py`, `file_service.py` | `100000000` |
`MAXIMUM_TASK_PAGE_NUMBER` |
### 3. Fix `parse_into_bboxes()` missing parameters
Added `from_page`/`to_page` parameters to `parse_into_bboxes()` so that
the `rag/flow/parser/parser.py` DeepDOC path no longer falls back to the
restrictive default.
## Files Changed (22)
- `common/constants.py`
- `deepdoc/parser/pdf_parser.py`
- `deepdoc/parser/mineru_parser.py`
- `deepdoc/parser/docling_parser.py`
- `deepdoc/parser/opendataloader_parser.py`
- `deepdoc/parser/paddleocr_parser.py`
- `deepdoc/parser/docx_parser.py`
- `rag/app/naive.py`
- `rag/app/book.py`
- `rag/app/qa.py`
- `rag/app/one.py`
- `rag/app/manual.py`
- `rag/app/paper.py`
- `rag/app/presentation.py`
- `rag/app/laws.py`
- `rag/app/resume.py`
- `rag/app/email.py`
- `rag/app/table.py`
- `api/db/db_models.py`
- `api/db/services/task_service.py`
- `api/db/services/document_service.py`
- `api/db/services/file_service.py`
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
---------
Signed-off-by: noob <yixiao121314@outlook.com>
2026-04-27 06:57:20 +00:00
to_page = MAXIMUM_PAGE_NUMBER ,
2026-01-09 17:48:45 +08:00
lang = " Chinese " ,
callback = None ,
pdf_cls = None ,
parse_method : str = " raw " ,
paddleocr_llm_name : str | None = None ,
tenant_id : str | None = None ,
* * kwargs ,
) :
pdf_parser = None
if tenant_id :
if not paddleocr_llm_name :
try :
from api . db . services . tenant_llm_service import TenantLLMService
env_name = TenantLLMService . ensure_paddleocr_from_env ( tenant_id )
candidates = TenantLLMService . query ( tenant_id = tenant_id , llm_factory = " PaddleOCR " , model_type = LLMType . OCR )
if candidates :
paddleocr_llm_name = candidates [ 0 ] . llm_name
elif env_name :
paddleocr_llm_name = env_name
except Exception as e : # best-effort fallback
logging . warning ( f " fallback to env paddleocr: { e } " )
if paddleocr_llm_name :
try :
2026-03-05 17:27:17 +08:00
ocr_model_config = get_model_config_by_type_and_name ( tenant_id , LLMType . OCR , paddleocr_llm_name )
ocr_model = LLMBundle ( tenant_id = tenant_id , model_config = ocr_model_config , lang = lang )
2026-01-09 17:48:45 +08:00
pdf_parser = ocr_model . mdl
sections , tables = pdf_parser . parse_pdf (
filepath = filename ,
binary = binary ,
callback = callback ,
parse_method = parse_method ,
* * kwargs ,
)
return sections , tables , pdf_parser
except Exception as e :
logging . error ( f " Failed to parse pdf via LLMBundle PaddleOCR ( { paddleocr_llm_name } ): { e } " )
return None , None , None
if callback :
callback ( - 1 , " PaddleOCR not found. " )
return None , None , None
Fix: Remove hardcoded page limits causing parsing failures on large PDFs (>300 pages) (#14382)
### What problem does this PR solve?
Fixes #14196
## Problem
When using DeepDOC to parse large PDFs (over 1000 pages), the parser
silently truncated processing at 300 pages due to a hardcoded default
`page_to=299` in `RAGFlowPdfParser.__images__()`. This caused:
- **Errors** on pages beyond the limit
- **Poor image quality** as the parser attempted to compensate with
missing page data
- **Inconsistent chunk splitting** between full PDF imports and partial
imports
Additionally, the codebase scattered magic numbers (`299`, `600`,
`10000`, `100000`, `100000000`, `10000000000`, `10**9`) across 22 files
as sentinel values for "parse all pages", making future maintenance
error-prone.
## Root Cause
```python
# deepdoc/parser/pdf_parser.py (before)
def __images__(self, fnm, zoomin=3, page_from=0, page_to=299, callback=None):
# Only the first 300 pages were rendered; everything beyond was silently dropped
```
While most callers in `rag/app/*.py` correctly passed `to_page=100000`,
the base class `RAGFlowPdfParser.__call__()` and `parse_into_bboxes()`
invoked `__images__` **without** forwarding `page_from`/`page_to`,
falling back to the restrictive default of 299.
## Solution
### 1. Define constants in `common/constants.py`
```python
MAXIMUM_PAGE_NUMBER = 100000 # Used by the parsing layer
MAXIMUM_TASK_PAGE_NUMBER = MAXIMUM_PAGE_NUMBER * 1000 # Used by the task/DB layer
```
### 2. Replace all hardcoded sentinel values
| Layer | Files Changed | Old Values | New Value |
|---|---|---|---|
| **Deepdoc parsers** | `pdf_parser.py`, `mineru_parser.py`,
`docling_parser.py`, `opendataloader_parser.py`, `paddleocr_parser.py`,
`docx_parser.py` | `299`, `600`, `10**9`, `100000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Chunk parsers** | `naive.py`, `book.py`, `qa.py`, `one.py`,
`manual.py`, `paper.py`, `presentation.py`, `laws.py`, `resume.py`,
`email.py`, `table.py` | `100000`, `10000`, `10000000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Task/DB layer** | `db_models.py`, `task_service.py`,
`document_service.py`, `file_service.py` | `100000000` |
`MAXIMUM_TASK_PAGE_NUMBER` |
### 3. Fix `parse_into_bboxes()` missing parameters
Added `from_page`/`to_page` parameters to `parse_into_bboxes()` so that
the `rag/flow/parser/parser.py` DeepDOC path no longer falls back to the
restrictive default.
## Files Changed (22)
- `common/constants.py`
- `deepdoc/parser/pdf_parser.py`
- `deepdoc/parser/mineru_parser.py`
- `deepdoc/parser/docling_parser.py`
- `deepdoc/parser/opendataloader_parser.py`
- `deepdoc/parser/paddleocr_parser.py`
- `deepdoc/parser/docx_parser.py`
- `rag/app/naive.py`
- `rag/app/book.py`
- `rag/app/qa.py`
- `rag/app/one.py`
- `rag/app/manual.py`
- `rag/app/paper.py`
- `rag/app/presentation.py`
- `rag/app/laws.py`
- `rag/app/resume.py`
- `rag/app/email.py`
- `rag/app/table.py`
- `api/db/db_models.py`
- `api/db/services/task_service.py`
- `api/db/services/document_service.py`
- `api/db/services/file_service.py`
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
---------
Signed-off-by: noob <yixiao121314@outlook.com>
2026-04-27 06:57:20 +00:00
def by_plaintext ( filename , binary = None , from_page = 0 , to_page = MAXIMUM_PAGE_NUMBER , callback = None , * * kwargs ) :
2025-12-17 19:48:24 +08:00
layout_recognizer = ( kwargs . get ( " layout_recognizer " ) or " " ) . strip ( )
if ( not layout_recognizer ) or ( layout_recognizer == " Plain Text " ) :
2025-11-05 13:00:42 +08:00
pdf_parser = PlainParser ( )
else :
2025-12-17 19:48:24 +08:00
tenant_id = kwargs . get ( " tenant_id " )
if not tenant_id :
raise ValueError ( " tenant_id is required when using vision layout recognizer " )
2026-03-05 17:27:17 +08:00
vision_model_config = get_model_config_by_type_and_name ( tenant_id , LLMType . IMAGE2TEXT , layout_recognizer )
2025-12-17 19:48:24 +08:00
vision_model = LLMBundle (
tenant_id ,
2026-03-05 17:27:17 +08:00
model_config = vision_model_config ,
2025-12-17 19:48:24 +08:00
lang = kwargs . get ( " lang " , " Chinese " ) ,
)
2025-11-05 13:00:42 +08:00
pdf_parser = VisionParser ( vision_model = vision_model , * * kwargs )
2025-11-20 10:08:42 +08:00
2026-01-09 17:48:45 +08:00
sections , tables = pdf_parser ( filename if not binary else binary , from_page = from_page , to_page = to_page , callback = callback )
2025-11-05 13:00:42 +08:00
return sections , tables , pdf_parser
PARSERS = {
2025-12-29 12:01:18 +08:00
" deepdoc " : by_deepdoc ,
" mineru " : by_mineru ,
" docling " : by_docling ,
Feat: add OpenDataLoader PDF parser backend (#14058) (#14097)
### What problem does this PR solve?
Closes #14058.
RAGFlow supports multiple PDF parsing backends (DeepDOC, MinerU,
Docling, TCADP, PaddleOCR). This PR adds **OpenDataLoader**
([opendataloader-project/opendataloader-pdf](https://github.com/opendataloader-project/opendataloader-pdf))
as a new optional backend, giving users a deterministic, local-first
alternative with competitive table extraction accuracy.
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
- [x] Documentation Update
---
### Changes
#### Backend
- `deepdoc/parser/opendataloader_parser.py` — new `OpenDataLoaderParser`
class inheriting `RAGFlowPdfParser`. Implements `check_installation()`
(guards Python package + Java 11+ runtime), `parse_pdf()` with
JSON-first extraction (heading/paragraph/table/list/image/formula) and
Markdown fallback, position-tag generation compatible with the shared
`@@page\tx0\tx1\ty0\ty1##` format, and temp-dir lifecycle with cleanup.
- `rag/app/naive.py` — new `by_opendataloader()` wrapper, registered in
`PARSERS` dict, added to `chunk_token_num=0` override list.
- `rag/flow/parser/parser.py` — `"opendataloader"` branch in the
pipeline PDF handler + check validation list.
#### Infrastructure
- `docker/entrypoint.sh` — `ensure_opendataloader()` function: opt-in
via `USE_OPENDATALOADER=true`, skips gracefully if Java is not on PATH.
#### Frontend
- `web/src/components/layout-recognize-form-field.tsx` —
`OpenDataLoader` added to `ParseDocumentType` enum and parser dropdown.
Cascades automatically to the pipeline editor's Parser component.
#### Docs
- `docs/guides/dataset/select_pdf_parser.md` — added OpenDataLoader
entry and full env-var reference.
---
### Environment variables
| Variable | Default | Description |
|---|---|---|
| `USE_OPENDATALOADER` | `false` | Set `true` to install
`opendataloader-pdf` on container startup |
| `OPENDATALOADER_VERSION` | latest | Pin the PyPI release (e.g.
`==2.2.1`) |
| `OPENDATALOADER_HYBRID` | _(unset)_ | Enable hybrid AI mode (e.g.
`docling-fast`) |
| `OPENDATALOADER_IMAGE_OUTPUT` | _(unset)_ | `off` / `embedded` /
`external` |
| `OPENDATALOADER_OUTPUT_DIR` | _(tmp)_ | Persistent output dir; temp
dir used + cleaned if unset |
| `OPENDATALOADER_DELETE_OUTPUT` | `1` | `0` to retain intermediate
files for debugging |
| `OPENDATALOADER_SANITIZE` | _(unset)_ | `1` to filter prompt-injection
patterns from output |
---
### Dependencies
- **Runtime**: `opendataloader-pdf` (PyPI, Apache 2.0) — opt-in, not
added to `pyproject.toml` core deps. Installed by
`ensure_opendataloader()` at container startup when
`USE_OPENDATALOADER=true`.
- **System**: Java 11+ on PATH (JVM is the underlying engine). The
installer skips with a warning if `java` is not found.
---
### How to test
**Standalone parser:**
```bash
source .venv/bin/activate
uv pip install opendataloader-pdf
python3 -c "
import sys; sys.path.insert(0, '.')
from deepdoc.parser.opendataloader_parser import OpenDataLoaderParser
p = OpenDataLoaderParser()
print('available:', p.check_installation())
s, t = p.parse_pdf('path/to/test.pdf', parse_method='pipeline')
print(f'sections={len(s)} tables={len(t)}')
"
```
### Benchmark vs Docling
```
file parser secs sections tables
----------------------------------------------------------------------
text-heavy.pdf docling 45.29 148 10
text-heavy.pdf opendataloader 3.14 559 0
table-heavy.pdf docling 7.05 76 3
table-heavy.pdf opendataloader 3.71 90 0
complex.pdf docling 42.67 114 8
complex.pdf opendataloader 3.51 180 0
```
2026-04-24 18:33:02 +02:00
" opendataloader " : by_opendataloader ,
2026-03-04 11:51:10 +08:00
" tcadp parser " : by_tcadp ,
2026-01-09 17:48:45 +08:00
" paddleocr " : by_paddleocr ,
2025-11-06 15:20:35 +08:00
" plaintext " : by_plaintext , # default
2025-11-05 13:00:42 +08:00
}
2024-09-30 16:59:39 +08:00
2024-08-15 09:17:36 +08:00
class Docx ( DocxParser ) :
def __init__ ( self ) :
pass
def __clean ( self , line ) :
line = re . sub ( r " \ u3000 " , " " , line ) . strip ( )
return line
2025-03-21 18:42:36 +08:00
def __get_nearest_title ( self , table_index , filename ) :
""" Get the hierarchical title structure before the table """
import re
from docx . text . paragraph import Paragraph
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
titles = [ ]
blocks = [ ]
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
# Get document name from filename parameter
doc_name = re . sub ( r " \ .[a-zA-Z]+$ " , " " , filename )
if not doc_name :
doc_name = " Untitled Document "
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
# Collect all document blocks while maintaining document order
try :
# Iterate through all paragraphs and tables in document order
for i , block in enumerate ( self . doc . _element . body ) :
2026-01-09 17:48:45 +08:00
if block . tag . endswith ( " p " ) : # Paragraph
2025-03-21 18:42:36 +08:00
p = Paragraph ( block , self . doc )
2026-01-09 17:48:45 +08:00
blocks . append ( ( " p " , i , p ) )
elif block . tag . endswith ( " tbl " ) : # Table
blocks . append ( ( " t " , i , None ) ) # Table object will be retrieved later
2025-03-21 18:42:36 +08:00
except Exception as e :
logging . error ( f " Error collecting blocks: { e } " )
return " "
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
# Find the target table position
target_table_pos = - 1
table_count = 0
for i , ( block_type , pos , _ ) in enumerate ( blocks ) :
2026-01-09 17:48:45 +08:00
if block_type == " t " :
2025-03-21 18:42:36 +08:00
if table_count == table_index :
target_table_pos = pos
break
table_count + = 1
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
if target_table_pos == - 1 :
return " " # Target table not found
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
# Find the nearest heading paragraph in reverse order
nearest_title = None
2025-12-29 12:01:18 +08:00
for i in range ( len ( blocks ) - 1 , - 1 , - 1 ) :
2025-03-21 18:42:36 +08:00
block_type , pos , block = blocks [ i ]
if pos > = target_table_pos : # Skip blocks after the table
continue
2025-07-15 13:03:01 +08:00
2026-01-09 17:48:45 +08:00
if block_type != " p " :
2025-03-21 18:42:36 +08:00
continue
2025-07-15 13:03:01 +08:00
2025-07-01 17:38:11 +08:00
if block . style and block . style . name and re . search ( r " Heading \ s*( \ d+) " , block . style . name , re . I ) :
2025-03-21 18:42:36 +08:00
try :
level_match = re . search ( r " ( \ d+) " , block . style . name )
if level_match :
level = int ( level_match . group ( 1 ) )
if level < = 7 : # Support up to 7 heading levels
title_text = block . text . strip ( )
if title_text : # Avoid empty titles
nearest_title = ( level , title_text )
break
except Exception as e :
logging . error ( f " Error parsing heading level: { e } " )
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
if nearest_title :
# Add current title
titles . append ( nearest_title )
current_level = nearest_title [ 0 ]
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
# Find all parent headings, allowing cross-level search
while current_level > 1 :
found = False
2025-12-29 12:01:18 +08:00
for i in range ( len ( blocks ) - 1 , - 1 , - 1 ) :
2025-03-21 18:42:36 +08:00
block_type , pos , block = blocks [ i ]
if pos > = target_table_pos : # Skip blocks after the table
continue
2025-07-15 13:03:01 +08:00
2026-01-09 17:48:45 +08:00
if block_type != " p " :
2025-03-21 18:42:36 +08:00
continue
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
if block . style and re . search ( r " Heading \ s*( \ d+) " , block . style . name , re . I ) :
try :
level_match = re . search ( r " ( \ d+) " , block . style . name )
if level_match :
level = int ( level_match . group ( 1 ) )
# Find any heading with a higher level
2025-07-15 13:03:01 +08:00
if level < current_level :
2025-03-21 18:42:36 +08:00
title_text = block . text . strip ( )
if title_text : # Avoid empty titles
titles . append ( ( level , title_text ) )
current_level = level
found = True
break
except Exception as e :
logging . error ( f " Error parsing parent heading: { e } " )
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
if not found : # Break if no parent heading is found
break
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
# Sort by level (ascending, from highest to lowest)
titles . sort ( key = lambda x : x [ 0 ] )
# Organize titles (from highest to lowest)
hierarchy = [ doc_name ] + [ t [ 1 ] for t in titles ]
return " > " . join ( hierarchy )
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
return " "
Fix: Remove hardcoded page limits causing parsing failures on large PDFs (>300 pages) (#14382)
### What problem does this PR solve?
Fixes #14196
## Problem
When using DeepDOC to parse large PDFs (over 1000 pages), the parser
silently truncated processing at 300 pages due to a hardcoded default
`page_to=299` in `RAGFlowPdfParser.__images__()`. This caused:
- **Errors** on pages beyond the limit
- **Poor image quality** as the parser attempted to compensate with
missing page data
- **Inconsistent chunk splitting** between full PDF imports and partial
imports
Additionally, the codebase scattered magic numbers (`299`, `600`,
`10000`, `100000`, `100000000`, `10000000000`, `10**9`) across 22 files
as sentinel values for "parse all pages", making future maintenance
error-prone.
## Root Cause
```python
# deepdoc/parser/pdf_parser.py (before)
def __images__(self, fnm, zoomin=3, page_from=0, page_to=299, callback=None):
# Only the first 300 pages were rendered; everything beyond was silently dropped
```
While most callers in `rag/app/*.py` correctly passed `to_page=100000`,
the base class `RAGFlowPdfParser.__call__()` and `parse_into_bboxes()`
invoked `__images__` **without** forwarding `page_from`/`page_to`,
falling back to the restrictive default of 299.
## Solution
### 1. Define constants in `common/constants.py`
```python
MAXIMUM_PAGE_NUMBER = 100000 # Used by the parsing layer
MAXIMUM_TASK_PAGE_NUMBER = MAXIMUM_PAGE_NUMBER * 1000 # Used by the task/DB layer
```
### 2. Replace all hardcoded sentinel values
| Layer | Files Changed | Old Values | New Value |
|---|---|---|---|
| **Deepdoc parsers** | `pdf_parser.py`, `mineru_parser.py`,
`docling_parser.py`, `opendataloader_parser.py`, `paddleocr_parser.py`,
`docx_parser.py` | `299`, `600`, `10**9`, `100000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Chunk parsers** | `naive.py`, `book.py`, `qa.py`, `one.py`,
`manual.py`, `paper.py`, `presentation.py`, `laws.py`, `resume.py`,
`email.py`, `table.py` | `100000`, `10000`, `10000000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Task/DB layer** | `db_models.py`, `task_service.py`,
`document_service.py`, `file_service.py` | `100000000` |
`MAXIMUM_TASK_PAGE_NUMBER` |
### 3. Fix `parse_into_bboxes()` missing parameters
Added `from_page`/`to_page` parameters to `parse_into_bboxes()` so that
the `rag/flow/parser/parser.py` DeepDOC path no longer falls back to the
restrictive default.
## Files Changed (22)
- `common/constants.py`
- `deepdoc/parser/pdf_parser.py`
- `deepdoc/parser/mineru_parser.py`
- `deepdoc/parser/docling_parser.py`
- `deepdoc/parser/opendataloader_parser.py`
- `deepdoc/parser/paddleocr_parser.py`
- `deepdoc/parser/docx_parser.py`
- `rag/app/naive.py`
- `rag/app/book.py`
- `rag/app/qa.py`
- `rag/app/one.py`
- `rag/app/manual.py`
- `rag/app/paper.py`
- `rag/app/presentation.py`
- `rag/app/laws.py`
- `rag/app/resume.py`
- `rag/app/email.py`
- `rag/app/table.py`
- `api/db/db_models.py`
- `api/db/services/task_service.py`
- `api/db/services/document_service.py`
- `api/db/services/file_service.py`
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
---------
Signed-off-by: noob <yixiao121314@outlook.com>
2026-04-27 06:57:20 +00:00
def __call__ ( self , filename , binary = None , from_page = 0 , to_page = MAXIMUM_PAGE_NUMBER ) :
2026-01-09 17:48:45 +08:00
self . doc = Document ( filename ) if not binary else Document ( BytesIO ( binary ) )
2024-08-15 09:17:36 +08:00
pn = 0
lines = [ ]
last_image = None
2026-01-07 15:08:17 +08:00
table_idx = 0
def flush_last_image ( ) :
nonlocal last_image , lines
if last_image is not None :
lines . append ( { " text " : " " , " image " : last_image , " table " : None , " style " : " Image " } )
last_image = None
for block in self . doc . _element . body :
2024-08-15 09:17:36 +08:00
if pn > to_page :
break
2026-01-07 15:08:17 +08:00
2026-01-09 17:48:45 +08:00
if block . tag . endswith ( " p " ) :
2026-01-07 15:08:17 +08:00
p = Paragraph ( block , self . doc )
if from_page < = pn < to_page :
text = p . text . strip ( )
style_name = p . style . name if p . style else " "
if text :
if style_name == " Caption " :
former_image = None
if lines and lines [ - 1 ] . get ( " image " ) and lines [ - 1 ] . get ( " style " ) != " Caption " :
former_image = lines [ - 1 ] . get ( " image " )
lines . pop ( )
elif last_image is not None :
former_image = last_image
last_image = None
lines . append (
{
" text " : self . __clean ( text ) ,
" image " : former_image if former_image else None ,
" table " : None ,
}
)
else :
flush_last_image ( )
lines . append (
{
" text " : self . __clean ( text ) ,
" image " : None ,
" table " : None ,
}
)
current_image = self . get_picture ( self . doc , p )
if current_image is not None :
lines . append (
{
" text " : " " ,
" image " : current_image ,
" table " : None ,
}
)
2024-08-15 09:17:36 +08:00
else :
current_image = self . get_picture ( self . doc , p )
2026-01-07 15:08:17 +08:00
if current_image is not None :
2024-08-15 09:17:36 +08:00
last_image = current_image
2026-01-07 15:08:17 +08:00
for run in p . runs :
xml = run . _element . xml
if " lastRenderedPageBreak " in xml :
pn + = 1
continue
if " w:br " in xml and ' type= " page " ' in xml :
pn + = 1
2026-01-09 17:48:45 +08:00
elif block . tag . endswith ( " tbl " ) :
2026-01-07 15:08:17 +08:00
if pn < from_page or pn > to_page :
table_idx + = 1
2024-08-15 09:17:36 +08:00
continue
2026-01-07 15:08:17 +08:00
flush_last_image ( )
tb = DocxTable ( block , self . doc )
title = self . __get_nearest_title ( table_idx , filename )
html = " <table> "
if title :
html + = f " <caption>Table Location: { title } </caption> "
for r in tb . rows :
html + = " <tr> "
col_idx = 0
try :
while col_idx < len ( r . cells ) :
span = 1
c = r . cells [ col_idx ]
for j in range ( col_idx + 1 , len ( r . cells ) ) :
if c . text == r . cells [ j ] . text :
span + = 1
col_idx = j
else :
break
col_idx + = 1
html + = f " <td> { c . text } </td> " if span == 1 else f " <td colspan= ' { span } ' > { c . text } </td> "
except Exception as e :
logging . warning ( f " Error parsing table, ignore: { e } " )
html + = " </tr> "
html + = " </table> "
lines . append ( { " text " : " " , " image " : None , " table " : html } )
table_idx + = 1
flush_last_image ( )
new_line = [ ( line . get ( " text " ) , line . get ( " image " ) , line . get ( " table " ) ) for line in lines ]
return new_line
2025-10-10 09:39:15 +08:00
def to_markdown ( self , filename = None , binary = None , inline_images : bool = True ) :
"""
This function uses mammoth , licensed under the BSD 2 - Clause License .
"""
import base64
import uuid
import mammoth
from markdownify import markdownify
docx_file = BytesIO ( binary ) if binary else open ( filename , " rb " )
def _convert_image_to_base64 ( image ) :
try :
with image . open ( ) as image_file :
image_bytes = image_file . read ( )
encoded = base64 . b64encode ( image_bytes ) . decode ( " utf-8 " )
base64_url = f " data: { image . content_type } ;base64, { encoded } "
alt_name = " image "
alt_name = f " img_ { uuid . uuid4 ( ) . hex [ : 8 ] } "
return { " src " : base64_url , " alt " : alt_name }
except Exception as e :
logging . warning ( f " Failed to convert image to base64: { e } " )
return { " src " : " " , " alt " : " image " }
try :
if inline_images :
2026-01-09 17:48:45 +08:00
result = mammoth . convert_to_html ( docx_file , convert_image = mammoth . images . img_element ( _convert_image_to_base64 ) )
2025-10-10 09:39:15 +08:00
else :
result = mammoth . convert_to_html ( docx_file )
html = result . value
markdown_text = markdownify ( html )
return markdown_text
finally :
if not binary :
docx_file . close ( )
2024-08-15 09:17:36 +08:00
class Pdf ( PdfParser ) :
2025-03-17 16:49:54 +08:00
def __init__ ( self ) :
super ( ) . __init__ ( )
2025-03-17 11:58:40 +08:00
Fix: Remove hardcoded page limits causing parsing failures on large PDFs (>300 pages) (#14382)
### What problem does this PR solve?
Fixes #14196
## Problem
When using DeepDOC to parse large PDFs (over 1000 pages), the parser
silently truncated processing at 300 pages due to a hardcoded default
`page_to=299` in `RAGFlowPdfParser.__images__()`. This caused:
- **Errors** on pages beyond the limit
- **Poor image quality** as the parser attempted to compensate with
missing page data
- **Inconsistent chunk splitting** between full PDF imports and partial
imports
Additionally, the codebase scattered magic numbers (`299`, `600`,
`10000`, `100000`, `100000000`, `10000000000`, `10**9`) across 22 files
as sentinel values for "parse all pages", making future maintenance
error-prone.
## Root Cause
```python
# deepdoc/parser/pdf_parser.py (before)
def __images__(self, fnm, zoomin=3, page_from=0, page_to=299, callback=None):
# Only the first 300 pages were rendered; everything beyond was silently dropped
```
While most callers in `rag/app/*.py` correctly passed `to_page=100000`,
the base class `RAGFlowPdfParser.__call__()` and `parse_into_bboxes()`
invoked `__images__` **without** forwarding `page_from`/`page_to`,
falling back to the restrictive default of 299.
## Solution
### 1. Define constants in `common/constants.py`
```python
MAXIMUM_PAGE_NUMBER = 100000 # Used by the parsing layer
MAXIMUM_TASK_PAGE_NUMBER = MAXIMUM_PAGE_NUMBER * 1000 # Used by the task/DB layer
```
### 2. Replace all hardcoded sentinel values
| Layer | Files Changed | Old Values | New Value |
|---|---|---|---|
| **Deepdoc parsers** | `pdf_parser.py`, `mineru_parser.py`,
`docling_parser.py`, `opendataloader_parser.py`, `paddleocr_parser.py`,
`docx_parser.py` | `299`, `600`, `10**9`, `100000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Chunk parsers** | `naive.py`, `book.py`, `qa.py`, `one.py`,
`manual.py`, `paper.py`, `presentation.py`, `laws.py`, `resume.py`,
`email.py`, `table.py` | `100000`, `10000`, `10000000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Task/DB layer** | `db_models.py`, `task_service.py`,
`document_service.py`, `file_service.py` | `100000000` |
`MAXIMUM_TASK_PAGE_NUMBER` |
### 3. Fix `parse_into_bboxes()` missing parameters
Added `from_page`/`to_page` parameters to `parse_into_bboxes()` so that
the `rag/flow/parser/parser.py` DeepDOC path no longer falls back to the
restrictive default.
## Files Changed (22)
- `common/constants.py`
- `deepdoc/parser/pdf_parser.py`
- `deepdoc/parser/mineru_parser.py`
- `deepdoc/parser/docling_parser.py`
- `deepdoc/parser/opendataloader_parser.py`
- `deepdoc/parser/paddleocr_parser.py`
- `deepdoc/parser/docx_parser.py`
- `rag/app/naive.py`
- `rag/app/book.py`
- `rag/app/qa.py`
- `rag/app/one.py`
- `rag/app/manual.py`
- `rag/app/paper.py`
- `rag/app/presentation.py`
- `rag/app/laws.py`
- `rag/app/resume.py`
- `rag/app/email.py`
- `rag/app/table.py`
- `api/db/db_models.py`
- `api/db/services/task_service.py`
- `api/db/services/document_service.py`
- `api/db/services/file_service.py`
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
---------
Signed-off-by: noob <yixiao121314@outlook.com>
2026-04-27 06:57:20 +00:00
def __call__ ( self , filename , binary = None , from_page = 0 , to_page = MAXIMUM_PAGE_NUMBER , zoomin = 3 , callback = None , separate_tables_figures = False ) :
2024-08-15 09:17:36 +08:00
start = timer ( )
2024-11-30 18:48:06 +08:00
first_start = start
callback ( msg = " OCR started " )
2026-01-09 17:48:45 +08:00
self . __images__ ( filename if not binary else binary , zoomin , from_page , to_page , callback )
2024-11-30 18:48:06 +08:00
callback ( msg = " OCR finished ( {:.2f} s) " . format ( timer ( ) - start ) )
logging . info ( " OCR( {} ~ {} ): {:.2f} s " . format ( from_page , to_page , timer ( ) - start ) )
2024-08-15 09:17:36 +08:00
start = timer ( )
self . _layouts_rec ( zoomin )
2024-11-30 18:48:06 +08:00
callback ( 0.63 , " Layout analysis ( {:.2f} s) " . format ( timer ( ) - start ) )
start = timer ( )
2024-08-15 09:17:36 +08:00
self . _table_transformer_job ( zoomin )
2024-11-30 18:48:06 +08:00
callback ( 0.65 , " Table analysis ( {:.2f} s) " . format ( timer ( ) - start ) )
start = timer ( )
2025-10-11 18:46:09 +08:00
self . _text_merge ( zoomin = zoomin )
2024-11-30 18:48:06 +08:00
callback ( 0.67 , " Text merged ( {:.2f} s) " . format ( timer ( ) - start ) )
2024-08-15 09:17:36 +08:00
2025-03-20 09:39:32 +08:00
if separate_tables_figures :
tbls , figures = self . _extract_table_figure ( True , zoomin , True , True , True )
self . _concat_downward ( )
logging . info ( " layouts cost: {} s " . format ( timer ( ) - first_start ) )
return [ ( b [ " text " ] , self . _line_tag ( b , zoomin ) ) for b in self . boxes ] , tbls , figures
else :
tbls = self . _extract_table_figure ( True , zoomin , True , True )
2025-08-20 17:29:15 +08:00
self . _naive_vertical_merge ( )
2025-03-20 09:39:32 +08:00
self . _concat_downward ( )
2025-12-30 20:24:27 +08:00
# self._final_reading_order_merge()
2025-03-20 09:39:32 +08:00
# self._filter_forpages()
logging . info ( " layouts cost: {} s " . format ( timer ( ) - first_start ) )
return [ ( b [ " text " ] , self . _line_tag ( b , zoomin ) ) for b in self . boxes ] , tbls
2024-08-15 09:17:36 +08:00
class Markdown ( MarkdownParser ) :
2025-11-03 09:34:12 +08:00
def md_to_html ( self , sections ) :
2025-04-25 18:35:28 +08:00
if not sections :
return [ ]
if isinstance ( sections , type ( " " ) ) :
text = sections
elif isinstance ( sections [ 0 ] , type ( " " ) ) :
text = sections [ 0 ]
else :
return [ ]
2025-07-15 13:03:01 +08:00
2025-04-25 18:35:28 +08:00
from bs4 import BeautifulSoup
2026-01-09 17:48:45 +08:00
2025-05-12 17:16:55 +08:00
html_content = markdown ( text )
2026-01-09 17:48:45 +08:00
soup = BeautifulSoup ( html_content , " html.parser " )
2025-11-03 09:34:12 +08:00
return soup
def get_hyperlink_urls ( self , soup ) :
if soup :
2026-01-09 17:48:45 +08:00
return set ( [ a . get ( " href " ) for a in soup . find_all ( " a " ) if a . get ( " href " ) ] )
2025-11-03 09:34:12 +08:00
return [ ]
2025-11-25 19:54:06 +08:00
def extract_image_urls_with_lines ( self , text ) :
md_img_re = re . compile ( r " ! \ [[^ \ ]]* \ ] \ (([^) \ s]+) " )
html_img_re = re . compile ( r ' src=[ " \\ \' ]([^ " \\ \' > \\ s]+) ' , re . IGNORECASE )
urls = [ ]
seen = set ( )
lines = text . splitlines ( )
for idx , line in enumerate ( lines ) :
for url in md_img_re . findall ( line ) :
if ( url , idx ) not in seen :
urls . append ( { " url " : url , " line " : idx } )
seen . add ( ( url , idx ) )
for url in html_img_re . findall ( line ) :
if ( url , idx ) not in seen :
urls . append ( { " url " : url , " line " : idx } )
seen . add ( ( url , idx ) )
# cross-line
try :
from bs4 import BeautifulSoup
2026-01-09 17:48:45 +08:00
soup = BeautifulSoup ( text , " html.parser " )
2025-11-25 19:54:06 +08:00
newline_offsets = [ m . start ( ) for m in re . finditer ( r " \ n " , text ) ] + [ len ( text ) ]
2026-01-09 17:48:45 +08:00
for img_tag in soup . find_all ( " img " ) :
src = img_tag . get ( " src " )
2025-11-25 19:54:06 +08:00
if not src :
continue
tag_str = str ( img_tag )
pos = text . find ( tag_str )
if pos == - 1 :
# fallback
pos = max ( text . find ( src ) , 0 )
line_no = 0
for i , off in enumerate ( newline_offsets ) :
if pos < = off :
line_no = i
break
if ( src , line_no ) not in seen :
urls . append ( { " url " : src , " line " : line_no } )
seen . add ( ( src , line_no ) )
2025-12-30 15:04:09 +08:00
except Exception as e :
logging . error ( " Failed to extract image urls: {} " . format ( e ) )
2025-11-25 19:54:06 +08:00
pass
return urls
def load_images_from_urls ( self , urls , cache = None ) :
2025-04-25 18:35:28 +08:00
import requests
2025-11-25 19:54:06 +08:00
from pathlib import Path
cache = cache or { }
2025-04-25 18:35:28 +08:00
images = [ ]
2025-11-25 19:54:06 +08:00
for url in urls :
if url in cache :
if cache [ url ] :
images . append ( cache [ url ] )
2025-11-10 13:28:07 +08:00
continue
2025-11-25 19:54:06 +08:00
img_obj = None
2025-04-25 18:35:28 +08:00
try :
2026-01-09 17:48:45 +08:00
if url . startswith ( ( " http:// " , " https:// " ) ) :
2025-07-18 17:06:58 +08:00
response = requests . get ( url , stream = True , timeout = 30 )
2026-01-09 17:48:45 +08:00
if response . status_code == 200 and response . headers . get ( " Content-Type " , " " ) . startswith ( " image/ " ) :
img_obj = Image . open ( BytesIO ( response . content ) ) . convert ( " RGB " )
2025-07-18 17:06:58 +08:00
else :
local_path = Path ( url )
2025-11-25 19:54:06 +08:00
if local_path . exists ( ) :
2026-01-09 17:48:45 +08:00
img_obj = Image . open ( url ) . convert ( " RGB " )
2025-11-25 19:54:06 +08:00
else :
2025-07-18 17:06:58 +08:00
logging . warning ( f " Local image file not found: { url } " )
2025-04-25 18:35:28 +08:00
except Exception as e :
logging . error ( f " Failed to download/open image from { url } : { e } " )
2025-11-25 19:54:06 +08:00
cache [ url ] = img_obj
if img_obj :
images . append ( img_obj )
return images , cache
2025-07-15 13:03:01 +08:00
2025-11-25 19:54:06 +08:00
def __call__ ( self , filename , binary = None , separate_tables = True , delimiter = None , return_section_images = False ) :
2024-08-15 09:17:36 +08:00
if binary :
encoding = find_codec ( binary )
txt = binary . decode ( encoding , errors = " ignore " )
else :
with open ( filename , " r " ) as f :
txt = f . read ( )
2025-08-21 09:36:18 +08:00
2026-01-09 17:48:45 +08:00
remainder , tables = self . extract_tables_and_remainder ( f " { txt } \n " , separate_tables = separate_tables )
2025-11-05 16:10:21 +08:00
# To eliminate duplicate tables in chunking result, uncomment code below and set separate_tables to True in line 410.
# extractor = MarkdownElementExtractor(remainder)
2025-08-21 09:36:18 +08:00
extractor = MarkdownElementExtractor ( txt )
2025-11-25 19:54:06 +08:00
image_refs = self . extract_image_urls_with_lines ( txt )
element_sections = extractor . extract_elements ( delimiter , include_meta = True )
sections = [ ]
section_images = [ ]
image_cache = { }
for element in element_sections :
content = element [ " content " ]
start_line = element [ " start_line " ]
end_line = element [ " end_line " ]
urls_in_section = [ ref [ " url " ] for ref in image_refs if start_line < = ref [ " line " ] < = end_line ]
imgs = [ ]
if urls_in_section :
imgs , image_cache = self . load_images_from_urls ( urls_in_section , image_cache )
combined_image = None
if imgs :
combined_image = reduce ( concat_img , imgs ) if len ( imgs ) > 1 else imgs [ 0 ]
sections . append ( ( content , " " ) )
section_images . append ( combined_image )
2024-08-15 09:17:36 +08:00
tbls = [ ]
for table in tables :
2026-01-09 17:48:45 +08:00
tbls . append ( ( ( None , markdown ( table , extensions = [ " markdown.extensions.tables " ] ) ) , " " ) )
2025-11-25 19:54:06 +08:00
if return_section_images :
return sections , tbls , section_images
2024-08-15 09:17:36 +08:00
return sections , tbls
2025-12-29 12:01:18 +08:00
2025-08-14 12:14:03 +08:00
def load_from_xml_v2 ( baseURI , rels_item_xml ) :
"""
Return | _SerializedRelationships | instance loaded with the
relationships contained in * rels_item_xml * . Returns an empty
collection if * rels_item_xml * is | None | .
"""
srels = _SerializedRelationships ( )
if rels_item_xml is not None :
rels_elm = parse_xml ( rels_item_xml )
for rel_elm in rels_elm . Relationship_lst :
2026-01-14 06:08:46 -05:00
if rel_elm . target_ref in ( " ../NULL " , " NULL " ) or rel_elm . target_ref . startswith ( " # " ) :
2025-08-14 12:14:03 +08:00
continue
srels . _srels . append ( _SerializedRelationship ( baseURI , rel_elm ) )
return srels
2024-08-15 09:17:36 +08:00
2025-12-29 12:01:18 +08:00
Fix: Remove hardcoded page limits causing parsing failures on large PDFs (>300 pages) (#14382)
### What problem does this PR solve?
Fixes #14196
## Problem
When using DeepDOC to parse large PDFs (over 1000 pages), the parser
silently truncated processing at 300 pages due to a hardcoded default
`page_to=299` in `RAGFlowPdfParser.__images__()`. This caused:
- **Errors** on pages beyond the limit
- **Poor image quality** as the parser attempted to compensate with
missing page data
- **Inconsistent chunk splitting** between full PDF imports and partial
imports
Additionally, the codebase scattered magic numbers (`299`, `600`,
`10000`, `100000`, `100000000`, `10000000000`, `10**9`) across 22 files
as sentinel values for "parse all pages", making future maintenance
error-prone.
## Root Cause
```python
# deepdoc/parser/pdf_parser.py (before)
def __images__(self, fnm, zoomin=3, page_from=0, page_to=299, callback=None):
# Only the first 300 pages were rendered; everything beyond was silently dropped
```
While most callers in `rag/app/*.py` correctly passed `to_page=100000`,
the base class `RAGFlowPdfParser.__call__()` and `parse_into_bboxes()`
invoked `__images__` **without** forwarding `page_from`/`page_to`,
falling back to the restrictive default of 299.
## Solution
### 1. Define constants in `common/constants.py`
```python
MAXIMUM_PAGE_NUMBER = 100000 # Used by the parsing layer
MAXIMUM_TASK_PAGE_NUMBER = MAXIMUM_PAGE_NUMBER * 1000 # Used by the task/DB layer
```
### 2. Replace all hardcoded sentinel values
| Layer | Files Changed | Old Values | New Value |
|---|---|---|---|
| **Deepdoc parsers** | `pdf_parser.py`, `mineru_parser.py`,
`docling_parser.py`, `opendataloader_parser.py`, `paddleocr_parser.py`,
`docx_parser.py` | `299`, `600`, `10**9`, `100000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Chunk parsers** | `naive.py`, `book.py`, `qa.py`, `one.py`,
`manual.py`, `paper.py`, `presentation.py`, `laws.py`, `resume.py`,
`email.py`, `table.py` | `100000`, `10000`, `10000000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Task/DB layer** | `db_models.py`, `task_service.py`,
`document_service.py`, `file_service.py` | `100000000` |
`MAXIMUM_TASK_PAGE_NUMBER` |
### 3. Fix `parse_into_bboxes()` missing parameters
Added `from_page`/`to_page` parameters to `parse_into_bboxes()` so that
the `rag/flow/parser/parser.py` DeepDOC path no longer falls back to the
restrictive default.
## Files Changed (22)
- `common/constants.py`
- `deepdoc/parser/pdf_parser.py`
- `deepdoc/parser/mineru_parser.py`
- `deepdoc/parser/docling_parser.py`
- `deepdoc/parser/opendataloader_parser.py`
- `deepdoc/parser/paddleocr_parser.py`
- `deepdoc/parser/docx_parser.py`
- `rag/app/naive.py`
- `rag/app/book.py`
- `rag/app/qa.py`
- `rag/app/one.py`
- `rag/app/manual.py`
- `rag/app/paper.py`
- `rag/app/presentation.py`
- `rag/app/laws.py`
- `rag/app/resume.py`
- `rag/app/email.py`
- `rag/app/table.py`
- `api/db/db_models.py`
- `api/db/services/task_service.py`
- `api/db/services/document_service.py`
- `api/db/services/file_service.py`
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
---------
Signed-off-by: noob <yixiao121314@outlook.com>
2026-04-27 06:57:20 +00:00
def chunk ( filename , binary = None , from_page = 0 , to_page = MAXIMUM_PAGE_NUMBER , lang = " Chinese " , callback = None , * * kwargs ) :
2024-08-15 09:17:36 +08:00
"""
2026-01-09 17:48:45 +08:00
Supported file formats are docx , pdf , excel , txt .
This method apply the naive ways to chunk files .
Successive text will be sliced into pieces using ' delimiter ' .
Next , these successive pieces are merge into chunks whose token number is no more than ' Max token number ' .
2024-08-15 09:17:36 +08:00
"""
2025-11-03 09:34:12 +08:00
urls = set ( )
url_res = [ ]
2025-10-27 17:04:13 +08:00
2024-12-01 22:28:00 +08:00
is_english = lang . lower ( ) == " english " # is_english(cks)
2026-01-09 17:48:45 +08:00
parser_config = kwargs . get ( " parser_config " , { " chunk_token_num " : 512 , " delimiter " : " \n !?。;!? " , " layout_recognize " : " DeepDOC " , " analyze_hyperlink " : True } )
2025-11-28 19:25:32 +08:00
2026-01-09 17:48:45 +08:00
child_deli = ( parser_config . get ( " children_delimiter " ) or " " ) . encode ( " utf-8 " ) . decode ( " unicode_escape " ) . encode ( " latin1 " ) . decode ( " utf-8 " )
2025-12-17 16:50:36 +08:00
cust_child_deli = re . findall ( r " `([^`]+)` " , child_deli )
child_deli = " | " . join ( re . sub ( r " `([^`]+)` " , " " , child_deli ) )
if cust_child_deli :
cust_child_deli = sorted ( set ( cust_child_deli ) , key = lambda x : - len ( x ) )
cust_child_deli = " | " . join ( re . escape ( t ) for t in cust_child_deli if t )
child_deli + = cust_child_deli
2025-11-28 19:25:32 +08:00
is_markdown = False
2025-11-27 10:21:44 +08:00
table_context_size = max ( 0 , int ( parser_config . get ( " table_context_size " , 0 ) or 0 ) )
image_context_size = max ( 0 , int ( parser_config . get ( " image_context_size " , 0 ) or 0 ) )
2025-11-28 19:25:32 +08:00
2026-01-09 17:48:45 +08:00
doc = { " docnm_kwd " : filename , " title_tks " : rag_tokenizer . tokenize ( re . sub ( r " \ .[a-zA-Z]+$ " , " " , filename ) ) }
2024-08-15 09:17:36 +08:00
doc [ " title_sm_tks " ] = rag_tokenizer . fine_grained_tokenize ( doc [ " title_tks " ] )
res = [ ]
pdf_parser = None
2025-04-25 18:35:28 +08:00
section_images = None
2025-10-17 18:46:47 +08:00
is_root = kwargs . get ( " is_root " , True )
embed_res = [ ]
if is_root :
# Only extract embedded files at the root call
embeds = [ ]
if binary is not None :
embeds = extract_embed_file ( binary )
else :
raise Exception ( " Embedding extraction from file path is not supported. " )
2025-10-27 17:04:13 +08:00
2025-10-17 18:46:47 +08:00
# Recursively chunk each embedded file and collect results
for embed_filename , embed_bytes in embeds :
try :
2026-01-09 17:48:45 +08:00
sub_res = chunk ( embed_filename , binary = embed_bytes , lang = lang , callback = callback , is_root = False , * * kwargs ) or [ ]
2025-10-17 18:46:47 +08:00
embed_res . extend ( sub_res )
except Exception as e :
2025-12-30 15:04:09 +08:00
error_msg = f " Failed to chunk embed { embed_filename } : { e } "
logging . error ( error_msg )
2025-10-17 18:46:47 +08:00
if callback :
2025-12-30 15:04:09 +08:00
callback ( 0.05 , error_msg )
2025-10-17 18:46:47 +08:00
continue
2024-08-15 09:17:36 +08:00
if re . search ( r " \ .docx$ " , filename , re . IGNORECASE ) :
callback ( 0.1 , " Start to parse. " )
2025-11-03 09:34:12 +08:00
if parser_config . get ( " analyze_hyperlink " , False ) and is_root :
urls = extract_links_from_docx ( binary )
for index , url in enumerate ( urls ) :
html_bytes , metadata = extract_html ( url )
if not html_bytes :
continue
try :
sub_url_res = chunk ( url , html_bytes , callback = callback , lang = lang , is_root = False , * * kwargs )
except Exception as e :
logging . info ( f " Failed to chunk url in registered file type { url } : { e } " )
2026-01-09 17:48:45 +08:00
sub_url_res = chunk ( f " { index } .html " , html_bytes , callback = callback , lang = lang , is_root = False , * * kwargs )
2025-11-03 09:34:12 +08:00
url_res . extend ( sub_url_res )
2025-03-20 11:24:44 +08:00
2025-08-14 12:14:03 +08:00
# fix "There is no item named 'word/NULL' in the archive", referring to https://github.com/python-openxml/python-docx/issues/1105#issuecomment-1298075246
_SerializedRelationships . load_from_xml = load_from_xml_v2
2024-08-15 09:17:36 +08:00
2026-01-07 15:08:17 +08:00
# sections = (text, image, tables)
sections = Docx ( ) ( filename , binary )
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2024-08-15 09:17:36 +08:00
2026-01-07 15:08:17 +08:00
# chunks list[dict]
# images list - index of image chunk in chunks
2026-01-09 17:48:45 +08:00
chunks , images = naive_merge_docx ( sections , int ( parser_config . get ( " chunk_token_num " , 128 ) ) , parser_config . get ( " delimiter " , " \n !?。;!? " ) , table_context_size , image_context_size )
2026-01-07 15:08:17 +08:00
vision_figure_parser_docx_wrapper_naive ( chunks = chunks , idx_lst = images , callback = callback , * * kwargs )
2024-08-15 09:17:36 +08:00
2026-01-07 15:08:17 +08:00
callback ( 0.8 , " Finish parsing. " )
st = timer ( )
res . extend ( doc_tokenize_chunks_with_images ( chunks , doc , is_english , child_delimiters_pattern = child_deli ) )
2024-11-14 17:13:48 +08:00
logging . info ( " naive_merge( {} ): {} " . format ( filename , timer ( ) - st ) )
2025-10-17 18:46:47 +08:00
res . extend ( embed_res )
2025-11-03 09:34:12 +08:00
res . extend ( url_res )
2024-08-15 09:17:36 +08:00
return res
elif re . search ( r " \ .pdf$ " , filename , re . IGNORECASE ) :
2026-01-09 17:48:45 +08:00
layout_recognizer , parser_model_name = normalize_layout_recognizer ( parser_config . get ( " layout_recognize " , " DeepDOC " ) )
2025-12-09 18:54:14 +08:00
2025-11-03 09:34:12 +08:00
if parser_config . get ( " analyze_hyperlink " , False ) and is_root :
urls = extract_links_from_pdf ( binary )
2025-03-20 14:50:59 +08:00
if isinstance ( layout_recognizer , bool ) :
2026-03-04 11:51:10 +08:00
layout_recognizer = " DeepDOC " if layout_recognizer else " PlainText "
2025-03-20 09:39:32 +08:00
2025-11-05 13:00:42 +08:00
name = layout_recognizer . strip ( ) . lower ( )
2025-11-06 15:20:35 +08:00
parser = PARSERS . get ( name , by_plaintext )
2025-11-05 13:00:42 +08:00
callback ( 0.1 , " Start to parse. " )
2025-10-17 18:46:47 +08:00
2025-11-06 15:20:35 +08:00
sections , tables , pdf_parser = parser (
2025-12-29 12:01:18 +08:00
filename = filename ,
binary = binary ,
from_page = from_page ,
to_page = to_page ,
lang = lang ,
callback = callback ,
layout_recognizer = layout_recognizer ,
mineru_llm_name = parser_model_name ,
2026-01-09 17:48:45 +08:00
paddleocr_llm_name = parser_model_name ,
* * kwargs ,
2025-11-05 13:00:42 +08:00
)
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2025-10-23 19:44:25 +08:00
2025-11-05 13:00:42 +08:00
if not sections and not tables :
return [ ]
2025-10-27 15:14:58 +08:00
2025-12-30 20:24:27 +08:00
if table_context_size or image_context_size :
tables = append_context2table_image4pdf ( sections , tables , image_context_size )
Feat: add OpenDataLoader PDF parser backend (#14058) (#14097)
### What problem does this PR solve?
Closes #14058.
RAGFlow supports multiple PDF parsing backends (DeepDOC, MinerU,
Docling, TCADP, PaddleOCR). This PR adds **OpenDataLoader**
([opendataloader-project/opendataloader-pdf](https://github.com/opendataloader-project/opendataloader-pdf))
as a new optional backend, giving users a deterministic, local-first
alternative with competitive table extraction accuracy.
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
- [x] Documentation Update
---
### Changes
#### Backend
- `deepdoc/parser/opendataloader_parser.py` — new `OpenDataLoaderParser`
class inheriting `RAGFlowPdfParser`. Implements `check_installation()`
(guards Python package + Java 11+ runtime), `parse_pdf()` with
JSON-first extraction (heading/paragraph/table/list/image/formula) and
Markdown fallback, position-tag generation compatible with the shared
`@@page\tx0\tx1\ty0\ty1##` format, and temp-dir lifecycle with cleanup.
- `rag/app/naive.py` — new `by_opendataloader()` wrapper, registered in
`PARSERS` dict, added to `chunk_token_num=0` override list.
- `rag/flow/parser/parser.py` — `"opendataloader"` branch in the
pipeline PDF handler + check validation list.
#### Infrastructure
- `docker/entrypoint.sh` — `ensure_opendataloader()` function: opt-in
via `USE_OPENDATALOADER=true`, skips gracefully if Java is not on PATH.
#### Frontend
- `web/src/components/layout-recognize-form-field.tsx` —
`OpenDataLoader` added to `ParseDocumentType` enum and parser dropdown.
Cascades automatically to the pipeline editor's Parser component.
#### Docs
- `docs/guides/dataset/select_pdf_parser.md` — added OpenDataLoader
entry and full env-var reference.
---
### Environment variables
| Variable | Default | Description |
|---|---|---|
| `USE_OPENDATALOADER` | `false` | Set `true` to install
`opendataloader-pdf` on container startup |
| `OPENDATALOADER_VERSION` | latest | Pin the PyPI release (e.g.
`==2.2.1`) |
| `OPENDATALOADER_HYBRID` | _(unset)_ | Enable hybrid AI mode (e.g.
`docling-fast`) |
| `OPENDATALOADER_IMAGE_OUTPUT` | _(unset)_ | `off` / `embedded` /
`external` |
| `OPENDATALOADER_OUTPUT_DIR` | _(tmp)_ | Persistent output dir; temp
dir used + cleaned if unset |
| `OPENDATALOADER_DELETE_OUTPUT` | `1` | `0` to retain intermediate
files for debugging |
| `OPENDATALOADER_SANITIZE` | _(unset)_ | `1` to filter prompt-injection
patterns from output |
---
### Dependencies
- **Runtime**: `opendataloader-pdf` (PyPI, Apache 2.0) — opt-in, not
added to `pyproject.toml` core deps. Installed by
`ensure_opendataloader()` at container startup when
`USE_OPENDATALOADER=true`.
- **System**: Java 11+ on PATH (JVM is the underlying engine). The
installer skips with a warning if `java` is not found.
---
### How to test
**Standalone parser:**
```bash
source .venv/bin/activate
uv pip install opendataloader-pdf
python3 -c "
import sys; sys.path.insert(0, '.')
from deepdoc.parser.opendataloader_parser import OpenDataLoaderParser
p = OpenDataLoaderParser()
print('available:', p.check_installation())
s, t = p.parse_pdf('path/to/test.pdf', parse_method='pipeline')
print(f'sections={len(s)} tables={len(t)}')
"
```
### Benchmark vs Docling
```
file parser secs sections tables
----------------------------------------------------------------------
text-heavy.pdf docling 45.29 148 10
text-heavy.pdf opendataloader 3.14 559 0
table-heavy.pdf docling 7.05 76 3
table-heavy.pdf opendataloader 3.71 90 0
complex.pdf docling 42.67 114 8
complex.pdf opendataloader 3.51 180 0
```
2026-04-24 18:33:02 +02:00
if name in [ " tcadp " , " docling " , " mineru " , " paddleocr " , " opendataloader " ] :
Fix: respect user-configured chunk_token_num for MinerU/docling/paddleocr parsers (#13234)
## Summary
When using MinerU, docling, TCADP, or paddleocr as the PDF parser with
the General (naive) chunk method, the user-configured `chunk_token_num`
is **unconditionally overwritten to 0** at
[rag/app/naive.py#L858-L859](https://github.com/infiniflow/ragflow/blob/main/rag/app/naive.py#L858-L859),
effectively disabling chunk merging regardless of what the user sets in
the UI.
### Problem
A user sets `chunk_token_num = 2048` in the dataset configuration UI,
expecting small parser blocks to be merged into larger chunks. However,
this line:
```python
if name in ["tcadp", "docling", "mineru", "paddleocr"]:
parser_config["chunk_token_num"] = 0
```
silently overrides the user's setting. As a result, every MinerU output
block becomes its own chunk. For short documents (e.g. a 3-page PDF fund
factsheet parsed by MinerU), this produces **47 tiny chunks** — some as
small as 11 characters (`"July 2025"`) or 15 characters (`"CIES
Eligible"`).
This severely degrades retrieval quality: vector embeddings of such
short fragments have minimal semantic value, and keyword search produces
excessive noise.
### Fix
Only apply the `chunk_token_num = 0` override when the user has **not**
explicitly configured a positive value:
```python
if name in ["tcadp", "docling", "mineru", "paddleocr"]:
if int(parser_config.get("chunk_token_num", 0)) <= 0:
parser_config["chunk_token_num"] = 0
```
This preserves the original default behavior (no merging) while
respecting the user's explicit configuration.
### Before / After (MinerU, 3-page PDF, chunk_token_num=2048)
| | Before | After |
|---|---|---|
| Chunks produced | 47 | ~8 (merged by token limit) |
| Smallest chunk | 11 chars | ~500 chars |
| User setting respected | No | Yes |
## Test plan
- [ ] Parse a PDF with MinerU and `chunk_token_num = 2048` → verify
chunks are merged up to token limit
- [ ] Parse a PDF with MinerU and `chunk_token_num = 0` (or default) →
verify original behavior (no merging)
- [ ] Parse a PDF with DeepDOC parser → verify no change in behavior
(not affected by this code path)
- [ ] Repeat with docling/paddleocr if available
2026-03-02 15:31:40 +08:00
if int ( parser_config . get ( " chunk_token_num " , 0 ) ) < = 0 :
parser_config [ " chunk_token_num " ] = 0
2025-11-20 10:08:42 +08:00
2025-11-05 13:00:42 +08:00
res = tokenize_table ( tables , doc , is_english )
callback ( 0.8 , " Finish parsing. " )
2024-08-15 09:17:36 +08:00
2025-03-12 19:20:50 +08:00
elif re . search ( r " \ .(csv|xlsx?)$ " , filename , re . IGNORECASE ) :
2024-08-15 09:17:36 +08:00
callback ( 0.1 , " Start to parse. " )
2025-11-20 10:08:42 +08:00
# Check if tcadp_parser is selected for spreadsheet files
layout_recognizer = parser_config . get ( " layout_recognize " , " DeepDOC " )
if layout_recognizer == " TCADP Parser " :
table_result_type = parser_config . get ( " table_result_type " , " 1 " )
markdown_image_response_type = parser_config . get ( " markdown_image_response_type " , " 1 " )
2026-01-09 17:48:45 +08:00
tcadp_parser = TCADPParser ( table_result_type = table_result_type , markdown_image_response_type = markdown_image_response_type )
2025-11-20 10:08:42 +08:00
if not tcadp_parser . check_installation ( ) :
callback ( - 1 , " TCADP parser not available. Please check Tencent Cloud API configuration. " )
return res
# Determine file type based on extension
file_type = " XLSX " if re . search ( r " \ .xlsx?$ " , filename , re . IGNORECASE ) else " CSV "
2026-01-09 17:48:45 +08:00
sections , tables = tcadp_parser . parse_pdf ( filepath = filename , binary = binary , callback = callback , output_dir = os . environ . get ( " TCADP_OUTPUT_DIR " , " " ) , file_type = file_type )
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2025-11-20 10:08:42 +08:00
parser_config [ " chunk_token_num " ] = 0
res = tokenize_table ( tables , doc , is_english )
callback ( 0.8 , " Finish parsing. " )
2024-09-20 15:33:38 +08:00
else :
2025-11-20 10:08:42 +08:00
# Default DeepDOC parser
excel_parser = ExcelParser ( )
if parser_config . get ( " html4excel " ) :
sections = [ ( _ , " " ) for _ in excel_parser . html ( binary , 12 ) if _ ]
2025-11-25 19:54:20 +08:00
parser_config [ " chunk_token_num " ] = 0
2025-11-20 10:08:42 +08:00
else :
sections = [ ( _ , " " ) for _ in excel_parser ( binary ) if _ ]
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2024-08-15 09:17:36 +08:00
elif re . search ( r " \ .(txt|py|js|java|c|cpp|h|php|go|ts|sh|cs|kt|sql)$ " , filename , re . IGNORECASE ) :
callback ( 0.1 , " Start to parse. " )
2026-01-09 17:48:45 +08:00
sections = TxtParser ( ) ( filename , binary , parser_config . get ( " chunk_token_num " , 128 ) , parser_config . get ( " delimiter " , " \n !?;。;!? " ) )
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2026-04-10 10:03:44 +08:00
print ( " \n " , " - " * 150 , " \n " )
print ( sections )
print ( " \n " , " - " * 150 , " \n " )
2024-08-15 09:17:36 +08:00
callback ( 0.8 , " Finish parsing. " )
2024-10-09 19:37:32 +08:00
2025-12-29 12:54:31 +08:00
elif re . search ( r " \ .(md|markdown|mdx)$ " , filename , re . IGNORECASE ) :
2024-08-15 09:17:36 +08:00
callback ( 0.1 , " Start to parse. " )
2025-04-25 18:35:28 +08:00
markdown_parser = Markdown ( int ( parser_config . get ( " chunk_token_num " , 128 ) ) )
2025-11-25 19:54:06 +08:00
sections , tables , section_images = markdown_parser (
filename ,
binary ,
separate_tables = False ,
delimiter = parser_config . get ( " delimiter " , " \n !?;。;!? " ) ,
return_section_images = True ,
)
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2025-11-25 19:54:06 +08:00
2025-11-28 19:25:32 +08:00
is_markdown = True
2025-07-15 13:03:01 +08:00
2025-09-18 09:44:17 +08:00
try :
2026-03-05 17:27:17 +08:00
vision_model_config = get_tenant_default_model_by_type ( kwargs [ " tenant_id " ] , LLMType . IMAGE2TEXT )
vision_model = LLMBundle ( kwargs [ " tenant_id " ] , vision_model_config )
2025-09-18 09:44:17 +08:00
callback ( 0.2 , " Visual model detected. Attempting to enhance figure extraction... " )
2025-12-30 15:04:09 +08:00
except Exception as e :
logging . warning ( f " Failed to detect figure extraction: { e } " )
2025-09-18 09:44:17 +08:00
vision_model = None
2025-10-10 09:39:15 +08:00
2025-09-18 09:44:17 +08:00
if vision_model :
# Process images for each section
for idx , ( section_text , _ ) in enumerate ( sections ) :
2025-11-25 19:54:06 +08:00
images = [ ]
if section_images and len ( section_images ) > idx and section_images [ idx ] is not None :
images . append ( section_images [ idx ] )
2025-09-18 09:44:17 +08:00
2025-11-25 19:54:06 +08:00
if images and len ( images ) > 0 :
2025-09-18 09:44:17 +08:00
# If multiple images found, combine them using concat_img
combined_image = reduce ( concat_img , images ) if len ( images ) > 1 else images [ 0 ]
2025-11-25 19:54:06 +08:00
if section_images :
section_images [ idx ] = combined_image
else :
section_images = [ None ] * len ( sections )
section_images [ idx ] = combined_image
2026-01-09 17:48:45 +08:00
markdown_vision_parser = VisionFigureParser ( vision_model = vision_model , figures_data = [ ( ( combined_image , [ " markdown image " ] ) , [ ( 0 , 0 , 0 , 0 , 0 ) ] ) ] , * * kwargs )
2025-09-18 09:44:17 +08:00
boosted_figures = markdown_vision_parser ( callback = callback )
2026-01-09 17:48:45 +08:00
sections [ idx ] = ( section_text + " \n \n " + " \n \n " . join ( [ fig [ 0 ] [ 1 ] for fig in boosted_figures ] ) , sections [ idx ] [ 1 ] )
2025-11-03 09:34:12 +08:00
2025-09-18 09:44:17 +08:00
else :
logging . warning ( " No visual model detected. Skipping figure parsing enhancement. " )
2025-07-15 13:03:01 +08:00
2025-11-03 09:34:12 +08:00
if parser_config . get ( " hyperlink_urls " , False ) and is_root :
for idx , ( section_text , _ ) in enumerate ( sections ) :
soup = markdown_parser . md_to_html ( section_text )
hyperlink_urls = markdown_parser . get_hyperlink_urls ( soup )
urls . update ( hyperlink_urls )
2024-12-01 22:28:00 +08:00
res = tokenize_table ( tables , doc , is_english )
2024-08-15 09:17:36 +08:00
callback ( 0.8 , " Finish parsing. " )
elif re . search ( r " \ .(htm|html)$ " , filename , re . IGNORECASE ) :
callback ( 0.1 , " Start to parse. " )
2025-08-27 12:43:55 +08:00
chunk_token_num = int ( parser_config . get ( " chunk_token_num " , 128 ) )
sections = HtmlParser ( ) ( filename , binary , chunk_token_num )
2024-09-29 10:29:56 +08:00
sections = [ ( _ , " " ) for _ in sections if _ ]
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2024-08-15 09:17:36 +08:00
callback ( 0.8 , " Finish parsing. " )
2026-03-17 15:14:06 +03:00
elif re . search ( r " \ .epub$ " , filename , re . IGNORECASE ) :
callback ( 0.1 , " Start to parse. " )
chunk_token_num = int ( parser_config . get ( " chunk_token_num " , 128 ) )
sections = EpubParser ( ) ( filename , binary , chunk_token_num )
sections = [ ( _ , " " ) for _ in sections if _ ]
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
callback ( 0.8 , " Finish parsing. " )
2025-07-30 09:48:20 +08:00
elif re . search ( r " \ .(json|jsonl|ldjson)$ " , filename , re . IGNORECASE ) :
2024-08-15 09:17:36 +08:00
callback ( 0.1 , " Start to parse. " )
2024-12-03 19:02:03 +08:00
chunk_token_num = int ( parser_config . get ( " chunk_token_num " , 128 ) )
sections = JsonParser ( chunk_token_num ) ( binary )
2024-09-29 10:29:56 +08:00
sections = [ ( _ , " " ) for _ in sections if _ ]
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2024-08-15 09:17:36 +08:00
callback ( 0.8 , " Finish parsing. " )
elif re . search ( r " \ .doc$ " , filename , re . IGNORECASE ) :
callback ( 0.1 , " Start to parse. " )
2025-11-03 09:34:12 +08:00
2025-11-07 11:46:10 +08:00
try :
from tika import parser as tika_parser
except Exception as e :
callback ( 0.8 , f " tika not available: { e } . Unsupported .doc parsing. " )
logging . warning ( f " tika not available: { e } . Unsupported .doc parsing for { filename } . " )
return [ ]
2024-08-15 09:17:36 +08:00
binary = BytesIO ( binary )
2025-11-07 11:46:10 +08:00
doc_parsed = tika_parser . from_buffer ( binary )
2026-01-09 17:48:45 +08:00
if doc_parsed . get ( " content " , None ) is not None :
sections = doc_parsed [ " content " ] . split ( " \n " )
2024-11-22 11:05:06 +08:00
sections = [ ( _ , " " ) for _ in sections if _ ]
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2024-11-22 11:05:06 +08:00
callback ( 0.8 , " Finish parsing. " )
else :
2025-12-30 15:04:09 +08:00
error_msg = f " tika.parser got empty content from { filename } . "
callback ( 0.8 , error_msg )
logging . warning ( error_msg )
2024-11-22 11:05:06 +08:00
return [ ]
2024-08-15 09:17:36 +08:00
else :
2026-01-09 17:48:45 +08:00
raise NotImplementedError ( " file type not supported yet(pdf, xlsx, doc, docx, txt supported) " )
2024-08-15 09:17:36 +08:00
st = timer ( )
2026-01-27 12:43:01 +08:00
overlapped_percent = normalize_overlapped_percent ( parser_config . get ( " overlapped_percent " , 0 ) )
2025-11-28 19:25:32 +08:00
if is_markdown :
2025-11-25 19:54:06 +08:00
merged_chunks = [ ]
merged_images = [ ]
chunk_limit = max ( 0 , int ( parser_config . get ( " chunk_token_num " , 128 ) ) )
current_text = " "
current_tokens = 0
current_image = None
for idx , sec in enumerate ( sections ) :
text = sec [ 0 ] if isinstance ( sec , tuple ) else sec
sec_tokens = num_tokens_from_string ( text )
sec_image = section_images [ idx ] if section_images and idx < len ( section_images ) else None
if current_text and current_tokens + sec_tokens > chunk_limit :
merged_chunks . append ( current_text )
merged_images . append ( current_image )
overlap_part = " "
if overlapped_percent > 0 :
overlap_len = int ( len ( current_text ) * overlapped_percent / 100 )
if overlap_len > 0 :
overlap_part = current_text [ - overlap_len : ]
current_text = overlap_part
current_tokens = num_tokens_from_string ( current_text )
current_image = current_image if overlap_part else None
if current_text :
current_text + = " \n " + text
else :
current_text = text
current_tokens + = sec_tokens
2025-07-15 13:03:01 +08:00
2025-11-25 19:54:06 +08:00
if sec_image :
current_image = concat_img ( current_image , sec_image ) if current_image else sec_image
if current_text :
merged_chunks . append ( current_text )
merged_images . append ( current_image )
chunks = merged_chunks
has_images = merged_images and any ( img is not None for img in merged_images )
2025-11-28 19:25:32 +08:00
2025-11-25 19:54:06 +08:00
if has_images :
2026-01-09 17:48:45 +08:00
res . extend ( tokenize_chunks_with_images ( chunks , doc , is_english , merged_images , child_delimiters_pattern = child_deli ) )
2025-11-25 19:54:06 +08:00
else :
2025-11-28 19:25:32 +08:00
res . extend ( tokenize_chunks ( chunks , doc , is_english , pdf_parser , child_delimiters_pattern = child_deli ) )
2025-11-25 19:54:06 +08:00
else :
if section_images :
if all ( image is None for image in section_images ) :
section_images = None
if section_images :
2026-01-27 12:43:01 +08:00
chunks , images = naive_merge_with_images ( sections , section_images , int ( parser_config . get ( " chunk_token_num " , 128 ) ) , parser_config . get ( " delimiter " , " \n !?。;!? " ) , overlapped_percent )
2026-01-09 17:48:45 +08:00
res . extend ( tokenize_chunks_with_images ( chunks , doc , is_english , images , child_delimiters_pattern = child_deli ) )
2025-11-25 19:54:06 +08:00
else :
2026-01-27 12:43:01 +08:00
chunks = naive_merge ( sections , int ( parser_config . get ( " chunk_token_num " , 128 ) ) , parser_config . get ( " delimiter " , " \n !?。;!? " ) , overlapped_percent )
2025-04-25 18:35:28 +08:00
2025-11-28 19:25:32 +08:00
res . extend ( tokenize_chunks ( chunks , doc , is_english , pdf_parser , child_delimiters_pattern = child_deli ) )
2025-07-15 13:03:01 +08:00
2025-11-03 09:34:12 +08:00
if urls and parser_config . get ( " analyze_hyperlink " , False ) and is_root :
for index , url in enumerate ( urls ) :
html_bytes , metadata = extract_html ( url )
if not html_bytes :
continue
try :
sub_url_res = chunk ( url , html_bytes , callback = callback , lang = lang , is_root = False , * * kwargs )
except Exception as e :
logging . info ( f " Failed to chunk url in registered file type { url } : { e } " )
sub_url_res = chunk ( f " { index } .html " , html_bytes , callback = callback , lang = lang , is_root = False , * * kwargs )
url_res . extend ( sub_url_res )
2025-11-25 19:54:06 +08:00
2024-11-14 17:13:48 +08:00
logging . info ( " naive_merge( {} ): {} " . format ( filename , timer ( ) - st ) )
2025-11-25 19:54:06 +08:00
2025-10-17 18:46:47 +08:00
if embed_res :
res . extend ( embed_res )
2025-11-03 09:34:12 +08:00
if url_res :
res . extend ( url_res )
2026-01-07 15:08:17 +08:00
# if table_context_size or image_context_size:
2025-12-30 20:24:27 +08:00
# attach_media_context(res, table_context_size, image_context_size)
feat: persist PDF bookmark outline as document metadata (#13287)
## Summary
PDF files often contain a bookmark/outline tree (table of contents built
into the file by the authoring tool). RAGFlow's `pdf_parser.outlines`
already extracts these `(title, depth)` tuples via pypdf, but they are
used ephemerally during chunking (`manual` parser uses them for
hierarchy detection) and then discarded.
This PR persists the outline as `doc.meta_fields["outline"]` — a JSON
array of `{"title": str, "depth": int}` objects — so downstream features
can use the structural information.
### Why this matters
- **Complementary to `toc_extraction`** — the existing `toc_extraction`
feature uses LLM calls to generate a TOC and only works for the `naive`
parser. The raw PDF outline is free (already extracted by pypdf), works
for all parsers, and captures the author's original document structure.
- **Document navigation** — frontends can render a clickable TOC from
the outline
- **Entity extraction** — the outline provides a structural map for
identifying document sections and key topics
- **Search result context** — knowing which section a chunk belongs to
helps users evaluate relevance
### Changes
| File | Change | LOC |
|------|--------|-----|
| `rag/app/naive.py` | Attach `pdf_parser.outlines` as `__outline__` on
first chunk dict | ~7 |
| `rag/app/manual.py` | Same for the manual parser | ~5 |
| `rag/svr/task_executor.py` | Extract `__outline__`, persist via
`DocMetadataService.update_document_metadata()` | ~12 |
### Design decisions
- **Transient key pattern**: The outline is passed from parser →
task_executor via `__outline__` on the first chunk dict, then removed
before indexing. This follows the same pattern as `metadata_obj` for
LLM-generated metadata.
- **No schema changes**: Uses the existing `meta_fields` JSON column on
the document table.
- **Graceful degradation**: If a PDF has no outline (common for scanned
docs), nothing is stored. If persistence fails, it logs a warning and
continues — parsing is not interrupted.
### Backward compatibility
- **Fully backward compatible** — no existing fields, behavior, or
schemas changed
- PDFs without outlines are unaffected
- Existing `meta_fields` data is preserved (merged, not overwritten)
## Test plan
- [ ] Parse a PDF with bookmarks (e.g. any multi-chapter document),
verify `meta_fields["outline"]` is populated
- [ ] Parse a PDF without bookmarks, verify no errors and no outline key
in meta_fields
- [ ] Verify existing `meta_fields` data is preserved (not overwritten)
when outline is added
- [ ] Verify `manual` parser also persists outlines
- [ ] Verify outline JSON structure: `[{"title": "Chapter 1", "depth":
0}, ...]`
Related: #9921 (Deterministic Document Access Layer)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: yuch85 <yuch85.1@gmail.com>
Co-authored-by: Wang Qi <wangq8@outlook.com>
2026-04-27 11:57:06 +08:00
# Attach PDF outline as transient metadata on the first chunk.
# task_executor.py will extract and persist it as document metadata.
if res and pdf_parser and getattr ( pdf_parser , " outlines " , None ) :
res [ 0 ] [ " __outline__ " ] = [
{ " title " : title , " depth " : depth }
2026-04-30 11:55:02 +08:00
for title , depth , * _ in pdf_parser . outlines
feat: persist PDF bookmark outline as document metadata (#13287)
## Summary
PDF files often contain a bookmark/outline tree (table of contents built
into the file by the authoring tool). RAGFlow's `pdf_parser.outlines`
already extracts these `(title, depth)` tuples via pypdf, but they are
used ephemerally during chunking (`manual` parser uses them for
hierarchy detection) and then discarded.
This PR persists the outline as `doc.meta_fields["outline"]` — a JSON
array of `{"title": str, "depth": int}` objects — so downstream features
can use the structural information.
### Why this matters
- **Complementary to `toc_extraction`** — the existing `toc_extraction`
feature uses LLM calls to generate a TOC and only works for the `naive`
parser. The raw PDF outline is free (already extracted by pypdf), works
for all parsers, and captures the author's original document structure.
- **Document navigation** — frontends can render a clickable TOC from
the outline
- **Entity extraction** — the outline provides a structural map for
identifying document sections and key topics
- **Search result context** — knowing which section a chunk belongs to
helps users evaluate relevance
### Changes
| File | Change | LOC |
|------|--------|-----|
| `rag/app/naive.py` | Attach `pdf_parser.outlines` as `__outline__` on
first chunk dict | ~7 |
| `rag/app/manual.py` | Same for the manual parser | ~5 |
| `rag/svr/task_executor.py` | Extract `__outline__`, persist via
`DocMetadataService.update_document_metadata()` | ~12 |
### Design decisions
- **Transient key pattern**: The outline is passed from parser →
task_executor via `__outline__` on the first chunk dict, then removed
before indexing. This follows the same pattern as `metadata_obj` for
LLM-generated metadata.
- **No schema changes**: Uses the existing `meta_fields` JSON column on
the document table.
- **Graceful degradation**: If a PDF has no outline (common for scanned
docs), nothing is stored. If persistence fails, it logs a warning and
continues — parsing is not interrupted.
### Backward compatibility
- **Fully backward compatible** — no existing fields, behavior, or
schemas changed
- PDFs without outlines are unaffected
- Existing `meta_fields` data is preserved (merged, not overwritten)
## Test plan
- [ ] Parse a PDF with bookmarks (e.g. any multi-chapter document),
verify `meta_fields["outline"]` is populated
- [ ] Parse a PDF without bookmarks, verify no errors and no outline key
in meta_fields
- [ ] Verify existing `meta_fields` data is preserved (not overwritten)
when outline is added
- [ ] Verify `manual` parser also persists outlines
- [ ] Verify outline JSON structure: `[{"title": "Chapter 1", "depth":
0}, ...]`
Related: #9921 (Deterministic Document Access Layer)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: yuch85 <yuch85.1@gmail.com>
Co-authored-by: Wang Qi <wangq8@outlook.com>
2026-04-27 11:57:06 +08:00
]
2024-08-15 09:17:36 +08:00
return res
if __name__ == " __main__ " :
import sys
def dummy ( prog = None , msg = " " ) :
pass
chunk ( sys . argv [ 1 ] , from_page = 0 , to_page = 10 , callback = dummy )