Skip to content

fix(chunking): record chunk offsets into the original source, not the normalized text - #44

Merged
charan-rathore merged 15 commits into
mainfrom
fix/original-source-offsets
Oct 19, 2026
Merged

charan-rathore merged 15 commits into
mainfrom
fix/original-source-offsets

Conversation

@charan-rathore

Copy link
Copy Markdown
Owner

Problem

The recursive chunker normalized whitespace before computing offsets, then recorded the normalized positions as offsets into the original source. Every space run, extra newline, or leading/trailing whitespace shifted all later offsets, so citations and highlight spans sliced the wrong text.

Before - source " Alpha beta.\n\nGamma delta. " produced one chunk with offsets (0, 25), but source[0:25] is " Alpha beta.\n\nGamma de" - it ends mid-word and includes leading whitespace:

before: offsets (0,25) -> source[0:25] == '  Alpha   beta.\n\nGamma de'   (WRONG)

After - the same chunk now carries (2, 31), and source[2:31] is exactly the chunk's content:

after:  offsets (2,31) -> source[2:31] == 'Alpha beta.\n\nGamma delta.'   (RIGHT)

(The chunk text itself is the normalized string; the offsets now point at where that content actually lives in the source, so slicing and normalizing the slice reproduces the chunk.)

Fix

  • chunking/utils.py gains normalize_whitespace_with_map, which performs the same transformation as normalize_whitespace but also returns an index map from normalized position to original-source position, plus remap_offsets_to_source to translate a chunk's offsets through that map (end offsets stay exclusive).
  • recursive.py, semantic.py (all three return paths), and structure_aware.py apply the remap as a post-pass before building the result.
  • github_chunker is untouched: its summary-chunk offsets index synthesized text (# {title}\n\n...), not the source document, so there is nothing to remap.

Tests

  • New failing-first tests: source[start:end] must cover the chunk text (after whitespace normalization) for the recursive, structure-aware, and semantic chunkers. Verified RED on main, GREEN with the fix.
  • Full chunking suite: 150 passed, 2 skipped.
  • Whole repo: 260 passed vs 256 on main (+4 new); the same 26 pre-existing failures outside chunking/ on both.

Signed-off-by: Charan Rathore 180254320+charan-rathore@users.noreply.github.com

…e_whitespace collapses space/newline runs and strips the ends before splitting, but span offsets were recorded as-is - source[start:end] missed the chunk's real bytes and citation spans pointed at the wrong text. normalize_whitespace_with_map returns the index map back to the source; chunkers remap through it. Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Added functions to normalize whitespace with source mapping and remap offsets.
…e Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Updated text normalization to include mapping to original source offsets for accurate citation handling.
… Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Updated text normalization to include mapping to source offsets for citation and highlighting purposes.
… source Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Replaced normalize_whitespace with normalize_whitespace_with_map to retain source offsets.
…Rathore <180254320+charan-rathore@users.noreply.github.com>
…l source Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Added tests to ensure offsets index the original source correctly, addressing issues with normalized whitespace affecting citation spans.
…riginal source Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Added unit tests for StructureAwareChunker to verify original-source offset coverage.
… source Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Add tests for semantic chunker to validate original-source offsets.
@vercel

vercel Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
intellirag Ready Ready Preview Oct 19, 2026 9:31am UTC
intellirag-live-own-track Ready Ready Preview Oct 19, 2026 9:31am UTC
irag-fix-0904 Ready Ready Preview Oct 19, 2026 9:31am UTC

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-19T09:56:09.173077Z 93a53c7 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 01c0f8eac3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread rag-platform/libs/rag/chunking/utils.py Outdated
Comment on lines +107 to +108
out.append(c)
idx_map.append(i)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid allocating an integer for every source character

For large documents with little collapsible whitespace, this appends a distinct Python integer for nearly every character, adding roughly tens of bytes per character on top of the original and normalized strings. A document of tens of megabytes can therefore require well over a gigabyte merely to construct the offset map, potentially terminating ingestion with an OOM. Represent normalization changes as sparse runs/boundaries or collect only the source positions needed for chunk endpoints instead.

Useful? React with 👍 / 👎.

Comment on lines 92 to +95
version_id=version_id,
base_metadata=base_metadata,
)
remap_offsets_to_source(chunks, norm_to_source, len(text))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Remap offsets to include the prepended overlap

Whenever overlap is enabled and the document produces multiple chunks, _create_chunks_with_overlap prepends text from the preceding span but leaves start_char_offset at the current span's start; this post-pass merely translates that unchanged start. For example, a second chunk whose text begins with has words\n\n. Second... can receive an offset pointing at . Second..., so slicing the source omits the overlap and citations/highlights do not cover the returned chunk text. Track the normalized source position of the selected overlap and remap that as the chunk start.

Useful? React with 👍 / 👎.

version_id=version_id,
base_metadata=base_metadata,
)
remap_offsets_to_source(chunks, norm_to_source, len(text))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve discarded separators before remapping recursive spans

Even with overlap disabled, this remaps span positions that are already inaccurate when a separator-prefixed segment requires deeper recursive splitting. _split_on_separator filters whitespace-only pieces without retaining their lengths, so _recursive_split does not advance over them; for example, splitting Alpha alpha alpha.\n\nBeta beta beta. with a small chunk size assigns the Beta chunk a source slice of \n\nBet, and every following offset remains shifted. Preserve the consumed separator gaps or derive spans from match positions before translating them through the source map.

Useful? React with 👍 / 👎.

…arators in offset accounting Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Normalize whitespace in chunk creation process.
…ed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>
…gressions Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Added tests for overlap coverage and discarded separators in RecursiveChunker.
@vercel

vercel Bot commented Oct 19, 2026

Copy link
Copy Markdown

Deployment failed for project intellirag-live-own-track with the following error:

Resource is limited - try again in 24 hours (more than 100, code: "api-deployments-free-per-day").

Learn More: https://vercel.com/own-track?upgradeToPro=build-rate-limit

@vercel

vercel Bot commented Oct 19, 2026

Copy link
Copy Markdown

Deployment failed for project intellirag with the following error:

Resource is limited - try again in 24 hours (more than 100, code: "api-deployments-free-per-day").

Learn More: https://vercel.com/own-track?upgradeToPro=build-rate-limit

…ints Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Refactor whitespace normalization to use NormalizationMap for tracking source indices. Update related functions to accommodate the new mapping structure.
@vercel

vercel Bot commented Oct 19, 2026

Copy link
Copy Markdown

Deployment failed for project irag-fix-0904 with the following error:

Resource is limited - try again in 24 hours (more than 100, code: "api-deployments-free-per-day").

Learn More: https://vercel.com/own-track?upgradeToPro=build-rate-limit

@charan-rathore

Copy link
Copy Markdown
Owner Author

@codex review

All three findings addressed, each with failing-first regressions:

  1. Overlap coverage (P1, recursive.py + semantic.py): chunk start now moves back over the prepended overlap (a suffix of the previous span), so source[start:end] covers the whole chunk text; the joined text is normalized so the synthetic separator cannot drift from the slice.
  2. Discarded separators (P1, recursive.py): _split_on_separator no longer drops whitespace-only pieces - _recursive_split skips them as spans but advances the offset over them, so deeper splits stay aligned.
  3. Sparse offset map (P2, utils.py): the per-character int list is replaced by a checkpoint list (one entry per actual collapse) with binary-search translation - memory scales with collapses, not document length (regression test: 100k-char document, <100 checkpoints).

Chunking suite: 153 passed, 2 skipped; whole repo 263 passed, same 26 pre-existing failures outside the change.

Signed-off-by: Charan Rathore 180254320+charan-rathore@users.noreply.github.com

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3480fda1aa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# The overlap is a suffix of the previous span: move the
# start back over it so citations/highlights cover the
# whole chunk text, not just the current span.
start = previous_end - len(overlap)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Anchor overlap starts to source-backed span lengths

When adjacent spans have been merged, merge_small_spans can add a synthetic "\n\n" even though the following span already contains its separator, so len(previous_text) may exceed previous_end - previous_start. With a sufficiently large overlap that returns the whole merged text, this subtraction produces a negative start (for example, four paragraph-sized spans with chunk_size=35, chunk_overlap=30, min_chunk_size=15, and max_chunk_size=40 yields start_char_offset=-2), and Python slicing then omits the entire overlap. Fresh evidence in the current tree is that the new length-based correction assumes the generated overlap is a source suffix; track its actual normalized source start instead. The identical calculation in the semantic chunker needs the same treatment.

Useful? React with 👍 / 👎.

Comment thread rag-platform/libs/rag/chunking/utils.py Outdated
if j != prev_src + (len(out) - prev_norm):
checkpoints.append((len(out), j))
else:
out.append(c)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid buffering every normalized character in a list

For documents with little collapsible whitespace, this branch appends one list element for every source character until the final join, so the supposedly sparse implementation still carries roughly 9 bytes of transient traced allocation per ASCII character in addition to the source and result strings. Large ingestion inputs can therefore consume hundreds of megabytes solely for out and still OOM; accumulate unchanged runs or use a streaming string buffer so auxiliary memory is governed by normalization boundaries rather than document length.

Useful? React with 👍 / 👎.

…n Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Refactor chunk overlap handling to clamp overlap length to prevent negative start offsets.
…egion Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Added a test to ensure overlap start does not go before the previous span's source region.
@charan-rathore

Copy link
Copy Markdown
Owner Author

@codex review

Fixed: the overlap is now clamped to the previous span's source-backed length (previous_end - previous_start), so a merge_small_spans-inflated span text can no longer push the overlap start negative or before the previous span's source region. Failing-first regression added with a post-merge-shaped span (RED before, GREEN after); chunking suite 154 passed, whole repo 264 passed with only main's pre-existing failures.

Signed-off-by: Charan Rathore 180254320+charan-rathore@users.noreply.github.com

…pends Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Updated normalization logic to handle newline characters and adjusted normalization length calculation.
@charan-rathore

Copy link
Copy Markdown
Owner Author

@codex review

Addressed the normalization-memory finding: the output side no longer appends one list element per source character - unchanged characters are flushed as whole source slices, so allocations scale with the number of runs instead of document length. Behavior-preserving: a 3000-case fuzz check against the previous implementation gives byte-identical output and in-bounds mapping, and the chunking suite (154) plus the whole repo suite pass unchanged.

Signed-off-by: Charan Rathore 180254320+charan-rathore@users.noreply.github.com

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: 93a53c7bb2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@charan-rathore
charan-rathore merged commit ea6fd85 into main Oct 19, 2026
3 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant