Telugu writes the vowel AU as one character, ఔ, or as two, ఒ + ౌ. Lucene's Telugu normalizer folds ఔ to ఓ (OO) and joins ఒ + ౌ into ఔ. Because it reads each word once, left to right, the ఔ it joins is never folded.
- Build an analyzer with the
teluguNormalizationfilter and noindicNormalizationin front of it, as in the factory's javadoc example (standard tokenizer + Telugu normalization). The filter folds AU ఔ (U+0C14) to OO ఓ (U+0C13), soఔషధం("medicine") becomesఓషధం; an upstream test pins this fold. - AU can also be typed as two characters: the letter ఒ (U+0C12) plus the vowel sign ౌ (U+0C4C). The filter recognizes this pair and joins it into ఔ, so you expect
ఒౌషధంto becomeఓషధంtoo, the way its other pair ఒ + ౕ becomes ఓ. - You get
ఔషధం. The normalizer reads the word once, left to right: it writes the joined ఔ at the position it is on (TeluguNormalizer.java:104) and never comes back, so the ఔ → ఓ rule (:60-62) never sees it. - With one document per spelling (run on main), each spelling's query finds 1 of 2 documents, and a prefix query typed
ఒౌషfinds only its own spelling; with the one-line fix every query finds 2 of 2. No error. The stockTeluguAnalyzeralready finds 2 of 2, because itsIndicNormalizationFilterjoins the pair first.
Applications that use TeluguNormalizationFilter or the teluguNormalization factory without IndicNormalizationFilter in front, as in the factory's javadoc example, on every release since 8.10.0. Words spelled with ఒ + ౌ do not match the ఔ spelling, at index and query time. The stock TeluguAnalyzer is unaffected since 9.10.0. A fix needs a reindex.
Mechanism, terms and numbers689 words
TeluguNormalizer (package-private since 10.0; used by TeluguNormalizationFilter, the teluguNormalization factory and TeluguAnalyzer) is one left-to-right pass over the term. It folds the long vowel AU ఔ (U+0C14) to OO ఓ (U+0C13) (TeluguNormalizer.java:59-62), a rule an upstream test pins for the filter alone, and it joins the two-character spelling ఒ + ౌ (U+0C12 U+0C4C) into ఔ (:102-106, comment (ఒ + ౌ) -> au (ఔ)). The joined ఔ is written at the current position, which the loop never visits again, so it is never folded: ఔషధం ("medicine") becomes ఓషధం while ఒౌషధం becomes ఔషధం. The class's other pair, ఒ + ౕ, writes ఓ directly. Chains that run the Telugu filter without IndicNormalizationFilter in front, such as the factory javadoc's example, give the two spellings different terms at index and query time; the stock TeluguAnalyzer composes the pair earlier and is unaffected from 9.10.0 on (before that, only its normalize() path was affected). Every release from 8.10.0 to 10.5.2 and main has it (10 releases and main were run; the releases in between have the same rule by the code history). The one-line fix (write ఓ at :104) changes the terms of words spelled with the pair, so those fields need a reindex.
- ఔ LETTER AU U+0C14 and ఓ LETTER OO U+0C13: independent vowels. The normalizer folds the long vowels to short ones, among them ఔ to ఓ (
// long -> short vowels,TeluguNormalizer.java:59-62). - ఒ + ౌ (LETTER O U+0C12 + VOWEL SIGN AU U+0C4C): a two-character spelling of AU, which the class joins into ఔ (
:102-106). It is not a Unicode canonical equivalent (NFC and NFD keep both spellings);IndicNormalizer's decomposition table, documented as following Unicode 5.2 and a page of graphical decompositions (IndicNormalizer.java:36-37,:71-74), maps it to AU for Telugu (:143-144). - ఒ + ౕ (LETTER O + LENGTH MARK U+0C55): a two-character spelling of OO, which the class joins into ఓ (
:98-101); an upstream test calls these "two ways to write oo letter" (it runs the fullTeluguAnalyzer, whereIndicNormalizationFilterjoins the pair first; the filter alone also gives ఓ, as the probe shows). TeluguNormalizer: a left-to-right rewrite of each token, oneswitch (s[i])case per character. It is package-private (public in 8.10.0-9.12.x) and reached throughTeluguNormalizationFilter, theteluguNormalizationfactory (for analysis andnormalize()) andTeluguAnalyzer. Keyword-marked tokens skip it.
case 'ఒ':(TeluguNormalizer.java:96-107): if the next character is ౌ, it writess[i] = 'ఔ'(:104) and deletes position i + 1.- The loop then continues with i + 1. Position i is never visited again, so
case 'ఔ'(:60-62) never sees the ఔ written there. Forఒౌషధం:ఒ ౌ ష ధ ం→ఔ ష ధ ం, done. - A precomposed ఔ always hits
case 'ఔ':ఔషధం→ఓషధం. So in a chain with only this filter, the joined ఔ renames the term but never merges it with the precomposed spelling. - The sibling compositions write final forms (ఒ + ౕ → ఓ at
:100, ె + ౖ → ై at:92), and the same switch already steps back (i--) after deleting a zero-width joiner (:52-57); the AU branch does neither. TeluguAnalyzerrunsIndicNormalizationFilterfirst (TeluguAnalyzer.java:120-121,:130-131), which joins ఒ + ౌ into ఔ before the Telugu filter folds it, so the stock analyzer gives one term.
| input | as the class's rules define | stock filter (a second pass) | stock TeluguAnalyzer (main) |
|---|---|---|---|
ఔ U+0C14 | ఓ | ఓ | ఓ |
ఒౌ U+0C12 U+0C4C | ఓ | ఔ U+0C14 (ఓ) | ఓ |
ఔషధం | ఓషధం | ఓషధం | ఓషధం |
ఒౌషధం | ఓషధం | ఔషధం (ఓషధం) | ఓషధం |
ఒౕ (control, the OO pair) | ఓ | ఓ | ఓ |
Over all 837,931 strings of up to 4 characters on the hunt's 30-character Telugu alphabet, ఔ survives in exactly the 2,760 inputs that contain the adjacent pair ఒౌ; over all 25,137,931 strings of up to 5 characters, in exactly the 110,670 such inputs, and the fixed output is always the stock output with ఔ replaced by ఓ.
Exposure and likelihood315 words
- Who is exposed. A
CustomAnalyzer, a hand-builtAnalyzeror a schema with the Telugu filter and noIndicNormalizationFilterbefore it, for example the factory javadoc's field type (StandardTokenizerFactory+TeluguNormalizationFilterFactory); on 8.10.0-9.12.x also direct callers of the then-publicTeluguNormalizer.normalize. No javadoc names Indic normalization as a prerequisite. - The stock analyzer.
TeluguAnalyzeris unaffected from 9.10.0 on. In 8.10.0-9.9.x itsnormalize()droppedIndicNormalizationFilter(fixed by GITHUB#13059), so its prefix, wildcard and fuzzy terms hit the bug there (executed on 8.10.0, 8.10.1, 8.11.0, 8.11.4, 9.0.0 and 9.9.2). - Why the chain is plausible. In the open issue gh#14659 about a Telugu rule of
IndicNormalizer, a maintainer (rmuir) replied "if you have perfect unicode text ... don't use this filter" (the filter under discussion isIndicNormalizationFilter), and a participant proposed makingTeluguAnalyzerleave it out by default. Either path leaves the Telugu filter alone with the pair. - What the damage looks like. Silent: no error, and identical spellings still match. With one document per spelling (run on main), each spelling's query finds 1 of 2 documents and a prefix query typed with the pair finds only its own spelling; with the fix, 2 of 2. The bug only keeps apart spellings that the fix joins (367,395 pairs over 837,931 short strings) and never joins two that the fix keeps apart.
- How likely. Rare: it needs the custom chain and text that spells AU with two characters. How often that spelling occurs in real text was not measured.
- Scale. One word reproduces it; assertions on and off behave the same; the recommended fix does the same write and shift per pair as the stock code, and a timing probe found no material difference between them.
- Reindex. A fix changes the term of every word spelled with ఒ + ౌ in affected chains, so fields analyzed before the fix keep the old terms until reindexed.
The test shows that TeluguNormalizationFilter composes ఒ + ౌ (U+0C12 U+0C4C) into AU (ఔ, U+0C14) but never applies its own AU -> OO fold to it.
/*
* 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.te;
import org.apache.lucene.analysis.Tokenizer;
import org.apache.lucene.tests.analysis.BaseTokenStreamTestCase;
public class TestTeluguComposedAu extends BaseTokenStreamTestCase {
public void testComposedAuFoldsToOo() throws Exception {
Tokenizer tokenizer = whitespaceMockTokenizer("ఒౌషధం");
// expected: ఒ + ౌ (AU) folds to OO like precomposed ఔ (ఔషధం -> ఓషధం); actual: AU is kept (ఔషధం)
assertTokenStreamContents(new TeluguNormalizationFilter(tokenizer), new String[] {"ఓషధం"});
}
}
# in a copy of this finding's repro folder (repro/LFV-669/, 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.te.TestTeluguComposedAu
- Expected vs actual
- Expected
ఒౌషధంto normalize toఓషధం(OO), as precomposedఔషధంdoes; actual:ఔషధం(AU kept):term 0 expected:<[ఓ]షధం> but was:<[ఔ]షధం>, with-eaand-da.
More files, environment, full output38 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:<[ఔ]షధం>
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:598)
org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertTokenStreamContents(BaseTokenStreamTestCase.java:622)
org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertTokenStreamContents(BaseTokenStreamTestCase.java:689)
org.apache.lucene.analysis.te.TestTeluguComposedAu.testComposedAuFoldsToOo(TestTeluguComposedAu.java:27)- The class calls the pair AU,
// (ఒ + ౌ) -> au (ఔ)(TeluguNormalizer.java:103), and folds every AU to OO; an upstream test pins that fold (ఔఐఆఈఊ→ఓఏఅఇఉ). - The sibling pair ఒ + ౕ is written straight as ఓ (
:100). - No javadoc requires
IndicNormalizationFilterfirst; the fold was added late in the introducing PR.
The full argument329 words
- The class's own rules. The composition comment names the result AU (
TeluguNormalizer.java:103), under the heading// composed oo or au -> oo or au(:96);IndicNormalizerhas the same mapping for Telugu (IndicNormalizer.java:143-144). The AU -> OO fold is unconditional (:59-62), andTestTeluguNormalizer.testVowelspins it with the filter alone (check("ఔఐఆఈఊ", "ఓఏఅఇఉ"),:35-40). - The sibling composition. ఒ + ౕ is written as ఓ (
TeluguNormalizer.java:98-101), and the filter alone gives ఓ for it (probe).TestTeluguAnalyzer.testBasicsnames the pair:// two ways to write oo letter.checkOneTerm(a, "ఒౕనమాల", "ఓనమాల")(:33-34, and throughnormalize()at:59); it runs the fullTeluguAnalyzer, whereIndicNormalizationFilteralready joins ఒ + ౕ (IndicNormalizer.java:145-146), so it does not by itself pin the Telugu filter's branch. In the filter alone AU also has two ways, and only one reaches ఓ. - No prerequisite documented. The filter "applies TeluguNormalizer to normalize the orthography" (
TeluguNormalizationFilter.java:27), and the factory javadoc shows it after the standard tokenizer alone (TeluguNormalizationFilterFactory.java:26-32). If Indic normalization always ran first, the class's own ఒ + ౌ branch would be dead code. - Tests. No upstream test has ఒ followed by ౌ; the 16 upstream Telugu tests pass with either fix.
- History. Both rules arrived in
544dbbea468(LUCENE-10083, PR #275, 2021-09-02). Inside the PR, the composition to ఔ was in the first commit (c143ef618e, 2021-08-13); the AU -> OO fold came with6c0c09933f"added long to short vowel conversion" (2021-09-01), which also changed the result of the ె + ౖ composition but left the AU one writing ఔ. The only review was an approval. That this was an oversight is an inference from the commit order; no record says so. - What is not claimed. Lucene documents no idempotence rule for its language normalizers, and the class promises to remove only "some differences in spelling variations" (
TeluguNormalizer.java:24). The case rests on the class's own rules.
The devil's-advocate check220 words
The strongest "intended" case (the devil's-advocate verifier and the intended-lens review by another model family, rated weak to moderate): Lucene's language normalizers are one-pass, best-effort rewrites with no idempotence promise, and an upstream Sorani test pins an output that a second pass would change (TestSoraniNormalizationFilter.testH, ھ → ه); the comment at :103 literally says the pair becomes "au (ఔ)", which the code does; ఒ + ౌ is not a Unicode canonical equivalent; and the stock TeluguAnalyzer runs IndicNormalizationFilter first and gives one term.
Why it fails: in the Sorani case the class's javadoc names two different letters: ھ (06BE) is the joining form of 'h' and becomes ه (0647), while a word-final ه is a form of the vowel 'e' and becomes ە (06D5) (SoraniNormalizer.java:29-30), so one pass keeps apart what that javadoc describes as different letters. Here the class itself says both spellings are the letter AU; the same method folds AU to OO without condition and an upstream test pins it; the sibling compositions write their final forms; the fold was added later in the same PR without updating the composition; and the filter and factory are public and documented without an Indic prerequisite. Verdict: a defect (adjudication: confirmed, high). What the counter-case gets right: the reach is narrow and the impact low.
Recommended Compose ఒ + ౌ straight to ఓ
Write ఓ instead of ఔ when joining ఒ + ౌ (TeluguNormalizer.java:104), as the sibling ఒ + ౕ branch two lines above does. The repro, the Telugu suites and all 1,911 tests of the module pass. It changes the terms of words spelled with the pair, so they need a reindex.
} else if (i + 1 < len && s[i + 1] == '\u0C4C') {
- // (ఒ + ౌ) -> au (ఔ)
- s[i] = '\u0C14';
+ // (ఒ + ౌ) -> au (ఔ), which is normalized to oo (ఓ) like a precomposed au
+ s[i] = '\u0C13';
len = delete(s, i + 1, len);All options and trade-offs4 options · 673 words
Options A and B give the same output on all 25,137,931 strings of the bounded check (A = B, and A = the stock output with ఔ replaced by ఓ). Add the ఒ + ౌ case to TestTeluguNormalizer with whichever is chosen. Not covered by any option (side observations, untriaged): ే + ౖ (U+0C47 U+0C56) is not a fixed point either, a visarga or a zero-width (non-)joiner between ఒ and ౌ leaves the pair unjoined in the filter with or without either fix (for the zero-width joiners even in the stock TeluguAnalyzer.normalize()), and the class javadoc says @since 9.0.0 although it shipped in 8.10.0.
Change the write at TeluguNormalizer.java:104 from '\u0C14' to '\u0C13' and update the comment (+2/-2), the same shape as the ఒ + ౕ branch at :100. The finder's fix.
} else if (i + 1 < len && s[i + 1] == '\u0C4C') {
- // (ఒ + ౌ) -> au (ఔ)
- s[i] = '\u0C14';
+ // (ఒ + ౌ) -> au (ఔ), which is normalized to oo (ఓ) like a precomposed au
+ s[i] = '\u0C13';
len = delete(s, i + 1, len);Trade-offs
- The smallest change; the stock
TeluguAnalyzeroutput is unchanged, because it never reaches this branch with the pair. - It repeats the knowledge "AU folds to OO" in a second place: if the AU -> OO fold were ever dropped, this line would have to change too.
- Changes the indexed term of every word spelled with ఒ + ౌ in affected chains: a CHANGES entry with a reindex note. Main is the natural target; a 10.x backport is debatable.
What the checks validated
- Scratch worktree at
a9779ef3c193, gradle, JDK 25: the new test fails 2 of 2 on stock (assertions on and off) and passes with the fix;org.apache.lucene.analysis.te.*: 5 suites, 18 tests (16 upstream + 2 new), 0 failures, with assertions on (seed 5EED0669) and off (seed 5EED066A);:lucene:analysis:common:checkGoogleJavaFormatpasses; the whole:lucene:analysis:common:test: 340 suites, 1,911 tests, 18 skipped, 0 failures, with assertions on. - Standalone runner with the fix shadowed: the new test 2 of 2 and the 4 upstream suites 16 of 16, each with
-eaand-da; earlier the finder's test 4 of 4, the independent reproducer's 7 of 7 and the devil's advocate's 8 of 8.
After the composition, step back one position so the loop's existing case 'ఔ' folds the result (+2/-1), the pattern the zero-width-joiner deletion already uses (:52-57).
s[i] = '\u0C14';
len = delete(s, i + 1, len);
+ i--;Trade-offs
Keeps "this pair is AU" as the single rule, so a later change to the AU fold applies to both spellings; less obvious to read. It terminates: the revisited ఔ is rewritten once by case 'ఔ' (the bounded run ends on all 25,137,931 inputs). Same reindex note.
What the checks validated
Gradle: the 5 Telugu suites, 18 tests, 0 failures with assertions on and off; checkGoogleJavaFormat passes. Standalone: the new test 2 of 2 and the upstream suites 16 of 16 (-ea, -da); identical output to option A on all 25,137,931 bounded inputs. The whole module was run with option A only.
Say so in the filter and factory javadoc and the factory example. Not run.
Trade-offs
Changes no indexed terms. But the class's own ఒ + ౌ branch stays inconsistent with its fold (and becomes dead code in documented use), and it runs against a maintainer's advice in gh#14659 to drop IndicNormalizationFilter for clean Unicode text.
Would also make the spellings agree (both ఔ). Not run.
Trade-offs
Breaks TestTeluguNormalizer.testVowels (:39) and changes the term of every ఔ word in every Telugu chain, the stock TeluguAnalyzer included: a much larger reindex for a question this bug does not raise.
A bounded-exhaustive check of the stock Telugu filter against its own unconditional rules (no character such a rule maps away, such as ఔ, may survive) found 6,286 violations over 837,931 short strings: 2,760 are this bug (ఔ survives exactly where the input has the pair ఒౌ), the rest are related side cases.
The method in full199 words
Method: bounded-exhaustive translation validation on the stock main jars (the study's "semi-formal" tier). A hunt unit, "script normalizers against their documented rules", 2026-10-27.
- A closure oracle (no character or pair that an unconditional rule rewrites, deletes or joins may survive: the long vowels and long vowel signs, candrabindu, visarga, the zero-width joiners, and the two-character spellings the class joins) ran over all 837,931 strings of length 0 to 4 on a 30-character Telugu alphabet: 6,286 violations. A recount with the same oracle splits them: 2,962 have ఒ and ౌ in the input (2,760 with the adjacent pair, 202 with a visarga or zero-width (non-)joiner between the two); the other 3,324 are side cases (3,138 leave ె + ౖ unjoined, for example U+0C47 U+0C56; 186 leave ఒ + ౕ after a deleted visarga or joiner). Over the same strings, ఔ survives in exactly the 2,760 inputs with the adjacent pair.
- A residual run that excludes inputs with ఒ, ె or ే (ే folds to ె) found 0, which isolates every violation to the compositions that start at ఒ or ె.
- Then an AI finder wrote the public-API JUnit, and independent AI tools reproduced, reviewed and judged it (see the checks).
No prior report found (best-effort search, 2026-10-27): 110 logged queries over GitHub issues, PRs and commits, downstream projects, four mailing lists, JIRA and the web, plus all 409 open PRs. The only Telugu normalization reports concern a different rule in IndicNormalizer.
Searches4 searches
Nothing reports that the composed AU escapes the fold. Upstream main a95c754ee003 (2026-10-26) and branch_10x still write U+0C14 at line 104; no open PR changes TeluguNormalizer.java.
Related, not the same bug: gh#14659 (open), PR #14699 (open) and PR #14657 (closed) concern IndicNormalizer's Telugu vu -> ma rule; PR #13059 (9.10.0) fixed TeluguAnalyzer.normalize() dropping IndicNormalizationFilter, which hides this bug in the stock analyzer; gh#11121 / PR #275 introduced the class without discussion of the rules.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues and PRs (apache/lucene), commits | 2026-10-27 | 24 issue/PR and 3 commit queries with a scripted prior-report search (class names, Telugu au/diphthong/vowel/composed/idempotent, the code points 0C12, 0C14, 0C4C and the characters), 24 manual searches, the PR #275 commit history | no report; only gh#14659 rated high, a different rule and class |
| Open PRs | 2026-10-27 | the changed files of all 409 open apache/lucene PRs | #14699 (removes IndicNormalizer's Telugu vu -> ma row and changes three Telugu test files; not TeluguNormalizer.java) and #16911 (removes all of branch_10x) |
| Mailing lists and JIRA | 2026-10-27 | 20 queries over dev@, java-user@, issues@ and commits@; 4 JIRA queries plus 2 JIRA REST queries (LUCENE, SOLR); 8 Solr list queries | no report |
| Downstream repositories and the web | 2026-10-27 | 3 downstream queries plus "telugu" in 6 downstream repositories; 8 web queries | no report |
Present since the file's first commit 544dbbea468 (2021-09-02, LUCENE-10083), first released in 8.10.0; the lines never changed except for formatting. It reproduces on every release tested from 8.10.0 to 10.5.2 and on main; 8.9.0 has no Telugu API.
Versions tested5 rows
Introduced by 544dbbea468 "LUCENE-10083: Analyzer and stemmer for Telugu language (#275)", 2021-09-02, the squash merge of apache/lucene PR #275 (issue gh#11121), backported to branch_8x the same day (lucene-solr 114c9dff46c); CHANGES lists it under 8.10.0. Inside the PR the composition to ఔ came first (c143ef618e, 2021-08-13) and the AU -> OO fold later (6c0c09933f, 2021-09-01). Later commits change no behaviour: 694d797526a (LUCENE-10561, visibility; package-private from 10.0.0), f99deb58b74 (#13661, formatting), 54283150a81 (#16255, a comment typo). Not a regression; never fixed. Separately, GITHUB#13059 (9caeb9395da, 9.10.0) made the stock TeluguAnalyzer.normalize() run IndicNormalizationFilter, which hides this bug in the stock analyzer.
| Version or commit | Verdict | Result |
|---|---|---|
main a9779ef3c193 | affected | the brief's test 2 of 2 fail (gradle and the standalone runner, assertions on and off) |
| 10.5.2, 9.12.3 | affected | the brief's test 2 of 2 fail; the finder's test 3 of 4 |
| 8.10.0, 8.10.1, 8.11.0, 8.11.4, 9.0.0, 9.9.2, 9.10.0, 10.0.0 | affected | the version-independent main() reports the bug (filter and factory chain); on 8.10.0-9.9.2 the stock TeluguAnalyzer.normalize() too |
| 8.9.0 | API absent | the repro does not compile: no org.apache.lucene.analysis.te package |
| main + fix | not affected | all repros pass; the 5 Telugu suites, 18 tests, and the module's 1,911 tests, 0 failed |
An independent reproducer, a devil's advocate, an adjudication and reviews by another model family (which ran their own tests) all found it real; the adjudication confirmed it with high confidence. A registry re-run and an audit reproduced it with new seeds.
Checks and verdicts346 words
Every check wrote or ran its own test against the stock jars, with assertions on and off unless noted.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | Public-API JUnit fails 3 of 4 on main (-ea, -da), 10.5.2 and 9.12.3; with the fix 4 of 4 pass and the upstream Telugu suites 16 of 16. |
| cross-model review, refute lens | real (high) | An independent reviewer from another model family (OpenAI GPT-5.6) ran its own JUnit on main ea57360912ed (analysis sources identical to a9779ef3c193) with -ea and -da; it fails, and passes with the fix; it checked that NFC/NFD do not equate the two spellings. |
| Independent reproducer | real | Own 7-test JUnit fails 3 of 7 on main, 10.5.2, 9.12.3, 8.11.4 and 8.10.0; its 4 controls pass; 8.9.0 has no Telugu API; 7 of 7 pass with only line 104 changed. |
| Cross-model review, intended lens | real (high) | OpenAI GPT-5.6, on main ea57360912ed (analysis sources identical to a9779ef3c193): the intended-behaviour argument is moderate and rejected; its fixed-point test fails on main and passes with the fix (-ea). |
| Devil's advocate | real | Own 8-test JUnit fails 3 of 8 on main, 10.5.2, 9.12.3 and 9.10.0, and 4 of 8 on 9.9.2 (the stock normalize() path); found the PR history and the sibling-composition controls. |
| adjudication | confirmed (high) | Re-ran the independent reproduction's test with new seeds (3 of 7 fail, -ea and -da), the finder's (3 of 4) and the independent reproduction's with the fix (7 of 7 pass); class minor. |
| security assessment | none | 0 exceptions over 25,137,931 generated strings; no output longer than its input; the bug only loses matches; outside Lucene's SECURITY.md scope. |
| registry re-run and audit | keep | The finder's test re-run on main with new seeds (3 of 4 fail, -ea and -da); code re-read at a9779ef3c193. |
| brief runs | reproduced | The brief's test through gradle (main) and the standalone runner (main, 10.5.2, 9.12.3); options A and B validated; A and B shown identical over 25,137,931 bounded inputs. |
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 470 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,564 words
- Registry title
- TeluguNormalizer composes ఒ + ౌ (U+0C12 LETTER O + U+0C4C VOWEL SIGN AU) into AU (U+0C14) but never applies its own AU -> OO fold to the result, so in a chain without IndicNormalizationFilter in front the two spellings of AU normalize to different terms
- Area
- analysis-common / Telugu normalizer (TeluguNormalizer.normalize; TeluguNormalizationFilter and the 'teluguNormalization' factory)
- Priority
- Low, rank 514: TeluguNormalizationFilter folds the two spellings of AU to different terms, which hurts relevance only, and it needs a chain without IndicNormalizationFilter plus a rare non-canonical spelling while the stock TeluguAnalyzer is unaffected.
- Impact class
- minor
- Impact note
- Relevance quality only, with no error: applications that run TeluguNormalizationFilter or the 'teluguNormalization' SPI factory without IndicNormalizationFilter in front of it (the factory javadoc's own example is StandardTokenizer + teluguNormalization) index and query a word whose AU is spelled with the two characters ఒ + ౌ (U+0C12 U+0C4C) as a different term from the same word spelled with precomposed ఔ (U+0C14), so the two spellings do not match each other, at index time and through Analyzer.normalize (prefix, wildcard and fuzzy terms); the stock TeluguAnalyzer is unaffected from 9.10.0 on (its normalize() path was affected in 8.10.0-9.9.x), no scale is needed, -ea and -da behave the same, and because a fix changes indexed terms, affected fields need reindexing after it.
- Summary
- You analyze Telugu text with TeluguNormalizationFilter (or the 'teluguNormalization' factory) without IndicNormalizationFilter in front of it, as in the factory javadoc example. The normalizer folds the letter AU (ఔ, U+0C14) to OO (ఓ, U+0C13), a rule the upstream test pins, and it also recognizes the two-character spelling ఒ + ౌ (U+0C12 U+0C4C) as AU, so both spellings of a word such as ఔషధం ('medicine') should give the same term. Instead the composition writes AU at a position that the single left-to-right pass never visits again, so ఒౌషధం keeps AU (term ఔషధం) while ఔషధం becomes ఓషధం, and the two spellings silently stop matching each other.
- Introduced
- 544dbbea46878b49be5c4c4e82d9b06a5679c891 (LUCENE-10083 'Analyzer and stemmer for Telugu language', apache/lucene PR #275, issue #11121, squash-merged on 2021-09-02; backported to branch_8x as lucene-solr 114c9dff46c) [read: git log and blame in the history clone, PR #275 commit list through the GitHub API]: the file's first commit, so both rules arrive together and no version ever had them consistent. Inside PR #275 the composition to U+0C14 was already in the first commit c143ef618e (2021-08-13), and commit 6c0c09933f 'added long to short vowel conversion' (2021-09-01) added the AU -> OO fold in the same single-pass switch without changing the composition (in that commit the e + ai composition result was corrected, the AU one was not); that this was an oversight is an inference from the commit sequence, no record says so. Not a regression: the later commits on the file (694d797526a LUCENE-10561 visibility, f99deb58b74 #13661 formatting, 54283150a81 #16255 a comment typo) do not change behaviour. (2021-09-02 (squash-merged on main and backported to branch_8x the same day; first released in 8.10.0))
- First affected version
- 8.10.0 [exec] (the first release with the org.apache.lucene.analysis.te package; the follow-up's history repro fails there, and the independent reproduction's JUnit fails there too). 8.9.0, the last release before it, has no Telugu analysis API [exec: the repro does not compile]. On the main line the first affected release is 9.0.0.
- Affected versions
- Every release that ships the Telugu analysis API, 8.10.0 through 10.5.2, and main a9779ef3c193; not fixed. [exec] the follow-up's version-independent public-API main() (TeluguNormalizationFilter, and CustomAnalyzer standard + teluguNormalization) fails on 8.10.0, 8.10.1, 8.11.0, 8.11.4, 9.0.0, 9.9.2, 9.10.0, 9.12.3, 10.0.0, 10.5.2 and main (-ea and -da), and passes on main with the fix shadowed; the finder's JUnit fails 3 of 4 on main (-ea and -da), 10.5.2 and 9.12.3 (it needs org.apache.lucene.tests.analysis, so it does not compile on 9.0.0); the independent reproduction's JUnit fails 3 of 7 on main (-ea and -da), 10.5.2, 9.12.3, 8.11.4 and 8.10.0; the intended-behaviour check's fails 3 of 8 on main, 10.5.2, 9.12.3 and 9.10.0 and 4 of 8 on 9.9.2. The stock TeluguAnalyzer token stream is unaffected on every version (IndicNormalizationFilter composes the pair first); its normalize() path (multi-term query terms) was affected in 8.10.0-9.9.x [exec on 8.10.0, 8.10.1, 8.11.0, 8.11.4, 9.0.0 and 9.9.2] until GITHUB#13059 (9.10.0) stopped it from discarding IndicNormalizationFilter. [read] upstream main a95c754ee003 (2026-10-26) and branch_10x still write U+0C14 at TeluguNormalizer.java:104 (read 2026-10-27).
- 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 (searched 2026-10-27, read-only; the prior-report record, verdict new): 110 logged queries, among them a scripted prior-report search (24 GitHub issue/PR queries on apache/lucene with the class names, Telugu au/diphthong/vowel/composed/idempotent, the code points 0C12, 0C14 and 0C4C and the characters themselves; 3 commit queries; 3 downstream queries; 20 mailing-list queries over the dev, java-user, issues and commits lists; 4 JIRA queries; 6 web queries; 117 distinct hits, only #14659 rated high), 24 more manual GitHub searches, 'telugu' in 6 downstream repositories, the touched files of all 409 open apache/lucene PRs, the PR #275 commit history, 2 JIRA REST queries (LUCENE and SOLR), 8 Solr list queries and 2 more web queries. Related, not the same bug: #14659 (open issue), #14699 (open PR; removes IndicNormalizer's Telugu vu -> ma row and changes three Telugu test files, not TeluguNormalizer.java) and #14657 (closed PR) are about IndicNormalizer's Telugu vu -> ma rule; #13059 (merged, 9.10.0) fixed TeluguAnalyzer.normalize() dropping IndicNormalizationFilter, which as a side effect hides this bug in the stock analyzer; #11121 / #275 introduced the class (one approving review, no discussion of the rules). No open PR changes TeluguNormalizer.java (#16911 deletes all of branch_10x). Not fixed: upstream main a95c754ee003 and branch_10x still write U+0C14 at TeluguNormalizer.java:104. Gaps: keyword search only, the first results page per GitHub query, no non-English forums beyond the general web search; private reports and chat are not searchable. 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(commit of 2026-10-26; tested 2026-10-28) through gradle in a scratch worktree and through the standalone JUnit runner, with assertions on and off; the 10.5.2 and 9.12.3 release jars (the brief's test); a version-independentmain()on 8.9.0 (no Telugu API), 8.10.0, 8.10.1, 8.11.0, 8.11.4, 9.0.0, 9.9.2, 9.10.0, 9.12.3, 10.0.0, 10.5.2 and main (2026-10-27). Fix A was validated in the scratch worktree (the new test, the 5 Telugu suites with assertions on and off,checkGoogleJavaFormat, the whole module's 1,911 tests with assertions on); fix B with the Telugu suites and the format check; both were compared with the stock code over 25,137,931 bounded inputs.
- Finding record
lucene/analysis/common/src/java/org/apache/lucene/analysis/te/TeluguNormalizer.java:21-27 (at a9779ef3c193) (class javadoc 'Normalizes text to remove some differences in spelling variations', '@since 9.0.0'; package-private class), :37-115 (normalize: one left-to-right in-place pass), :59-62 ('// long -> short vowels', case U+0C14 -> U+0C13), :96-107 ('// composed oo or au -> oo or au'; U+0C12 U+0C55 -> U+0C13 at :98-101; U+0C12 U+0C4C -> U+0C14 at :102-105, comment '(ఒ + ౌ) -> au (ఔ)' at :103, the write at :104)lucene/analysis/common/src/java/org/apache/lucene/analysis/te/TeluguNormalizationFilter.java:26-35 (at a9779ef3c193) (javadoc 'applies TeluguNormalizer to normalize the orthography'; :29-31 'the normalization may cause unrelated terms to conflate'), :47-53 (keyword tokens are skipped); TeluguNormalizationFilterFactory.java:23-35 (javadoc example: StandardTokenizerFactory + TeluguNormalizationFilterFactory only), :40 (SPI name 'teluguNormalization'), :55-63 (normalize() delegates to create()); TeluguAnalyzer.java:113-133 (IndicNormalizationFilter before TeluguNormalizationFilter in createComponents and normalize)lucene/analysis/common/src/java/org/apache/lucene/analysis/in/IndicNormalizer.java:33-38 (at a9779ef3c193) (javadoc: Unicode guidelines and graphical decompositions), :144 ({0x12, 0x4C, -1, 0x14, TELUGU | KANNADA}), :146 ({0x12, 0x55, -1, 0x13, TELUGU})lucene/analysis/common/src/test/org/apache/lucene/analysis/te/TestTeluguNormalizer.java:35-40 (at a9779ef3c193) (testVowels: 'ఔఐఆఈఊ' -> 'ఓఏఅఇఉ' with the filter alone); no Telugu test mentions U+0C4C after U+0C12 (grep)GitHub, read-only (2026-10-27): apache/lucene PR #275 commit list (c143ef618e, 6c0c09933f, 641b1fe040) and review, #11121, #14659 (body and comments), PR #14699 (diff), PR #14657, PR #13059, PR #16911, upstream main a95c754ee003 and branch_10x TeluguNormalizer.java (line 104 unchanged)
- The finding in 30 seconds
lucene/analysis/common/src/java/org/apache/lucene/analysis/te/TeluguNormalizer.java:21-28 (at a9779ef3c193) (class javadoc, package-private), :37-115 (the pass), :52-57 (zwj/zwnj, i--), :59-62 (AU -> OO), :89-95 (e + ai), :96-107 (O compositions; :100, :103, :104)lucene/analysis/common/src/java/org/apache/lucene/analysis/te/TeluguNormalizationFilter.java:26-54; (at a9779ef3c193) TeluguNormalizationFilterFactory.java:26-32, :40, :55-63; TeluguAnalyzer.java:113-133lucene/analysis/common/src/java/org/apache/lucene/analysis/in/IndicNormalizer.java:33-38, (at a9779ef3c193) :143-146lucene/analysis/common/src/test/org/apache/lucene/analysis/te/TestTeluguAnalyzer.java:31-36 (at a9779ef3c193) (testBasics, 'two ways to write oo letter')lucene/analysis/common/src/java/org/apache/lucene/analysis/in/IndicNormalizer.java:71-74 (at a9779ef3c193) (table javadoc)
- Who is affected
lucene/analysis/common/src/java/org/apache/lucene/analysis/te/TeluguNormalizationFilterFactory.java:26-32, (at a9779ef3c193) :55-63; TeluguNormalizationFilter.java:26-35; TeluguAnalyzer.java:113-133GitHub, read-only (2026-10-28): apache/lucene issue #14659 (comments by rmuir and praveen-d291)
- Why it looks unintended
lucene/analysis/common/src/java/org/apache/lucene/analysis/te/TeluguNormalizer.java:24, (at a9779ef3c193) :59-62, :96-107; TeluguNormalizationFilter.java:26-35; TeluguNormalizationFilterFactory.java:26-32lucene/analysis/common/src/java/org/apache/lucene/analysis/in/IndicNormalizer.java:33-38, (at a9779ef3c193) :143-144lucene/analysis/common/src/test/org/apache/lucene/analysis/te/TestTeluguNormalizer.java:35-46; (at a9779ef3c193) TestTeluguAnalyzer.java:31-36, :52-61lucene/analysis/common/src/java/org/apache/lucene/analysis/ckb/SoraniNormalizer.java:29-30; (at a9779ef3c193) TestSoraniNormalizationFilter.java:60-68 (testH)
- Suggested fix
lucene/analysis/common/src/test/org/apache/lucene/analysis/te/TestTeluguNormalizer.java:39 (at a9779ef3c193)
- Prior reports
GitHub, read-only (2026-10-28): apache/lucene pulls/14699/files (IndicNormalizer.java -2; TestTeluguAnalyzer, TestTeluguFilters, TestTeluguNormalizer)
- History
lucene/CHANGES.txt:5215 (at a9779ef3c193) (under 8.10.0, heading :5192), :3048 (under 9.10.0)
- TestTeluguComposedAu.java (JUnit test):
repro/LFV-669/src/org/apache/lucene/analysis/te/TestTeluguComposedAu.java(29 lines)
A light, step-by-step picture of the repro (the word ఔషధం and its two spellings), made in parallel with this doc from executed runs on main a9779ef3c193 and the code.
assets/LFV-669/bug.png(470 KB)
