All findings · Lucene finding · LFV-514 · phonetic-tokens-changed-by-undocumented-codec-upgrades

Bundled commons-codec upgrades changed phonetic filter tokens (RefinedSoundex in 10.2.0; Metaphone, ColognePhonetic and Daitch-Mokotoff on main), and no release note says to re-index

Lucene's phonetic filters get their codes from the bundled commons-codec, whose upgrades changed the codes of ordinary words (on main Metaphone chair is XR, not KR; since 10.2.0 RefinedSoundex codes Köln as K378). No release note says to re-index, so an index kept across the upgrade silently changes sound-alike matches.

Summary

Priority
Low, rank 1227A documentation-only gap, but released (RefinedSoundex since 10.2) and upgraders who keep their index silently get different phonetic matches.
Impact
minor
Since
10.2.0 for RefinedSoundex (#14129 / #14177); main only for the other three encoders (#14982, #16035)introduced 2025-01-27 · about 1 year
Affected
10.2.0 to 10.6.0 (RefinedSoundex only, 11 releases); main and branch_11x (all four encoders)
Status
documented
Found by
non-formal: a release-jar token differential across Lucene versions

AI-assisted page: this finding was first signalled by code reading or testing and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

PhoneticFilter passes each word to a commons-codec Encoder and DaitchMokotoffSoundexFilter to commons-codec's DaitchMokotoffSoundex; Lucene has no code tables of its own. So the indexed codes follow the bundled commons-codec: 1.13 up to 10.1.0, 1.17.2 in 10.2.0 to 10.6.0, 1.23.0 on main. Fixes in those releases changed four encoders' codes, and none of the bumps touched phonetic code, phonetic tests or a note about commons-codec.

Background: phonetic fields

  1. A phonetic field indexes a sound code for each word, so a query spelled differently but sounding the same still matches: with Metaphone on Lucene 10.x, kair and chair are both KR.
  2. The same analysis runs when you index and when you search, and a query matches only when both produce the same code. Segments written before an upgrade keep the codes the old version computed.
  3. inject=true, the factory default, indexes the code and the original word; inject=false indexes only the code.
Diagram of the bug: Lucene 10.5.2, with commons-codec 1.17.2, indexes the word chair as the Metaphone code KR; after the upgrade, main, with commons-codec 1.23.0, analyzes the query chair as XR, which no longer matches the old document until the field is re-indexed.
How a phonetic code written by Lucene 10.x stops matching after the commons-codec upgrade on main. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. You index chair, kair and share in a field analyzed with PhoneticFilter(new Metaphone(), false) on Lucene 10.5.2 (10.6.0 gives the same codes). The field stores the sound codes KR, KR and XR, so a query for kair finds chair too.
  2. You upgrade to main, the future 11.0, and keep the index; its migration guide says "Simply open the index with the new version". You expect the same codes, or a note telling you to re-index, as Lucene gave in 2014 for a commons-codec upgrade (LUCENE-6058) and gives in 11.0 for Snowball.
  3. Instead, main bundles commons-codec 1.23.0 rather than 1.17.2, and its Metaphone fix codes chair as XR. On the old index the query chair now finds only share; a freshly built index finds chair and share.
  4. Across four encoders, 12 of 72 test queries return different documents than a fresh re-index (0 in same-version controls). With the same inject=false, ColognePhonetic codes quick as 4 instead of 44, so the query quick finds 0 documents instead of 2.
  5. Nothing in CHANGES.txt or MIGRATE.md mentions these upgrades. The new codes are correct upstream fixes; the missing re-index note is the bug.

Who is affected

Applications that use PhoneticFilter (Metaphone, ColognePhonetic, RefinedSoundex) or DaitchMokotoffSoundexFilter and keep their index across an upgrade. Released: RefinedSoundex words with letters outside A-Z, from 10.2.0 (19.0% of 2,901 city names). With 11.0: ColognePhonetic changes 11.8-16.7% of distinct words in two test corpora, Metaphone 0.7-2.1%. No error; matches change silently.

Mechanism, terms and numbers627 words

Lucene's phonetic filters compute no codes themselves: PhoneticFilter calls a commons-codec Encoder and DaitchMokotoffSoundexFilter calls commons-codec's DaitchMokotoffSoundex, so the indexed codes are whatever the bundled commons-codec computes. Lucene bundled 1.13 up to 10.1.0, 1.17.2 from 10.2.0 to 10.6.0, and 1.23.0 on main and branch_11x. Bug fixes in those releases changed the codes of ordinary words: RefinedSoundex Köln gets K378 since 10.2.0 (10.1.0 indexed only the word), and on main Metaphone chair is XR instead of KR, ColognePhonetic quick is 4 instead of 44 and Daitch-Mokotoff it's is 040000 instead of 034000. Neither CHANGES.txt nor MIGRATE.md mentions any of these upgrades or asks for re-indexing, although Lucene announced the same kind of change in 2014 (LUCENE-6058, "any index using it will need to be rebuilt"), gives re-index advice for the 11.0 Snowball upgrade, and tells 11.0 users to "Simply open the index with the new version". An index written by the older version and searched by the newer one silently changes its sound-alike matches in that field until it is re-indexed. The new codes are correct; the fix is a migration note.

The code (main 645d4fe14a20)

// PhoneticFilter.java:70-78 (verbatim, indentation removed)
String value = termAtt.toString();
String phonetic = null;
try {
  String v = encoder.encode(value).toString();
  if (v.length() > 0 && !value.equals(v)) phonetic = v;
} catch (Exception _) {
} // just use the direct text

if (phonetic == null) return true;

Line 73 is where commons-codec computes the code; if the encoder throws, lines 75-76 keep only the word.

  • Delegation: PhoneticFilter encodes every token with the configured Encoder (PhoneticFilter.java:73); DaitchMokotoffSoundexFilter calls encoder.soundex(term) on a DaitchMokotoffSoundex (DaitchMokotoffSoundexFilter.java:38, :79).
  • inject: with inject=false the code replaces the word (PhoneticFilter.java:80-84); with inject=true, the factory default (PhoneticFilterFactory.java:52-53, :106), the code is emitted and the word is kept at the same position (:86-96). So with inject=true exact-word matches survive an upgrade and only sound-alike matches change.
  • Why 10.1.0 kept only the word for Köln: commons-codec 1.13's RefinedSoundex throws ArrayIndexOutOfBoundsException for letters outside A-Z (Müller: "Index 155 out of bounds for length 26"), and PhoneticFilter.java:75-76 swallows it. From 1.16.1 (CODEC-311) the encoder skips those letters and codes the rest of the word (Köln -> K378, the code of Kln). With inject=false the code now replaces the word; with inject=true the word stays and the code is added.
  • The pin: gradle/libs.versions.toml:8 is commons-codec = "1.23.0" on main; the release POMs declare 1.17.2 for 10.2.0 to 10.6.0 and 1.13 for 9.0.0 to 10.1.0.

The four changes (commons-codec's own changelog)

encoderexample, old -> newcommons-codec fixLucene bumpfirst Lucene release
RefinedSoundexKöln: the word only -> K378words with letters outside A-Z get a code (those letters are skipped) instead of the encoder throwing (CODEC-311), 1.16.1#14129 / #1417710.2.0
Daitch-Mokotoffit's: 034000 -> 040000cleanup(String) removes punctuation (listed under CODEC-331), 1.19.0#1498211.0.0 (unreleased)
Metaphonechair: KR -> XRCH digraph per the basic Metaphone rules (CODEC-249), 1.22.0#1603511.0.0 (unreleased)
ColognePhoneticquick: 44 -> 4no duplicate consecutive codes (CODEC-317), 1.22.0#1603511.0.0 (unreleased)

commons-codec's changelog lists two fixes under CODEC-331; the JIRA issue's own title is the Beider-Morse one.

Why CI did not notice

Lucene's phonetic tests pin no input that changed: the Metaphone assertions use "aaa bbb ccc easgasg" (TestPhoneticFilter.java:36-44), and TestDaitchMokotoffSoundexFilter uses the same words. None of the bumps touched phonetic code, phonetic tests or a note about commons-codec: the commons-codec part of each changed only the version catalog, the lock file and license checksums, plus one test module-info line (requires org.apache.commons.codec;) in #14129 and #14177, and #14177 also carried the OpenNLP upgrade with its CHANGES line.

Exposure and likelihood275 words
  • Who: applications that put PhoneticFilter (with Metaphone, ColognePhonetic or RefinedSoundex) or DaitchMokotoffSoundexFilter into their own analyzer and keep the index across a Lucene upgrade. analysis-phonetic is an opt-in module with no analyzer of its own; default constructors and ordinary words suffice.
  • Released today: RefinedSoundex, for words with letters outside A-Z, when upgrading from 10.1 or earlier to 10.2.0 or later: 19.0% of the 2,901 names in Lucene's world-cities-points.txt test resource, 0% of the ASCII-only Reuters words.
  • With 11.0.0 (now on branch_11x), distinct words whose code changes from 10.5.2 to main, inject=false:
encoderReuters test corpus (9,413 words)world cities (2,901 names)
ColognePhonetic16.7%11.8%
Metaphone0.7%2.1%
Daitch-Mokotoff0.6% (possessives such as board's)0.0%
  • Effect: no exception and no index damage. In that field old documents and new queries stop matching, or match other sound-alike documents, until the field is re-indexed. An independent cross-version index test found 11 of 14 queries missing their document when main searched a 10.5.2 index, two of them inject=true sound-alike queries (chorous, Kexe).
  • A normal path: keeping an index across a minor upgrade is ordinary, and 11.0's migration guide allows upgrades across majors "without reindexing when no format breaks occur" (MIGRATE.md:138). That policy is about index formats and promises nothing about analysis output, which is why an analysis change needs its own release note (as Snowball got, MIGRATE.md:196-202).
  • Not measured (not claimed): DoubleMetaphone, Soundex, Caverphone2 and Nysiis were unchanged on a 53-word probe and BeiderMorseFilter (GENERIC/APPROX) on 6,901 words; the Beider-Morse changes in the commons-codec 1.19.0 and 1.23.0 notes and the icu4j and morfologik bumps on main were not measured otherwise.

Minimal repro

One test method on the public API: WhitespaceTokenizer plus PhoneticFilter(new Metaphone(), false) on the word chair, pinned to the token of 10.5.2 and 10.6.0, KR. It fails on main because the code is now XR. That shows the silent change; XR is the correct new code, so a release note will not make it pass.

TestMetaphoneTokenSinceLastRelease.java33 lines
/*
 * 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.phonetic;

import org.apache.commons.codec.language.Metaphone;
import org.apache.lucene.analysis.Tokenizer;
import org.apache.lucene.analysis.core.WhitespaceTokenizer;
import org.apache.lucene.tests.analysis.BaseTokenStreamTestCase;

public class TestMetaphoneTokenSinceLastRelease extends BaseTokenStreamTestCase {

  public void testChairKeepsTokenOfLastRelease() throws Exception {
    Tokenizer tokenizer = new WhitespaceTokenizer();
    tokenizer.setReader(new java.io.StringReader("chair"));
    // expected KR as in 10.5.2 (no CHANGES/MIGRATE note on changed codes); actual on main: XR
    assertTokenStreamContents(
        new PhoneticFilter(tokenizer, new Metaphone(), false), new String[] {"KR"});
  }
}
Command
cp $REPRO/TestMetaphoneTokenSinceLastRelease.java \
   $LUCENE/lucene/analysis/phonetic/src/test/org/apache/lucene/analysis/phonetic/
cd $LUCENE
./gradlew :lucene:analysis:phonetic:test \
  --tests org.apache.lucene.analysis.phonetic.TestMetaphoneTokenSinceLastRelease -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same failure)
# control: the same test passes on the 10.5.2 and 10.6.0 release jars (commons-codec 1.17.2)
Expected
KR, the token Lucene 10.5.2 and 10.6.0 emit for chair (the test passes on both).
Observed
term 0 expected:<[K]R> but was:<[X]R> on main, with assertions on and off.

All files of this repro: repro/LFV-514/

More files, environment, full output321 words
  • Smallest input: one word, one encoder, inject=false, public API only; no index is needed, because the mismatch is between the token an old index holds and the token a new query gets.
  • Read it honestly: the failure demonstrates the change. It is not a regression test for the fix: the recommended fix is a migration note, after which this test still fails. The suggested regression test is this same test with KR changed to XR (fix B).
  • Why Metaphone: the hunt's first example was ColognePhonetic quick, but a verifier noticed that the new Cologne output collapses equal codes separated by vowels (Kekse 448 -> 48), which may differ from the published Kölner Phonetik rules; a Metaphone example cannot be misread as a claim that the new code is wrong. That Cologne question belongs to commons-codec and is not part of this bug.
  • Not covered by this test: the RefinedSoundex change that shipped in 10.2.0. The hunt's testRefinedSoundexSameAs10_1 (Köln, expected:<K[öln]> but was:<K[378]>) fails on every release from 10.2.0 to 10.6.0.
  • Determinism: no random input; seed DEADBEEF and a random seed with 5 iterations fail the same way.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT, commons-codec 1.23.0); passes on 10.5.2 and 10.6.0 (commons-codec 1.17.2)
jdkOpenJDK 25.0.3
osLinux aarch64
Command (environment)
cp $REPRO/TestMetaphoneTokenSinceLastRelease.java \
   $LUCENE/lucene/analysis/phonetic/src/test/org/apache/lucene/analysis/phonetic/
cd $LUCENE
./gradlew :lucene:analysis:phonetic:test \
  --tests org.apache.lucene.analysis.phonetic.TestMetaphoneTokenSinceLastRelease -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same failure)
# control: the same test passes on the 10.5.2 and 10.6.0 release jars (commons-codec 1.17.2)

Observed output

# gradle, main 645d4fe14a20, seed DEADBEEF, assertions on (stack frames shortened: module prefixes removed)
TestMetaphoneTokenSinceLastRelease > testChairKeepsTokenOfLastRelease FAILED
    org.junit.ComparisonFailure: term 0 expected:<[K]R> but was:<[X]R>
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:76A30ABE1062EA6B]:0)
        at org.junit.Assert.assertEquals(Assert.java:117)
        at org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertTokenStreamContents(BaseTokenStreamTestCase.java:262)
        ...
        at org.apache.lucene.analysis.phonetic.TestMetaphoneTokenSinceLastRelease.testChairKeepsTokenOfLastRelease(TestMetaphoneTokenSinceLastRelease.java:30)
1 test completed, 1 failed
BUILD FAILED
# gradle, assertions off (-Ptests.asserts=false): the same ComparisonFailure
# random seed, 5 iterations: 5 of 5 fail
# 10.5.2 and 10.6.0 release jars (commons-codec 1.17.2): PASS, 1 test, 0 failures

Why it looks unintended

  • LUCENE-6058 (2014), same module and dependency: a commons-codec upgrade changed an experimental filter, "so any index using it will need to be rebuilt".
  • 11.0's guide asks for re-indexing after the Snowball upgrade, yet says "Simply open the index with the new version".
  • The 10.2.0 bump was hand-written; its OpenNLP sibling got a CHANGES line, commons-codec none.
The full argument239 words
  • Same module, same dependency: CHANGES.txt:11240-11243, LUCENE-6058: "With the upgrade to Apache commons codec 1.10, the experimental BeiderMorseFilter has changed its behavior, so any index using it will need to be rebuilt." On that issue Michael McCandless wrote: "It's marked experimental so it's allowed to change between releases... but I'll updated the CHANGES.txt entry to advertise this." (#7120).
  • Current practice for analysis-output changes: Snowball in 11.0 (MIGRATE.md:196-202: "Existing indexes that used Snowball stemming should be reindexed so search-time analysis matches indexed tokens"), Thai in 10.6 (:757-764: "may therefore stop matching queries analyzed with 10.6 until it is reindexed"), Romanian in 10.0 (:871-873: "you should reindex Romanian documents").
  • The released bump was hand-written: #14129 ("Upgrade commons-codec from 1.13.0 to 1.17.2", msfroh, merged by msokolov) is not a dependabot PR. Its backport #14177 also upgraded OpenNLP, which got a 10.2.0 CHANGES line (GITHUB#14130, CHANGES.txt:2026-2027); commons-codec got none.
  • The new upgrade policy raises the stakes: 11.0 invites in-place upgrades across majors (MIGRATE.md:136-154), exactly where phonetic matches change.
  • Nothing points users to the codec notes: no Lucene documentation (javadoc, the module overview, CHANGES.txt or MIGRATE.md) says which commons-codec version is bundled; only the build files and the published POM do, and the module overview names Apache Commons Codec without a version (lucene/analysis/phonetic/src/java/overview.html:28-31). By contrast the ICU module's overview has a "Backwards Compatibility" section about the Unicode version it tracks (lucene/analysis/icu/src/java/overview.html:343-350).
The devil's-advocate check203 words

The intended-behaviour verifier and the cross-model lenses built the strongest case for "intended": no Lucene document promises stable phonetic codes (the javadoc says only "Create tokens for phonetic matches" and links to Commons Codec), DaitchMokotoffSoundexFilter is @lucene.experimental, dependabot PRs carry the skip-changelog label in every ecosystem, including the gradle bumps (.github/dependabot.yml:53; also :13 GitHub Actions and :35 uv) and 349 of 351 dependabot commits touch no CHANGES.txt, CONTRIBUTING.md says only "You may want to add a CHANGES entry", the bump PR bodies quote the commons-codec release notes, the 10.0 Snowball upgrade got only an API note (MIGRATE.md:867-869), and the main-only part is still unreleased.

Why it fails: LUCENE-6058 is the same module, the same dependency and an experimental filter, and was announced with a rebuild note; #14661 added skip-changelog to the bot labels so the changelog-check workflow skips bot PRs, which is not a decision about migration notes for changed analysis output (that workflow has since been turned off, #16366); the released 10.2.0 change came from a hand-written PR; and no Lucene documentation tells a user which codec notes to read. These arguments are why the bug is minor and documentation-only, not why the omission would be intended.

Suggested fix

Recommended A. An 11.0 MIGRATE.md section next to the Snowball note

Add an 11.0 migration-guide section on main and branch_11x: Metaphone, ColognePhonetic and Daitch-Mokotoff codes changed with commons-codec 1.23.0 (plus RefinedSoundex since 10.2), so re-index fields that use the affected encoders. Validated: the guide renders and the phonetic suite passes. Optionally pin chair -> XR in a test so the next codec bump is noticed in CI.

All options and trade-offs5 options · 660 words

The defect is a missing note, so the main fix is text. Two small additions make it last; reverting the codec is not an option. Options A, B and C were run in a scratch worktree of 645d4fe14a20 (OpenJDK 25.0.3).

A. An 11.0 MIGRATE.md section next to the Snowball note (validated by the checks) (recommended)

Add to "Migration from Lucene 10.x to Lucene 11.0" on main and branch_11x (a draft):

### Phonetic analysis: commons-codec upgrade changes some phonetic codes

The analysis-phonetic module gets its encoders from the bundled commons-codec, which
Lucene 11 upgrades to 1.23.0 (Lucene 10.2 to 10.6 bundle 1.17.2). Fixes in commons-codec
change the codes of some ordinary words:

- `Metaphone`: "CH" follows the basic Metaphone rules, for example `chair` is `XR`
  instead of `KR` (CODEC-249).
- `ColognePhonetic`: repeated codes are collapsed, for example `quick` is `4` instead
  of `44` (CODEC-317).
- `DaitchMokotoffSoundexFilter`: punctuation is ignored, for example `it's` is `040000`
  instead of `034000` (commons-codec 1.19.0).

Indexes upgraded from Lucene 10.1 or earlier (commons-codec 1.13) also see a
`RefinedSoundex` change from Lucene 10.2: words with letters outside A-Z, such as `Köln`,
now get a code (`K378`) where only the original word was indexed before (CODEC-311).

An existing index whose fields use these encoders may stop matching queries analyzed
with Lucene 11 until it is reindexed. With `inject=true` (the `PhoneticFilterFactory`
default) the original words are indexed too, so exact-word matches still work, but
sound-alike matches change. Reindex such fields.

Trade-offs

  • Docs only; no behaviour change. It reaches users who read the migration guide before upgrading.
  • The example codes are executed facts for these words only; the draft names no Beider-Morse change because none was measured.

What the checks validated

  • ./gradlew :lucene:documentation:markdownToHtml succeeds; the rendered MIGRATE.html has the new phonetic-analysis-commons-codec-upgrade-changes-some-phonetic-codes section.
  • ./gradlew :lucene:analysis:phonetic:test with the note (and B's test): 9 classes, 42 tests, 0 failures.
  • The minimal repro still fails with expected:<[K]R> but was:<[X]R>, as it must: the note documents the change and does not undo it.

B. Pin the current code in a test, so the next bump is noticed (validated by the checks)

The minimal test with KR changed to XR, kept permanently (for example as one method in TestPhoneticFilter).

assertTokenStreamContents(
    new PhoneticFilter(tokenizer, new Metaphone(), false), new String[] {"XR"});

Trade-offs

Guards one word of one encoder; it tells users nothing by itself and only makes the next change visible to the committer who bumps commons-codec.

What the checks validated

  • Passes on main through gradle with assertions on and off; checkGoogleJavaFormat is clean.
  • Fails on the 10.6.0 jars (commons-codec 1.17.2) with expected:<[X]R> but was:<[K]R>, so a codec that codes chair differently fails CI at the bump.

C. Say in the module overview that codes follow the bundled commons-codec (validated by the checks)

One paragraph in lucene/analysis/phonetic/src/java/overview.html, like the ICU module's "Backwards Compatibility" section: the codes come from the bundled Apache Commons Codec, an upgrade can change them, and affected fields need re-indexing.

Trade-offs

A lasting statement, but few users read module overviews before upgrading; it complements A. It depends on A: the validated draft ends with "Such changes are listed in the migration guide; reindex the affected fields", which is true only if A lands with it and later code-changing codec upgrades get a note too; without A it would end with "See the migration guide for release-specific compatibility notes."

What the checks validated

./gradlew :lucene:analysis:phonetic:renderJavadoc succeeds with the paragraph.

D. One CHANGES.txt line for 11.0.0

For example under "Changes in Runtime Behavior", pointing to the migration note. Not run.

Trade-offs

Visible in the changelog; one summary line, not a line per dependabot bump.

Not recommended: revert or pin an older commons-codec

The new codes are upstream bug fixes, and 1.13's RefinedSoundex even throws on non-A-Z letters.

Trade-offs

An application that must keep its old codes until it re-indexes can run its own commons-codec version at its own risk (the history proxy runs ran the main jars with 1.17.2 and 1.13); Lucene should not.

Downstream note

Search servers that bundle their own commons-codec version follow their own pin rather than Lucene's, so the same re-index advice applies when they upgrade it.

How it was found

Non-formal: release-jar differential testing. The finder compared the token streams of 55 analyzers and about 650 filter chains across five Lucene versions and matched every difference to a CHANGES or MIGRATE entry. Only the phonetic differences had none; the release POMs and the commons-codec changelog then showed the cause.

The method in full129 words
  • Method: a harness dumped canonical token streams of 55 analyzers and about 650 SPI filter chains on the stock jars of 9.12.3, 10.0.0, 10.3.2, 10.5.2 and main, each with its own declared dependencies, over five corpora (edge texts, 662 natural texts, 11,288 stemmer-vocabulary texts, 133,872 code points in 4 contexts, CJK with user dictionaries) and 249 generated per-parameter factory configurations.
  • Oracle: the previous release's output plus the release notes; every adjacent-version difference was mapped to a CHANGES or MIGRATE entry. The phonetic differences were the only family with none.
  • Cause: reading the release POMs (commons-codec 1.13, 1.17.2, 1.23.0) and the commons-codec changelog, then a four-encoder JUnit test pinning the previous release's tokens.
  • Tier: non-formal (no model or proof), because the oracle is a previous release, not an exact semantics.

Prior reports, history and checks

Prior reports

No prior report found (searched 2026-10-26, rechecked 2026-11-01): GitHub issues, PRs and commits, the JIRA archive, mailing lists, the web and all open PRs' changed files. The bump PRs are causes, not reports, though dependabot pasted the commons-codec notes into two of them. Precedent: LUCENE-6058 (2014).

Searches4 searches

Best effort: the planned search runs were cut short both times, so coverage is reduced; GitHub Discussions and the commons-codec user list were not searched.

Caveat: dependabot pasted the commons-codec release notes into #14982 and #16035, naming the Daitch-Mokotoff, Metaphone and Cologne fixes; nobody commented or reviewed, and nothing connects them to Lucene tokens or docs. A strict reader could call that "disclosed in the PR".

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene incl. the migrated JIRA archive)2026-10-26a reduced scripted prior-report search (20 GitHub, 3 commit, 3 downstream queries) plus 73 manual GraphQL searches (one failed with HTTP 504); the finder's, the independent reproduction's and the intended-behaviour check's own searchesno report; the bump PRs #14129, #14177, #14581, #14982, #15418, #15693, #16035, #16503, #16878 and the closed #13269 are causes; LUCENE-6058 (#7120) and LUCENE-6059 (#7121) are precedents
Open PRs2026-10-26the changed files of all 408 open apache/lucene PRsnone touches lucene/analysis/phonetic except the 2022 lint PR #681; none adds phonetic text to MIGRATE.md
Mailing lists, JIRA and the web2026-10-2632 list (dev@, java-user@, issues@, commits@), 6 JIRA and 8 web queriesno report; related only: elastic/elasticsearch#74614 (their own plugin, older codec versions) and Solr's own commons-codec bumps
GitHub issues and PRs updated since 2026-10-202026-11-01the titles and bodies of all 184 issues and PRs updated since 2026-10-20 were listed and grepped (phonetic, commons-codec, Metaphone, Cologne, Soundex, Daitch); comments not readonly the known bump #16878

History

RefinedSoundex part since 10.2.0 (#14129 and its backport #14177, January 2025): 10.2.0 to 10.6.0. Metaphone, ColognePhonetic and Daitch-Mokotoff came with later main-only bumps (#14982, #16035) and are now on branch_11x, so they ship first in 11.0.0. Not affected: 9.12.3 to 10.1.0 (commons-codec 1.13). Not fixed upstream as of 2026-11-01.

Versions tested7 rows

Introduced by the commons-codec pin in Lucene's build files, not by a change to Lucene's phonetic code: #14129 "Upgrade commons-codec from 1.13.0 to 1.17.2" (msfroh, merged by msokolov, 2025-01-27, 118afb7054fc) and its branch_10x backport #14177 (279502600af3, 2025-01-29 UTC), first released in 10.2.0; on main the dependabot bumps #14982 (d8cf8f7fb301, 2025-07-24, 1.18.0 -> 1.19.0: Daitch-Mokotoff) and #16035 (43f310fca2c5, 2026-05-06, 1.21.0 -> 1.22.0: Metaphone, ColognePhonetic), both merged by dweiss. None touches phonetic code, phonetic tests or a note about commons-codec: the commons-codec part of each changes only the version catalog, the lock file and license checksums, plus one test module-info line in #14129 and #14177; #14177 also carries the OpenNLP upgrade with its CHANGES line.

Bisect: 17 runs of the stock 10.1.0 or main jars with exactly one commons-codec jar each (15 versions, SHA-1-checked) put every change at the commons-codec version whose changelog names the fix; 10.1.0 + 1.17.2 reproduces stock 10.2.0 exactly, so the codec jar alone explains every transition.

Upstream since the report (read 2026-11-01): branch_11x was cut (#16929) and pins 1.23.0 with no phonetic note; main is now 12.0.0-SNAPSHOT (#16942); branch_10x was decommissioned (#16911); branch_10_6 (10.6.1 in progress) pins 1.17.2. Watch item: on #16878 javanna asked whether a selective backport should go to branch_10_6; a newer commons-codec there would ship the main-only changes in 10.6.x too.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 (commons-codec 1.23.0) affectedminimal test fails, assertions on and off (gradle and standalone); the hunt's test fails 4 of 4
branch_11x d0c0decc6606, upstream main f815e679bf6c readpin 1.23.0, no phonetic note in MIGRATE.md or CHANGES.txt (2026-11-01)
10.6.0 (commons-codec 1.17.2) RefinedSoundex onlyminimal test passes; the hunt's test fails 1 of 4 (Köln -> K378)
10.2.0, 10.2.1, 10.2.2, 10.3.0, 10.3.1, 10.3.2, 10.4.0, 10.5.0, 10.5.1, 10.5.2 RefinedSoundex onlythe hunt's test fails 1 of 4 on each; 10.5.2 passes the minimal test
9.12.3, 10.0.0, 10.1.0 (commons-codec 1.13) not affectedthe hunt's test passes 4 of 4
10.1.0 + commons-codec 1.16.0 / 1.16.1 bisect1.16.0 passes, 1.16.1 fails (RefinedSoundex, CODEC-311)
main + commons-codec 1.18.0 / 1.19.0 / 1.21.0 / 1.22.0 bisectDaitch-Mokotoff changes at 1.19.0; Metaphone and ColognePhonetic at 1.22.0

Independent checks

The finder, cross-model reviews (OpenAI GPT-5.6, reading only), an encoder diff, an independent reproducer with its own words and a cross-version index test, a devil's-advocate verifier and the adjudication agree on the cause; every executing check calls it real, as a documentation defect. This doc's runs repeat the failure on main and the pass on 10.6.0.

Checks and verdicts279 words

Each check ran its own test or probe except the cross-model reviews, which could not run Java; a re-run of the finder's test for them failed (4 of 4).

Each independent check and its verdict
CheckVerdictWhat it found
finder reproduced, docFour-encoder JUnit: fails 4 of 4 on main (assertions on and off), 1 of 4 on 10.2.0 and 10.5.2, passes on 10.1.0.
cross-model intended lens real, mediumOpenAI GPT-5.6, reading only; rated "no stable-code contract" moderate; root cause agreed.
cross-model refute lens not real, low (procedural)It had no executed repro of its own; it agreed on the root cause, found no refuting artifact and called the missing note plausible and relevant to maintainers.
encoder diff real16 words on commons-codec 1.13, 1.17.2 and 1.23.0: Metaphone chair KR -> XR, character KRKT -> XRKT; Cologne Ackermann 04766 -> 0476; RefinedSoundex Müller throws on 1.13.
Independent reproducer real (doc only)Own words (chorus, Pfeffer, board's, Müller): fails 4 of 4 on main, 1 of 4 on 10.5.2 and 10.2.0; cross-version index test: 11 of 14 queries miss; it also measured the corpus rates.
Devil's advocate real (doc only, minor)Built the strongest "intended" case and rejected it; stale-index check: 12 of 72 queries differ for main on a 10.5.2 index, 16 on a 10.1.0 index, 0 in same-version controls.
adjudication confirmed, doc, minor, mediumRe-ran the independent reproduction's test: main fails 4 of 4 with assertions on and off, 10.1.0 passes.
re-runs and minimal repro still reproducesRegistry re-runs (2026-10-26), re-verification on main 645d4fe14a2 (2026-10-29), minimal repro (2026-10-29) and this doc's gradle runs (2026-11-01).

Diagram file

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.

Sources and provenance

Finding record, Lucene and upstream sources, repro files and diagram notes1,453 words

Finding record

LFV-514, snapshot of 2026-11-06

Registry title
Bundled commons-codec upgrades changed phonetic filter tokens (RefinedSoundex in 10.2.0; Metaphone, ColognePhonetic and Daitch-Mokotoff on main), and neither CHANGES.txt nor MIGRATE.md mentions it or asks for re-indexing (documentation defect)
Area
analysis / phonetic (PhoneticFilter and DaitchMokotoffSoundexFilter output from the bundled commons-codec); release notes (lucene/CHANGES.txt, lucene/MIGRATE.md)
Priority
Low, rank 1227: A documentation-only gap, but released (RefinedSoundex since 10.2) and upgraders who keep their index silently get different phonetic matches.
Impact class
minor
Impact note
Only applications with an analysis-phonetic field (PhoneticFilter with RefinedSoundex, Metaphone or ColognePhonetic, or DaitchMokotoffSoundexFilter; default constructors and ordinary words) that keep their index across a Lucene upgrade without re-indexing are hit: from 10.1 to 10.2 or later, RefinedSoundex words with letters outside A-Z gain a new code, and from 10.x to main (the future 11.0) ordinary words change code under the other three encoders (in the independent reproduction's corpora 11.8-16.7% of distinct words under ColognePhonetic, 0.7-2.1% under Metaphone, 0.6% under Daitch-Mokotoff), so in that field old documents and new queries stop matching each other until it is re-indexed; no scale is needed, there is no exception and no index damage, and the encoder code itself is correct.
Summary
You index a field with a phonetic filter, for example PhoneticFilter with ColognePhonetic, upgrade Lucene and keep searching the same index; you expect the same tokens or a CHANGES/MIGRATE note telling you to re-index, as Lucene gave in 2014 for the Beider-Morse change of the commons-codec 1.10 upgrade (LUCENE-6058) and gives on main for the Snowball upgrade (GITHUB#15505). Instead the bundled commons-codec moved from 1.13 (up to 10.1.0) to 1.17.2 (10.2.0 to 10.6.0) and to 1.23.0 (main) with no note anywhere, and its encoder fixes change the tokens: 'Köln' gets the RefinedSoundex code K378 from 10.2.0 (10.1.0 kept only the word), and on main 'chantaient' becomes XNTN instead of KNTN (Metaphone), 'quick' 4 instead of 44 (ColognePhonetic) and "it's" 040000 instead of 034000 (Daitch-Mokotoff). An index written by the older version and searched by the newer one silently misses or changes matches in that field until it is re-indexed (executed: ColognePhonetic with inject=false, the query 'quick' finds no document in a 10.5.2 index searched by main, against 2 in a fresh index).
Introduced
118afb7054fc (main, GITHUB#14129 'Upgrade commons-codec from 1.13.0 to 1.17.2', a hand-written PR) and its branch_10x backport 279502600af3 (GITHUB#14177 'Upgrade opennlp and codec 10x'), which shipped the RefinedSoundex change in 10.2.0; main only: the dependabot bumps d8cf8f7fb301 (GITHUB#14982, 1.18.0 -> 1.19.0: Daitch-Mokotoff) and 43f310fca2c5 (GITHUB#16035, 1.21.0 -> 1.22.0: Metaphone, ColognePhonetic). No Lucene source line introduced the defect: these are dependency pins whose commits touch only build files (version catalog, lock file, license checksums; #14129 also one test module-info line) and add no CHANGES or MIGRATE text [read: history clone, git show --stat] [exec: commit-level proxy runs, the stock 10.1.0 or main jars with exactly the commons-codec jar a given commit pinned; the first commons-codec release with the RefinedSoundex change is 1.16.1 (CODEC-311), with Daitch-Mokotoff 1.19.0 (CODEC-331), with Metaphone and ColognePhonetic 1.22.0 (CODEC-249, CODEC-317)] (2025-01-27 (#14129 on main); 2025-01-29 UTC (#14177 on branch_10x); main-only parts 2025-07-24 (#14982) and 2026-05-06 (#16035))
First affected version
10.2.0 [exec] (RefinedSoundex part only; the first release after #14177). The Metaphone, ColognePhonetic and Daitch-Mokotoff parts are unreleased: main only, the first release would be 11.0.0
Affected versions
10.2.0, 10.2.1, 10.2.2, 10.3.0, 10.3.1, 10.3.2, 10.4.0, 10.5.0, 10.5.1 and 10.5.2 [exec] (RefinedSoundex part only: the finder's JUnit fails 1 of 4 on each, stock release jars with their commons-codec 1.17.2); main ea57360912ed (11.0.0-SNAPSHOT, commons-codec 1.23.0) [exec] all four parts, -ea and -da. Not affected [exec]: 9.12.3, 10.0.0 and 10.1.0 (commons-codec 1.13; every release from 9.0.0 to 10.1.0 pins 1.13 [read]). branch_10x and branch_10_6 [read, 2026-10-26] still pin 1.17.2, and 10.6.0 (released 2026-10-29) carries only the RefinedSoundex part [exec 2026-11-01: the finder's JUnit fails 1 of 4 (RefinedSoundex) on the 10.6.0 release jars], and a later 10.6.x would too unless a newer commons-codec is backported. Measured only for the four encoders above; DoubleMetaphone, Soundex, Caverphone2 and Nysiis were unchanged on a 53-word probe and BeiderMorseFilter (GENERIC/APPROX) on 6,901 words, and the Beider-Morse changes listed in the 1.19.0 and 1.23.0 release notes were not otherwise measured. Executed only on aarch64 Linux with JDK 25 (plain Java string logic); main 645d4fe14a2 [exec 2026-10-29, all four parts, -ea and -da]; branch_10x 657014b3c57 and branch_10_6 4ae946e5a9b (10.6.0 RC1) still pin commons-codec 1.17.2 [read 2026-10-29]
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code (its low-confidence “not real” is procedural), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
non-formal
Upstream and prior reports
none found (2026-10-26, plus the finder's, the independent reproduction's and the intended-behaviour check's searches): a scripted prior-report search over apache/lucene GitHub issues, PRs and commits (a reduced run of 20 GitHub, 3 commit and 3 downstream queries), the dev@, java-user@, issues@ and commits@ lists, JIRA and the web (32 list, 6 JIRA and 8 web queries), 73 manual GitHub GraphQL searches (one failed with HTTP 504) and a scan of all 408 open PRs by changed file (none touches lucene/analysis/phonetic except the 2022 lint PR #681; none adds phonetic text to MIGRATE.md). Causes, not reports: #14129 and its backport #14177 (discussion only about JPMS packaging, CODEC-324), #14982 and #16035 (the dependabot bodies quote the commons-codec release notes for CODEC-331, CODEC-249 and CODEC-317; no comments or reviews), and #14581, #15418, #15693, #16503, #16878; #13269 (closed unmerged, about resource loading). Precedents: LUCENE-6058 (#7120) and LUCENE-6059 (#7121), 2014. Related, not this bug: elastic/elasticsearch#74614 (open since 2021; an Elasticsearch maintainer notes that newer commons-codec Cologne fixes would require reindexing, for Elasticsearch's own plugin and older codec versions), Solr's own commons-codec 1.22.x bumps (apache/solr#4566 and siblings, no phonetic note), and the upstream fixes CODEC-311, CODEC-249, CODEC-317 and CODEC-331 (no mention of downstream index compatibility). Not fixed (recheck 2026-10-29): upstream main 645d4fe14a2, branch_10x 657014b3c579 and branch_10_6 4ae946e5a9b (10.6.0 RC1) have no phonetic or commons-codec note. Coverage limits: GitHub Discussions and the commons-codec user list were not searched.
Lucene commit tested
main 645d4fe14a20: the minimal test through gradle with assertions on and off, before and after the recommended migration note, in a scratch worktree, plus the analysis-phonetic suite, the migration-guide rendering and the module javadoc (2026-11-01); the minimal test with a random seed (5 of 5 fail); the 10.5.2 and 10.6.0 release jars (the minimal test passes) and the hunt's four-encoder test on 10.6.0 (1 of 4 fails, RefinedSoundex). Earlier: the hunt's test on every release from 9.12.3 to 10.5.2 and 17 proxy runs of the 10.1.0 or main jars with exactly one commons-codec jar each (2026-10-26).

Lucene and upstream sources

Finding record
  • lucene/analysis/phonetic/src/java/org/apache/lucene/analysis/phonetic/PhoneticFilter.java:27 (at ea57360912ed) (class javadoc 'Create tokens for phonetic matches', with an @see link to Apache Commons Codec and no promise of stable codes), :73 (encoder.encode), :75-76 ('catch (Exception _) {} // just use the direct text', why 10.1.0 emits no RefinedSoundex code for non-A-Z words)
  • lucene/analysis/phonetic/src/java/org/apache/lucene/analysis/phonetic/PhoneticFilterFactory.java:42 (at ea57360912ed) ('Commons Codec' encoders), :106 (inject default true)
  • lucene/analysis/phonetic/src/java/org/apache/lucene/analysis/phonetic/DaitchMokotoffSoundexFilter.java:31 (at ea57360912ed) (@lucene.experimental), :79 (encoder.soundex)
  • Lucene main ea57360912ed gradle/libs.versions.toml:8 (commons-codec = "1.23.0"), :68
  • lucene/CHANGES.txt:11203-11210 (at ea57360912ed) (LUCENE-6059 and LUCENE-6058, the only commons-codec entries: 'any index using it will need to be rebuilt'), :1993 (GITHUB#14130 OpenNLP upgrade announced in 10.2.0; no entry for the commons-codec upgrade of the same backport)
  • lucene/MIGRATE.md:136-154 (at ea57360912ed) ('Relaxed Index Upgrade Policy', :152 'Simply open the index with the new version'), :196-202 ('Snowball dependency upgrade (GITHUB#15505)', re-index advice), :757-764 (10.6 Thai analysis note); the only 'phonetic' text is the 9.0 module-rename table at :1170
  • Lucene main ea57360912ed .github/dependabot.yml:13, :35, :53 (labels [dependencies, skip-changelog]); CONTRIBUTING.md:107 ('You may want to add a CHANGES entry')
The finding in 30 seconds
  • lucene/analysis/phonetic/src/java/org/apache/lucene/analysis/phonetic/PhoneticFilter.java:26-32, :70-78, :80-96 (at 645d4fe14a20)
  • lucene/analysis/phonetic/src/java/org/apache/lucene/analysis/phonetic/PhoneticFilterFactory.java:40-42, :52-53, :106 (at 645d4fe14a20)
  • lucene/analysis/phonetic/src/java/org/apache/lucene/analysis/phonetic/DaitchMokotoffSoundexFilter.java:28-31, :38, :79 (at 645d4fe14a20)
  • gradle/libs.versions.toml:8 (at 645d4fe14a20)
  • lucene/analysis/phonetic/src/test/org/apache/lucene/analysis/phonetic/TestPhoneticFilter.java:36-44; TestDaitchMokotoffSoundexFilter.java:30-45 (at 645d4fe14a20)
Who is affected
  • lucene/MIGRATE.md:136-154 (at 645d4fe14a20)
Why it looks unintended
  • lucene/CHANGES.txt:11236-11243, :2026-2027, :161 (at 645d4fe14a20)
  • lucene/MIGRATE.md:136-154, :196-202, :757-764, :867-873, :1170 (at 645d4fe14a20)
  • .github/dependabot.yml:3, :13 (github-actions), :28, :35 (uv), :45, :53 (gradle); CONTRIBUTING.md:107; lucene/analysis/phonetic/src/java/overview.html:28-31 (at 645d4fe14a20)
  • lucene/analysis/icu/src/java/overview.html:343-350 (at 645d4fe14a20)
  • apache/lucene#7120 (LUCENE-6058) comment by Michael McCandless (read-only gh issue view, 2026-11-01)
  • a Lucene main checkout: git show --stat 260c5f25723 (Dawid Weiss, 2026-07-06, "Turn off verify-changelog-and-set-milestone. #16366": .yml renamed to .disabled)
History
  • a Lucene clone: git show releases/lucene/{10.1.0,10.2.0,10.6.0}:...versions.toml (1.13, 1.17.2, 1.17.2)

Other statements trace to the hunt's working files (models, logs, reviews and briefs), which are not published.

Repro files

About the diagrams

Built 2026-11-06 from the hunt's record of this finding and its doc file. Lucene main under test in this hunt: ea57360912ed. Every listed finding was re-run on Lucene main e1faeba08bde on 2026-11-02.