You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I have searched for existing issues (including closed ones).
I confirm that I am using English to submit this report.
Non-English title submissions will be closed directly.
I have not modified this template.
Describe the problem
The single string field parser_config.delimiter is consumed by seven different parsing implementations depending only on the file extension. They disagree on the parsing rule, at least one silently makes the shipped default value a no-op, and the whitespace handling has five distinct bugs (see "Whitespace-specific gotchas" below).
Affected functions, all of which read parser_config.delimiter:
Location
Triggered by
Behavior
rag/nlp/__init__.py::get_delimiters
PDF/DOCX/HTML/EPUB/JSON/CSV/XLSX/email/book, only when no backticks present
Bare chars + backtick-wrapped tokens, longest-first sort, no dedupe, uses re.I.
rag/nlp/__init__.py::naive_merge (inline regex)
Same files, whenever any backtick is present
Drops bare chars, only backtick-wrapped tokens, deduped, longest-first sort.
Only backtick-wrapped, deduped, longest-first sort. Different field (consumes an array of strings, not parser_config.delimiter). Out of scope.
Concrete user-visible bugs
The shipped default is a no-op for markdown.
The default for .md/.mdx/.markdown is parser_config.get("delimiter", "\n!?;。;!?") (see rag/app/naive.py:1132). MarkdownElementExtractor.get_delimiters only matches backtick-wrapped tokens, so the default delimiter string is silently dropped. The markdown path splits purely by markdown structure (headers/code blocks/lists); the user's delimiter setting is ignored.
Mixing bare chars with backtick-wrapped tokens loses the bare chars in five paths.
For input \n`##`; (the example in the tooltip), naive_merge, naive_merge_with_images, and _build_cks enter their has_custom branch and drop \n and ;, keeping only ##. PDF/DOCX/email/book files chunk only on ## until chunk_token_num is reached. The same string is interpreted correctly by parser_txt.
TxtParser doesn't sort longest-first.
For input `##`# the regex alternation ##|# matches # first (Python alternation is leftmost-first with no backtracking), so the .txt/code paths mis-split overlapping delimiters. The .md path on the same string sorts longest-first and gets it right.
Inconsistent semantics for markdown vs everything else.
In .md, the delimiter is an additional splitter on top of markdown structure (element extraction is the primary split). In .txt, it is the primary splitter. In .pdf/.docx, it is a sentence-boundary-aware splitter inside naive_merge. Same field name, three different roles.
Duplicates aren't deduped in get_delimiters.
Input `a`a`a` produces regex a|a|a instead of a. Cosmetic, but symptomatic of the duplication.
The parser does handle whitespace characters as delimiters (re.escape(" ") → "\\ ", re.escape("\n") → "\\n", etc.) but with several sharp edges. Empirical results from running the three parser variants against text = "alpha beta\tgamma\n\ndelta epsilon zeta\n":
Bare whitespace is silently dropped by markdown and has_custom.
This is the same bug as impl tags api #2 above, but for whitespace. A user who pastes a paragraph break \n\n for a .pdf gets the same splits as a single newline because the bare-char dedupe-via-alternation produces \\\n|\\\n. The correct input for paragraph splitting is `\n\n` (backtick-wrapped), but nothing in the UI tells users this.
Double-whitespace bare form is wrong. \n\n (bare two newlines) and \t\t (bare two tabs) and (bare two spaces) all collapse to the single-char equivalent in the bare-char fallback path. Multi-character whitespace delimiters MUST be backtick-wrapped to work correctly. The frontend gives no hint about this.
CRLF normalization only happens in naive_merge. naive_merge line 1163 normalizes line endings before splitting:
But _build_cks, naive_merge_with_images, parser_txt, and markdown_parser have no equivalent normalization. On Windows-line-ending files, parser_txt and _build_cks will produce double-splits (one at \r, one at \n) for any user with a \r or \r\n in their delimiter.
The frontend only round-trips \n / \t / \r. web/src/components/delimiter-form-field.tsx translates the literal 2-char escape sequences to real characters on input and back again on display, but only for those three. Other whitespace escapes survive as literal characters:
User types
Stored as
Works as delimiter?
\n
real newline char
✓ (split on every newline)
\t
real tab char
✓ (split on every tab)
\r
real CR char
✓ (but see CRLF double-split above)
\f
literal \f (2 chars)
matches literal backslash-f in text, not form-feed
\v
literal \v (2 chars)
matches literal backslash-v, not vertical-tab
\u00a0
literal \u00a0 (6 chars)
matches literal backslash-u-0-0-a-0, not NBSP
\\n (4 source chars)
literal \\n (3 chars)
matches literal \n in text
An actual NBSP pasted in
real \u00a0 char
✓
So the supported whitespace escapes are only \n, \t, \r. Other whitespace requires either pasting the actual character or using the API.
Whitespace-only delimiters are invisible in the input.
A delimiter value of " " (single space) makes the input box look empty — the value is stored as a single space character, but the user can't tell whether they typed anything. There's no character count, no separator visualization. This is why users default to multi-character delimiters and then trip on bug init python part #7.
Reproduction
# In a Python shell with the same modules:fromrag.nlpimportnaive_merge, get_delimitersfromdeepdoc.parser.txt_parserimportRAGFlowTxtParserfromdeepdoc.parser.markdown_parserimportMarkdownElementExtractors="\n`##`;"# what the tooltip tells users to typeprint(get_delimiters(s)) # backslash-n + ## + ;print(naive_merge(["x##y"], 128, s)) # only '##' is usedprint(RAGFlowTxtParser.parser_txt("x##y\n", 128, s)) # all three usedprint(MarkdownElementExtractor("x##y").extract_elements(s)) # only '##'
The four functions return four different sets of effective delimiters for the same input string. The same divergence applies to whitespace: bare \n works in parser_txt but is dropped by markdown and naive_merge's has_custom branch.
Proposed solution
Collapse all six Python implementations into one shared helper (e.g. in rag/nlp/__init__.py or a new rag/nlp/delim.py) and call it from every site. The helper should:
Accept a string.
Treat anything between matching ` characters as one multi-character delimiter (escape its content with re.escape).
Treat every character outside backticks as its own single-character delimiter.
Deduplicate.
Sort longest-first so ## matches before #.
Return the compiled regex pattern, or "" for empty / whitespace-only input.
Apply CRLF/CR normalization to the input string once at the top of the helper, before parsing. \r\n → \n, standalone \r → \n. This single change fixes the Windows-line-ending double-split bug across all five paths.
The .md path additionally needs a fallback when the helper returns "": it should fall back to the existing markdown structure-based extraction (_extract_delimited_elements with no pattern), which is what already happens for the default delimiter. That is the correct behavior for the markdown case — the user has not opted into extra splitting — and matches what the markdown parser does today for the bare-chars-only default.
The proposed behavior (extended to include whitespace cases):
Input
Effective delimiters
""
none
"!"
!
"!?!?;"
!, ?, ;
" " (single bare space)
" " (two bare spaces)
(deduped)
"\t"
\t
"\n"
\n
"\n\n" (bare two newlines)
\n (deduped — document this, do not silently change)
"\r\n"
\n (after CRLF normalization)
` `
`\n\n`
\n\n (paragraph break)
`\r\n`
\r\n (after normalization, \n)
`######`
###, ##, #
`\t\t`
\t\t
Behavioral note on bare multi-whitespace: collapsing "\n\n" to "\n" (via dedupe) is a change from today's behavior where the regex alternation \\\n|\\\n happens to match the same text. The output for any given text is identical, but if a downstream caller introspects the produced delimiter list (none do today), the result changes. The acceptance test should pin this down explicitly.
Scope
Six implementations of the same parser → one helper.
CRLF normalization centralized in the helper.
No API or schema change.
No behavior change for inputs that previously worked; only the divergent cases become consistent.
Files to change
rag/nlp/__init__.py — remove the three inline re.finditer snippets from naive_merge, naive_merge_with_images, _build_cks; remove the get_delimiters function (or re-export it from the new helper for backwards compat); have all three call the helper. Also drop the per-call-site CRLF normalization in naive_merge (now done in the helper).
deepdoc/parser/txt_parser.py — replace the inline regex with a call to the helper. Drop the encode("utf-8").decode("unicode_escape").encode("latin1").decode("utf-8") dance: that round-trip was defending against users passing literal \n (2 chars) over the API; the frontend already converts \n → real newline, and the helper does no further escaping, so the dance is redundant.
deepdoc/parser/markdown_parser.py — replace MarkdownElementExtractor.get_delimiters with a call to the helper. The fallback to structure-based extraction when the helper returns "" should be preserved.
web/src/components/delimiter-form-field.tsx — extend the escape round-trip table to either (a) include \f, \v, \u00a0, \\ for consistency, or (b) explicitly reject unknown escapes with a validation error. Pick one and document it. Today they are silently mishandled.
New: rag/nlp/delim.py (or similar) — single source of truth.
Acceptance criteria
All six sites produce the same regex pattern for the same input string.
The shipped default "\n!?;。;!?" keeps working for .txt, .pdf, .docx, etc.
The shipped default for .md/.mdx continues to result in structure-based splitting (no extra delimiter splits), because the helper returns "" for it.
The tooltip's example \n`##`; produces three effective delimiters regardless of file type.
A bare-whitespace input (" ", "\t", "\n") splits on every occurrence of that whitespace character.
A backtick-wrapped whitespace input (`\n\n`, `\t\t`) splits only on the exact N-character whitespace sequence.
A \r\n-line-ending document is split identically to a \n-line-ending document for every delimiter, regardless of file type.
Whitespace-only delimiter values render visibly in the input (e.g. a placeholder glyph or character counter). Optional UX criterion; can ship in a follow-up if it bloats this PR.
New unit tests in test/unit_test/rag/test_naive_merge.py (and a new test/unit_test/rag/test_delim.py) cover the table above plus empty input, dedupe, longest-first, unicode characters, embedded backticks, CRLF normalization, and every escape in the frontend's round-trip table.
Self Checks
Describe the problem
The single string field
parser_config.delimiteris consumed by seven different parsing implementations depending only on the file extension. They disagree on the parsing rule, at least one silently makes the shipped default value a no-op, and the whitespace handling has five distinct bugs (see "Whitespace-specific gotchas" below).Affected functions, all of which read
parser_config.delimiter:rag/nlp/__init__.py::get_delimitersre.I.rag/nlp/__init__.py::naive_merge(inline regex)rag/nlp/__init__.py::naive_merge_with_images(inline regex)naive_merge.rag/nlp/__init__.py::_build_cks(inline regex).docxnaive_merge.deepdoc/parser/txt_parser.py::parser_txt(inline regex).txt,.py,.js,.java,.c,.cpp,.h,.php,.go,.ts,.sh,.cs,.kt,.sqlre.I, no CRLF normalization.deepdoc/parser/markdown_parser.py::MarkdownElementExtractor.get_delimiters.md,.mdx,.markdownrag/flow/chunker/token_chunker.py::_compile_delimiter_patternToken chunkercomponentparser_config.delimiter). Out of scope.Concrete user-visible bugs
The shipped default is a no-op for markdown.
The default for
.md/.mdx/.markdownisparser_config.get("delimiter", "\n!?;。;!?")(seerag/app/naive.py:1132).MarkdownElementExtractor.get_delimitersonly matches backtick-wrapped tokens, so the default delimiter string is silently dropped. The markdown path splits purely by markdown structure (headers/code blocks/lists); the user's delimiter setting is ignored.Mixing bare chars with backtick-wrapped tokens loses the bare chars in five paths.
For input
\n`##`;(the example in the tooltip),naive_merge,naive_merge_with_images, and_build_cksenter theirhas_custombranch and drop\nand;, keeping only##. PDF/DOCX/email/book files chunk only on##untilchunk_token_numis reached. The same string is interpreted correctly byparser_txt.TxtParserdoesn't sort longest-first.For input
`##`#the regex alternation##|#matches#first (Python alternation is leftmost-first with no backtracking), so the.txt/code paths mis-split overlapping delimiters. The.mdpath on the same string sorts longest-first and gets it right.Inconsistent semantics for markdown vs everything else.
In
.md, the delimiter is an additional splitter on top of markdown structure (element extraction is the primary split). In.txt, it is the primary splitter. In.pdf/.docx, it is a sentence-boundary-aware splitter insidenaive_merge. Same field name, three different roles.Duplicates aren't deduped in
get_delimiters.Input
`a`a`a`produces regexa|a|ainstead ofa. Cosmetic, but symptomatic of the duplication.Case-insensitive delimiter matching (
re.I). Tracked separately as [Bug] Delimiter parsing usesre.I(case-insensitive), causing unintended splits on both cases of the same character #17384 — applies only toget_delimitersandparser_txt.Whitespace-specific gotchas
The parser does handle whitespace characters as delimiters (
re.escape(" ")→"\\ ",re.escape("\n")→"\\n", etc.) but with several sharp edges. Empirical results from running the three parser variants againsttext = "alpha beta\tgamma\n\ndelta epsilon zeta\n":get_delimiterspattern (naive_merge bare-char fallback)has_custompattern(bare)\\✓` `(wrapped)\\✓\\✓\t(bare)\\\t✓\n(bare)\\\n✓\n\n(bare)\\\n|\\\n✗ splits on single newlines`\n\n`(wrapped)\\\n\\\n✓\\\n\\\n✓\r\n(bare)\\\r|\\\n✗ double-split, empty chunks`\r\n`(wrapped)\\\r\\\n✓\\\r\\\n✓\t\t(bare)\\\t|\\\t✗ same as single tab`\t\t`(wrapped)\\\t\\\t✓\\\t\\\t✓\u00a0(bare)\xa0✓`\u00a0`(wrapped)\xa0✓\xa0✓Bare whitespace is silently dropped by markdown and
has_custom.This is the same bug as impl tags api #2 above, but for whitespace. A user who pastes a paragraph break
\n\nfor a.pdfgets the same splits as a single newline because the bare-char dedupe-via-alternation produces\\\n|\\\n. The correct input for paragraph splitting is`\n\n`(backtick-wrapped), but nothing in the UI tells users this.Double-whitespace bare form is wrong.
\n\n(bare two newlines) and\t\t(bare two tabs) and(bare two spaces) all collapse to the single-char equivalent in the bare-char fallback path. Multi-character whitespace delimiters MUST be backtick-wrapped to work correctly. The frontend gives no hint about this.CRLF normalization only happens in
naive_merge.naive_mergeline 1163 normalizes line endings before splitting:But
_build_cks,naive_merge_with_images,parser_txt, andmarkdown_parserhave no equivalent normalization. On Windows-line-ending files,parser_txtand_build_ckswill produce double-splits (one at\r, one at\n) for any user with a\ror\r\nin their delimiter.The frontend only round-trips
\n/\t/\r.web/src/components/delimiter-form-field.tsxtranslates the literal 2-char escape sequences to real characters on input and back again on display, but only for those three. Other whitespace escapes survive as literal characters:\n\t\r\f\f(2 chars)\v\v(2 chars)\u00a0\u00a0(6 chars)\\n(4 source chars)\\n(3 chars)\nin text\u00a0charSo the supported whitespace escapes are only
\n,\t,\r. Other whitespace requires either pasting the actual character or using the API.Whitespace-only delimiters are invisible in the input.
A delimiter value of
" "(single space) makes the input box look empty — the value is stored as a single space character, but the user can't tell whether they typed anything. There's no character count, no separator visualization. This is why users default to multi-character delimiters and then trip on bug init python part #7.Reproduction
The four functions return four different sets of effective delimiters for the same input string. The same divergence applies to whitespace: bare
\nworks inparser_txtbut is dropped by markdown andnaive_merge'shas_custombranch.Proposed solution
Collapse all six Python implementations into one shared helper (e.g. in
rag/nlp/__init__.pyor a newrag/nlp/delim.py) and call it from every site. The helper should:`characters as one multi-character delimiter (escape its content withre.escape).##matches before#.""for empty / whitespace-only input.re.I— see [Bug] Delimiter parsing usesre.I(case-insensitive), causing unintended splits on both cases of the same character #17384.\r\n→\n, standalone\r→\n. This single change fixes the Windows-line-ending double-split bug across all five paths.The
.mdpath additionally needs a fallback when the helper returns"": it should fall back to the existing markdown structure-based extraction (_extract_delimited_elementswith no pattern), which is what already happens for the default delimiter. That is the correct behavior for the markdown case — the user has not opted into extra splitting — and matches what the markdown parser does today for the bare-chars-only default.The proposed behavior (extended to include whitespace cases):
"""!"!"!?!?;"!,?,;" "(single bare space)" "(two bare spaces)(deduped)"\t"\t"\n"\n"\n\n"(bare two newlines)\n(deduped — document this, do not silently change)"\r\n"\n(after CRLF normalization)` ``\n\n`\n\n(paragraph break)`\r\n`\r\n(after normalization,\n)`######`###,##,#`\t\t`\t\tScope
Files to change
rag/nlp/__init__.py— remove the three inlinere.finditersnippets fromnaive_merge,naive_merge_with_images,_build_cks; remove theget_delimitersfunction (or re-export it from the new helper for backwards compat); have all three call the helper. Also drop the per-call-site CRLF normalization innaive_merge(now done in the helper).deepdoc/parser/txt_parser.py— replace the inline regex with a call to the helper. Drop theencode("utf-8").decode("unicode_escape").encode("latin1").decode("utf-8")dance: that round-trip was defending against users passing literal\n(2 chars) over the API; the frontend already converts\n→ real newline, and the helper does no further escaping, so the dance is redundant.deepdoc/parser/markdown_parser.py— replaceMarkdownElementExtractor.get_delimiterswith a call to the helper. The fallback to structure-based extraction when the helper returns""should be preserved.web/src/components/delimiter-form-field.tsx— extend the escape round-trip table to either (a) include\f,\v,\u00a0,\\for consistency, or (b) explicitly reject unknown escapes with a validation error. Pick one and document it. Today they are silently mishandled.rag/nlp/delim.py(or similar) — single source of truth.Acceptance criteria
"\n!?;。;!?"keeps working for.txt,.pdf,.docx, etc..md/.mdxcontinues to result in structure-based splitting (no extra delimiter splits), because the helper returns""for it.\n`##`;produces three effective delimiters regardless of file type." ","\t","\n") splits on every occurrence of that whitespace character.`\n\n`,`\t\t`) splits only on the exact N-character whitespace sequence.\r\n-line-ending document is split identically to a\n-line-ending document for every delimiter, regardless of file type.test/unit_test/rag/test_naive_merge.py(and a newtest/unit_test/rag/test_delim.py) cover the table above plus empty input, dedupe, longest-first, unicode characters, embedded backticks, CRLF normalization, and every escape in the frontend's round-trip table.test_naive_merge.pystill pass.Related
re.I(case-insensitive), causing unintended splits on both cases of the same character #17384 — case-insensitive matching (re.I), independent small fix