One capitalised letter in an optional setting flips the transform.
ICUTransformFilterrewrites every token with an ICU transliterator named byid:Cyrillic-LatinturnsФедерацияintoFederaciâ.- The optional
directionsays which way it runs:forward(the default) as named, orreverse, the inverse (here Latin-Cyrillic, which leaves Cyrillic text unchanged).
direction value travels through the factory: the case-insensitive check accepts Forward and passes it on unchanged, the case-sensitive "forward".equals test fails, and the reverse transliterator leaves Федерация unchanged instead of producing Federaciâ. Select the still to open it at full size. Generated with AI, checked against the code.- Build the ICU transform filter with
id=Cyrillic-Latinanddirection="Forward"(throughnew ICUTransformFilterFactory(args),CustomAnalyzeror the SPI nameicuTransform) and analyze the tokenФедерация. - You expect
Federaciâ, as withdirection="forward"or no direction at all. The factory checks the value case-insensitively, soForwardis accepted;sidewayswould be rejected with an error. - But the check hands back your own spelling, and the factory then tests
"forward".equals(direction)."Forward"is not equal to"forward", so it builds the reverse (Latin-Cyrillic) transliterator. - You get
Федерацияunchanged, with no exception, at index and query time alike.FORWARDor any other capitalised spelling does the same; every spelling ofreversestill works.
Users of the opt-in analysis-icu module who configure this filter with a capitalised direction (constructor, CustomAnalyzer, SPI name or a schema). Every token is transformed the wrong way at index and query time, silently. Whether searches still match depends on the transform. The default, forward and any reverse work, so the trigger is rare.
Mechanism, terms and numbers310 words
ICUTransformFilterFactory validates its optional direction argument case-insensitively (get(args, "direction", [forward, reverse], "forward", false), ICUTransformFilterFactory.java:51-52 at 5adff78bb168), so Forward and FORWARD are accepted while sideways is rejected. The validation returns the caller's own spelling (AbstractAnalysisFactory.java:190-191), and the factory then picks the direction with a case-sensitive "forward".equals(direction) (:53). Every accepted spelling other than exact lower-case forward therefore builds the reverse transliterator: with id=Cyrillic-Latin and direction="Forward", Федерация comes back unchanged instead of Federaciâ, at index and query time, with no error and the same result with assertions on and off. Until 4.2.1 the factory compared with equalsIgnoreCase("forward"); the LUCENE-4877 parameter-parsing refactor changed that in 4.3.0, and every release through 10.6.0 has it. A one-line equalsIgnoreCase fix restores the old meaning. The module is opt-in and the trigger is a capitalised optional value, so the priority is Low.
ICUTransformFilterFactory.java:51-52:get(args, "direction", Arrays.asList("forward", "reverse"), "forward", false). The last argument iscaseSensitive = false, soForward,FORWARDandFoRwArDpass validation. Values outside the list fail:IllegalArgumentException("Configuration Error: 'direction' value must be one of [forward, reverse]")(AbstractAnalysisFactory.java:195-196).AbstractAnalysisFactory.java:190-191: on anequalsIgnoreCasematch,getreturnss, the caller's own spelling, not the allowed value.ICUTransformFilterFactory.java:53:int dir = "forward".equals(direction) ? Transliterator.FORWARD : Transliterator.REVERSE;. For"Forward"the test is false, so:54buildsTransliterator.getInstance(id, REVERSE).- The one transliterator serves both
create()andnormalize()(:65-73), so index-time analysis, query-time analysis andAnalyzer.normalizeall run the reverse transform.
This factory is the only caller in lucene/ that passes caseSensitive=false.
Hiragana-Katakana with direction="Forward" through CustomAnalyzer returns ひらがな instead of ヒラガナ; Upper with "FoRwArD" turns MiXeD into mixed instead of MIXED. The default, forward, reverse and ReVeRsE behave as documented in the same runs.
Exposure and likelihood136 words
- Who is hit. Analyzers built from factory arguments: the public Map constructor,
TokenFilterFactory.forName("icuTransform", ...),CustomAnalyzer.builder().addTokenFilter("icuTransform", ...)or a schema-driven configuration, withdirectionspelled with any capital letter. - What happens. The stock inverse ICU transform runs on every token, at index and query time. The indexed terms are in the wrong script or form. One analyzer used on both sides applies the same wrong transform to both, so whether searches still match depends on the transform (by reading, not measured).
- Not affected. The parameter omitted, exact
forward, and every spelling ofreverse. - Scale and likelihood. One token shows it; no data size or load is involved. Rare: the javadoc and the Solr reference guide spell the values in lower case, and the dated searches (2026-11-02 and 2026-11-03) found no report of it since 2013.
One JUnit test method on the public factory API: build the factory with id=Cyrillic-Latin and direction="Forward", analyze one token, expect the Latin form. Deterministic, single thread; it fails with assertions on and off.
/*
* 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.icu;
import java.io.StringReader;
import java.util.HashMap;
import java.util.Map;
import org.apache.lucene.tests.analysis.BaseTokenStreamTestCase;
public class TestICUTransformForwardDirection extends BaseTokenStreamTestCase {
// "Forward" passes validation, so it must transliterate like the default "forward".
public void testCapitalizedForward() throws Exception {
Map<String, String> args =
new HashMap<>(Map.of("id", "Cyrillic-Latin", "direction", "Forward"));
ICUTransformFilterFactory factory = new ICUTransformFilterFactory(args);
assertTokenStreamContents(
factory.create(whitespaceMockTokenizer(new StringReader("Федерация"))),
new String[] {"Federaciâ"});
}
}
cp $REPRO/TestICUTransformForwardDirection.java $LUCENE/lucene/analysis/icu/src/test/org/apache/lucene/analysis/icu/
cd $LUCENE && ./gradlew :lucene:analysis:icu:test --tests org.apache.lucene.analysis.icu.TestICUTransformForwardDirection -Ptests.seed=DEADBEEF
# add -Ptests.asserts=false: the same failure
- Expected
Federaciâ, the forward Cyrillic-to-Latin result, as withdirection="forward".- Observed
term 0 expected:<[Federaciâ]> but was:<[Федерация]>: the reverse transform ran; the same with assertions off.
More files, environment, full output134 words
TestICUTransformForwardDirection (one method, extends BaseTokenStreamTestCase, package org.apache.lucene.analysis.icu of the lucene/analysis/icu tests). It is the hunt's minimal repro re-wrapped to Lucene's 100-column format, with its one comment line reworded (same code); checkGoogleJavaFormat accepts it. As the regression test, the same method fits in TestICUTransformFilterFactory.
The expected value Federaciâ is the forward Cyrillic-Latin output that the upstream testForwardDirection asserts for the default direction.
| lucene | main 5adff78bb168 (12.0.0-SNAPSHOT, icu4j 78.3): fails; 4.3.0 to 10.6.0 (9 releases run): fail; 4.0.0 and 4.2.1: pass |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux aarch64 (nothing platform-specific) |
cp $REPRO/TestICUTransformForwardDirection.java $LUCENE/lucene/analysis/icu/src/test/org/apache/lucene/analysis/icu/
cd $LUCENE && ./gradlew :lucene:analysis:icu:test --tests org.apache.lucene.analysis.icu.TestICUTransformForwardDirection -Ptests.seed=DEADBEEF
TestICUTransformForwardDirection > testCapitalizedForward FAILED
org.junit.ComparisonFailure: term 0 expected:<[Federaciâ]> but was:<[Федерация]>
at __randomizedtesting.SeedInfo.seed([DEADBEEF:5FEDD4318FC2297A]:0)
at app/junit@4.13.2/org.junit.Assert.assertEquals(Assert.java:117)
at app/org.apache.lucene.test_framework@12.0.0-SNAPSHOT/org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertTokenStreamContents(BaseTokenStreamTestCase.java:262)
...
at app//org.apache.lucene.analysis.icu.TestICUTransformForwardDirection.testCapitalizedForward(TestICUTransformForwardDirection.java:31)
BUILD FAILED
# -Ptests.asserts=false: the same ComparisonFailure- The factory opts into case-insensitive validation (the only caller that does), so
Forwardis accepted; the javadoc names just two values, with no case rule. - Through 4.2.1 it compared with
equalsIgnoreCase("forward")under the same javadoc; the 4.3.0 refactor aimed to reject invalid arguments, not to change valid ones. - No test, CHANGES or MIGRATE entry pins the current behaviour.
The full argument165 words
- Contract. Javadoc (
ICUTransformFilterFactory.java:32): "direction (optional): Either 'forward' or 'reverse'. Default is forward." The code accepts any case (:51-52) and rejects other values loudly; an accepted spelling offorwardthat means reverse is documented nowhere. - History. The factory was added by SOLR-2210 (
5c6b4f4f650, 2010) withdirection == null || direction.equalsIgnoreCase("forward")-> FORWARD,equalsIgnoreCase("reverse")-> REVERSE, else an exception; 4.2.1 still has that (ICUTransformFilterFactory.java:56-62at the 4.2.1 tag). LUCENE-4877 (#5942; 4.3.0 CHANGES: "Throw exception for invalid arguments in analysis factories") replaced it with the two current lines. Its thread proposed the case-insensitivegetoverload but never mentions this factory ordirection. - Tests.
TestICUTransformFilterFactoryuses only the default direction (:41-49) and lower-casereverse(:57). - Caveat. No document promises that the values are case-insensitive; the javadoc only spells them in lower case. The case for "not intended" rests on the code's own opt-in (
:51-52) and on the pre-4.3 behaviour: strong evidence, not a documented guarantee.
The devil's-advocate check79 words
Strongest case for "intended": the javadoc spells the values in lower case, and sibling factories compare their values case-sensitively (PatternReplaceFilterFactory.java:56, ICUNormalizer2FilterFactory.java:58, :63), so Forward is a user error. Why it fails: those siblings also validate case-sensitively, so a wrong case gets an exception there; this factory alone opts into case-insensitive acceptance and then misreads what it accepted. The same lower-case javadoc stood from 2010 to 2013 beside case-insensitive parsing, so it was never a case-sensitive contract.
Recommended Compare case-insensitively at the fix site (fix A)
Compare with "forward".equalsIgnoreCase(direction) in ICUTransformFilterFactory (one line, wrapped to two by the formatter). The repro then passes with assertions on and off, the whole analysis-icu test suite passes (159 tests, 10 skipped) and the format check is clean.
int dir =
"forward".equalsIgnoreCase(direction) ? Transliterator.FORWARD : Transliterator.REVERSE;All options and trade-offs3 options · 315 words
Option A was built and checked through gradle on 5adff78bb168; B and C were not run. PR #15 (LUCENE-8972, open on 2026-11-03, last updated 2025-10-18) copies the same two lines into a new ICUTransform2CharFilterFactory, so a fix should cover both.
Replace "forward".equals(direction) with "forward".equalsIgnoreCase(direction) at ICUTransformFilterFactory.java:53, which restores the pre-4.3 meaning.
int dir =
"forward".equalsIgnoreCase(direction) ? Transliterator.FORWARD : Transliterator.REVERSE;Trade-offs
Every spelling that works today keeps working (reverse, Reverse and REVERSE still mean reverse); only the capitalised spellings of forward change, to what they always meant before 4.3.0. Add a CHANGES.txt line under Bug Fixes.
What the checks validated
- Gradle in a scratch worktree at
5adff78bb168, seedDEADBEEF: the repro fails without the fix (assertions on and off) and passes with it (on and off). - With the fix, all of
:lucene:analysis:icu:test: 159 tests, 0 failures, 10 skipped (TestICUTokenizerCJK), includingTestICUTransformFilterFactory(4) andTestICUTransformFilter(8).:lucene:analysis:icu:checkGoogleJavaFormatpasses with the fix and the repro in place. - Earlier hunt runs of the same logic (on one unwrapped line) passed the finder's test, the cross-model reviewer's two tests and the upstream
TestICUTransformFilterFactory(4 of 4); the independent reproducer's test was not run with the fix.
Return allowedValue instead of s at AbstractAnalysisFactory.java:191, so callers always see the canonical spelling.
Trade-offs
Would also protect future callers such as the char-filter factory in PR #15, but it changes a public method of lucene/core that downstream factories may call, so it needs its own review; A is the minimal fix.
Pass true instead of false at ICUTransformFilterFactory.java:52, so Forward fails loudly.
Trade-offs
Working Reverse and REVERSE configurations would start throwing: a behaviour break for users who are not affected today.
Solr schemas configure this same factory (its reference guide documents the direction option, read 2026-11-02), so a capitalised value there is hit the same way; as read on 2026-11-02, Elasticsearch and OpenSearch parse their own setting and do not use this factory.
Code reading of ICUTransformFilterFactory (non-formal): a case-insensitive check followed by a case-sensitive comparison of the same value stood out, and a one-test JUnit on stock main witnessed the reversed transform. Independent checks then reproduced it.
The method in full116 words
Method: code reading of ICUTransformFilterFactory's parameter parsing (2026-11-02, main 5adff78bb168). The reader noticed that direction is validated with caseSensitive=false and then compared with "forward".equals, wrote a one-test JUnit with direction="Forward" on the public constructor, and ran it on main (fails with assertions on and off), 10.6.0 and 9.12.3, plus the one-line fix (passes).
An independent reproducer wrote its own test from the claim through CustomAnalyzer and the SPI name with a different transform; a cross-model reviewer (OpenAI GPT-5.6, refute lens, executed) wrote two more; a devil's-advocate verifier argued the intended case; an adjudication re-ran the tests and the fix. A final repro check then reduced the repro to one method and one token.
No matching prior report found in apache/lucene issues and PRs (including the JIRA archive), the mailing lists, downstream trackers or the web (searched 2026-11-02, re-checked 2026-11-03). PR #15 (open on 2026-11-03) copies the same pattern into a new char-filter factory.
Searches4 searches
No item found reports this bug; the nearest items are background. Search limits: keyword search (50 results per GitHub query); private reports are not visible.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues and PRs (apache/lucene, incl. the migrated JIRA archive) | 2026-11-02, re-checked 2026-11-03 | 16 scripted queries and about 35 manual phrasings (ICUTransformFilterFactory, icuTransform direction, transliterator direction, ...); the file lists of all 408 open PRs and of every PR updated since 2026-10-31; commit search | no report; #10663 (module descriptors) only lists the class |
| Related items | 2026-11-02 | the full LUCENE-4877 discussion (23 comments) and PR #15's reviews and conversation | #5942 / LUCENE-4877 is the origin and never mentions this factory or direction; PR #15 (LUCENE-8972, open on 2026-11-03) copies the two lines without discussion; #10015, #3484, #3584 are background |
| Mailing lists and JIRA | 2026-11-02 | dev@, java-user@, issues@, commits@, solr-user@ (all 13 mails naming the factory), users@solr; 4 JQL queries on LUCENE and SOLR | configuration questions only; nothing about direction or case |
| Downstream and web | 2026-11-02 | apache/solr, apache/lucene-solr, apache/lucenenet, Elasticsearch, OpenSearch; GitHub code search; 6 web queries | no report; Lucene.NET has the same pattern; Elasticsearch and OpenSearch use their own factories |
Introduced in 4.3.0 by the LUCENE-4877 parameter-parsing refactor (2013), which kept the case-insensitive check but compared the result case-sensitively; 4.2.1 is correct. Every release from 4.3.0 through 10.6.0 and main is affected; not fixed upstream.
Versions tested6 rows
Introduced by LUCENE-4877 "Fix analyzer factories to throw exception when arguments are invalid" (#5942; SVN commits, no PR): branch_4x 14a85ce98c9 (Robert Muir, 2013-04-06, "LUCENE-4877: additional improvements"), the copy that shipped first, in 4.3.0 and every later 4.x and every 5.x release; trunk 6de43dea0af (Steven Rowe, 2013-04-01, "LUCENE-4877: more param parsing verbs"), in every release from 6.0.0. Both replaced the equalsIgnoreCase parsing with the two current lines and added the get(..., caseSensitive) overload. git tag --contains: 118 release tags. The equals line has not changed since; the last commit on the factory is 7f8b7ffbcad (2021, reformatting). Upstream main 1c2072f0284 (2026-11-02) has both files byte-identical to 5adff78bb168.
4.2.1 and 4.3.0 ran with the same icu4j 49.1, so the flip comes from Lucene alone. Runs before 9.0 use a main() probe through the public factory API, since the test's org.apache.lucene.tests.analysis package exists only from 9.1.0 (LUCENE-10301).
| Version or commit | Verdict | Result |
|---|---|---|
main 5adff78bb168 | affected | fails through gradle and a standalone runner, assertions on and off |
| 10.6.0, 10.5.2, 9.12.3 | affected | the JUnit tests fail the same way (Российская / ひらがな unchanged) |
| 8.11.4, 7.7.3, 6.6.6, 5.5.5, 4.10.4 | affected | main() probe: Forward accepted, gives the reverse output |
| 4.3.0 | first affected | Forward gives Российская (reverse), forward gives Rossijskaâ; icu4j 49.1 |
| 4.2.1, 4.0.0 | not affected | Forward gives Rossijskaâ, the same as forward; icu4j 49.1 |
fix A on 5adff78bb168 | not affected | repro passes; analysis-icu suite 159 tests, 0 failures |
A finder, an independent reproducer, an executed cross-model review (OpenAI GPT-5.6, refute lens), a devil's-advocate verifier, an adjudication, an audit and a final repro check reproduced it and agree on the root cause. This doc's gradle runs reproduced it on main and validated the fix.
Checks and verdicts263 words
The cross-model review ran on main ea57360912ed, where ICUTransformFilterFactory.java and AbstractAnalysisFactory.java are byte-identical to 5adff78bb168; its tests were then re-run on 5adff78bb168 (fail with assertions on and off). Every other check ran on 5adff78bb168.
| Check | Verdict | What it found |
|---|---|---|
| finder (code reading) | reproduced | One-test JUnit fails on main (assertions on and off), 10.6.0 and 9.12.3; the one-line fix passes it. |
| independent reproducer | real | Own test through CustomAnalyzer and the SPI name with Hiragana-Katakana: Forward gives ひらがな instead of ヒラガナ; fails on main, 10.6.0, 10.5.2 and 9.12.3; the lower-case control passes. |
| cross-model review, refute lens (OpenAI GPT-5.6, executed) | real (high) | Own tests (Upper with FoRwArD; forName with FORWARD) fail with assertions on and off, and the Upper test also over five iterations; controls pass; the fix passes them and the upstream test (4 of 4); intended argument weak. |
| devil's-advocate verifier | real | sideways is rejected while Forward and FORWARD are accepted and act as reverse; the lower-case-javadoc defence fails. |
| adjudication | confirmed (high), Low | Re-ran the finder's and the reproducer's tests on clean main (fail), and the finder's test with the fix (pass). |
| separate re-run, audit and final repro check | confirmed (Low) | Fails on main with assertions on and off; the final check reduced the repro to one method and one token, which passes with the fix. |
| this doc | reproduced; fix A validated | Gradle without the fix fails (on, off); with it the repro and 159 analysis-icu tests pass and the format check is clean. |
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 541 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,320 words
- Registry title
- ICUTransformFilterFactory accepts direction="Forward" or "FORWARD" without error (it validates the value case-insensitively) but then builds the reverse transliterator, so every token is silently transformed the wrong way (regression of LUCENE-4877, since 4.3.0)
- Area
- analysis / icu (ICUTransformFilterFactory: 'direction' parameter parsing through AbstractAnalysisFactory.get(..., caseSensitive=false))
- Priority
- Low, rank 498: Every token is silently transformed in the wrong direction in every release since 4.3.0, but only in the opt-in analysis-icu ICUTransformFilterFactory when the optional direction value is spelled with a capital letter, such as Forward. The documented lower-case spelling and the default both work, so the trigger is rare.
- Impact class
- silent wrong results
- Impact note
- Only analyzers built with the opt-in analysis-icu module's ICUTransformFilterFactory (public Map constructor, TokenFilterFactory.forName("icuTransform", ...), CustomAnalyzer or a schema-driven configuration) whose optional direction value is spelled other than exact lower-case 'forward' (for example 'Forward' or 'FORWARD') are hit: the value passes validation and the factory silently applies Transliterator.REVERSE to every token at index and query time, so terms come out in the wrong script or form, identically with -ea and -da [exec]; whether searches still match depends on the transform, because one analyzer used on both sides applies the same wrong transform to both [read, not measured]; no scale is needed, every release from 4.3.0 through 10.6.0 is affected, and the default (parameter omitted), 'forward' and any spelling of 'reverse' work.
- Summary
- You build an ICUTransformFilterFactory with id=Cyrillic-Latin and direction="Forward", which the factory accepts because it checks direction case-insensitively, and analyze 'Российская Федерация'. You expect the forward Cyrillic-Latin transliteration [Rossijskaâ, Federaciâ], as with direction="forward". You get the Cyrillic tokens back unchanged, with no error: the validation hands back the caller's own spelling, the factory then tests "forward".equals(direction), and every accepted spelling other than exact lower-case 'forward' builds the reverse (Latin-Cyrillic) transliterator.
- Introduced
- 14a85ce98c91ed8b03b98379ac8e0db086d25120 (branch_4x, svn r1465225, Robert Muir, 'LUCENE-4877: additional improvements'; the copy that shipped first, in 4.3.0, and the one in every 4.x and 5.x release) and trunk commit 6de43dea0af9d0407d7c218d077c6025b04a4282 (svn r1463335, Steven Rowe, 'LUCENE-4877: more param parsing verbs'; in every release from 6.0.0), both for LUCENE-4877 'Fix analyzer factories to throw exception when arguments are invalid' (migrated as apache/lucene#5942; SVN commits, no GitHub PR). Both replaced the factory's own parsing (direction == null || direction.equalsIgnoreCase("forward") -> FORWARD; equalsIgnoreCase("reverse") -> REVERSE; else IAE) with get(args, "direction", [forward, reverse], "forward", false) plus "forward".equals(direction), and added that get() overload, which returns the caller's spelling on an equalsIgnoreCase match; the equals line is unchanged since [read: git show of both commits; git merge-base --is-ancestor against the release tags; exec: the history probe passes on 4.2.1 and fails on 4.3.0 with the same icu4j 49.1] (2013-04-01 (author date of the trunk commit 6de43dea0af); 2013-04-06 (author date of the branch_4x commit 14a85ce98c9, which shipped first))
- First affected version
- 4.3.0 [exec: the history probe fails on 4.3.0 and passes on 4.2.1, both with icu4j 49.1; releases/lucene-solr/4.2.1 contains neither commit and still has the equalsIgnoreCase parsing], so this is a regression of LUCENE-4877
- Affected versions
- every release from 4.3.0 through 10.6.0 (released 2026-10-29, 4ae946e5a9b) and main 5adff78bb168 (12.0.0-SNAPSHOT): 118 release tags in the history clone, 4.3.0-5.5.5 through 14a85ce98c9 and 6.0.0-10.6.0 through 6de43dea0af [read: git tag --contains]. Executed: the one-test JUnit fails on main (-ea and -da, also in the re-run), 10.6.0 (-ea and -da) and 9.12.3 (-ea and -da); the independent reproduction's SPI-path test fails on main (-ea and -da), 10.6.0, 10.5.2 and 9.12.3 (-ea); a main() history probe through the public factory API fails on 4.3.0, 4.10.4, 5.5.5, 6.6.6, 7.7.3, 8.11.4, 9.12.3, 10.6.0 and main and passes on 4.0.0 and 4.2.1 (-ea) [exec]. Lucene 4.0.0-4.2.1 are not affected; before 4.0 the factory was a Solr class. Not new in 10.6 (10.5.2 fails). Not fixed upstream: upstream main 1c2072f0284 (author date 2026-11-02) has ICUTransformFilterFactory.java and AbstractAnalysisFactory.java byte-identical to 5adff78bb168 (blobs 3880c4a6816 and 53a456aeaea, also at 10.6.0 and ea57360912ed), and the newest commit on the factory is 7f8b7ffbcad (author date 2021-01-03, reformatting) [read through the GitHub API, 2026-11-02]. Executed on Linux aarch64 with JDK 25.0.3 only.
- 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
- non-formal: code reading
- Upstream and prior reports
- none found (2026-11-02, on top of the finder's, the independent reproduction's, the intended-behaviour check's and the adjudication's GitHub searches; rechecked the same day): a scripted prior-report search over 16 GitHub issue and PR queries on apache/lucene, 3 commit queries, 2 downstream-repository queries, 20 mailing-list queries (dev@, java-user@, issues@, commits@), 4 JIRA-archive queries and 6 web queries (541 distinct hits, none with high overlap), plus manual GitHub searches on apache/lucene, apache/lucene-solr, apache/solr, apache/lucenenet, Elasticsearch and OpenSearch, GitHub code search, the file lists of all 408 open apache/lucene PRs and of the 31 PRs updated since 2026-10-31 (none touches ICUTransformFilterFactory.java or AbstractAnalysisFactory.java), the full LUCENE-4877 discussion (#5942), JIRA full-text queries over SOLR and LUCENE and the solr-user@ threads on the factory: no report of a mixed-case direction building the reverse transliterator. Same-day recheck: GitHub search on apache/lucene for 'ICUTransformFilterFactory' (only #10663, module descriptors), 'icuTransform direction' (0), PRs 'ICUTransform' (0), 'transliterator direction' (0), and PRs or issues updated since 2026-10-31 that mention icu, analysis factory or transliterator (0); upstream main 1c2072f0284 has both files byte-identical to 5adff78bb168. Related, not reports: #5942 / LUCENE-4877 (the change that introduced it; its discussion never mentions this factory or the direction parameter), open PR #15 / LUCENE-8972 (its new ICUTransform2CharFilterFactory copies the same two lines, without discussion), #10015, #3484 and #3584.
- Lucene commit tested
- main
5adff78bb168(2026-11-02) through gradle in a scratch worktree with assertions on and off, without and with the fix (the repro, the wholeanalysis-icutest suite,checkGoogleJavaFormat), and through a standalone JUnit runner on the main jars. The hunt ran the test, an independentCustomAnalyzertest or amain()probe through the public factory API on 4.0.0 and 4.2.1 (pass) and on 4.3.0, 4.10.4, 5.5.5, 6.6.6, 7.7.3, 8.11.4, 9.12.3, 10.5.2 and 10.6.0 (fail). Only Linux aarch64 with OpenJDK 25.0.3 was run.
- Finding record
lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/ICUTransformFilterFactory.java:28-33 (at 5adff78bb168) (class javadoc: 'direction (optional): Either 'forward' or 'reverse'. Default is forward.'; no case rule), :48-58 (Map constructor: :51-52 get(args, "direction", Arrays.asList("forward", "reverse"), "forward", false); :53 "forward".equals(direction) ? Transliterator.FORWARD : Transliterator.REVERSE, fix site; :54 Transliterator.getInstance(id, dir))lucene/core/src/java/org/apache/lucene/analysis/AbstractAnalysisFactory.java:174-198 (at 5adff78bb168) (get(args, name, allowedValues, defaultVal, caseSensitive): :190-191 returns the caller's own spelling s on an equalsIgnoreCase match; :195-196 IllegalArgumentException "Configuration Error: ... must be one of ..." for other values); grep of lucene/ on main: ICUTransformFilterFactory.java:52 is the only non-test caller that passes caseSensitive=falselucene/analysis/icu/src/test/org/apache/lucene/analysis/icu/TestICUTransformFilterFactory.java:41-49 (at 5adff78bb168) (testForwardDirection uses the default direction and expects [Rossijskaâ, Federaciâ]), :57 (the only explicit direction in the upstream tests is lower-case 'reverse'; no test passes an explicit 'forward' or a mixed case)https://github.com/apache/lucene/issues/5942 (LUCENE-4877), https://github.com/apache/lucene/pull/15 (LUCENE-8972, open; ICUTransform2CharFilterFactory copies the pattern), https://github.com/apache/lucene/issues/10015, https://github.com/apache/lucene/issues/10663, read 2026-11-02
- The finding in 30 seconds
lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/ICUTransformFilterFactory.java:48-58 (at 5adff78bb168) (constructor; :51-52 validation, :53 fix site, :54 getInstance), :65-73 (create and normalize)lucene/core/src/java/org/apache/lucene/analysis/AbstractAnalysisFactory.java:174-198 (at 5adff78bb168) (get with caseSensitive; :190-191 return s; :195-196 IllegalArgumentException)grep of lucene/ on main 5adff78bb168: ICUTransformFilterFactory.java:52 is the only get(..., false) caller
- Minimal repro
lucene/analysis/icu/src/test/org/apache/lucene/analysis/icu/TestICUTransformFilterFactory.java:41-49 (at 5adff78bb168) (expected forward output)
- Why it looks unintended
lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/ICUTransformFilterFactory.java:32 (at 5adff78bb168) (javadoc), :51-53lucene/analysis/icu/src/test/org/apache/lucene/analysis/icu/TestICUTransformFilterFactory.java:41-49, (at 5adff78bb168) :57lucene/analysis/common/src/java/org/apache/lucene/analysis/pattern/PatternReplaceFilterFactory.java:56; (at 5adff78bb168) lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/ICUNormalizer2FilterFactory.java:58 (case-sensitive validation), :63 ("compose".equals(mode))https://github.com/apache/lucene/issues/5942 (LUCENE-4877 discussion)
- Suggested fix
lucene/core/src/java/org/apache/lucene/analysis/AbstractAnalysisFactory.java:190-191 (at 5adff78bb168)
- Diagrams
lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/ICUTransformFilterFactory.java:51-54 (at 5adff78bb168)lucene/core/src/java/org/apache/lucene/analysis/AbstractAnalysisFactory.java:190-191 (at 5adff78bb168)
- TestICUTransformForwardDirection.java (lucene/analysis/icu, package org.apache.lucene.analysis.icu, 1 test):
repro/LFV-1119/TestICUTransformForwardDirection.java(35 lines)
The diagram illustrates the repro's scenario (main 5adff78bb168, ICUTransformFilterFactory.java:51-54, AbstractAnalysisFactory.java:190-191).
assets/LFV-1119/bug.png(541 KB)
