perf(chunking): normalize each table cell once when splitting tables (about 1.7x faster)

DE
3d ago
+25-5

Summary

Splitting a table during chunking normalizes the text of every cell several times. _RowAccumulator asks each HtmlRow for its cell texts in will_fit(), again in add_rows(), again in flush() when it builds the chunk text, and once more when a row is retried after a flush. Each call ran normalize_html_cell_text() on every cell, and that is most of the cost of splitting a large table.

HtmlRow.iter_cell_texts() now reads a cached_property that normalizes the row's cells once, the same way HtmlRow.html and HtmlRow.text_len are already cached. Nothing changes a row's <tr> after the HtmlRow is built (_clipped_tr() works on a deep copy), so the cached texts can't go stale. Chunk output is unchanged.

Case (chunking only)beforeafterspeedup
CSV, 5 rows x 4 cols, max_characters=5000.74 ms0.73 ms1.0x
CSV, 30 rows x 4 cols, max_characters=5003.8 ms2.9 ms1.3x
CSV, 500 rows x 12 cols, max_characters=100088 ms49 ms1.8x
CSV, 3,000 rows x 6 cols, max_characters=500, chunk_elements()301 ms193 ms1.6x
HTML report, 20 sections each with a 30-row table, max_characters=1500131 ms95 ms1.4x

CPU time, median of 5 alternating same-process rounds, Python 3.13, lxml 6.1.3, Ryzen 5 5600X, Linux. Token-based chunking (max_tokens=100, 1,000-row CSV) went from 146 ms to 108 ms.

Validation

  • Old and new iter_cell_texts() give identical chunk_by_title() and chunk_elements() output (type, text and text_as_html) and identical per-row cell texts on randomly generated tables with non-ASCII text, entities, <br>, inline markup, nested tables, blank cells, <th>, colspan/rowspan (including 0 and invalid values), <thead>/<tbody>, character and token limits, overlap, repeat_table_headers and isolate_table.
  • A new test checks that iterating a row's cell texts three times normalizes each cell once.
  • test_unstructured/chunking/ and test_unstructured/common/ pass; ruff check and ruff format --check are clean.

Checklist

  • The PR title is a user-facing release-note entry with a conventional commit prefix.
  • Apply a category label (enhancement, bug, or an appropriate existing label), or ask a maintainer to apply it.
  • For breaking changes, use ! in the title and explain impact and migration above.