Skip to content

Commit b16090a

Browse files
jgravelleclaude
andcommitted
release: v1.108.161 — BM25 tokenizer: Unicode word splitting + CJK character bigrams
_TOKEN_RE was [a-zA-Z0-9]{2,}, treating every non-ASCII character as a separator: CJK symbol names/summaries/docstrings produced zero BM25 tokens and accented Latin was mangled. Now splits on Unicode word boundaries and expands CJK runs into overlapping character bigrams, identical at index and query time. camelCase/snake_case splitting, stemming, abbreviation expansion, and pure-ASCII tokenization are unchanged. Suite parity with jdocmunch-mcp v1.114.1 (#91) and jdatamunch-mcp v1.23.1. No reindex needed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent e888ee9 commit b16090a

4 files changed

Lines changed: 102 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22

33
All notable changes to jcodemunch-mcp are documented here.
44

5+
## [1.108.161] - 2026-07-23 - BM25 tokenizer: Unicode word splitting + CJK character bigrams
6+
7+
### Fixed
8+
- **The BM25 tokenizer no longer discards non-ASCII text.** `_TOKEN_RE` was
9+
`[a-zA-Z0-9]{2,}`, so every non-ASCII character acted as a separator: CJK
10+
symbol names, summaries, and docstrings produced zero BM25 tokens (the
11+
lexical channel contributed nothing for those corpora) and accented Latin
12+
identifiers were mangled (`café` → `caf`). The tokenizer now splits on
13+
Unicode word boundaries and expands CJK runs (Hangul, Hiragana/Katakana,
14+
Han) into overlapping character bigrams — applied identically at index and
15+
query time, so bigram overlap is the match signal. Mixed-script tokens
16+
split cleanly; camelCase/snake_case splitting, stemming, abbreviation
17+
expansion, and pure-ASCII tokenization are unchanged. Suite parity with
18+
jdocmunch-mcp v1.114.1 (#91 there) and jdatamunch-mcp v1.23.1. No reindex
19+
needed — BM25 tokenizes stored index fields at scoring time.
20+
521
## [1.108.160] - 2026-07-23 - Multi-checkout "different working tree" responses
622

723
### Changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "jcodemunch-mcp"
3-
version = "1.108.160"
3+
version = "1.108.161"
44
description = "Token-efficient MCP server for source code exploration via tree-sitter AST parsing"
55
readme = "README.md"
66
requires-python = ">=3.10"

src/jcodemunch_mcp/tools/search_symbols.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,29 @@
3535

3636
# Pre-compiled regexes for _tokenize (called ~9000× on cold BM25 build)
3737
_CAMEL_RE = re.compile(r"([a-z])([A-Z])")
38-
_TOKEN_RE = re.compile(r"[a-zA-Z0-9]{2,}")
38+
# Unicode word runs (was [a-zA-Z0-9]{2,}, which silently dropped ALL
39+
# non-ASCII — CJK names/docstrings produced zero BM25 tokens; jdoc #91 class).
40+
_TOKEN_RE = re.compile(r"[^\W_]+")
41+
# CJK scripts carry no whitespace word boundaries; runs in these ranges are
42+
# expanded to overlapping character bigrams (same expansion at index and
43+
# query time, so bigram overlap is the match signal).
44+
_CJK_RE = re.compile(
45+
"[ᄀ-ᇿ" # Hangul Jamo
46+
"぀-ヿ" # Hiragana + Katakana
47+
"㄰-㆏" # Hangul Compatibility Jamo
48+
"ㇰ-ㇿ" # Katakana Phonetic Extensions
49+
"㐀-䶿" # CJK Unified Ideographs Extension A
50+
"一-鿿" # CJK Unified Ideographs
51+
"가-힯" # Hangul Syllables
52+
"豈-﫿]+" # CJK Compatibility Ideographs
53+
)
54+
55+
56+
def _cjk_bigrams(run: str) -> list[str]:
57+
"""Overlapping character bigrams for a CJK run; a lone char passes through."""
58+
if len(run) == 1:
59+
return [run]
60+
return [run[i : i + 2] for i in range(len(run) - 1)]
3961

4062
# Search result cache (Feature 5 — session-aware routing)
4163
import threading
@@ -181,11 +203,21 @@ def _tokenize(text: str) -> list[str]:
181203
if not text:
182204
return []
183205
text = _CAMEL_RE.sub(r"\1_\2", text)
206+
# Pad CJK runs with spaces so mixed-script tokens split cleanly.
207+
text = _CJK_RE.sub(lambda m: " " + m.group(0) + " ", text)
184208
raw_tokens = [t.lower() for t in _TOKEN_RE.findall(text)]
185209

186210
result = []
187211
seen: set[str] = set()
188212
for tok in raw_tokens:
213+
if _CJK_RE.fullmatch(tok):
214+
# Bigram expansion; stemming/abbreviations are English-only.
215+
for bg in _cjk_bigrams(tok):
216+
result.append(bg)
217+
seen.add(bg)
218+
continue
219+
if len(tok) < 2:
220+
continue
189221
result.append(tok)
190222
seen.add(tok)
191223
# Stemmed form

tests/test_v1_108_161.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# -*- coding: utf-8 -*-
2+
"""v1.108.161 — BM25 tokenizer no longer discards non-ASCII text (jdoc #91 class).
3+
4+
_TOKEN_RE was [a-zA-Z0-9]{2,}, so every non-ASCII character acted as a
5+
separator: CJK symbol names/summaries/docstrings produced zero BM25 tokens
6+
and accented Latin was mangled. Now splits on Unicode word boundaries and
7+
expands CJK runs into overlapping character bigrams — identical at index and
8+
query time, so bigram overlap is the match signal. Suite parity with
9+
jdocmunch-mcp v1.114.1 and jdatamunch-mcp v1.23.1.
10+
"""
11+
12+
from jcodemunch_mcp.tools.search_symbols import _cjk_bigrams, _tokenize
13+
14+
15+
class TestCJKTokenization:
16+
def test_korean_produces_bigrams(self):
17+
toks = _tokenize("초과근무 승인 규칙")
18+
assert toks == ["초과", "과근", "근무", "승인", "규칙"]
19+
20+
def test_mixed_camelcase_and_korean(self):
21+
toks = _tokenize("OvertimeService 초과근무 계산")
22+
assert "overtime" in toks and "service" in toks
23+
assert "초과" in toks and "근무" in toks and "계산" in toks
24+
25+
def test_accented_latin_survives_intact(self):
26+
# Was ['caf', 've'] (single-char fragments dropped by the old {2,}).
27+
toks = _tokenize("café naïve")
28+
assert "café" in toks and "naïve" in toks
29+
30+
def test_japanese_bigrams(self):
31+
toks = _tokenize("日本語のドキュメント")
32+
assert "日本" in toks and "本語" in toks
33+
34+
def test_single_cjk_char_kept(self):
35+
assert _tokenize("車") == ["車"]
36+
37+
def test_mixed_script_run_splits(self):
38+
toks = _tokenize("초과근무OvertimeService")
39+
assert "overtime" in toks and "초과" in toks
40+
41+
def test_bigram_helper(self):
42+
assert _cjk_bigrams("가") == ["가"]
43+
assert _cjk_bigrams("가나") == ["가나"]
44+
assert _cjk_bigrams("가나다") == ["가나", "나다"]
45+
46+
def test_ascii_behavior_unchanged(self):
47+
# camelCase split + stemming + abbreviation expansion all intact.
48+
assert _tokenize("getUserData") == ["get", "user", "data"]
49+
toks = _tokenize("config")
50+
assert "config" in toks and "configuration" in toks
51+
# Single-char ASCII tokens still dropped (old {2,} behavior).
52+
assert _tokenize("a b") == []

0 commit comments

Comments
 (0)