UAX #29 keeps an ASCII apostrophe after a Hebrew letter (BET') and an ASCII quotation mark between two Hebrew letters (TAV"ALEF) inside a word. StandardTokenizer applies these rules on their own, but not next to a digit, an underscore or a mid-word ., : or quote.
- Index the Hebrew text
כיתה ב'2("class BET'2", the geresh typed as the ASCII apostrophe) with the defaultStandardAnalyzer, then runTermQuery(body:2). - You expect 1 hit.
StandardTokenizersays it implements the UAX #29 word-break rules, and they break after the apostrophe:[BET', 2], as ICU4J and Lucene's ownICUTokenizerdo. The Latinabc'3already gives[abc, 3]. - You get 0 hits: the term is
BET'2. Three more no-space shapes go wrong just as silently: HE.SHIN' loses its apostrophe, and ALEF.TSADI"LAMED, ALEF"BET"GIMEL and TAV"ALEF.BET (ת"א.ב) are each split into two terms.UAX29URLEmailTokenizergives the same tokens. - Why: the grammar's word rule glues pieces together and writes the Hebrew quote rules as a piece of their own, "Hebrew letter + quote". That piece can end in an apostrophe, so a following digit gets glued on; it must start with its own letter, so a letter already used after a
.cannot take the quote. - Only the ASCII
'and"trigger it: the Hebrew marks U+05F3/U+05F4, plain acronyms such asתש"פand ordinary words ending in a geresh such asג'ורג'work. Folding the quote rules into the letter chain of both grammars fixes all four shapes, and 2,088 analysis tests pass.
Anyone who indexes Hebrew with the default StandardAnalyzer, StandardTokenizer, the 42 other Lucene analyzers built on it, or UAX29URLEmailTokenizer, when a geresh or gershayim typed as ' or " sits right next to a digit, _ or a mid-word ., : or quote. No setting is needed; the triggers are rare; a fix needs a reindex.
Mechanism, terms and numbers781 words
UAX #29 has three rules for Hebrew written with ASCII quotes: WB7a keeps an apostrophe after a Hebrew letter in the word (BET', a geresh typed on a keyboard), and WB7b/WB7c keep a quotation mark between two Hebrew letters (TAV"ALEF, an acronym whose gershayim is typed as "). StandardTokenizer, the tokenizer of the default StandardAnalyzer, says it implements the UAX #29 word-break rules, and its grammar lists WB7a-WB7c for its word rule. It applies them correctly on their own, but its grammar writes "Hebrew letter + quote" as a separate piece that is glued to whatever piece follows and must start with its own Hebrew letter. So four no-space shapes come out wrong, silently, in StandardTokenizer and UAX29URLEmailTokenizer: BET'2 is one term (a TermQuery for 2 finds 0 documents), HE.SHIN' loses its apostrophe, ALEF.TSADI"LAMED and ALEF"BET"GIMEL are split, and TAV"ALEF.BET is split. ICU4J and Lucene's own ICUTokenizer give the UAX #29 answer, and the same words written with U+05F3/U+05F4 are tokenized correctly. Every release from 4.7.0 to 10.5.2, 10.6.0 (by reading) and main is affected. Folding the quote rules into the letter chain of both grammars fixes all four shapes; a reindex of affected fields is needed after the fix.
- Geresh and gershayim. Hebrew marks an abbreviation or a number-letter with a geresh (BET' = 2nd, as in the class name TET'3) and an acronym with a gershayim before its last letter (TAV SHIN"PE,
תש"פ). Their Unicode characters are U+05F3 and U+05F4, but text often types them as the ASCII apostrophe U+0027 and quotation mark U+0022 (for the gershayim, the reporter of a 2010 Lucene issue estimated that 99.9% of Hebrew text files use"; not measured here). - UAX #29 word breaks. The Unicode annex that
StandardTokenizerimplements. Each character has a Word_Break class (ALetter, Hebrew_Letter, Numeric, MidLetter such as:, MidNumLet such as., Single_Quote', Double_Quote", ExtendNumLet_), and rules say where a word may not break: WB5 letter x letter, WB6/WB7 letter x mid-word character x letter, WB9/WB10 letter x digit, WB13a/b around_, and for Hebrew WB7a (Hebrew_Letter x') and WB7b/WB7c (Hebrew_Letter x"x Hebrew_Letter). Everywhere else the word ends. - The grammar. The scanner is generated by JFlex from
StandardTokenizerImpl.jflex. Its word rule (:238-252) is a list of pieces glued by+: a letter chainAHLetter ((ExtendNumLet* | MidLetter) AHLetter)*, a digit chain, and the Hebrew pieceHebrewLetter (SingleQuote | DoubleQuote HebrewLetter)(:239,:245). The scanner takes the longest match; characters outside every token are skipped.
- Gluing two pieces with nothing between them is right when the left piece ends in a letter or digit (WB5, WB8-WB10). Of the pieces
+glues directly, the Hebrew piece is the only one that can end in an apostrophe, so+also glues a digit chain to it (BET'2), and the outerExtendNumLet+loop glues_ato it (ALEF'_a). No UAX #29 rule joins an apostrophe to a following digit or underscore. - The Hebrew piece must start with its own Hebrew letter. In HE.SHIN' and ALEF.TSADI"LAMED, SHIN and TSADI come right after a mid-word
., so they are inside a letter chain, and a chain cannot end in.to hand the letter on. The longest match is HE.SHIN or ALEF.TSADI; the quote is skipped and LAMED starts a new token. The same happens after a mid-word:, and in ALEF"BET"GIMEL, whose BET already ends the piece ALEF"BET. - The piece TAV"ALEF ends at ALEF. Going on with
.BETwould need a letter chain that starts at ALEF, which the piece already used:[TAV"ALEF, BET]. - Upstream's conformance file has the Hebrew rules only in their shortest form, ALEF' and ALEF"ALEF (
WordBreakTestUnicode_12_1_0.java:7165,:7169), so its tests pass. - U+05F3 is ALetter and U+05F4 is MidLetter, so text with the Hebrew marks goes through the letter chain and is correct.
UAX29URLEmailTokenizerImpl.jflex:372-386has the same word rule.
| shape | input | UAX #29 (ICU4J) | main | with the fix |
|---|---|---|---|---|
| 1 | BET'2 | [BET', 2] | [BET'2] | [BET', 2] |
| 1 | "class TET'3" | [class, TET', 3] | [class, TET'3] | [class, TET', 3] |
| 1 | ALEF'_a | [ALEF', _a] | [ALEF'_a] | [ALEF', _a] |
| 2 | HE.SHIN' | [HE.SHIN'] | [HE.SHIN] | [HE.SHIN'] |
| 3 | ALEF.TSADI"LAMED | one word | [ALEF.TSADI, LAMED] | one word |
| 3 | ALEF"BET"GIMEL | one word | [ALEF"BET, GIMEL] | one word |
| 4 | TAV"ALEF.BET | one word | [TAV"ALEF, BET] | one word |
| control | ALEF', TAV SHIN"PE, GIMEL'VAV RESH GIMEL', abc'3, the U+05F3/U+05F4 forms | UAX #29 | correct | unchanged |
Search with the default StandardAnalyzer over one document "class BET'2": body:2 0 hits on main, 1 with the fix; body:BET' 0 and 1; body:BET'2 1 and 0.
Exposure and likelihood326 words
- Who is hit. Users of
new StandardAnalyzer()(core), a bareStandardTokenizer, 42 other Lucene analyzers that construct one (for exampleEnglishAnalyzer,ArabicAnalyzer), andUAX29URLEmailTokenizer(analysis-common). No setting or opt-in API is involved: these classes in their default configuration are affected. - What triggers it. Hebrew text that types the geresh as
'and the gershayim as". For the gershayim, the reporter of LUCENE-2465 (#3539, 2010) wrote that "no user has such a character on his keyboard" and "In 99.9% of Hebrew text files, old and new, double-quotes is being used as GERSHAYIM" (their estimate); that the geresh is likewise usually typed as'is likely but not sourced. Then only four no-space adjacencies go wrong. The most natural is a letter with a geresh followed by a digit, as in class names (TET'3, BET'2); the others need mid-word punctuation next to a quoted Hebrew word (TAV"ALEF.BET). Plain acronyms (TAV SHIN"PE) and ordinary words ending in a geresh (GIMEL'VAV RESH GIMEL') are correct; the geresh is lost when the letter before it comes right after a mid-word character such as.,:,"or U+05F4 (HE.SHIN', HE:SHIN', ALEF"BET'). How often these shapes occur in real text was not measured. - What goes wrong. Wrong terms, silently: a merged term hides the digit from search, a dropped apostrophe conflates two forms, a split word indexes two terms. Text analyzed the same way at index and query time still matches itself, so whole-form queries work; queries for a part, exact-term lookups built another way and term statistics see the wrong terms, and so, depending on their configuration, can facets on analyzed text and highlighting (not run).
- Not more than that. A 1,200,000-character run of BET'2 tokenizes in 49-53 ms into 4,706 tokens of at most 255 characters; neighbouring tokens and the next value are unchanged.
- Long-lived, and a fix changes terms. Every release since 4.7.0 (2014). Affected fields need a reindex after a fix.
The test shows that StandardAnalyzer (StandardTokenizer) keeps a digit after a Hebrew letter plus ASCII apostrophe in the same term: "\u05D1'2" (BET ' 2).
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.analysis.standard;
import org.apache.lucene.tests.analysis.BaseTokenStreamTestCase;
public class TestStandardTokenizerHebrewApostrophe extends BaseTokenStreamTestCase {
public void testHebrewLetterApostropheThenDigit() throws Exception {
try (StandardAnalyzer analyzer = new StandardAnalyzer()) {
// UAX #29: WB7a joins BET and ', no rule joins ' and 2 -> expected [BET', 2]; actual [BET'2]
assertAnalyzesTo(analyzer, "\u05D1'2", new String[] {"\u05D1'", "2"});
}
}
}
# in a copy of this finding's repro folder (repro/LFV-734/, which holds src/)
# Lucene checkout at the tested commit, built with: ./gradlew --no-daemon assemble (JDK 25)
LUCENE=/path/to/lucene; V=12.0.0-SNAPSHOT
CP=$LUCENE/lucene/core/build/libs/lucene-core-$V.jar
CP=$CP:$LUCENE/lucene/codecs/build/libs/lucene-codecs-$V.jar
CP=$CP:$LUCENE/lucene/test-framework/build/libs/lucene-test-framework-$V.jar
CP=$CP:randomizedtesting-runner-2.9.2.jar:junit-4.13.2.jar:hamcrest-3.0.jar # test-framework dependencies (Maven Central)
CP=$CP:junit-platform-console-standalone-6.1.3.jar # JUnit Platform console launcher (Maven Central)
javac -d classes -cp "$CP" $(find src -name '*.java')
# run 1: assertions on, seed DEADBEEF
java -ea -esa --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED -Dtests.seed=DEADBEEF \
-cp "classes:$CP" org.junit.platform.console.ConsoleLauncher \
execute --select-class org.apache.lucene.analysis.standard.TestStandardTokenizerHebrewApostrophe
- Expected vs actual
- Expected (UAX #29 WB7a joins only BET and the apostrophe, nothing joins the apostrophe and 2): terms [BET', 2]. Actual: one term BET'2 (
term 0 expected:<ב'[]> but was:<ב'[2]>).
More files, environment, full output40 words
| lucene | Lucene main e1faeba08bde (2026-11-02) |
|---|---|
| jdk | 25.0.3+9-LTS |
# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
=> org.junit.ComparisonFailure: term 0 expected:<ב'[]> but was:<ב'[2]>
org.junit.Assert.assertEquals(Assert.java:117)
org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertTokenStreamContents(BaseTokenStreamTestCase.java:262)
org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertTokenStreamContents(BaseTokenStreamTestCase.java:513)
org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertTokenStreamContents(BaseTokenStreamTestCase.java:573)
org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertTokenStreamContents(BaseTokenStreamTestCase.java:637)
org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertAnalyzesTo(BaseTokenStreamTestCase.java:792)
org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertAnalyzesTo(BaseTokenStreamTestCase.java:777)
org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertAnalyzesTo(BaseTokenStreamTestCase.java:862)
org.apache.lucene.analysis.standard.TestStandardTokenizerHebrewApostrophe.testHebrewLetterApostropheThenDigit(TestStandardTokenizerHebrewApostrophe.java:26)- The class promises the UAX #29 word-break rules, and the grammar comment lists WB7a-WB7c for this very rule.
- Its author wrote that WB7b/WB7c were followed "to the letter" (#7165); ICU4J,
ICUTokenizerand the Latinabc'3all break per the annex. - The documented deviations (emoji, Southeast Asian scripts) never mention Hebrew; the conformance file has only two-letter Hebrew cases.
The full argument326 words
- The contract. "This class implements the Word Break rules from the Unicode Text Segmentation algorithm, as specified in Unicode Standard Annex #29" (
StandardTokenizer.java:31-32; nearly the same, "implements Word Break rules from ...", inUAX29URLEmailTokenizer.java:31-33). The grammar comment of this rule lists WB7a, WB7b and WB7c (StandardTokenizerImpl.jflex:226-237). - The author's statement. In LUCENE-6103 (#7165, 2014), asked about ASCII
"in Hebrew, sarowe wrote: "I did follow the specs to the letter, and it does the right thing: Rules WB7b and WB7c forbid breaks around the ASCII double quote character, but only when surrounded by Hebrew letters"; later in the same thread, answering a request to tailor the colon rule, they wrote "StandardTokenizer implements UAX#29 word break rules. Done." Only the plain Hebrew"Hebrew case was discussed; it works. - Documented deviations. The grammar explains its TR#51 emoji rules in place of WB3c/WB15/WB16 (
StandardTokenizerImpl.jflex:166-208) and its Complex_Context run rule (:255-274); the conformance-test generator skips twoa ZWJ ExtPictcases "because it conflicts with TR#51 v11.0 rules" (generateJavaUnicodeWordBreakTest.pl:130-140). Nothing mentions the Hebrew rules. - Consistency. The Latin analogue
abc'3gives[abc, 3]; the U+05F3/U+05F4 spellings of the same words follow UAX #29; ICU4J 78.3 andICUTokenizergive[class, TET', 3],[TAV"ALEF.BET]and[ALEF"BET"GIMEL](ICUTokenizer has its own tailorings and is not an oracle for every row; it differs from ICU4J on ALEF'_a). - Precedent. LUCENE-5447 (#6510, fixed in 4.7.0) was a composition gap in the same word rule that the conformance lines missed, fixed as a bug.
- No decision on record. The piece came with the Unicode 6.3 upgrade (LUCENE-5357, #6421), which does not discuss how it combines with the other pieces; no later commit changed it.
- Framing. UAX #29 allows tailoring and a tokenizer emits tokens, not raw segments, so this is best framed as an undocumented rule-composition error in the rules the grammar itself names, not as a claim that every token must equal an ICU4J segment.
The devil's-advocate check253 words
The intended-behaviour check, the devil's-advocate verifier, built the strongest case for "intended": the javadoc invites users to copy the grammar if it does not suit them (StandardTokenizer.java:34-36); the official conformance tests pass; UAX #29 allows tailoring; in #3539 (2010) rmuir said mid-word ASCII quotes in Hebrew are the application's job ("that U+0022 should really be U+05F4. then it will work correctly with Lucene (including StandardTokenizer)"; mapping ' to U+05F3 is the same advice extended), and in the same thread described ICUTokenizer's tailored Hebrew grammar of the time (' as Extend in Hebrew context, so BET'2 stays one word) as the recommended approach; with U+05F3, UAX #29 itself keeps BET GERESH 2 as one word, which is what the tokenizer emits today for BET'2; and identical text still matches itself.
Why it fails: the invitation to copy is general, not a documented Hebrew deviation, and the grammar names WB7a-WB7c for this rule; the conformance file has only the shortest Hebrew cases; #3539 predates WB7a-WB7c (Unicode 6.3), which LUCENE-5357 adopted for the ASCII forms on purpose, and the ICUTokenizer tailoring it describes is gone (today's Default.rbbi:142-146 states WB7a-WB7c as in the annex, and ICUTokenizer splits BET'2), and with U+05F4 TAV GERSHAYIM ALEF.BET is one word while the ASCII form is split; for the ASCII BET'2 the annex, ICUTokenizer and the Latin abc'3 all break; symmetry helps only whole-form queries. Verdict: not intended (the cross-model review, the cross-model "intended" lens, rated the argument weak; the intended-behaviour check, the adjudication and audit 4 agree).
Recommended A: fold the Hebrew quote rules into the letter chain (both grammars)
In both grammars, make a Hebrew acronym X"Y one letter of the letter chain and let an apostrophe after a Hebrew letter end the word unless a letter follows it, then regenerate both scanners. All four shapes get the UAX #29 answer; 2,088 analysis tests and the static checks pass. Affected fields need a reindex.
ALetterSansHebrewEx = \p{WB:ALetter} {ExtFmtZwj}
HebrewAtom = {HebrewLetterEx} ( {DoubleQuoteEx} {HebrewLetterEx} )*
LetterAtom = {HebrewAtom} | {ALetterSansHebrewEx}
LetterChain = {LetterAtom} ( ( {ExtendNumLetEx}* | {MidLetterEx} ) {LetterAtom} )*
LetterChainHebrewEnd = ( {LetterAtom} ( {ExtendNumLetEx}* | {MidLetterEx} ) )* {HebrewAtom}
NumericChain = {NumericEx} ( ( {ExtendNumLetEx}* | {MidNumericEx} ) {NumericEx} )*
KatakanaChain = {KatakanaEx} ( {ExtendNumLetEx}* {KatakanaEx} )*
WordGroup = {KatakanaChain} | ( {NumericChain} | {LetterChain} )+
WordGroupHebrewEnd = ( {NumericChain} | {LetterChain} )* {LetterChainHebrewEnd}
// the word rule, replacing StandardTokenizerImpl.jflex:238-251:
{ExtendNumLetEx}* {WordGroup} ( {ExtendNumLetEx}+ {WordGroup} )* {ExtendNumLetEx}*
| {ExtendNumLetEx}* ( {WordGroup} {ExtendNumLetEx}+ )* {WordGroupHebrewEnd} {SingleQuoteEx}
{ return WORD_TYPE; }All options and trade-offs4 options · 842 words
Option A was validated in a scratch worktree at a9779ef3c193 (OpenJDK 25.0.3): grammar edit, both scanners regenerated with the build's own tasks, then the repro and its twin, the earlier repros, the analysis tests of lucene/core, the whole analysis-common module, analysis.tests and the static checks, with and without assertions. Options B-D were not run. A rough timing shows at most a small difference (StandardTokenizer about 1-3% slower); no controlled (JMH) benchmark was run. Until a fix ships, a char filter that maps the ASCII quotes next to Hebrew letters to U+05F4/U+05F3 avoids shapes 2-4 (option C).
A: fold the Hebrew quote rules into the letter chain (both grammars) (validated by the checks) (recommended)
The same change in StandardTokenizerImpl.jflex and UAX29URLEmailTokenizerImpl.jflex (17 lines added, 14 removed in each), then ./gradlew :lucene:core:regenerateStandardTokenizer :lucene:analysis:common:regenerateUAX29URLEmailTokenizer (with -x :lucene:analysis:common:regenerateTlds so the TLD list is not re-downloaded). X"Y becomes one letter of the chain, so it can follow or precede a mid-word character (shapes 3, 4); an apostrophe after a Hebrew letter ends the word unless a letter follows it (WB7a; shapes 1, 2); with a letter after it, the apostrophe stays a WB6/WB7 mid-word character through MidLetterEx (GIMEL'VAV RESH GIMEL' stays one word). Every row of the table gets the UAX #29 answer in both tokenizers; the controls are unchanged.
ALetterSansHebrewEx = \p{WB:ALetter} {ExtFmtZwj}
HebrewAtom = {HebrewLetterEx} ( {DoubleQuoteEx} {HebrewLetterEx} )*
LetterAtom = {HebrewAtom} | {ALetterSansHebrewEx}
LetterChain = {LetterAtom} ( ( {ExtendNumLetEx}* | {MidLetterEx} ) {LetterAtom} )*
LetterChainHebrewEnd = ( {LetterAtom} ( {ExtendNumLetEx}* | {MidLetterEx} ) )* {HebrewAtom}
NumericChain = {NumericEx} ( ( {ExtendNumLetEx}* | {MidNumericEx} ) {NumericEx} )*
KatakanaChain = {KatakanaEx} ( {ExtendNumLetEx}* {KatakanaEx} )*
WordGroup = {KatakanaChain} | ( {NumericChain} | {LetterChain} )+
WordGroupHebrewEnd = ( {NumericChain} | {LetterChain} )* {LetterChainHebrewEnd}
// the word rule, replacing StandardTokenizerImpl.jflex:238-251:
{ExtendNumLetEx}* {WordGroup} ( {ExtendNumLetEx}+ {WordGroup} )* {ExtendNumLetEx}*
| {ExtendNumLetEx}* ( {WordGroup} {ExtendNumLetEx}+ )* {WordGroupHebrewEnd} {SingleQuoteEx}
{ return WORD_TYPE; }Trade-offs
- Indexed terms change for text with the four shapes (and, within the exhaustive alphabets and the module tests, nowhere else). Affected fields need a reindex; it probably belongs under "Changes in Runtime Behavior" and may be main-only.
- Generated code.
StandardTokenizerImpl.javachanges 43 lines of packed tables;UAX29URLEmailTokenizerImpl.javarewrites 24,701 lines of tables with the same line count; the compiled classes stay within 15 bytes of the stock ones. Regenerating the email scanner takes minutes and a 12 GB heap. - Speed. Rough timing on about 24,000,000 characters of mixed Latin and Hebrew words, best of 5: StandardTokenizer 298-304 ms stock vs 305-308 ms fixed (about 1-3% slower), UAX29URLEmailTokenizer 330-331 vs 331-334 ms (three runs per configuration, two of them alternating, on a loaded host; the fix emits 6% more tokens on this text). No controlled (JMH) benchmark, so this shows neither a regression nor its absence.
- Two grammars carry the same rule; open #6527 (LUCENE-5464) proposes sharing rule text.
What the checks validated
- Regeneration ran cleanly and updated both checksum files.
- The repro and its twin: 5 of 6 and 4 of 5 fail on stock, 6 of 6 and 5 of 5 pass with the fix (gradle,
-eaand-da). With the regenerated scanners on the stock jars, the finder's JUnit passes 7 of 7 and the independent reproduction's tests 7 of 7 and 6 of 6. :lucene:core:test --tests 'org.apache.lucene.analysis.*'(22 suites, 174 tests, includingTestStandardAnalyzer45 of 45 with the WordBreakTest-12.1 cases) and all of:lucene:analysis:common:test(340 suites, 1,914 tests, includingTestUAX29URLEmailTokenizer, 46 tests with 1 nightly-only test skipped): 0 failed, 18 skipped, with-Ptests.asserts=trueandfalse.:lucene:analysis.tests:test(TestRandomChains, TestBugInSomething, TestAllAnalyzersHaveFactories): 7 tests, 0 failed, 1 skipped.:lucene:core:check :lucene:analysis:common:check -x test: every regenerate checksum check, format, ecjLint, forbiddenApis and license checks pass.- The finder's exhaustive comparison with the independent UAX #29 reference (8,108,730 strings up to length 6 over a 14-code-point word alphabet): 0 StandardTokenizer mismatches with the fix (33,321 on stock); UAX29URLEmailTokenizer keeps only 2 minimal cases of its URL rules, which are not Hebrew.
Make HebrewLetter ("HebrewLetter)* an atom of the letter chain and drop the " branch of the Hebrew piece; leave HebrewLetter ' alone. Not run.
Trade-offs
Smaller, but shapes 1 and 2 stay, and shape 1 (BET'2, TET'3) is the most natural trigger.
A char filter that turns " between Hebrew letters into U+05F4 (the 2010 advice in #3539) and, by extension, ' after a Hebrew letter into U+05F3. Not run as a filter; the shapes with the marks mapped by hand were run.
Trade-offs
Shapes 2-4 then come out as one word (executed with the marks mapped by hand), but shape 1 stays joined, because U+05F3 is ALetter and UAX #29 itself keeps BET GERESH 2 as one word. It changes the indexed characters, so queries need the same filter.
State in the javadoc that WB7a-WB7c are applied only in isolation.
Trade-offs
Cheap, but keeps a deviation from the annex the class promises, while ICUTokenizer and the U+05F3/U+05F4 forms already follow it.
Search products that analyze Hebrew text with this tokenizer index the same wrong terms; nothing beyond wrong term boundaries inside one value was found (no hang, no amplification, no effect on other fields or documents).
An independent UAX #29 reference, written from the annex and the Unicode 12.1 data, was compared with the stock tokenizers on all 8,108,730 strings up to length 6 over a 14-character alphabet. Every Hebrew mismatch reduced to four shapes, first seen at length 3; a read of the word rule gave the cause.
The method in full238 words
Method: bounded-exhaustive translation validation (the hunt's semi-formal tier; no model checker, SMT solver or proof).
- The reference. A hunt unit on UAX #29 word boundaries wrote a word-break reference for Unicode 12.1 from the annex rules, driven only by the Unicode 12.1 data files. It had 0 mismatches on all 1,823 lines of the official WordBreakTest-12.1.0, killed 6 single-rule mutants, and had 0 mismatches against ICU4J 78.3 on 168,420 strings.
- The comparison. Every string of length 1-4 over three alphabets (27, 24 and 14 code points, the last supplementary-plane) and of length 1-6 over a 14-code-point word-rule alphabet (8,108,730 strings) was tokenized by the stock
StandardTokenizerandUAX29URLEmailTokenizerand compared with the reference. Mismatches were reduced to minimal strings and classified: every Hebrew mismatch is one of four shapes (98, 186, 54 and 36 minimal strings for StandardTokenizer), first seen at length 3 (2026-10-27). - Root cause and repro. A read of the word rule found the stand-alone Hebrew piece; the finder wrote a public-API JUnit and a fix, and re-ran the exhaustive comparison with the fix (0 StandardTokenizer mismatches).
- Checks. Claude verifiers with their own tests (the independent reproduction with an ICU4J oracle, the intended-behaviour check a devil's advocate with ICUTokenizer and ICU4J side by side), review records by a model from another family (OpenAI GPT-5.6) that ran their own tests, an adjudication with fresh seeds, a registry re-run and an independent audit with its own ICU4J probe.
No report found (2026-10-27, 544 recorded queries, 515 of them run, over GitHub issues, PRs and commits, JIRA, the mailing lists, downstream projects and the web). Nearest: #7165 (only the working Hebrew"Hebrew case) and #6510 (a similar gap in the same rule, fixed in 4.7.0).
Searches5 searches
Novelty new. No issue, PR, commit, JIRA entry, list thread, downstream report or web page describes this defect. Closest items, none this bug: #7165 / LUCENE-6103 (closed Not A Problem, 2014: the only upstream thread on the Hebrew quote rules; only the plain Hebrew"Hebrew case, which works); #6421 / LUCENE-5357 (introduced the piece); #6510 / LUCENE-5447 (a composition gap in the same rule, fixed in 4.7.0); #3539 / LUCENE-2465 (2010, QueryParser and mid-word "; the map-to-U+05F4 advice); #8050; #6527 (open, sharing rules between the grammars); open PR #16911 (deletes branch_10x). Gaps: only five GitHub queries used Hebrew script (all empty); GitHub search does not cover attachments or code; private security reports are not visible; a search cannot prove absence.
| Where | When | How | Found |
|---|---|---|---|
| scripted prior-report search (6 runs) | 2026-10-27 | grammar identifiers, symptoms, UAX #29 conformance and ICU differences, code points and geresh/gershayim wording | 167 GitHub issue/PR, 15 commit, 10 downstream, 100 mailing-list, 20 JIRA and 42 web queries (27 of the web ones planned only and covered by the AI tools' own web searches, 2 empty responses); no report |
| GitHub search by hand | 2026-10-27 | 54 queries over apache/lucene, lucene-solr, solr, lucenenet, Elasticsearch, OpenSearch, JFlex, HebMorph and all of GitHub; 5 in Hebrew script | application issues and re-implementations only |
| Open apache/lucene PRs | 2026-10-27 | changed-file lists of all 410 open PRs | only #16911 (deletes branch_10x) lists these grammars |
| JIRA archive and mailing lists | 2026-10-27 | 15 JQL queries over LUCENE, SOLR, LUCENENET; 49 more lists.apache.org queries | only the issues listed above; pre-4.7 or unrelated threads |
| Web, mediawiki.org, Stack Overflow, forums | 2026-10-27 | 18 web, 12 mediawiki.org, 28 Stack Overflow and 12 forum searches | nothing relevant |
Introduced by LUCENE-5357, the Unicode 6.3 upgrade that added the Hebrew rules (d516948bbde, 2013-12-06; first release 4.7.0). A probe shows it on all 119 releases from 4.7.0 to 10.5.2 and main; 4.6.1 and older have no Hebrew rules. Never changed since; not fixed upstream.
Versions tested6 rows
Introduced by d516948bbde8774d668c79432f33f5384d4075f9, "LUCENE-5357: Upgrade StandardTokenizer and UAX29URLEmailTokenizer to Unicode 6.3; ..." (sarowe, trunk, 2013-12-06; now GitHub #6421; an svn-era JIRA patch, no PR). It added the Hebrew_Letter, Single_Quote and Double_Quote classes and wrote WB7a-WB7c as the stand-alone piece in both grammars. The branch_4x merge d53a21ef180 (2013-12-07) shipped first, in 4.7.0.
Never changed: git log -L238,251 on the word rule lists LUCENE-5357's Katakana sync, LUCENE-5447 (2014), LUCENE-8527 (2019, a rename that also re-aligned the Hebrew piece's whitespace) and #14820 (2025, whitespace), plus beac046c633, an accidental early commit of the LUCENE-5447 change, and its revert 25e0cc33c47; none changed what the Hebrew piece matches or how pieces are joined. Upstream main a95c754ee00, branch_10x and branch_10_6 carry the same piece (2026-10-27).
Limits: 3.x and older were not run (no Hebrew rules, by reading); in 5.x, StandardAnalyzer.setVersion before 4.7.0 selects StandardTokenizer40 (not affected by reading, not executed).
| Version or commit | Verdict | Result |
|---|---|---|
main a9779ef3c193 | affected | this page's JUnit fails 5 of 6 with -ea and -da; the probe deviates on all four shapes in both tokenizers |
| 10.5.2, 9.12.3 | affected | this page's JUnit fails 5 of 6 with -ea and -da; the email twin 4 of 5 on 10.5.2 (-da) |
| 4.7.0 to 10.5.2 (119 releases) | affected | version-portable probe: 5 of 5 inputs deviate in both tokenizers, with main's output |
| 4.7.0 with matchVersion LUCENE_46 | back-compat scanners | the old Unicode 6.1 scanners: not affected |
| 4.0.0 to 4.6.1 (11 releases) | not affected | Unicode 6.1 rules, which have no Hebrew rules; conform to them on all 5 inputs |
fix (scratch worktree at a9779ef3c193) | not affected | the repro 6 of 6 and the twin 5 of 5 pass; 2,088 analysis tests pass |
Reproduced by the finder, independent verifiers with their own tests, cross-model reviews by OpenAI GPT-5.6 that ran their own tests (both real, high), an adjudication with fresh seeds, a registry re-run and an independent audit, all with and without assertions.
Checks and verdicts384 words
Each check worked from the code and its own runs. This page added a new repro and its email twin, the gradle-regenerated fix, gradle runs of the core analysis tests, the whole analysis-common module, analysis.tests and the static checks, and a token, search and timing probe.
| Check | Verdict | What it found |
|---|---|---|
| Finder's exhaustive comparison and JUnit | candidate | Four Hebrew shapes in 8,108,730 strings; the JUnit fails 6 of 7 on main with -ea and -da, on 10.5.2 and 9.12.3; 0 mismatches with the fix. |
| Cross-model review, refute lens (OpenAI GPT-5.6, executed) | real (high) | Its own test (GIMEL'8, x:FINAL-MEM') fails 4 of 6 with -ea and -da over 3 iterations; passes with the fix. Intended argument: weak. |
| Independent reproducer | real (high) | Own tests for both tokenizers fail on main, 10.5.2 and 9.12.3; ICU4J agrees with all 13 expectations; the fix makes them pass. |
| Cross-model adversary, intended lens (OpenAI GPT-5.6, executed) | real (high) | Own test (ZAYIN'4, q:NUN') fails 4 of 6 on main and 10.5.2; with the fix it and TestStandardAnalyzer pass. Intended argument: weak. |
| Intended-behaviour devil's advocate | real; not intended (high) | ICUTokenizer and ICU4J give the UAX #29 answer where StandardTokenizer does not; with U+05F4 all four segmenters agree. |
| Adjudication | confirmed (high), duplicate of none | Re-ran the independent reproduction's tests on main: 6 of 7 fail with -ea and -da, the email test 5 of 6. |
| Registry re-run and audit 4 | keep | Finder's JUnit 6 of 7 fail with -ea and -da, 7 of 7 with the fix; the audit's own ICU4J probe: 8 of 16 new inputs deviate, all 8 controls agree. |
| This page's runs | fix validated | New repro 5 of 6 fail on main, 10.5.2 and 9.12.3; with the regenerated scanners 2,088 analysis tests and the static checks pass. |
| Fact checks of this page (an independent Claude check; OpenAI GPT-5.6, file-read only) | real; wording fixed | The Claude check re-ran the repro on main with gradle and the repro runner (four seeds, -ea and -da) and on 10.5.2 and 9.12.3 with -ea: 5 of 6 fail; with the fix the repro, its twin, TestStandardAnalyzer and TestUAX29URLEmailTokenizer pass. The GPT-5.6 review re-checked the claims against the code and the run records. Both confirm the bug; their wording corrections are applied. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 462 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,401 words
- Registry title
- StandardTokenizer (StandardAnalyzer's tokenizer) and UAX29URLEmailTokenizer misapply the UAX #29 Hebrew quote rules WB7a-WB7c next to other word characters: a Hebrew letter + ASCII apostrophe swallows a following digit or underscore (BET'2 is one term), an ASCII apostrophe after a Hebrew letter that ends a mid-word chain is dropped (HE.SHIN' -> HE.SHIN), and a word with an ASCII quotation mark between Hebrew letters is split when it also contains a mid-word '.', ':' or quote (ALEF.TSADI"LAMED, TAV"ALEF.BET)
- Area
- analysis / StandardTokenizer JFlex word rule (the <ALPHANUM> rule of core StandardTokenizerImpl.jflex; the same rule in analysis-common UAX29URLEmailTokenizerImpl.jflex)
- Priority
- Low, rank 293: Default StandardTokenizer, but only Hebrew written with ASCII quotes in a few rare no-space adjacencies, and a fix changes indexed terms.
- Impact class
- silent wrong results
- Impact note
- Anyone who analyzes Hebrew text with the default StandardAnalyzer or StandardTokenizer (default settings, no scale), or with UAX29URLEmailTokenizer, and writes geresh or gershayim with the ASCII apostrophe U+0027 or quotation mark U+0022 (the usual keyboard form) gets wrong terms in four adjacencies, silently: 'כיתה ב'2' (a class name, 'class BET'2') indexes BET'2 as one term so a TermQuery for 2 misses the document, HE.SHIN' loses its apostrophe, and ALEF.TSADI"LAMED, TAV"ALEF.BET and ALEF"BET"GIMEL are split into two terms; text that uses only the Hebrew punctuation marks U+05F3/U+05F4 (no ASCII quotes) is not affected, plain acronyms such as TAV SHIN"PE and words ending in a geresh are tokenized correctly, -ea and -da behave the same, and because a fix changes indexed terms, affected fields need reindexing after it.
- Summary
- You index Hebrew text with the stock StandardAnalyzer, whose StandardTokenizer says it implements the UAX #29 word-break rules, among them WB7a-WB7c, which keep an ASCII apostrophe after a Hebrew letter (BET') and an ASCII quotation mark between two Hebrew letters (TAV"ALEF) inside a word, and you expect them to combine with the other rules as the annex and ICU do: BET'2 gives [BET', 2], HE.SHIN' gives [HE.SHIN'], and TAV"ALEF.BET and ALEF.TSADI"LAMED are single words. Instead the grammar writes 'Hebrew letter + quote' as a separate piece that may be glued to any following piece but cannot continue a letter chain, so BET'2 becomes one term (a search for 2 misses the document), HE.SHIN' loses the apostrophe, and the quoted words are split into [TAV"ALEF, BET] and [ALEF.TSADI, LAMED]. StandardTokenizer and UAX29URLEmailTokenizer behave the same, with no error.
- Introduced
- d516948bbde8774d668c79432f33f5384d4075f9 (LUCENE-5357 'Upgrade StandardTokenizer and UAX29URLEmailTokenizer to Unicode 6.3', apache/lucene#6421; trunk svn r1548595 by sarowe, an svn-era JIRA patch, no pull request): it added the Hebrew_Letter, Single_Quote and Double_Quote classes and wrote WB7a-WB7c as the stand-alone piece '( {HebrewLetterEx} ( {SingleQuoteEx} | {DoubleQuoteEx} {HebrewLetterEx} ) )' of the '+'-repeated piece list in the <ALPHANUM> rule of both grammars; the branch_4x merge d53a21ef180ea4369f148b7541ae05fe71ae9bb6 (svn r1548762, 2013-12-07) is the commit that shipped first, in 4.7.0 [read: git log -S HebrewLetterEx and git blame of StandardTokenizerImpl.jflex:239,245 and UAX29URLEmailTokenizerImpl.jflex:373,379 in the full-history clone; exec: 4.6.1 not affected, 4.7.0 affected]. Not a regression: no later commit changed the Hebrew piece or how the pieces are joined (git log -L238,251: LUCENE-5447, LUCENE-8527 and #14820 touched other parts, names or whitespace only) (2013-12-06 (trunk; branch_4x merge 2013-12-07; first release 4.7.0))
- First affected version
- 4.7.0 [exec] (the follow-up's version-portable probe: 4.6.1, the last release before, implements the Unicode 6.1 rules, which have no Hebrew rules, and conforms to them on all 5 inputs; 4.7.0 deviates on 5 of 5 inputs in both tokenizers; 4.7.0 with matchVersion LUCENE_46 selects the old 6.1 scanners and is not affected)
- Affected versions
- Every release from 4.7.0 to 10.5.2, 10.6.0 (by reading) and main a9779ef3c193; not fixed. [exec] the follow-up's version-portable probe (public API, one source for 4.0.0 to main) reports the bug present, 5 of 5 inputs deviating in StandardTokenizer and in UAX29URLEmailTokenizer with output identical to main's, on all 119 releases 4.7.0-4.10.4, 5.0.0-5.5.5, 6.0.0-6.6.6, 7.0.0-7.7.3, 8.0.0-8.11.4, 9.0.0-9.12.3 and 10.0.0-10.5.2 (-ea) and on main (-ea and -da), and absent on the 11 releases 4.0.0-4.6.1 and on 4.7.0 with matchVersion LUCENE_46 (133 runs, SHA-1-checked jars, all as expected); the finder's JUnit fails 6 of 7 on 9.12.3, 10.5.2 and main (-ea and -da), the independent reproduction's tests fail on 9.12.3 and 10.5.2, and the cross-model reviewers' own tests fail on 10.5.2. [read] upstream main a95c754ee00, branch_10x and branch_10_6 carry the same Hebrew piece in both grammars (read-only GitHub API, 2026-10-27); in 5.x, StandardAnalyzer.setVersion before 4.7.0 selects StandardTokenizer40 (not affected, not executed).
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal: exhaustive check
- Upstream and prior reports
- none found (the prior-report record, verdict new, searched 2026-10-27 plus, read-only, 544 recorded queries: six scripted prior-report searches with 167 GitHub issue/PR, 15 commit, 10 downstream, 100 mailing-list (dev@, java-user@, issues@, commits@) and 20 JIRA queries; 54 manual GitHub searches over apache/lucene, apache/lucene-solr, apache/solr, apache/lucenenet, Elasticsearch, OpenSearch, JFlex, the HebMorph repositories and all of GitHub, 5 of them in Hebrew script; the changed-file lists of all 410 open apache/lucene PRs; 15 JQL queries over LUCENE, SOLR and LUCENENET; 49 more lists.apache.org queries; web, mediawiki.org, Stack Overflow and forum searches; the local full history). Nearest items, not this bug: #7165 (LUCENE-6103, closed Not A Problem, 2014: the only upstream thread on the Hebrew quote rules in StandardTokenizer; the grammar's author (sarowe) wrote that WB7b/WB7c were followed 'to the letter'; only the plain Hebrew"Hebrew case, which works, is discussed), #6421 (LUCENE-5357, the change that introduced the piece; no discussion of how it composes), #6510 (LUCENE-5447, fixed in 4.7.0: a composition gap of the same <ALPHANUM> rule, treated as a bug; different sub-expression and inputs), #8050, #3539, #6527. Not fixed upstream: upstream main a95c754ee00, branch_10x and branch_10_6 carry the same piece, and the only open PR touching these grammars is #16911, which deletes branch_10x. Update 2026-11-02: #16911 (open when searched) merged 2026-10-29 as 48cf3f515d6, which emptied branch_10x to a README tombstone (10.6 is the last 10.x minor; 10.x fixes go to branch_10_6); it fixes nothing here.
- Lucene commit tested
- main
a9779ef3c193(2026-10-27), with and without assertions; the 10.5.2 and 9.12.3 release jars; all 130 releases from 4.0.0 to 10.5.2 with a version-portable probe; the fix in a scratch worktree ata9779ef3c193(both scanners regenerated with the build's own tasks). OpenJDK 25.0.3, Linux aarch64. The cross-model reviews ran onea57360912ed, whose analysis sources are identical toa9779ef3c193.
- Finding record
lucene/core/src/java/org/apache/lucene/analysis/standard/StandardTokenizer.java:31-32 (at a9779ef3c193) ('This class implements the Word Break rules from the Unicode Text Segmentation algorithm, as specified in Unicode Standard Annex #29'), :34-36 ('If this tokenizer does not suit your application, please consider copying this source code directory ...')lucene/core/src/java/org/apache/lucene/analysis/standard/StandardTokenizerImpl.jflex:42 (at a9779ef3c193) (%unicode 12.1), :226-237 (comment: the word rule implements WB5, WB6, WB7, WB7a, WB7b, WB7c, WB9, WB10, WB13, WB13a, WB13b), :238-252 (the <ALPHANUM> rule; the Hebrew piece at :239 and :245)lucene/analysis/common/src/java/org/apache/lucene/analysis/email/UAX29URLEmailTokenizer.java:31-33 (at a9779ef3c193) (same UAX #29 wording, plus URLs and emails per the RFCs); UAX29URLEmailTokenizerImpl.jflex:45 (%unicode 12.1), :360-386 (identical word rule; the Hebrew piece at :373 and :379)lucene/test-framework/src/java/org/apache/lucene/tests/analysis/standard/WordBreakTestUnicode_12_1_0.java (at a9779ef3c193) (official conformance cases; the Hebrew cases are pairs only: 05D0 x 0027 and 05D0 x 0022 x 05D0, with or without 0308)upstream read-only GitHub API (2026-10-27): StandardTokenizerImpl.jflex:239,245 and UAX29URLEmailTokenizerImpl.jflex:373,379 on main a95c754ee00, branch_10x 657014b3c57 and branch_10_6 4ae946e5a9b carry the same Hebrew pieceSECURITY.md (In-scope reports; Untrusted query and analysis input; Reporting out-of-scope issues publicly)
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/analysis/standard/StandardTokenizerImpl.jflex:42 (%unicode 12.1), :88, :91, :96-98 (classes), :226-237 (comment listing WB7a-WB7c), :238-252 (word rule; Hebrew piece :239, :245)lucene/analysis/common/src/java/org/apache/lucene/analysis/email/UAX29URLEmailTokenizerImpl.jflex:45, :360-386 (the same word rule; Hebrew piece :373, :379)lucene/test-framework/src/java/org/apache/lucene/tests/analysis/standard/WordBreakTestUnicode_12_1_0.java:7163-7169 (the official ALEF' and ALEF"ALEF cases)
- Who is affected
lucene/core/src/java/org/apache/lucene/analysis/standard/StandardAnalyzer.java:29-30, :86 (StandardAnalyzer builds a StandardTokenizer)grep of 'new StandardTokenizer(' over lucene/*/src/java and lucene/analysis/*/src/java: 43 *Analyzer.java files plus StandardTokenizerFactory (demo excluded)
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/analysis/standard/StandardTokenizer.java:31-36; lucene/analysis/common/src/java/org/apache/lucene/analysis/email/UAX29URLEmailTokenizer.java:31-33lucene/core/src/java/org/apache/lucene/analysis/standard/StandardTokenizerImpl.jflex:166-208, :226-237, :255-274lucene/test-framework/src/java/org/apache/lucene/tests/analysis/standard/generateJavaUnicodeWordBreakTest.pl:130-140
- Suggested fix
lucene/core/build.gradle:101-117, :202-231 (regenerate tasks, 12g heap, regenerateTlds dependency)
- Diagrams
lucene/core/src/java/org/apache/lucene/analysis/standard/StandardTokenizerImpl.jflex:238-252
- TestStandardTokenizerHebrewApostrophe.java (JUnit test):
repro/LFV-734/src/org/apache/lucene/analysis/standard/TestStandardTokenizerHebrewApostrophe.java(29 lines)
Hebrew is shown in logical order.
assets/LFV-734/bug.png(462 KB)
