Skip to content

fix(markdown): use a real tokenizer for the heading-split length check - #1394

Open
MsfPablo wants to merge 1 commit into
khoj-ai:masterfrom
MsfPablo:fix/cjk-chunking
Open

fix(markdown): use a real tokenizer for the heading-split length check#1394
MsfPablo wants to merge 1 commit into
khoj-ai:masterfrom
MsfPablo:fix/cjk-chunking

Conversation

@MsfPablo

@MsfPablo MsfPablo commented Aug 3, 2026

Copy link
Copy Markdown

Summary

Fixes #1354. Markdown files with CJK (Chinese, Japanese, Korean) content are indexed as a single entry instead of being chunked by heading, because the heading-split decision in MarkdownToEntries.process_single_markdown_file uses TextToEntries.tokenizer() (text.split()) to estimate length. text.split() relies on whitespace between words, which CJK text doesn't have, so it drastically undercounts length and the <= max_tokens check passes when it shouldn't — the doc never reaches the heading-splitting logic. This degrades search quality since the bi-encoder ends up averaging the semantics of the whole document.

Note on approach: the issue's suggested fix was to swap TextToEntries.tokenizer() itself for a tiktoken-based implementation. I went with a narrower fix instead — tokenizer() is also used as the length_function for RecursiveCharacterTextSplitter in split_entries_by_max_tokens, which controls chunk sizing for every content type (markdown, org, plaintext, PDF, etc.), not just this heading-split check. Swapping it globally would change chunk sizes for all users and file types, since tiktoken's subword counts don't map 1:1 to text.split()'s word counts even for English. Instead I added TextToEntries.token_count() (tiktoken cl100k_base) and used it only at the specific gate described in the issue, leaving the rest of the chunking pipeline untouched.

Test plan

Verified directly against MarkdownToEntries.extract_markdown_entries (couldn't run the full suite locally — it needs a Postgres instance for Django's test DB setup, unrelated to this change):

  • CJK doc with 3 headed sections (per the issue's repro): before the fix, collapses to 1 entry; after the fix, correctly splits into 4 (YAML frontmatter + 3 sections)
  • Equivalent English doc with 2 headed sections: 2 entries before and after — no regression
  • Short doc with no content over the token budget: stays as 1 entry before and after — no regression

TextToEntries.tokenizer() approximates length via text.split(), which
undercounts CJK text (no whitespace between words). This made the
heading-split decision in MarkdownToEntries.process_single_markdown_file
think CJK documents were short enough to keep as a single entry, even
when they had multiple headed sections well over the token budget —
degrading search quality since embeddings average the whole document.

Add TextToEntries.token_count(), a tiktoken-based counter, and use it
only at this specific gate. Left the existing word-split tokenizer()
untouched everywhere else (including the general chunk-size splitter in
split_entries_by_max_tokens), since swapping it globally would change
chunk sizing for every file type and language, not just this bug.

Fixes khoj-ai#1354
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.

Markdown chunking fails for CJK languages due to text.split() tokenizer undercounting tokens

1 participant