IrishLowerCaseFilter lower-cases with Character.toLowerCase(chArray[i]), one 16-bit char at a time. A letter outside the BMP is two chars, a surrogate pair; each half alone has no case and comes back unchanged, so the capital letter survives. The loop already advances by code point, like GreekLowerCaseFilter; only the read is per char.
- You index the Irish sentence "Tá X anseo" ("X is here") with the default
IrishAnalyzer, where X is the Adlam text U+1E900 U+1E923, a capital and a small letter. Each letter is stored as two Java chars, a surrogate pair. - You search for X in lower case, U+1E922 U+1E923, through the same analyzer. You expect 1 hit: the analyzer lower-cases, and
LowerCaseFiltermaps U+1E900 to U+1E922. - You get 0 hits. The filter passes each char alone to
Character.toLowerCase; half of a pair has no case, so the capital is written back unchanged and the indexed term is still U+1E900 U+1E923. - Only the 282 capitals outside the BMP are hit:
AB+ Osage U+104B0 +CbecomesabU+104B0c. Exact-case searches still match, nothing throws, and such an index passesCheckIndex.
Fields analysed with the stock IrishAnalyzer, or with IrishLowerCaseFilter or the irishLowercase factory in a chain, at index and query time. It needs a capital from one of eight scripts outside the BMP, which Irish text never has, so only mixed-script content such as a quoted Adlam or Osage name is hit; there it happens every time.
Mechanism, terms and numbers558 words
The javadoc of IrishLowerCaseFilter says it "Normalises token text to lower case"; it is the only lower-casing step of IrishAnalyzer, for indexing and in normalize. Its loop passes one UTF-16 char at a time to Character.toLowerCase (IrishLowerCaseFilter.java:55). Letters outside the Basic Multilingual Plane (BMP) are stored as two chars, a surrogate pair, and each half alone has no case, so the 282 such capitals (Adlam, Deseret, Garay, Medefaidrin, Old Hungarian, Osage, Vithkuqi, Warang Citi) stay upper-case while BMP letters in the same token are lower-cased. A document indexed with IrishAnalyzer that contains the Adlam text U+1E900 U+1E923 is found by that exact spelling but not by its lower-case form U+1E922 U+1E923 (0 hits instead of 1). Nothing throws, and an index built from such text passes CheckIndex. Irish text itself has no such letters, so only mixed-script content in an Irish field is affected. The code has been there since the class was added in 3.6.0 (2012); reading the code point instead of the char fixes it.
- UTF-16 char: Java stores text as 16-bit
charvalues. Most letters, including every Irish one, fit in one char: they are in the Basic Multilingual Plane (BMP). - Supplementary character: a code point above U+FFFF, stored as two chars, a surrogate pair (a high and a low surrogate). The Adlam capital alif U+1E900 is the pair U+D83A U+DD00. A surrogate alone is not a letter and has no case.
- Code point lower-casing:
Character.toLowerCase(int)maps a whole code point (U+1E900 to U+1E922). Thecharoverload cannot: the JDK documents that it "cannot handle supplementary characters".
IrishLowerCaseFilter.incrementTokenfirst handles the Irish prefixes (n-eclipsis and t-prothesis:nAthairbecomesn-athair), then lower-cases the rest of the term:i += Character.toChars(Character.toLowerCase(chArray[i]), chArray, i);(IrishLowerCaseFilter.java:54-56on main).chArray[i]is one char. For U+D83A,Character.toLowerCasereturns U+D83A;toCharswrites it back and returns 1; the low half U+DD00 goes the same way. The capital letter is unchanged.- The loop is otherwise written for code points: it advances by the number of chars
toCharswrote, asGreekLowerCaseFilterdoes (GreekLowerCaseFilter.java:46, which readsCharacter.codePointAt(chArray, i, chLen)).LowerCaseFilterlower-cases each code point throughCharacterUtils.toLowerCase(CharacterUtils.java:47-63). IrishAnalyzerrunsStandardTokenizer, which keeps words of every script, and this filter is its only lower-casing step (IrishAnalyzer.java:126-135);IrishAnalyzer.normalize, which the classic query parser calls for range, wildcard, regexp, prefix and fuzzy terms (QueryParserBase.java:550-729, read; not run end to end), runs the same filter (:138-142).
input (after KeywordTokenizer) | expected, as LowerCaseFilter | IrishLowerCaseFilter |
|---|---|---|
| U+10400 (Deseret capital long I) | U+10428 | U+10400 |
| U+104B0 (Osage capital A) | U+104D8 | U+104B0 |
| U+1E900 U+1E923 (Adlam capital alif, small daali) | U+1E922 U+1E923 | U+1E900 U+1E923 |
AB U+104B0 C | ab U+104D8 c | ab U+104B0 c |
nA U+10400 (n-eclipsis, then Deseret) | n-a U+10428 | n-a U+10400 |
nAthair, ÉIRE (controls) | n-athair, éire | n-athair, éire |
Every code point whose lower case differs under Character.toLowerCase(int): 1,178 in the BMP, all handled correctly, and 282 outside it, all left unchanged: Adlam 34, Deseret 40, Garay 22, Medefaidrin 32, Old Hungarian 51, Osage 36, Vithkuqi 35, Warang Citi 32 (JDK 25 tables). Emoji and CJK ideographs have no case and are not affected. No lower-case mapping crosses between the BMP and the supplementary planes, so the term length never changes.
Exposure and likelihood295 words
- Who is exposed. The stock
IrishAnalyzerwith any constructor (indexing andnormalize), any chain withIrishLowerCaseFilter, and theirishLowercasefactory (IrishLowerCaseFilterFactory.java:40; itsnormalizeis the same filter,:60-64). - What it takes. An upper-case letter of Adlam (a living script for Fulani), Osage, Deseret, Old Hungarian, Warang Citi, Medefaidrin, Vithkuqi or Garay in an Irish-analysed field. Irish orthography has none, so it takes mixed-script content: a quoted name or word, or a historic-script passage.
- What changes for the user. The capitalised and the lower-case spellings become two different terms, so a case-insensitive search misses in both directions (0 hits instead of 1 in the repro). In the intended-behaviour check's probe, "Tá X agus Y anseo" analysed to
[tá, U+1E900 U+1E923, U+104B0, anseo]with capital Adlam and Osage words and[tá, U+1E922 U+1E923, U+104D8, anseo]with lower-case ones. Exact-case matches still work. - What does not happen. No exception, no change in term length, valid UTF-16 terms and no index damage: an index built by the stock
IrishAnalyzerfrom such text passesCheckIndexin two segments and afterforceMerge(1), and every term decodes as strict UTF-8 (assertions on and off). BMP letters in the same token are lower-cased. - Scale. Deterministic, with assertions on or off, at any index size: every occurrence of such a capital is indexed (and queried) unchanged.
- JDK dependence. The affected set follows the JDK's Unicode tables: 282 code points with JDK 25. Deseret letters exist in every JDK Lucene supports; scripts added in later Unicode versions (Osage and Adlam in 9.0, Garay in 16.0) need a newer JDK. Only JDK 25 was run.
- How likely. Rare: no public report of it was found (searches of 2026-10-27 and 2026-10-28), and the trigger needs mixed-script text in an Irish field.
The test shows that IrishAnalyzer (through IrishLowerCaseFilter, which lower-cases one UTF-16 char at a time) does not lower-case an upper-case letter outside the BMP.
/*
* 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.ga;
import java.util.Locale;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestIrishLowerCaseSupplementary extends LuceneTestCase {
public void testSupplementaryUpperCaseIsLowerCased() throws Exception {
try (Analyzer a = new IrishAnalyzer()) {
// expected: Deseret capital U+10400 lower-cased to U+10428; actual: U+10400 unchanged
int cp = a.normalize("f", "\uD801\uDC00").utf8ToString().codePointAt(0);
assertEquals("U+10428", String.format(Locale.ROOT, "U+%X", cp));
}
}
}
# in a copy of this finding's repro folder (repro/LFV-685/, 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:$LUCENE/lucene/analysis/common/build/libs/lucene-analysis-common-$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.ga.TestIrishLowerCaseSupplementary
- Expected vs actual
- Expected the Deseret capital U+10400 to become U+10428; actual: it stays U+10400 (
expected:<U+104[28]> but was:<U+104[00]>), with-eaand-da.
More files, environment, full output32 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: expected:<U+104[28]> but was:<U+104[00]>
org.junit.Assert.assertEquals(Assert.java:117)
org.junit.Assert.assertEquals(Assert.java:146)
org.apache.lucene.analysis.ga.TestIrishLowerCaseSupplementary.testSupplementaryUpperCaseIsLowerCased(TestIrishLowerCaseSupplementary.java:29)- The javadoc says "Normalises token text to lower case", with no BMP limit;
LowerCaseFilterlower-cases these letters. - Lucene made
LowerCaseFilterhandle supplementary characters in 3.1 (LUCENE-2069), before this class was added in 3.6.0. - The loop already advances by code point; only the read is per char. TruncateTokenFilter's same defect was fixed as a bug (#15899).
The full argument278 words
- Contract. "Normalises token text to lower case, handling t-prothesis and n-eclipsis (i.e., that 'nAthair' should become 'n-athair')" (
IrishLowerCaseFilter.java:24-27, word for word as in 3.6.0). No script or BMP restriction;LowerCaseFilter's javadoc is nearly the same ("Normalizes token text to lower case.",LowerCaseFilter.java:22) and it lower-cases these letters. - Policy before the class existed.
lucene/CHANGES.txt:17927-17928(3.1.0): "LUCENE-2069: Added Unicode 4 support to LowerCaseFilter. Due to the switch to Java 5, supplementary characters are now lowercased correctly."TurkishLowerCaseFilter(LUCENE-2102, 2009) andGreekLowerCaseFilter(LUCENE-2463, 2010) map code points. In 3.6.0, the release that added this filter,LowerCaseFilteralready mapped by code point: the 3.6.0 jar lower-cases U+10400, U+104B0 and U+1E900 (executed on JDK 25). Of these, only Deseret U+10400 (Unicode 3.1) was in the JDKs of 2012; Osage and Adlam entered Unicode in 9.0 (2016). - Same defect class treated as a bug.
lucene/CHANGES.txt:1198-1200(10.5.0): "GITHUB#15899, GITHUB#15900: Improve TruncateTokenFilter to truncate on codepoints not chars and no longer produce half surrogates". On #15899 uschindler wrote "We modified all other similar filters in the past (like ngrams), they all work correctly". That is precedent for treating char-wise handling as a bug, not proof of a complete audit:CapitalizationFilter(LFV-533) is still char-wise too. - Nothing presents it as a choice. The class was new in 3.6.0 with no legacy switch; the upstream tests assert expected output only for BMP input (
TestIrishAnalyzer.testRandomStringsfeeds random text but checks only token-stream consistency, not case); the LUCENE-3883 thread (#4956, 25 comments) never mentions supplementary characters; the loop is written in the code-point idiom (i += Character.toChars(...)) but reads a char, and the JDK documents that thecharoverload "cannot handle supplementary characters".
The devil's-advocate check234 words
The strongest case for "intended" (the intended-behaviour check and the cross-model reviewer): the constructor says it "normalises Irish token text" (IrishLowerCaseFilter.java:31), Irish is written in Latin script with no supplementary letters, so Adlam or Osage text is out of scope; and the factory says "this will 'mostly work', except for special cases, just like most other filters" (IrishLowerCaseFilterFactory.java:60).
Why it fails: the class javadoc promises lower case without restriction; IrishAnalyzer tokenises every script and has no other lower-casing step; the filter does lower-case non-Irish BMP scripts (Greek, Cyrillic, Armenian, executed by the intended-behaviour check), so the only gap is the supplementary planes, which comes from the char API, not from a choice of domain; TurkishLowerCaseFilter uses the same wording ("Normalizes Turkish token text to lower case", TurkishLowerCaseFilter.java:25) and maps code points; and the factory comment came with 44695f0f3f5 ("LUCENE-3883: add multitermaware to the irish case filter") and is about multi-term normalisation, not supplementary characters. Every verifier rejected the intended-behaviour argument, and the cross-model reviewer rated it weak twice. A later cross-model fact check of this doc rated it moderate: domain scope, BMP-only tests, the reindex cost and the factory comment give it some weight, although no evidence shows that the BMP limit was considered and chosen. What survives is the narrow reach and the reindex cost: reasons for low priority and a CHANGES note, not for keeping the behaviour.
Recommended A: read the code point, not the char (one statement)
Read the code point instead of the char: Character.toLowerCase(Character.codePointAt(chArray, i, chLen)) at IrishLowerCaseFilter.java:55. The repro then passes, assertions on and off, and so does the whole analysis-common test task (1,913 tests). Only the 282 affected capitals change, so a CHANGES note should mention reindexing.
for (int i = idx; i < chLen; ) {
- i += Character.toChars(Character.toLowerCase(chArray[i]), chArray, i);
+ i +=
+ Character.toChars(
+ Character.toLowerCase(Character.codePointAt(chArray, i, chLen)), chArray, i);
}All options and trade-offs3 options · 449 words
Two equivalent fixes were built and validated in a scratch worktree at a9779ef3c193; A is the smallest diff and the one every checker ran.
The finder's one-line fix, wrapped by google-java-format. The loop already advances by the number of chars written, so nothing else changes.
for (int i = idx; i < chLen; ) {
- i += Character.toChars(Character.toLowerCase(chArray[i]), chArray, i);
+ i +=
+ Character.toChars(
+ Character.toLowerCase(Character.codePointAt(chArray, i, chLen)), chArray, i);
}Trade-offs
- Changes output only for the 282 supplementary capitals: every BMP code point already matched, and no lower-case mapping crosses the BMP boundary, so the term length never changes.
- Indexed terms change for text with these letters: indexes built with
IrishAnalyzerneed reindexing for exact consistency. For TruncateTokenFilter the project added factory parameters for the old behaviour (#15900); an option seems unnecessary here, but that is the maintainers' call.
What the checks validated
- Stock main: the paste-ready test fails 3 of 4, assertions on and off; the 10 upstream Irish tests pass.
- With fix A,
org.apache.lucene.analysis.ga.*: 4 classes, 14 tests, 0 failures, assertions on and off. - With fix A, the whole
:lucene:analysis:common:testtask: 340 classes, 1,913 tests, 0 failures, 18 skipped (skips not examined). ecjLintMain,ecjLintTestandcheckGoogleJavaFormatof the module pass.- Earlier checks: the finder's test (4 of 4), the independent reproduction's (7 of 7, and 17 of 17 with the upstream Irish tests), the cross-model reviewer's (5 of 5), the intended-behaviour check's (8 of 8), and the history probe on 3.6.0 and main all pass with the same change.
B: call CharacterUtils.toLowerCase(chArray, idx, chLen), as LowerCaseFilter does (validated by the checks)
Replace the loop with the shared helper and import org.apache.lucene.analysis.CharacterUtils (+2/-3 lines). It maps each code point with Character.toLowerCase(int), with an ASCII fast path, so the output is the same as A.
- for (int i = idx; i < chLen; ) {
- i += Character.toChars(Character.toLowerCase(chArray[i]), chArray, i);
- }
+ CharacterUtils.toLowerCase(chArray, idx, chLen);Trade-offs
Reuses one documented code path instead of a second hand-written loop; a slightly larger diff than A, and only this doc's runs checked it.
What the checks validated
Validated the same way: ga.* 14 tests, 0 failures, assertions on and off; the whole module 340 classes, 1,913 tests, 0 failures, 18 skipped; lint and format pass; the standalone runner passes on main with -ea and -da.
A javadoc note that only BMP letters are lower-cased.
Trade-offs
Cheap, but contradicts LowerCaseFilter, the Turkish and Greek filters and the project's own past fixes, for no benefit.
Search servers built on Lucene that configure an Irish field type with this filter, as the factory's javadoc example text_ga does, inherit the behaviour and the same missed case-insensitive matches.
Semi-formal. A bounded-exhaustive comparison checked the stock Greek, Irish and Turkish lower-case filters against a reference written from their javadoc (lower case per code point, as LowerCaseFilter) on every code point alone, 1,112,064 per filter. Irish differed on exactly 282, all capitals outside the BMP; a public-API JUnit then confirmed it end to end.
The method in full155 words
Method: bounded-exhaustive translation validation (the hunt's semi-formal tier). The finder wrote a reference for each language lower-case filter from its javadoc and listed mappings: lower case means Character.toLowerCase(int) per code point, as LowerCaseFilter does, plus each filter's documented special rules. It compared the stock Greek, Irish and Turkish filters with the reference:
- on every code point 0..0x10FFFF alone (1,112,064 checks per filter): Greek 0 and Turkish 0 mismatches, Irish 282, all supplementary upper-case letters;
- on 12 Latin, Greek and combining blocks in alone, initial, medial, final, after-n, after-t and after-I positions (15,984 checks per filter): Irish 0 mismatches, so the n-eclipsis and t-prothesis rules and every BMP letter behave as documented.
A public-API JUnit on the stock main jar then showed the effect through the filter and through an IrishAnalyzer index and search (found 2026-10-27). IrishLowerCaseFilter was one of the rule-table classes in the hunt unit's scope; the bug came from its every-code-point sweep.
No prior report found: two scripted prior-report search runs (45 queries plus 6 web left pending, then 79 including 6 web; GitHub, JIRA, mailing lists), GitHub code and open-PR searches on 2026-10-27, and a GitHub re-check on 2026-10-28. Nearest: #2763, the 2009 policy that analysis code must handle supplementary characters, which never names this class. Not fixed upstream.
Searches5 searches
No issue, PR, review, commit message, JIRA entry, mailing-list thread or web page reports that IrishLowerCaseFilter or IrishAnalyzer lower-cases per char or leaves supplementary capitals unchanged. The nearest items are context or precedent: #4956 (LUCENE-3883, which added the class; its 25 comments do not mention supplementary characters), #2763 (LUCENE-1689, the 2009 umbrella policy; closed in 2013 with "Any remaining problems can be opened as separate issues"), #3145 (LUCENE-2069, the same defect fixed in LowerCaseFilter), #3178 (LUCENE-2102, Turkish), LUCENE-2463 (Greek) and #15899 / #15900 (TruncateTokenFilter). The registry sibling LFV-533 (CapitalizationFilter) has the same pattern with a different fix site.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene), downstream repositories | 2026-10-27 | the scripted prior-report search, two runs (the finder's: 45 queries, its 6 web queries left pending; the follow-up's: 79 queries including 6 web, 531 distinct hits, all high and medium hits and every hit naming Irish reviewed), plus REST and GraphQL searches: class names, Irish, eclipsis, prothesis, Deseret, Adlam, lowercase with supplementary or surrogate | no report; only #4956, #4993 and #11389 name the class, none about this |
| Open PRs | 2026-10-27 | the changed files of all 409 open apache/lucene PRs | only #16911 (decommission branch_10x) touches the file; not a fix |
| Mailing lists and JIRA archive | 2026-10-27 | dev@, java-user@, issues@ and commits@ queries; LUCENE and SOLR JIRA queries on IrishLowerCaseFilter, Irish, text_ga and supplementary-character analysis issues | no report |
| Web and ports | 2026-10-27 | web queries (titles and snippets); Lucene.NET history of its own copy of the filter | no report; the port's changes were for culture and allocation reasons |
| GitHub re-check | 2026-10-28 | 11 read-only queries (IrishLowerCaseFilter issues and PRs, IrishAnalyzer, Adlam, Deseret and Osage with lowercase, items created since 2026-10-26, open PRs mentioning Irish) and the commits on the file | nothing new; the file's last upstream commit is from 2024-08-16 (formatting) |
As old as the class: added in 2012 by f597b9a1cc4 (LUCENE-3883) and its branch_3x backport 69321091b9e, first released in 3.6.0. The line has not changed since; all 133 releases from 3.6.0 to 10.5.2 and main reproduce it.
Versions tested7 rows
Introduced by f597b9a1cc4 (trunk, svn r1304836, 2012-03-24, "LUCENE-3883: Irish Analyzer"; issue #4956; CHANGES: "LUCENE-3883: Add Analyzer for Irish. (Jim Regan via Robert Muir)") and the same-day branch_3x backport 69321091b9e (svn r1304845, identical file), which shipped in 3.6.0. A JIRA patch, no PR. They are the only commits that add the line: git log --all -S restricted to the file also lists aad44bc6e36 (2021), which deletes the file on the retired branch_8x. Later commits only moved the file, changed licence headers and reformatted it. The irishLowercase factory is in Lucene's analysis module from 4.0.0, and IrishAnalyzer.normalize runs the filter from 6.2.0 (LUCENE-7355). Not a regression: the line is as old as the class, and LowerCaseFilter in the same release already mapped by code point (the 3.6.0 jar, run on JDK 25, lower-cases these letters; of them, Deseret was already in the JDKs of 2012).
| Version or commit | Verdict | Result |
|---|---|---|
| 3.5.0 | no class | the class does not exist (compile error) |
| 3.6.0 | affected | first release: Adlam, Deseret after n-eclipsis, Osage and the IrishAnalyzer match fail, with -ea and -da; controls pass |
f597b9a1cc4 / 69321091b9e file over the 3.6.0 jars | introducing commits | the same four checks fail |
| all 133 releases 3.6.0 to 10.5.2 | affected | the history probe fails on every one; controls pass on every one |
| 9.12.3, 10.5.2 | affected | this doc's test: 4 run, 3 failed (-da), the same messages as main |
main a9779ef3c193 | affected | this doc's test: 3 of 4 fail through gradle and the JUnit runner, -ea and -da |
| 3.6.0 and main with the one-line fix | fix | all checks pass |
The finder, an independent reproducer, an intended-behaviour devil's advocate, the adjudication and a credibility audit all ran code and said real; a reviewer from another model family (OpenAI GPT-5.6) ran its own tests twice and agreed. This doc re-derived the 282 count and validated two fixes through gradle.
Checks and verdicts342 words
Every check ran on main a9779ef3c193 with JDK 25.0.3, except the cross-model review, which ran on main ea57360912ed, where the three Irish files are identical.
| Check | Verdict | What it found |
|---|---|---|
| finder: exhaustive sweep and JUnit | 282 of 1,112,064 code points wrong | All supplementary capitals; 0 of 15,984 positional checks wrong. The JUnit fails 3 of 4 on main (-ea, -da), 10.5.2 and 9.12.3; with the fix 4 of 4 pass. |
| cross-model reviewer, refute lens (OpenAI GPT-5.6) | real, high | Reproduced with its own Osage test through the filter and IrishAnalyzer.normalize; the fix passed the 10 upstream Irish tests; intended argument weak. |
| Independent reproducer | real | Own 7-test JUnit (Deseret, Osage, Adlam, mixed token, t-prothesis, normalize, search) fails 6 of 7 on main (-ea, -da), 10.5.2 and 9.12.3; with the fix 17 of 17 pass, including the upstream Irish tests. |
| Cross-model reviewer, intended lens (OpenAI GPT-5.6) | real, high | Own Osage test fails 2 of 5 on main, 10.5.2 and 9.12.3 and passes 5 of 5 with the fix; intended argument weak. |
| Intended-behaviour devil's advocate | real; no intended argument survives | Own 8-test probe fails 3 of 8 with all 5 controls passing (the Turkish and Greek filters and three other analyzers lower-case the same letters; the Irish filter lower-cases Greek, Cyrillic and Armenian); 8 of 8 with the fix. |
| adjudication | confirmed, high | New seed: the independent reproduction's test 6 of 7 fail (-ea), the finder's 3 of 4 (-da); with the fix 7 of 7 pass. |
| registry re-run and credibility audit | keep | Clean checkout: 3 of 4 fail with -ea and -da, 4 of 4 pass with the fix; the audit re-ran with another seed. |
| this doc | confirmed; two fixes validated | New format-clean test fails 3 of 4 through gradle (-ea, -da) and the runner (main, 10.5.2, 9.12.3); both fixes pass it and the whole analysis-common test task; the 282 count and per-script split re-derived with a plain JDK loop; an index built from such text passes CheckIndex (-ea, -da). |
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 493 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,185 words
- Registry title
- IrishLowerCaseFilter lower-cases one UTF-16 unit at a time, so IrishAnalyzer leaves upper-case letters outside the BMP (Adlam, Osage, Deseret and others) unchanged
- Area
- analysis-common / ga (IrishLowerCaseFilter and IrishAnalyzer case mapping)
- Priority
- Low, rank 525: IrishAnalyzer leaves supplementary-plane capitals unlowered, which only rare mixed-script text in an Irish field hits.
- Impact class
- silent wrong results
- Impact note
- Hits applications that analyse a field with the stock IrishAnalyzer (or put IrishLowerCaseFilter or its 'irishLowercase' factory in a chain) when the field holds upper-case letters of one of the eight cased scripts encoded outside the BMP (Adlam, Deseret, Garay, Medefaidrin, Old Hungarian, Osage, Vithkuqi, Warang Citi; 282 code points in the JDK 25 tables): those letters stay upper-case at index time and at query time (IrishAnalyzer.normalize too), deterministically with -ea and -da, at any scale and on every release since 3.6.0, so a capitalised word is not found by its lower-case form; BMP letters in the same token are lower-cased, nothing throws, the index is not damaged, and Irish text itself has no such letters, so only mixed-script content in an Irish field is affected.
- Summary
- You index text with IrishAnalyzer (or put IrishLowerCaseFilter in a chain), whose filter is documented to 'normalise token text to lower case', and expect a capitalised word to match its lower-case form, as it does with LowerCaseFilter. For letters outside the BMP it does not: the Adlam capital U+1E900 stays U+1E900 instead of becoming U+1E922 (the Deseret capital U+10400 likewise stays, also after n-eclipsis), so a document holding the Adlam word U+1E900 U+1E923 is not found by U+1E922 U+1E923 (0 hits instead of 1). The loop at IrishLowerCaseFilter.java:55 passes one UTF-16 char to Character.toLowerCase, which returns a surrogate unit unchanged; BMP letters in the same token are lower-cased and nothing is thrown.
- Introduced
- f597b9a1cc4d9ad326ade156c630a90063fb8c5f (trunk, 'LUCENE-3883: Irish Analyzer', svn r1304836; LUCENE-3883 = #4956, committed from a JIRA patch, no pull request): the class's first commit already has the char-wise loop 'i += Character.toChars(Character.toLowerCase(chArray[i]), chArray, i)'; the branch_3x backport 69321091b9e56d5a672f014c552e8d7d47178a79 (svn r1304845, same file content) put it into 3.6.0 [read: git blame of main :55, git log -S on the line; exec: shadowing either commit's file over the 3.6.0 jars fails the same way, and 3.6.0's file with the one-line fix passes]; the line is unchanged since (later commits only moved the file, changed licence headers and reformatted) (2012-03-24 (trunk f597b9a1cc4 and branch_3x backport 69321091b9e, same day))
- First affected version
- 3.6.0 [exec] (the first release with IrishLowerCaseFilter and IrishAnalyzer; 3.5.0 has no such class [exec: compile error]); the 'irishLowercase' factory is in Lucene's analysis module from 4.0.0 (in 3.6 it was Solr's class), and IrishAnalyzer.normalize runs the filter from 6.2.0 (LUCENE-7355) [read]
- Affected versions
- every release from 3.6.0 to 10.5.2 [exec: all 133 Maven Central releases in that range with the follow-up's release-portable history probe, -ea, and also -da on 3.6.0, 9.12.3 and 10.5.2] and main a9779ef3c193 [exec: -ea and -da, by the finder, the independent reproduction, the cross-model review, the intended-behaviour check, the adjudication and the re-run]; the finder's JUnit fails 3 of 4 on 9.1.0, 9.12.3, 10.0.0, 10.5.2 and main; not fixed upstream: upstream main a95c754ee003 has byte-identical IrishLowerCaseFilter, IrishAnalyzer and IrishLowerCaseFilterFactory [read, 2026-10-27]; unaffected: 3.5.0 and earlier (no class) [exec on 3.5.0]; the prereleases 4.0.0-ALPHA and 4.0.0-BETA contain the commit but were not run
- 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 (2026-10-27, the prior-report record verdict new, confidence high, all read-only: the finder's scripted prior-report search (45 queries: 16 GitHub issue and PR, 3 commit, 2 downstream, 20 mailing-list over dev@, java-user@, issues@ and commits@, 4 JIRA archive) and the follow-up's run (79 queries including 6 web; 531 distinct hits, none the same bug), GitHub REST and GraphQL issue, PR, commit and code searches on apache/lucene, all of GitHub and downstream repositories (Elasticsearch, OpenSearch, Solr, Lucene.NET), and the open-PR file lists; the independent reproduction's gh search and the intended-behaviour check's own scripted prior-report search (179 hits) found nothing either); upstream main a95c754ee003 has byte-identical Irish files, and the only open PR whose file list includes IrishLowerCaseFilter.java (#16911, decommissioning branch_10x) is not a fix; related items, context and precedent only: #4956 (LUCENE-3883, introduced the class; its 25 comments do not mention supplementary characters), #2763 (LUCENE-1689, the 2009 umbrella policy that char-based analysis code must handle supplementary characters; it never names this class and was closed with 'Any remaining problems can be opened as separate issues'), #3145 (LUCENE-2069, the same defect fixed in LowerCaseFilter), #3178 (LUCENE-2102, TurkishLowerCaseFilter by code point), LUCENE-2463 (GreekLowerCaseFilter by code point), #3259 and #3170 (LUCENE-2183, LUCENE-2094), #15899 and #15900 (TruncateTokenFilter surrogate pairs; a committer wrote that all other similar filters had been fixed); the Lucene.NET port's commits 3c4cfa4 and 56c8e08 touched its own copy of the filter for culture and allocation reasons without describing this defect; gaps: GitHub search does not index inline PR review comments, and web search covered titles and snippets only
- Lucene commit tested
- main
a9779ef3c193(gradle in a scratch worktree, assertions on and off, and the standalone JUnit runner); the release jars 10.5.2 and 9.12.3 (this doc's test); all 133 releases from 3.6.0 to 10.5.2 (the history probe); an index-integrity probe on main (CheckIndex, assertions on and off); two fixes on main (gradle: the Irish test package and the whole:lucene:analysis:common:testtask, plus the module's lint and format checks). JDK 25.0.3 on Linux aarch64 only.
- Finding record
a Lucene main checkout lucene/analysis/common/src/java/org/apache/lucene/analysis/ga/IrishLowerCaseFilter.java:24-27 (class javadoc 'Normalises token text to lower case, handling t-prothesis and n-eclipsis'), :31 (constructor 'normalises Irish token text'), :54-56 (char-wise Character.toLowerCase(chArray[i]) loop)a Lucene main checkout lucene/analysis/common/src/java/org/apache/lucene/analysis/ga/IrishAnalyzer.java:121-130 (StandardTokenizer then IrishLowerCaseFilter in createComponents), :138-140 (normalize uses IrishLowerCaseFilter); IrishLowerCaseFilterFactory.java:40 (SPI name 'irishLowercase'); the three files are byte-identical in Lucene main ea57360912ed
- The finding in 30 seconds
lucene/analysis/common/src/java/org/apache/lucene/analysis/ga/IrishLowerCaseFilter.java:24-27, :31, :37-61 (javadoc, constructor, incrementToken loop at :54-56)lucene/analysis/common/src/java/org/apache/lucene/analysis/ga/IrishAnalyzer.java:126-135 (createComponents), :138-142 (normalize)lucene/analysis/common/src/java/org/apache/lucene/analysis/el/GreekLowerCaseFilter.java:46lucene/core/src/java/org/apache/lucene/analysis/CharacterUtils.java:47-63; LowerCaseFilter.java:22, :38JDK 25 src.zip java.base/java/lang/Character.java (toLowerCase(char) javadoc: 'cannot handle supplementary characters')
- Who is affected
lucene/analysis/common/src/java/org/apache/lucene/analysis/ga/IrishAnalyzer.java:126-142; lucene/analysis/common/src/java/org/apache/lucene/analysis/ga/IrishLowerCaseFilterFactory.java:40, :60-64
- Why it looks unintended
lucene/analysis/common/src/java/org/apache/lucene/analysis/ga/IrishLowerCaseFilter.java:24-27, :31; lucene/analysis/common/src/java/org/apache/lucene/analysis/ga/IrishLowerCaseFilterFactory.java:60lucene/core/src/java/org/apache/lucene/analysis/LowerCaseFilter.java:22; lucene/analysis/common/src/java/org/apache/lucene/analysis/tr/TurkishLowerCaseFilter.java:25lucene/CHANGES.txt:17927-17928 (3.1.0, LUCENE-2069), :1198-1200 (10.5.0, GITHUB#15899/#15900)lucene/analysis/common/src/test/org/apache/lucene/analysis/ga/TestIrishAnalyzer.java:65-67 (testRandomStrings, checkRandomData)lucene/analysis/common/src/java/org/apache/lucene/analysis/miscellaneous/CapitalizationFilter.java:152, :159, :190, :193 (char-wise case mapping, LFV-533)
- Suggested fix
lucene/CHANGES.txt:1198-1200 (TruncateTokenFilter legacy parameters)
- History
a Lucene clone: git tag --contains 69321091b9e (releases/lucene-solr/3.6.0); releases/lucene-solr/3.6.0 IrishLowerCaseFilter.java:59
- Diagrams
lucene/analysis/common/src/java/org/apache/lucene/analysis/ga/IrishLowerCaseFilter.java:54-56
- TestIrishLowerCaseSupplementary.java (JUnit test):
repro/LFV-685/src/org/apache/lucene/analysis/ga/TestIrishLowerCaseSupplementary.java(32 lines)
A step-by-step picture made in parallel with this doc from the executed repro on main a9779ef3c193; every value drawn should come from the repro output and the source at that commit. The PNG is the final summary frame.
assets/LFV-685/bug.png(493 KB)
