Skip to content

fix: use each chunk's own position for its wider_text window - #276

Open
MohammadHijjawi97 wants to merge 3 commits into
zilliztech:masterfrom
MohammadHijjawi97:fix/splitter-wider-text-repeated-chunks
Open

MohammadHijjawi97 wants to merge 3 commits into
zilliztech:masterfrom
MohammadHijjawi97:fix/splitter-wider-text-repeated-chunks

Conversation

@MohammadHijjawi97

Copy link
Copy Markdown

_sentence_window_split located each split chunk with original_text.index(doc_text), which always returns the first occurrence. When a chunk's text also appears earlier in the document (e.g. a short trailing chunk, or a repeated paragraph), its wider_text was cut from the earlier location. NaiveRAG, DeepSearch and ChainOfRAG send wider_text to the LLM when text_window_splitter is enabled, so the answer was built from unrelated context.

Example with offset=3, original text "x" + "."*20 + "y" + "-"*20 + "x", and chunks x, y, x:

  • before: the third chunk's wider_text is "x..", the window of the first x
  • after: "---x"

Changes:

  • Search for each chunk starting from the previous chunk's position, since split docs are in document order. If it is not found there, fall back to a search from the start.
  • The right-hand window now includes offset characters, like the left-hand one. It was one character short, contrary to the docstring.

Tests:

  • Added test_sentence_window_split_repeated_text in tests/loader/test_splitter.py; it fails on master and passes with this change.
  • pytest tests/loader/test_splitter.py: 4 passed
  • ruff format --diff and ruff check: clean

_sentence_window_split located every split chunk with
original_text.index(doc_text), which always returns the first
occurrence. When a chunk's text also appears earlier in the document
(e.g. a short trailing chunk or a repeated paragraph), its wider_text
was taken from the earlier location, so the agents that use
text_window_splitter passed unrelated context to the LLM. Search from
the previous chunk's position instead, since split docs are in document
order.

Also make the right-hand window include `offset` characters like the
left-hand one; it was one character short.

Signed-off-by: Mohammad Hijjawi <mohammad.hijjawi1997@gmail.com>
@sre-ci-robot

Copy link
Copy Markdown
Collaborator

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: MohammadHijjawi97
To complete the pull request process, please assign xiaofan-luan after the PR has been reviewed.
You can assign the PR to them by writing /assign @xiaofan-luan in a comment when ready.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

Comment thread deepsearcher/loader/splitter.py Outdated
start_index = original_text.find(doc_text, search_start)
if start_index == -1:
start_index = original_text.index(doc_text)
search_start = start_index + 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

deepsearcher/loader/splitter.py line:74
Medium ---- The search_start = start_index + 1 heuristic still mislocates a chunk whose exact text also occurs inside the previously matched span. Example: original_text = "XABCABCY" with chunks ["XABC", "ABCY"] — after matching XABC at 0, find("ABCY", 1) returns 1 (inside XABC) instead of the chunk's real position 4, so its wider_text is still cut from the wrong location. This is the same repeated-text class the PR targets. Consider deriving the next search start from the previous match's end (accounting for chunk overlap) or validating the monotonic position, and add a regression test for a chunk whose text is a substring of the previous chunk.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in 821f980. A small correction to the example: "XABCABCY".find("ABCY", 1) returns 4, not 1.

The problem is real, though, whenever a chunk's text also occurs inside the previous chunk. For example, with "XABCABC" and chunks ["XABC", "ABC"], the old search found "ABC" at 1 instead of 4. The splitter produces this too: "ab ab ab ab ab" with chunk_size=5 and chunk_overlap=0 was placed at 0, 3, 6, 9 instead of 0, 6, 9, 12.

The fix:

  • _sentence_window_split now takes chunk_overlap, which split_docs_to_chunks passes in.
  • The next search starts at max(previous_start + 1, previous_end - chunk_overlap), the same bound langchain uses for start_index.
  • If nothing is found there, it falls back to the looser searches.

Regression tests cover the "XABCABC" case, and compare every wider_text against the splitter's own add_start_index positions with overlap 0 and 1.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The issue is confirmed fixed.

Comment thread deepsearcher/loader/splitter.py Outdated
# locate repeated text at its own position instead of its first occurrence.
start_index = original_text.find(doc_text, search_start)
if start_index == -1:
start_index = original_text.index(doc_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.

deepsearcher/loader/splitter.py line:73
Low ---- If find returns -1 and the chunk text is not present anywhere in original_text, this fallback still raises ValueError, which propagates through split_docs_to_chunks and aborts the whole document load. The failure mode is pre-existing, but the fallback path is now newly reachable when a chunk's text only occurs before the previous match (e.g., after splitter whitespace normalization). Consider handling the missing-text case explicitly or documenting the substring assumption.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in d31ccba. The fallback now uses find() from the start of the text instead of index(). If the chunk text is not in the original text at all, wider_text falls back to the chunk text itself and the search position is left unchanged, so the rest of the document still loads. Before this PR index() raised ValueError in this case too; it now degrades to no extra context. test_sentence_window_split_text_not_in_original covers it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The issue is confirmed fixed.

If a split chunk's text cannot be found anywhere in the original document
(for example after splitter normalization), use the chunk text itself as
wider_text instead of raising ValueError and aborting the document load.

Signed-off-by: Mohammad Hijjawi <mohammad.hijjawi1997@gmail.com>
Starting the search one character after the previous match could place a
chunk inside the previous chunk when its text also occurs there. Consecutive
split pieces overlap by at most chunk_overlap characters, so start the search
at max(previous start + 1, previous end - chunk_overlap), and pass
chunk_overlap from split_docs_to_chunks.

Signed-off-by: Mohammad Hijjawi <mohammad.hijjawi1997@gmail.com>

This branch has not been deployed

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants