[PR #29551] perf(core/rag): optimize Excel extractor performance and memory usage #32461

Closed
opened 2026-02-21 20:51:27 -05:00 by yindo · 0 comments
Owner

Original Pull Request: https://github.com/langgenius/dify/pull/29551

State: closed
Merged: Yes


Important

  1. Make sure you have read our contribution guidelines
  2. Ensure there is an associated issue and you have been assigned to it
  3. Use the correct syntax to link this PR: Fixes #<issue number>.

Summary

The ExcelExtractor implementation for .xlsx files has severe memory leak issues due to full loading of data into Pandas DataFrames, especially when processing files with "ghost columns" (empty columns that are technically part of the sheet). Additionally, the header detection logic is simplistic (always assuming the first row is the header), which fails for files with titles or metadata in the first few rows, leading to incorrect column mapping and data loss.

Steps To Reproduce

  1. Upload an .xlsx file that has:
    • A title in the first row (merged cells).
    • The actual header in the second row.
    • A large number of empty "ghost columns" (e.g., formatting applied to 16,000+ columns).
  2. Trigger the indexing estimate or extraction process.
  3. Observe:
    • Memory Usage: Rapid spike in memory usage leading to OOM (Out Of Memory) crashes because openpyxl's sheet.values combined with pd.DataFrame attempts to materialize millions of None cells.
    • Data Extraction: Incorrect data parsing because the first row (title) is treated as the header, causing subsequent rows to misalign or lose data.

Root Cause

  1. Memory Leak: Using pd.DataFrame(sheet.values) forces the loading of all cells, including empty ones in the used range. If the used range is large (due to ghost columns), this consumes gigabytes of memory.
  2. Header Logic: The code assumes row[0] is the header.
  3. Data Consistency: The extraction logic if value: skips empty cells, resulting in missing keys in the JSON output for sparse columns.

Proposed Fix

  1. Switch to Streaming: Use openpyxl.load_workbook(..., read_only=True) and sheet.iter_rows() to process data row by row.
  2. Smart Header Detection: Implement a heuristic to scan the first N rows (e.g., 10) to find the best header candidate (prioritizing rows with multiple string columns).
  3. Boundary Limiting: Calculate max_col_idx from the detected header and enforce this limit in iter_rows(max_col=...) to prevent reading ghost columns.
  4. Consistency: Ensure all columns defined in the header are present in the output Document, setting empty values to "" instead of omitting the key.

Acceptance Criteria

  • Process large .xlsx files with ghost columns with constant low memory usage.
  • Correctly identify headers in files where the header is not the first row (e.g., Row 2).
  • Output documents must contain all keys defined in the header, even for empty cells.
  • Preserve existing hyperlink formatting logic.

Severity

  • High: The memory leak can crash the entire API service when a user uploads a single malformed/complex Excel file.

#29523

Screenshots

Before After
... ...

Checklist

  • This change requires a documentation update, included: Dify Document
  • I understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!)
  • I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
  • I've updated the documentation accordingly.
  • I ran dev/reformat(backend) and cd web && npx lint-staged(frontend) to appease the lint gods
**Original Pull Request:** https://github.com/langgenius/dify/pull/29551 **State:** closed **Merged:** Yes --- > [!IMPORTANT] > > 1. Make sure you have read our [contribution guidelines](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) > 1. Ensure there is an associated issue and you have been assigned to it > 1. Use the correct syntax to link this PR: `Fixes #<issue number>`. ## Summary <!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. --> The ExcelExtractor implementation for .xlsx files has severe memory leak issues due to full loading of data into Pandas DataFrames, especially when processing files with "ghost columns" (empty columns that are technically part of the sheet). Additionally, the header detection logic is simplistic (always assuming the first row is the header), which fails for files with titles or metadata in the first few rows, leading to incorrect column mapping and data loss. **Steps To Reproduce** 1. Upload an `.xlsx` file that has: - A title in the first row (merged cells). - The actual header in the second row. - A large number of empty "ghost columns" (e.g., formatting applied to 16,000+ columns). 2. Trigger the indexing estimate or extraction process. 3. Observe: - **Memory Usage**: Rapid spike in memory usage leading to OOM (Out Of Memory) crashes because `openpyxl`'s `sheet.values` combined with `pd.DataFrame` attempts to materialize millions of `None` cells. - **Data Extraction**: Incorrect data parsing because the first row (title) is treated as the header, causing subsequent rows to misalign or lose data. **Root Cause** 1. **Memory Leak**: Using `pd.DataFrame(sheet.values)` forces the loading of all cells, including empty ones in the used range. If the used range is large (due to ghost columns), this consumes gigabytes of memory. 2. **Header Logic**: The code assumes `row[0]` is the header. 3. **Data Consistency**: The extraction logic `if value:` skips empty cells, resulting in missing keys in the JSON output for sparse columns. **Proposed Fix** 1. **Switch to Streaming**: Use `openpyxl.load_workbook(..., read_only=True)` and `sheet.iter_rows()` to process data row by row. 2. **Smart Header Detection**: Implement a heuristic to scan the first N rows (e.g., 10) to find the best header candidate (prioritizing rows with multiple string columns). 3. **Boundary Limiting**: Calculate `max_col_idx` from the detected header and enforce this limit in `iter_rows(max_col=...)` to prevent reading ghost columns. 4. **Consistency**: Ensure all columns defined in the header are present in the output `Document`, setting empty values to `""` instead of omitting the key. **Acceptance Criteria** - Process large `.xlsx` files with ghost columns with constant low memory usage. - Correctly identify headers in files where the header is not the first row (e.g., Row 2). - Output documents must contain all keys defined in the header, even for empty cells. - Preserve existing hyperlink formatting logic. **Severity** - **High**: The memory leak can crash the entire API service when a user uploads a single malformed/complex Excel file. #29523 ## Screenshots | Before | After | |--------|-------| | ... | ... | ## Checklist - [ ] This change requires a documentation update, included: [Dify Document](https://github.com/langgenius/dify-docs) - [x] I understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!) - [x] I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change. - [x] I've updated the documentation accordingly. - [x] I ran `dev/reformat`(backend) and `cd web && npx lint-staged`(frontend) to appease the lint gods
yindo added the pull-request label 2026-02-21 20:51:27 -05:00
yindo closed this issue 2026-02-21 20:51:27 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langgenius/dify#32461