Skip to content

fix(retrieval): unicode-aware tokenization; empty chunk text is never a relevant hit - #45

Merged
charan-rathore merged 18 commits into
mainfrom
fix/unicode-tokenize-empty-relevance
Oct 19, 2026
Merged

charan-rathore merged 18 commits into
mainfrom
fix/unicode-tokenize-empty-relevance

Conversation

@charan-rathore

Copy link
Copy Markdown
Owner

Finding 1: silent recall loss for non-ASCII text

keyword.py tokenized with re.compile(r"[a-z0-9]+"), dropping every non-ASCII letter.

Before: a BM25 index with one document 東京は日本の首都です returned [] for query 東京; München tokenized to ["m", "nchen"]. Any CJK or accented content was invisible to keyword and hybrid retrieval.

After: TOKEN_PATTERN is unicode-aware ([^\W_]+, letters/digits from any script) over casefold()ed text (Straße -> strasse). CJK runs additionally emit each character (Han/kana text is not space-separated, so a single-ideograph query must still match) plus the whole run so exact phrases score highest:

tokenize("München")  -> ["münchen"]            (was ["m", "nchen"])
tokenize("東京")      -> ["東京", "東", "京"]    (was [])
BM25("東京")         -> [("doc-1", ..., 0.575)] (was [])

The TF-IDF mock embedder in pipeline/embedders.py had its own ASCII-only copy of the same pattern and now shares tokenize from keyword.py.

Finding 2: empty chunk text counted as a relevant hit

_is_relevant in retrieval_benchmark.py substring-matched both ways (ref in text or text in ref). A chunk with missing text (dense.py maps absent text to "") made "" in ref_lower true against every reference, inflating recall/MRR/NDCG.

Before: _is_relevant("missing-text-chunk", "", sample) -> True
After: empty text returns False; an explicit relevant_chunk_ids match still counts.

Tests

  • Failing-first regressions for both findings (unicode tokenization x5, empty-text relevance x2), verified RED on main and GREEN here.
  • Retrieval + evaluation + pipeline suites: 58 passed vs 51 on main (+7 new); the 3 failures in those packages are identical on pristine main (pre-existing).
  • Whole repo: 263 passed vs 256 on main (+7 new), same 26 pre-existing failures outside the change.

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

…n-ASCII letter: BM25 over CJK text returned nothing, 'München' split into ['m','nchen']. Unicode-aware pattern + casefold; CJK runs emit each character. Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Updated the tokenization process to be casefolded and unicode-aware, allowing for better handling of multi-script tokens and CJK characters.
…by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Added a check to handle empty text cases in relevance evaluation.
…er Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Removed unused regex import and custom tokenize function.
…sions Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Add tests for tokenization and relevance checks with Unicode and accented characters.
@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 11:03am UTC
intellirag-live-own-track Ready Ready Preview Oct 19, 2026 11:03am UTC
irag-fix-0904 Ready Ready Preview Oct 19, 2026 11:03am 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 ⚠️ Failed 2026-10-19T11:04:13.679307Z f6df221 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: e14382165d

ℹ️ 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 on lines +38 to +41
if _CJK_CHAR.search(word):
if len(word) > 1:
tokens.append(word)
tokens.extend(word)

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 Preserve non-CJK tokens within mixed-script runs

When Latin text directly adjoins Han or kana, as in Python東京 or 使用Python开发, TOKEN_PATTERN treats the entire string as one word; finding any CJK character then causes this branch to emit the combined run and every individual character, but never python. Consequently, a Python query no longer matches such a BM25 document even though it did with the previous tokenizer, and the shared TF-IDF and lexical-overlap consumers are affected too. Split mixed-script runs or emit intact non-CJK subruns while expanding only the CJK portions.

Useful? React with 👍 / 👎.

still match, plus the whole run so exact phrases score highest.
"""
tokens: List[str] = []
for match in TOKEN_PATTERN.finditer(text.casefold()):

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 Normalize canonically equivalent Unicode before tokenizing

When indexed text uses decomposed Unicode from PDF/web extraction but the query uses its precomposed equivalent, casefold() alone does not normalize them. For example, Mu\u0308nchen becomes ['mu', 'nchen'] because the combining diaeresis breaks the regex match, while München becomes ['münchen'], yielding no BM25 match; the old tokenizer at least shared nchen, so this is a recall regression for canonically equivalent input. Normalize both forms to a common Unicode normalization form before applying the token regex.

Useful? React with 👍 / 👎.

Comment on lines +39 to +41
if len(word) > 1:
tokens.append(word)
tokens.extend(word)

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 Preserve adjacency when tokenizing CJK phrases

For a multi-character CJK query within a normal unspaced sentence, the whole-run token cannot provide the promised phrase boost: 東京は日本の首都です produces one full-sentence token plus character unigrams, whereas query 東京 produces 東京, 東, and 京, so only the two independent characters match. A document containing 東 and 京 far apart therefore receives the same matching terms and can outrank the actual phrase based on length or term frequency. Emit overlapping CJK n-grams or otherwise retain substring boundaries rather than relying only on the entire run and unigrams.

Useful? React with 👍 / 👎.

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

Add NFC normalization and improve tokenization logic.
@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 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

@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

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

Copy link
Copy Markdown
Owner Author

@codex review

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

  1. Mixed-script runs: Python東京 now yields python plus the CJK tokens - runs are split at script boundaries and each side tokenizes its own way.
  2. Canonical equivalence: text is NFC-normalized before casefolding, so decomposed input (Mu¨nchen with combining diaeresis) matches precomposed queries.
  3. CJK phrase ranking: CJK segments emit overlapping bigrams alongside unigrams, so a 東京 query matches adjacent 東京 in the document and no longer ties with documents containing 東 and 京 far apart (regression: 東と京 does not produce the 東京 bigram).

Retrieval + evaluation + pipeline: 62 passed vs 51 on main (+11 new across both rounds); 3 failures in those packages are identical on pristine main.

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: 2743373996

ℹ️ 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 on lines +40 to +43
is_cjk = bool(_CJK_CHAR.match(word[i]))
j = i + 1
while j < n and bool(_CJK_CHAR.match(word[j])) == is_cjk:
j += 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.

P2 Badge Split mixed non-CJK scripts before emitting tokens

When Latin text directly adjoins another non-CJK script, both sides are classified as is_cjk=False and emitted as one token. This occurs naturally with Korean particles: tokenize("Python으로") produces ['python으로'], so a Python query produces no BM25 hit, whereas the previous ASCII tokenizer indexed python. Split non-CJK runs at script transitions as well, rather than only separating Han/kana from everything else.

Useful? React with 👍 / 👎.

TOKEN_PATTERN = re.compile(r"[a-z0-9]+")
# Unicode-aware: letters and digits from ANY script form word tokens, so
# "München" stays one token instead of splitting into ["m", "nchen"].
TOKEN_PATTERN = re.compile(r"[^\W_]+")

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 Include combining marks in Unicode word tokens

For scripts where vowel signs and viramas remain combining marks after NFC, [^^\W_]-style word matching drops those marks and collapses distinct words to the same token sequence. For example, की, का, कि, and कु all tokenize as ['क'], so a BM25 query for one can rank documents containing any of the others identically; the same problem affects Bengali, Telugu, Tamil, and other supported Unicode scripts. Keep combining marks attached to their preceding base character when forming non-CJK word tokens.

Useful? React with 👍 / 👎.

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

Refactor tokenization logic to handle mixed scripts more effectively, ensuring proper segmentation and script identification.
…it-in-word regressions Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>
@charan-rathore

Copy link
Copy Markdown
Owner Author

@codex review

Both new findings addressed:

  1. Non-CJK script mixing: script segmentation is no longer CJK-specific - runs now split at every script boundary, so Python으로 yields python + 으로 (and Python東京 still yields python plus CJK unigrams/bigrams). Digits and unattributed characters inherit the current script, keeping bm25 and k1 whole.
  2. Combining marks: token characters now include Unicode marks, so vowel signs/viramas that stay decomposed after NFC attach to their base character - की, का, कि, कु tokenize as four distinct words instead of collapsing to क.

New failing-first regressions cover Latin-Hangul mixing, Devanagari combining marks, and digits inside words (RED on the previous tokenizer, GREEN now). Suite: 65 passed with only the 3 pre-existing eval/factory failures unchanged from main.

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: 29db5f276d

ℹ️ 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 on lines +51 to +54
if script is None:
# Digits and unattributed characters inherit the current script.
segment += ch
continue

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 Keep numeric runs searchable next to CJK text

When digits adjoin CJK text, inheriting the CJK script causes each digit to be emitted only as a unigram or bigram. For example, 東京2026年 produces 20, 02, and 26 but not 2026, so BM25Index(...).search("2026") returns no result; the previous tokenizer did index that numeric run. Preserve contiguous numeric tokens in addition to any CJK n-grams so dates, versions, and identifiers remain searchable.

Useful? React with 👍 / 👎.

Comment on lines +17 to +19
# Han ideographs and Japanese kana are not space-separated, so a run of
# them cannot rely on word boundaries; they tokenize per character.
_CJK_SCRIPTS = frozenset({"CJK", "HIRAGANA", "KATAKANA"})

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 Tokenize scripts that omit spaces between words

For Thai and other scripts that commonly omit word-separating spaces, limiting subword emission to this CJK set leaves an entire sentence as one token. For example, the document กรุงเทพมหานครเป็นเมืองหลวงของประเทศไทย receives one token while the query ประเทศไทย receives another, so BM25 returns no hit despite the literal word being present. Include suitable word breaking or character n-grams for these scripts rather than treating every non-CJK run as a single word.

Useful? React with 👍 / 👎.

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

Updated handling of scripts that do not use spaces between words to include Thai, Lao, Khmer, and Myanmar. Adjusted tokenization logic to accommodate digit runs while maintaining script association.
…age Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>
@charan-rathore

Copy link
Copy Markdown
Owner Author

@codex review

Both findings addressed:

  1. Digits next to CJK: contiguous digit runs inside n-gram segments are now emitted whole, so 東京2026年 keeps a searchable 2026 token alongside the CJK unigrams/bigrams.
  2. Spaceless scripts beyond CJK: Thai, Lao, Khmer, and Burmese now tokenize per character plus overlapping bigrams, so a word query like ประเทศไทย matches a document where it appears inside an unspaced sentence.

Two more failing-first regressions (RED on the previous head, GREEN now). Suite: 67 passed with only the 3 pre-existing eval/factory failures unchanged from main.

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: a244d5c8e3

ℹ️ 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".

# etc. - NFC leaves many of them decomposed, and dropping them collapses
# distinct words like "की" / "का" / "कि" onto one token).
def _is_word_char(ch: str) -> bool:
return ch.isalnum() or unicodedata.category(ch).startswith("M")

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 Exclude detached combining marks from word tokens

When a combining mark follows a non-word symbol, treating it as a standalone word character creates meaningless shared tokens. In particular, the variation selector in common emoji forms means tokenize("❤️"), tokenize("☀️"), and tokenize("✈️") all produce the same token, so a BM25 search for a heart can return unrelated sun or airplane chunks with equal scores. Keep marks only when they extend an existing alphanumeric token, rather than starting tokens on their own.

Useful? React with 👍 / 👎.

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

Copy link
Copy Markdown
Owner Author

@codex review

Fixed: tokens now start only on letters/digits - a detached combining mark (like the emoji variation selector) no longer forms its own token, so ❤️, ☀️, and ✈️ stop collapsing onto one shared token. Marks inside a word still extend it and inherit its script. Two failing-first regressions added (RED before, GREEN after); retrieval/eval/pipeline 69 passed with only main's 3 pre-existing failures.

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: 104bf7f238

ℹ️ 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 on lines +72 to +73
tokens.extend(seg)
tokens.extend(seg[k : k + 2] for k in range(len(seg) - 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.

P2 Badge Build n-grams from grapheme clusters

For Thai, Lao, Khmer, and Myanmar words, tokens.extend(seg) iterates Unicode code points, so combining marks accepted into a word become standalone query terms. For example, tokenize("กิ") emits "ิ", allowing a BM25 index containing only "มิ" to return a positive hit for "กิ" solely because both contain that vowel mark; the shared lexical-overlap consumers inherit the same false match. Build unigrams and bigrams from grapheme clusters (base plus attached marks) rather than individual code points.

Useful? React with 👍 / 👎.

Comment on lines +38 to +39
name = unicodedata.name(ch, "")
return name.split(" ")[0] if name else None

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 Keep Japanese word marks in the surrounding script

When Japanese text contains common marks such as 々 or ー, taking the first word of the Unicode name classifies them as IDEOGRAPHIC or KATAKANA-HIRAGANA rather than CJK/Katakana. _emit_word therefore splits around them and loses adjacency evidence—for example, tokenize("人々") is only ["人", "々"], and in a BM25 index "人と時々会う" can rank above "人々が集まる" for that query even though only the latter contains the phrase. Treat these characters as inheriting the surrounding Japanese script, or use Unicode Script Extensions, so their phrase n-grams are preserved.

Useful? React with 👍 / 👎.

…ion marks inherit script Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>
…eep adjacency Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>
@charan-rathore

Copy link
Copy Markdown
Owner Author

@codex review

Both findings addressed:

  1. Grapheme clusters: n-grams now build from base+marks clusters, so Thai กิ emits กิ only - a bare vowel sign can no longer be a shared term (new BM25 regression: an index holding only มิ returns nothing for กิ).
  2. Japanese iteration/prolonged marks: 々 and ー now inherit the surrounding script, so 人々 stays one segment with its bigram and katakana words like ユーザー keep adjacency.

Both RED before the fix, GREEN after; retrieval/eval/pipeline 71 passed with only main's 3 pre-existing failures.

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: d85cb72d98

ℹ️ 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 on lines +37 to +38
if clusters and _is_mark(ch):
clusters[-1] += ch

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 Keep Japanese word marks out of unigram tokens

Because _clusters attaches only Unicode category-M characters, the newly inherited 々 and ー remain standalone clusters and are emitted as searchable unigrams. Thus tokenize("ユーザー") and tokenize("スーパー") share only ー, yet a BM25 index containing only スーパー returns it for the ユーザー query; similarly, 時々 matches 人々 solely through 々. Preserve these marks in adjacency n-grams without emitting them as independent terms.

Useful? React with 👍 / 👎.

Comment on lines +96 to +98
# Digits inherit the surrounding script but stay searchable as
# whole runs: "東京2026年" must still match a "2026" query.
tokens.extend(m.group(0) for m in _DIGIT_RUN.finditer(seg) if len(m.group(0)) > 2)

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 Preserve digit runs beside every non-Latin script

Digit-run extraction occurs only for _NGRAM_SCRIPTS, so ASCII numbers adjoining other Unicode scripts are absorbed into an opaque whole-word token. For example, tokenize("إصدار2026") produces only إصدار2026, and BM25Index([("d", "إصدار2026")]).search("2026") returns nothing; the previous ASCII tokenizer indexed 2026, so this regresses searches for versions and identifiers in Arabic, Devanagari, and other non-ngram scripts. Emit contiguous digit runs independently for all script segments.

Useful? React with 👍 / 👎.

Comment on lines +81 to +84
if script != segment_script:
segments.append((segment, segment_script))
segment = ch
segment_script = script

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 Preserve adjacency across Japanese script transitions

When a Japanese word transitions between kanji and kana, this split prevents any n-gram from spanning the boundary. Consequently, tokenize("食べる") is identical to tokenize("食、べる"), so a BM25 index containing the punctuated decoy before the actual phrase gives both the same score and ranks the decoy first. Emit an adjacency n-gram across contiguous Japanese-script segments so normal mixed-script words retain phrase evidence.

Useful? React with 👍 / 👎.

precomposed query must still match ("Mu\u0308nchen" == "München").
"""
tokens: List[str] = []
normalized = unicodedata.normalize("NFC", text.casefold())

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 Normalize compatibility-equivalent Unicode forms

NFC preserves compatibility characters, so visually equivalent text from East Asian documents remains in different tokens from an ordinary query. For example, tokenize("Python") yields python, and a BM25 index containing that full-width spelling returns nothing for Python; half-width katakana similarly fails to match its standard-width equivalent. Apply compatibility normalization, such as NFKC followed by case folding, so these common presentation forms remain searchable.

Useful? React with 👍 / 👎.

…ide any script, kanji/kana bigrams, NFKC Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Refactor tokenization logic for Japanese scripts and update normalization method.
…na adjacency, full-width forms Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>
@charan-rathore

Copy link
Copy Markdown
Owner Author

Fixed all 4:

  1. Standalone iteration/prolonged-mark unigrams: 々 and ー were emitted as unigrams shared by every Japanese word (ユーザー matched スーパー). They now only appear inside bigrams; single-char tokens from _INHERIT_CODEPOINTS are skipped.
  2. Digit runs beside non-Latin scripts: "إصدار2026" absorbed 2026 into an opaque token; digits adjoining a non-Latin script now form their own token, Latin words keep theirs whole (bm25 unchanged). The old \d+ n-gram rescue hack is gone since the boundary handles it.
  3. Kanji->kana transitions split phrases (食べる scored the same as 食、べる): contiguous CJK/HIRAGANA/KATAKANA segments now merge into one n-gram run, so bigrams span script boundaries.
  4. NFC left compatibility forms unmatched (Python vs Python, half-width katakana): normalization is now NFKC after case folding (per Unicode default caseless matching).

4 new regression tests (test_iteration_marks_are_not_standalone_unigrams, test_digit_runs_beside_non_latin_scripts, test_kanji_kana_boundary_keeps_bigram, test_compatibility_forms_match_plain_query), RED before, GREEN after; full libs/workers suite 312 passed, 26 pre-existing failures (Docker Postgres); unused re import removed.

@codex review

@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: 2c27ad6159

ℹ️ 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".

# Digits and unattributed characters inherit the current script -
# except digits beside a non-Latin script, which form their own
# whole token so "إصدار2026" keeps a searchable "2026".
if ch.isdigit() and segment_script not in (None, "LATIN", "DIGIT"):

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 Separate leading digit runs before non-Latin scripts

When digits precede a non-Latin word, segment_script is still None, so this condition leaves them in the pending segment and the following script absorbs them. For example, tokenize("2026إصدار") returns only "2026إصدار", and a BM25 query for 2026 cannot find that document; 2026東京 similarly emits digit unigrams and bigrams but not the complete 2026 token. Handle an initial digit run independently when its following character starts a non-Latin script.

Useful? React with 👍 / 👎.

# NFKC folds compatibility forms (full-width Latin, half-width katakana)
# onto their ordinary equivalents so extracted East Asian text matches a
# plain query; casefold first per Unicode default caseless matching.
normalized = unicodedata.normalize("NFKC", text.casefold())

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 Normalize compatibility forms before case folding

Case-folding before NFKC misses compatibility characters whose decomposition introduces uppercase letters. For example, tokenize("㎒") produces "MHz" while tokenize("MHz") produces "mhz", and mathematical 𝐀 similarly fails to match a. Normalize before case-folding (and normalize again afterward if needed) so all compatibility-derived letters receive the same casing.

Useful? React with 👍 / 👎.

# script buckets ("IDEOGRAPHIC ITERATION MARK", "KATAKANA-HIRAGANA
# PROLONGED SOUND MARK"); they inherit the surrounding script so "人々" and
# "ユーザー" stay single segments.
_INHERIT_CODEPOINTS = frozenset({"\u3005", "\u3006", "\u3007", "\u30fc"})

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 Keep ideographic zero as a searchable unigram

U+3007 is IDEOGRAPHIC NUMBER ZERO, not an iteration or prolonged mark, but including it here causes the n-gram path to suppress it as a unigram. Consequently, a standalone 〇 query produces "〇", while a document such as 二〇二六 omits that unigram and cannot match the query. Classify this character as CJK rather than as a mark that should be filtered.

Useful? React with 👍 / 👎.

# script buckets ("IDEOGRAPHIC ITERATION MARK", "KATAKANA-HIRAGANA
# PROLONGED SOUND MARK"); they inherit the surrounding script so "人々" and
# "ユーザー" stay single segments.
_INHERIT_CODEPOINTS = frozenset({"\u3005", "\u3006", "\u3007", "\u30fc"})

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 Exclude kana iteration marks from standalone tokens

The remaining hiragana and katakana iteration marks (ゝ, ゞ, ヽ, and ヾ) are naturally classified into n-gram scripts but are absent from this exclusion set, so they are still emitted as shared unigrams. For example, an index containing only さゝ returns it for the unrelated query くゝ solely because both contain ゝ; include these marks in the adjacency-only handling used for 々 and ー.

Useful? React with 👍 / 👎.

… zero, kana iteration marks Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Updated handling of Japanese iteration and prolonged marks, clarified comments on digit handling beside non-Latin scripts, and improved normalization process for Unicode compatibility.
…ero, kana iteration marks Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Added tests for tokenization behavior with leading digits, compatibility forms, ideographic zero, and kana iteration marks.
@charan-rathore

Copy link
Copy Markdown
Owner Author

Fixed all 4:

  1. Leading digit runs: digits opening a word beside a non-Latin script were absorbed by the following segment ("2026إصدار" had no 2026 token; "2026東京" only had n-gram fragments). Digits now open their own segment unless the word is Latin-led (bm25 unchanged).
  2. NFKC ordering: casefold-before-NFKC left compatibility letters unfolded (㎒ -> MHz never matched mhz; U0001d400 vs a). Normalization is now NFKC(casefold(NFKC(s))) per Unicode default caseless matching.
  3. 〇 is IDEOGRAPHIC NUMBER ZERO, not an iteration mark: removed from the inherit/skip set and classified CJK, so 二〇二六 answers a 〇 query.
  4. Kana iteration marks ゝゞヽヾ leaked as shared unigrams (さゝ matched くゝ): they now get the same bigram-only treatment as 々 and ー.

4 new regression tests (leading digit runs, squared-unit casefold, ideographic zero, kana iteration marks), RED before, GREEN after; full libs/workers suite 316 passed, 26 pre-existing failures.

@codex reviewFixed all 4:

  1. Leading digit runs: digits opening a word beside a non-Latin script were absorbed by the following segment ("2026إصدار" had no 2026 token; "2026東京" only had n-gram fragments). Digits now open their own segment unless the word is Latin-led (bm25 unchanged).
  2. NFKC ordering: casefold-before-NFKC left compatibility letters unfolded (㎒ -> MHz never matched mhz; U0001d400 vs a). Normalization is now NFKC(casefold(NFKC(s))) per Unicode default caseless matching.
  3. 〇 is IDEOGRAPHIC NUMBER ZERO, not an iteration mark: removed from the inherit/skip set and classified CJK, so 二〇二六 answers a 〇 query.
  4. Kana iteration marks ゝゞヽヾ leaked as shared unigrams (さゝ matched くゝ): they now get the same bigram-only treatment as 々 and ー.

4 new regression tests (leading digit runs, squared-unit casefold, ideographic zero, kana iteration marks), RED before, GREEN after; full libs/workers suite 316 passed, 26 pre-existing failures.

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@charan-rathore
charan-rathore merged commit 3f58c05 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