All findings · Lucene finding · LFV-439 · dictionarycompound-factory-removed-key-warning-inverted

DictionaryCompoundWordTokenFilterFactory on main warns that onlyLongestMatch "has been removed" when the key is not set

On main, the dictionaryCompoundWord filter factory logs "onlyLongestMatch has been removed" for every configuration that never sets that key, the default included, but not for onlyLongestMatch="false". It checks the key's value (default true), not its presence; tokens are unaffected, and no release has this code.

Summary

Priority
Low, rank 1068The main-only DictionaryCompoundWordTokenFilterFactory logs a removed-key warning when the key is absent and stays silent when it is set to false: a cosmetic factory-parameter slip.
Impact
minor
Since
main only, since #14356 (ebc63504bd6, merged 2025-05-10)introduced 2025-05-10 · about 1 year
Affected
main (11.0.0-SNAPSHOT); no release
Status
documented
Found by
non-formal: code reading, then an executed JUnit test

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

The factory checks the removed key's value, whose default is true, instead of checking whether you set the key.

Background: the compound-word filter and its two settings

  1. DictionaryCompoundWordTokenFilter splits compound words with a word list: with {donau, dampf, dampfschiff, schiff}, the word donaudampfschiff also yields donau and dampfschiff.
  2. Its factory, SPI name dictionaryCompoundWord, builds it from string settings, as CustomAnalyzer, TokenFilterFactory.forName and Solr-style schemas do.
  3. 10.2.0 added onlyLongestMatchIgnoreSubwords (default true) to replace the older onlyLongestMatch. On main the old key does nothing any more; the factory still accepts it and logs a warning that it was removed.
Diagram of the bug: factory settings for the dictionaryCompoundWord filter go through getBoolean(args, "onlyLongestMatch", true) and then if (onlyLongestMatch) LOG.warning(...). A missing key and onlyLongestMatch="true" both become true and log "onlyLongestMatch has been removed and replaced by onlyLongestMatchIgnoreSubwords", including the default and the onlyLongestMatchIgnoreSubwords-only configurations; onlyLongestMatch="false" becomes false and logs nothing. The tokens are the same either way. The fix warns when the key is present.
Which configurations of the factory get the "has been removed" warning on main: the missing key reads as true, so the default and the migrated settings are warned, and onlyLongestMatch="false" stays silent. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. On main, build the compound-word filter factory with only a dictionary, new DictionaryCompoundWordTokenFilterFactory(new HashMap<>(Map.of("dictionary", "d.txt"))), as Lucene's own factory test does, or add the replacement setting onlyLongestMatchIgnoreSubwords="true".
  2. You expect no message about onlyLongestMatch: you never set it. 10.5.2 and 10.6.0 log nothing, and 10.5.2 gives the same tokens as main: with the dictionary {donau, dampf, dampfschiff, schiff}, donaudampfschiff gives [donaudampfschiff, donau, dampfschiff] on both.
  3. Instead each such construction logs WARNING: onlyLongestMatch has been removed and replaced by onlyLongestMatchIgnoreSubwords. Of 7 configurations tried, 5 are warned; the 2 silent ones set the removed key to false: within the factory settings, only passing the ignored key with a value other than "true" (ignoring case) silences it.
  4. Why: the factory reads the removed key with getBoolean(args, "onlyLongestMatch", true) and warns if (onlyLongestMatch). A missing key returns the default true, so it looks set. The value feeds nothing else, so only the log is wrong.

Who is affected

Applications on main (unreleased 11.0) that build this opt-in filter through its factory: CustomAnalyzer, TokenFilterFactory.forName or a Solr-style schema. Every construction without the key logs the warning, so migrated users can silence it, within the factory settings, only by adding the removed key, while a stale onlyLongestMatch="false" goes unflagged. Tokens, indexes and queries are unchanged.

Mechanism, terms and numbers506 words

On main, DictionaryCompoundWordTokenFilterFactory (SPI name dictionaryCompoundWord) still reads the removed onlyLongestMatch key, now only to decide whether to log: boolean onlyLongestMatch = getBoolean(args, "onlyLongestMatch", true); if (onlyLongestMatch) LOG.warning("onlyLongestMatch has been removed and replaced by onlyLongestMatchIgnoreSubwords");. Because getBoolean returns the default true for a missing key, every construction that does not mention the key logs the warning: the plain dictionary=... configuration that Lucene's own factory test uses, and configurations already migrated to onlyLongestMatchIgnoreSubwords. A configuration that names the now-ignored key with false gets no warning, and the class javadoc example still shows onlyLongestMatch="true". Only the log is affected: create() passes only onlyLongestMatchIgnoreSubwords to the filter. The lines came with #14356 (ebc63504bd6, merged 2025-05-10, an 11.0 API removal) and are in no release; 9.12.3 through 10.6.0 log nothing. Warning when the key is present, whatever its value, fixes it.

Terms

  • AbstractAnalysisFactory.getBoolean(args, name, defaultVal): removes the setting from the map and returns its value, or defaultVal when it is absent: String s = args.remove(name); return s == null ? defaultVal : Boolean.parseBoolean(s); (lucene/core/src/java/org/apache/lucene/analysis/AbstractAnalysisFactory.java:213-216 at 645d4fe14a20).
  • onlyLongestMatchIgnoreSubwords: the replacement setting, read with default true (DictionaryCompoundWordTokenFilterFactory.java:74). The filter javadoc says it supersedes onlyLongestMatch (DictionaryCompoundWordTokenFilter.java:54-56).

The bug

  1. The constructor reads the removed key and warns on its value (DictionaryCompoundWordTokenFilterFactory.java:69-72):
boolean onlyLongestMatch = getBoolean(args, "onlyLongestMatch", true);
if (onlyLongestMatch)
  LOG.warning(
      "onlyLongestMatch has been removed and replaced by onlyLongestMatchIgnoreSubwords");
  1. A missing key and "true" (in any letter case) give true. "false" gives false, and so does any other present value such as "yes" or "", because Boolean.parseBoolean returns true only for "true" (executed: a six-value probe on main). The default true goes back to the first factory version (2008), when the key still had a meaning.
  2. The value is used for nothing else: create() passes only the dictionary, the size limits and onlyLongestMatchIgnoreSubwords to the filter (:97-103), whose 6-argument constructor hands false to the base class (DictionaryCompoundWordTokenFilter.java:58-66).
  3. So the warning fires for every configuration that never set the key, and is silent for the one that set it to false. The class javadoc example still writes onlyLongestMatch="true" (:35-36), so following it also gives the warning.

The numbers

Dictionary {donau, dampf, dampfschiff, schiff}, word donaudampfschiff, 7 configurations through the SPI name on main 645d4fe14a20:

settings besides dictionarywarning on maintokens on mainsame tokens on 10.5.2?
none (the default)yes[donaudampfschiff, donau, dampfschiff]yes
onlyLongestMatchIgnoreSubwords=trueyes[donaudampfschiff, donau, dampfschiff]yes
onlyLongestMatchIgnoreSubwords=falseyes[donaudampfschiff, donau, dampf, dampfschiff, schiff]no (10.5.2 has no dampf)
onlyLongestMatch=trueyes[donaudampfschiff, donau, dampfschiff]yes
onlyLongestMatch=true, onlyLongestMatchIgnoreSubwords=falseyesthe 5 tokens with dampfno (10.5.2 has no dampf)
onlyLongestMatch=falseno[donaudampfschiff, donau, dampfschiff]yes
onlyLongestMatch=false, onlyLongestMatchIgnoreSubwords=falsenothe 5 tokens with dampfyes

The tokens depend only on onlyLongestMatchIgnoreSubwords, never on the warning. 10.5.2 and 10.6.0 log nothing in any configuration. 10.1.0 logs nothing either, and rejects the four configurations that set onlyLongestMatchIgnoreSubwords (added in 10.2.0) with "Unknown parameters".

Exposure and likelihood174 words
  • Who is hit. Applications on main (11.0.0-SNAPSHOT) that build the dictionaryCompoundWord factory: its public Map constructor, TokenFilterFactory.forName, CustomAnalyzer, or an analyzer definition loaded by SPI name. Solr schemas use this factory (solr.DictionaryCompoundWordTokenFilterFactory). In the Elasticsearch and OpenSearch code inspected on 2026-10-26 (a read-only code search), the filter is built directly, not through this factory; other versions, plugins and custom applications were not surveyed. No default analysis chain builds it.
  • What it takes. Nothing unusual: the default configuration, exactly what Lucene's own TestDictionaryCompoundWordTokenFilterFactory.testDecompounding builds, or the replacement setting the warning itself recommends.
  • What happens. One constant WARNING per factory construction, so once per analyzer build, never per document or query. Within the factory settings, a migrated configuration can silence it only by adding the removed key with a value other than "true" (ignoring case), conventionally false (otherwise only the logging configuration hides it); a configuration that still sets onlyLongestMatch="false", an ignored key, is not told.
  • Scale. Minor: log diagnostics only, on unreleased code. It would ship in 11.0.0 unless fixed.

Minimal repro

One JUnit test on the public API: it builds the factory with only dictionary=d.txt and records WARNING messages on the factory's java.util.logging logger. Deterministic, single thread, no randomness; the test method takes about 30 ms.

TestDictionaryCompoundFactoryRemovedKeyWarning.java (JUnit, one test)58 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.compound;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestDictionaryCompoundFactoryRemovedKeyWarning extends LuceneTestCase {

  public void testNoWarningWhenRemovedKeyIsAbsent() {
    List<String> logged = new ArrayList<>();
    Handler handler =
        new Handler() {
          @Override
          public void publish(LogRecord r) {
            if (r.getLevel().intValue() >= Level.WARNING.intValue()) {
              logged.add(r.getMessage());
            }
          }

          @Override
          public void flush() {}

          @Override
          public void close() {}
        };
    Logger log = Logger.getLogger(DictionaryCompoundWordTokenFilterFactory.class.getName());
    log.addHandler(handler);
    try {
      new DictionaryCompoundWordTokenFilterFactory(new HashMap<>(Map.of("dictionary", "d.txt")));
    } finally {
      log.removeHandler(handler);
    }
    // expected: no WARNING, since onlyLongestMatch was never passed
    assertEquals(List.of(), logged);
  }
}
Command
cp TestDictionaryCompoundFactoryRemovedKeyWarning.java $LUCENE/lucene/analysis/common/src/test/org/apache/lucene/analysis/compound/
cd $LUCENE && ./gradlew :lucene:analysis:common:test --tests org.apache.lucene.analysis.compound.TestDictionaryCompoundFactoryRemovedKeyWarning -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off: -Ptests.asserts=false (same result)
Expected
No WARNING is logged: the configuration never sets onlyLongestMatch (10.5.2 and 10.6.0 log nothing).
Observed
AssertionError: expected:<[]> but was:<[onlyLongestMatch has been removed and replaced by onlyLongestMatchIgnoreSubwords]>, with assertions on and off.

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

More files, environment, full output225 words

The dictionary file is read only in inform(), so d.txt need not exist and no resource loader is needed. The map is a mutable HashMap because the factory removes the settings it consumes. Pass -Ptests.asserts explicitly: on main, gradle picks assertions on or off from the seed, and seed DEADBEEF picks off. Seeds DEADBEEF, 5EED1234, C0FFEE01 and 1234ABCD all fail the same way. On 10.5.2 and 10.6.0 the test passes.

As the regression test: the same test, for example as a new method in TestDictionaryCompoundWordTokenFilterFactory. It covers the absent-key half only; that the fix still warns once when onlyLongestMatch is set (true or false) is shown by the executed matrix with the fix, not by this test.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT); 10.6.0 and earlier releases do not have the code
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64
Command (environment)
cp TestDictionaryCompoundFactoryRemovedKeyWarning.java $LUCENE/lucene/analysis/common/src/test/org/apache/lucene/analysis/compound/
cd $LUCENE && ./gradlew :lucene:analysis:common:test --tests org.apache.lucene.analysis.compound.TestDictionaryCompoundFactoryRemovedKeyWarning -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off: -Ptests.asserts=false (same result)

Observed output

# gradle, main 645d4fe14a20, seed DEADBEEF (the same with assertions on and off)
TestDictionaryCompoundFactoryRemovedKeyWarning > testNoWarningWhenRemovedKeyIsAbsent FAILED
    java.lang.AssertionError: expected:<[]> but was:<[onlyLongestMatch has been removed and replaced by onlyLongestMatchIgnoreSubwords]>
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:B2ABB632A32114A6]:0)
        ...
        at org.apache.lucene.analysis.compound.TestDictionaryCompoundFactoryRemovedKeyWarning.testNoWarningWhenRemovedKeyIsAbsent(TestDictionaryCompoundFactoryRemovedKeyWarning.java:56)

# the factory's own log line in the test output
  2> ... org.apache.lucene.analysis.compound.DictionaryCompoundWordTokenFilterFactory <init>
  2> WARNING: onlyLongestMatch has been removed and replaced by onlyLongestMatchIgnoreSubwords

Why it looks unintended

  • The message says the key "has been removed", yet configurations that never used it get it.
  • For deprecated factory keys a warning is the only mechanism (Uwe Schindler, #14311); Uwe Schindler's #15900 sketch warns "if you set" the old key.
  • #14356's author asked on this line "Does this check make sense?"; nobody answered. No doc or test sets it.
The full argument233 words
  • The message. "onlyLongestMatch has been removed and replaced by onlyLongestMatchIgnoreSubwords" is a statement about a key the configuration used. Logging it because a key is missing contradicts its own text.
  • The project's stated mechanism for deprecated factory keys. On #14311, the PR that introduced the replacement setting, Uwe Schindler (uschindler) wrote on 2025-03-05: "We have no mechanism for deprecating those map keys of the factory. We can only log a warning, but have no standard way for this yet." On #15900 (2026-04-03) Uwe Schindler sketched the same pattern for another factory: "if you set prefixLength it uses the old buggy behaviour and logs a warning that it is deprecated". The first names the mechanism without saying when it should fire; the second ties the warning to the user setting the key. Both are context, not a rule for this factory.
  • The author's own question. The only review comment of #14356's author, renatoh, sits on the if (onlyLongestMatch) hunk: "@rmuir Does this check make sense?" (2025-03-14). Nobody answered; the PR merged on 2025-05-10.
  • No documentation or test sets it. No javadoc, CHANGES.txt entry, MIGRATE.md entry or test says when the factory logs. MIGRATE.md:648-668 (GITHUB#14356) covers only the removed filter constructor. TestDictionaryCompoundWordTokenFilterFactory has only testDecompounding and testBogusArguments.
  • Lucene's canonical use is warned. testDecompounding builds the factory with only dictionary, as the minimal test does, so Lucene's own factory test takes the warning path.
The devil's-advocate check184 words

The strongest case for "intended" (a read-only reviewer from another model family, OpenAI GPT-5.6, rated it moderate): the check flags everyone whose legacy onlyLongestMatch is effectively true, including by default, because that is the behaviour that went away, and spares an explicit false, because that configuration lost nothing.

Why it does not hold up (executed): with onlyLongestMatchIgnoreSubwords at its default true, onlyLongestMatch already had no effect on 10.5.2, because the filter ORs the two flags. So the default and the migrated configurations give the same tokens on 10.5.2 and main and never depended on the key, yet they are warned. Against 10.1.0, where onlyLongestMatch=false did change tokens, main does not warn that configuration. On neither upgrade path does the value-based condition pick the configurations whose output changed, the check ignores onlyLongestMatchIgnoreSubwords read on the next line, and nothing records such a policy as a decision. This weakens the intended reading but does not rule it out: the cross-model fact check of the brief (OpenAI GPT-5.6, read-only) again rated it moderate, a plausible migration-warning policy with an inconsistent condition. The maintainers decide.

Suggested fix

Recommended A. Warn when the removed key is present, whatever its value; fix the javadoc example

Warn only when the key is present, if (args.remove("onlyLongestMatch") != null) LOG.warning(...), and change the javadoc example to onlyLongestMatchIgnoreSubwords. The repro then passes with assertions on and off, and the compound tests and check pass. Add a MIGRATE line too: onlyLongestMatchIgnoreSubwords="false" alone loses its accidental warning, though its tokens changed in 11.0.

@@ -33,7 +33,8 @@
  *     &lt;filter class="solr.DictionaryCompoundWordTokenFilterFactory" dictionary="dictionary.txt"
- *         minWordSize="5" minSubwordSize="2" maxSubwordSize="15" onlyLongestMatch="true"/&gt;
+ *         minWordSize="5" minSubwordSize="2" maxSubwordSize="15"
+ *         onlyLongestMatchIgnoreSubwords="true"/&gt;
@@ -66,10 +67,11 @@
-    boolean onlyLongestMatch = getBoolean(args, "onlyLongestMatch", true);
-    if (onlyLongestMatch)
+    // removed key: still accepted, so old configurations load, but ignored; warn only if set
+    if (args.remove("onlyLongestMatch") != null) {
       LOG.warning(
           "onlyLongestMatch has been removed and replaced by onlyLongestMatchIgnoreSubwords");
+    }
All options and trade-offs3 options · 559 words

One option was run through gradle in a scratch worktree of 645d4fe14a20 and is recommended, with a documentation add-on; the alternative was not run.

A. Warn when the removed key is present, whatever its value; fix the javadoc example (validated by the checks) (recommended)

In the constructor (DictionaryCompoundWordTokenFilterFactory.java:69-72), replace the value check with a presence check that still consumes the key, so old configurations keep loading without "Unknown parameters". In the class javadoc (:36), replace onlyLongestMatch="true" with onlyLongestMatchIgnoreSubwords="true". The snippet is an excerpt of the full diff with context lines trimmed; the full diff applies with git apply at 645d4fe14a20. It is an equivalent one-call form of the adjudication's fix, which tests args.containsKey("onlyLongestMatch") and then calls args.remove(...).

@@ -33,7 +33,8 @@
  *     &lt;filter class="solr.DictionaryCompoundWordTokenFilterFactory" dictionary="dictionary.txt"
- *         minWordSize="5" minSubwordSize="2" maxSubwordSize="15" onlyLongestMatch="true"/&gt;
+ *         minWordSize="5" minSubwordSize="2" maxSubwordSize="15"
+ *         onlyLongestMatchIgnoreSubwords="true"/&gt;
@@ -66,10 +67,11 @@
-    boolean onlyLongestMatch = getBoolean(args, "onlyLongestMatch", true);
-    if (onlyLongestMatch)
+    // removed key: still accepted, so old configurations load, but ignored; warn only if set
+    if (args.remove("onlyLongestMatch") != null) {
       LOG.warning(
           "onlyLongestMatch has been removed and replaced by onlyLongestMatchIgnoreSubwords");
+    }

Trade-offs

  • Effect, executed on the 7-configuration matrix: the warning is logged for exactly the 4 configurations that set onlyLongestMatch (true or false) and for none of the 3 that do not; no token changes anywhere.
  • onlyLongestMatchIgnoreSubwords="false" without the old key loses the warning it gets today by accident. Its tokens did change from 10.5.2 to main (dampf is now emitted for donaudampfschiff), because the 10.5.2 factory passed the default onlyLongestMatch=true through the constructor that #14356 removed. That change follows from the intended removal and is not this bug, but MIGRATE.md does not mention it: pair this fix with the MIGRATE line below.
  • Diagnostics for unusual values (executed, six-value probe on main and with this change): today a present value other than "true", such as "yes" or "", is consumed with no message; with the fix it warns like any other use of the key. Probably wanted, but a behaviour change to mention.

What the checks validated

  • Gradle in a scratch worktree of 645d4fe14a20 (2026-11-01): stock, the minimal test fails with assertions on and off; with the fix it passes both ways.
  • With the fix: the compound package (TestCompoundWordTokenFilter, TestDictionaryCompoundWordTokenFilterFactory, TestHyphenationCompoundWordTokenFilterFactory, TestPatternParser) the repro and the nightly TestFactories (every factory with random arguments): 6 classes, 27 tests, 0 failures with assertions on; :lucene:analysis:common:check -x test passes (google-java-format, ecjLint, forbiddenApis).
  • The same change shadowed into the stock main jars: the minimal test passes with -ea and -da, and the matrix shows the effect above.

A2. Add-on: a MIGRATE.md line for the factory

Under the existing GITHUB#14356 entry, note that the factory setting onlyLongestMatch is ignored (a warning is logged when it is set), and that with onlyLongestMatchIgnoreSubwords="false" the filter now emits every dictionary match. Optionally fix the 10.2.0 CHANGES.txt entry, which names the option onlyLongestMatchNoSubwords while the factory key is onlyLongestMatchIgnoreSubwords.

Trade-offs

Documentation only. Not run.

B. Also warn for onlyLongestMatchIgnoreSubwords="false" without the old key

Keep a notice, with its own message, for the one configuration whose output changed in 11.0, or leave the exact condition to maintainers.

Trade-offs

A design choice beyond this bug; it adds a second warning that later releases would have to retire. Not run.

Downstream note

In the Elasticsearch and OpenSearch code inspected on 2026-10-26, the filter is built directly, not through this factory; Solr-style schemas that use DictionaryCompoundWordTokenFilterFactory would log the warning on a Lucene build with this code.

How it was found

Non-formal: code reading. While comparing every analysis factory's parameter reads with the arguments it passes to its filter, a finder saw that this factory reads the removed key only to decide whether to warn, with default true. A public-API JUnit test with log capture on the stock main jar confirmed it.

The method in full111 words

Method: non-formal (code reading, then executed tests). The hunt unit checked the analysis SPI chain: for each in-scope factory, a static inventory of its get/getBoolean reads was compared with the arguments the factory passes to its filter constructor. In this factory the onlyLongestMatch value reaches no constructor and only guards the warning. A public-API JUnit with java.util.logging capture reproduced it on the stock main jar, and a release matrix dated it to #14356. The finder's first framing (an explicit false being "silently ignored") was rejected as intended by the verifiers and the adjudication, who re-scoped the bug to the warning condition; it was then cut to the one-test repro.

Prior reports, history and checks

Prior reports

No prior report found in GitHub issues, PRs and commits (incl. the JIRA archive), all 410 open PRs, the Lucene and Solr lists, downstream trackers or the web (2026-10-26, re-checked 2026-11-01). Near-miss: #14356's author asked on this line whether the check makes sense, unanswered. Not fixed upstream as of 2026-10-30.

Searches3 searches

Closest items, none a report of this bug: #14356 (introduces the warning; the author's unanswered review question "@rmuir Does this check make sense?", 2025-03-14, names no symptom and is treated as not a report); #14311 and #14278 (introduced onlyLongestMatchIgnoreSubwords and its default; the "We can only log a warning" comment); #16756 (the MIGRATE entry for #14356, constructor only); elastic/elasticsearch#163528 (open, 2026-10-27: Elasticsearch's own factory adapting to the constructor removal; it never mentions Lucene's factory or this warning); #12478 and #12485 (2023, the only open PRs touching the factory or its test, older than the warning); older filter-semantics items #4096 (LUCENE-3022), #12484, #4112, #4490, #7867, #9231.

Limits: GitHub search is phrase-like; list search matches words, not code; JIRA predates the warning; private reports are not visible, and searches cannot prove absence.

What was searched, when, and what turned up
WhereWhenHowFound
apache/lucene issues, PRs (incl. the migrated JIRA issues) and commits2026-10-26 and 2026-11-01scripted prior-report searches (51 then 55 queries) and manual GitHub searches (26 then 53): class and SPI names, onlyLongestMatch, the exact warning text, removed or deprecated factory keysno report; the exact warning text only in the #14356 mail mirror
Open PRs and the factory's history2026-11-01changed files of all 410 open apache/lucene PRs; the factory on upstream main, branch_11x, branch_10_6 and 10.6.0; #14356 comments, reviews and timelineno open PR changes the warning; #14356's unanswered review question (near-miss)
Mailing lists, JIRA, downstream, web2026-10-26 and 2026-11-01dev@, java-user@, issues@ and commits@ (Lucene); dev@, users@ and issues@ (Solr); LUCENE and SOLR JIRA; Elasticsearch, OpenSearch and Solr trackers; 6 then 10 web queriescontext only (filter-semantics threads, Elasticsearch adapting to the constructor removal)

History

Introduced on main by #14356 (ebc63504bd6, merged 2025-05-10), an 11.0 API removal; no release contains it. 9.12.3 through 10.6.0 log nothing; it would ship in 11.0.0.

Versions tested7 rows

Introduced by ebc63504bd627ff15974efb86c7883ecb489d10f "removing constructor with deprecated attribute 'onlyLongestMatch (#14356)" (author renatoh, merged by rmuir on 2025-05-10, base main). It turned the existing read onlyLongestMatch = getBoolean(args, "onlyLongestMatch", true); into the local read plus if (onlyLongestMatch) LOG.warning(...), and removed the filter constructor through which create() had passed the value on. git blame of the four lines and git log -S on the warning text return only this commit. The factory, filter and base class of ebc63504bd6 shadowed over the main jars fail the test; those of its parent 38f81b84ba4 pass.

git tag --contains ebc63504bd6 lists no tag, and the factory at every 9.x and 10.x release tag, 10.6.0 included, and on branch_10_6 has no LOG.warning. branch_11x (d0c0decc660) and upstream main 2f424fd428d (2026-10-30) have the same lines.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedminimal test fails with assertions on and off (through Gradle and standalone); passes with fix A
main ea57360912ed, a9779ef3c193 affectedthe history test fails 2 of 3 (absent key warned; true warned, false silent)
ebc63504bd6 (#14356), shadowed over main introducing commitminimal test fails
38f81b84ba4 (parent), shadowed over main not affectedminimal test passes
10.6.0 not affectedminimal test passes; no warning in any configuration
10.5.2, 10.2.2, 10.2.1, 10.2.0, 10.1.0, 9.12.3 not affectedminimal test passes (release jars)
10.0.0, 10.3.0, 10.4.0, 10.5.0 not affectedthe history test passes 3 of 3

Independent checks

Confirmed with high confidence, impact minor: the finder, an independent reproducer, an intended-behaviour devil's advocate, an adjudication and a tie-breaker reproduced it. Read-only refute reviews from another model family, unable to run code, leaned "not real" on procedure but agreed with the root cause.

Checks and verdicts303 words

Everyone agrees on the root cause. The adjudication re-scoped the finder's claim to the warning condition, and the tie-breaker narrowed the framing: not "inverted", and the presence check alone hides the one configuration whose tokens changed in 11.0.

Each independent check and its verdict
CheckVerdictWhat it found
finder candidate, later re-scoped3-test JUnit fails 3 of 3 on main with assertions on and off; its third test (an explicit false must emit every match) was rejected as intended.
cross-model reviews before the candidate (OpenAI GPT-5.6, read-only) refute lens not real (low, procedural); works-as-designed lens real (medium)Could not run code; both agreed on the root cause. Separate re-runs failed 3 of 3.
independent reproducer real, narrowerIts own JUnit fails 2 of 3 on main with assertions on and off; 10.1.0 and 9.12.3 pass; produced the 7-configuration matrix.
cross-model adversary, refute lens (OpenAI GPT-5.6, read-only) not real, low, proceduralRoot cause and inputs agreed; its notes say the intended reading does not justify warning for an absent key while staying silent for an explicit false.
intended-behaviour devil's advocate overall uncertain; split: warning condition realOverall verdict "uncertain", split into two parts: the explicit-false token behaviour is intended (10.2.0 replacement setting); the warning condition is real, low severity.
adjudication confirmed, highRe-ran the reproducer's JUnit with assertions on and off (2 of 3 fail each); re-scoped to the main-only warning condition; impact minor.
re-check and tie-breaker on 645d4fe14a20 (2026-11-01) real, high for the core defectMinimal test fails with -ea and -da, passes on 10.5.2 and 10.6.0 and with the presence fix; a second read-only refute review (OpenAI GPT-5.6) said not real, low, procedural; the tie-breaker tested the strongest intended reading by execution and rejected it. Its framing corrections are adopted here.

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,185 words

Finding record

LFV-439, snapshot of 2026-11-06

Registry title
DictionaryCompoundWordTokenFilterFactory (main only) warns 'onlyLongestMatch has been removed' when the key is absent and stays silent when onlyLongestMatch="false" is passed
Area
analysis-common / compound word factory (DictionaryCompoundWordTokenFilterFactory parameter handling)
Priority
Low, rank 1068: The main-only DictionaryCompoundWordTokenFilterFactory logs a removed-key warning when the key is absent and stays silent when it is set to false: a cosmetic factory-parameter slip.
Impact class
minor
Impact note
Only applications on unreleased main (11.0.0-SNAPSHOT) that build the dictionaryCompoundWord factory (its Map constructor, TokenFilterFactory.forName, CustomAnalyzer, or a Solr-style schema) are affected: every construction that leaves onlyLongestMatch out (the factory default, including configurations already migrated to onlyLongestMatchIgnoreSubwords) or follows the class-javadoc example with onlyLongestMatch="true" logs one constant WARNING that the key 'has been removed', while the one configuration whose value is now ignored, onlyLongestMatch="false", gets no warning; tokens, indexes and queries are unchanged, and Elasticsearch and OpenSearch build the filter directly and never run this constructor.
Summary
You build DictionaryCompoundWordTokenFilterFactory on main without the onlyLongestMatch key, for example with only onlyLongestMatchIgnoreSubwords, and expect no message about a key you never used; instead every construction logs the WARNING 'onlyLongestMatch has been removed and replaced by onlyLongestMatchIgnoreSubwords'. A configuration that does pass onlyLongestMatch="false", the one setting whose meaning is now ignored, gets no warning, because the factory reads the removed key with default true and warns when the value is true (getBoolean(args, "onlyLongestMatch", true); if (onlyLongestMatch) LOG.warning(...)), not when the key is present. Only the log is wrong: the tokens are the same with or without the warning.
Introduced
ebc63504bd627ff15974efb86c7883ecb489d10f, apache/lucene#14356 'removing constructor with deprecated attribute 'onlyLongestMatch' (author renatoh, merged by rmuir, main only) [read: git blame of DictionaryCompoundWordTokenFilterFactory.java:69-72 and git log -S on the warning text give only this commit; it turned the existing read getBoolean(args, "onlyLongestMatch", true), default true since the first factory version 7ad6fc5bb06 (2008), into a value used only for 'if (onlyLongestMatch) LOG.warning(...)'; exec: the factory and filter of ebc63504bd6 shadowed over the main jars fail 2 of 3, those of its parent 38f81b84ba4 pass 3 of 3] (2025-05-10 (merged; the warning line was already in the PR on 2025-03-14, the date of the author's review comment on it))
First affected version
unreleased (main / 11.0.0-SNAPSHOT only: no release tag contains ebc63504bd6; would first ship in 11.0.0 unless fixed)
Affected versions
main from ebc63504bd6 (2025-05-10) through ea57360912ed and 645d4fe14a20 [exec: the one-test minimal repro fails on main 645d4fe14a20 with -ea and -da (2026-11-01); the warning-condition JUnit fails 2 of 3 with -ea and with -da; the independent reproduction's JUnit fails 2 of 3 with -ea and -da]; upstream main c50bc9523db still has the same lines [read 2026-10-26]; not affected: 9.12.3, 10.0.0, 10.1.0, 10.2.0, 10.2.1, 10.2.2, 10.3.0, 10.4.0, 10.5.0, 10.5.2 [exec: pass 3 of 3, no warning in any configuration] and 10.6.0 [exec 2026-11-01: the one-test minimal repro and the 3-test warning check pass on the release jars]; the factory at every release tag 9.1.0-10.5.2 and on upstream branch_10x, branch_10_5 and branch_10_6 has no LOG.warning [read]
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 adjudication that ruled it real, 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-10-26: 96 logged query lines, 51 by a scripted prior-report search (GitHub 16, commits 3, downstream 2, mailing lists 20 over dev@, java-user@, issues@ and commits@, JIRA 4, web 6), 26 manual GitHub searches (apache/lucene, apache/solr, elastic/elasticsearch, opensearch-project/OpenSearch), 13 direct reads and scans, 6 web, plus the finder's earlier 44-query run and a scan of the files of all 410 open apache/lucene PRs): no issue, PR, commit, JIRA entry, list thread or downstream report describes the warning firing for an absent key or staying silent for an explicit false; exact-phrase searches for the warning text find only the #14356 mail mirror; not fixed: upstream main still has the lines unchanged, and no open PR changes the factory's onlyLongestMatch handling (#12478, the only open PR that touches the factory file, is a 2023 feature that predates the warning); near-miss, judged not a report: in #14356 the PR author's only review comment, on exactly this check, asks '@rmuir Does this check make sense?' (2025-03-14) and was never answered; related context, different items: #14278 and #14311 (introduced onlyLongestMatchIgnoreSubwords and its default; uschindler in #14311: 'We have no mechanism for deprecating those map keys of the factory. We can only log a warning'), #16756 (MIGRATE entry for #14356, constructor removal only), and older filter-semantics items #4096 (LUCENE-3022), #12484, #12485, #4112, #4490, #7867, #9231; gaps: GitHub search is phrase-like, list search matches words not code, JIRA predates the warning, private reports are not visible
Lucene commit tested
main 645d4fe14a20: the minimal test through gradle in a scratch worktree (assertions on and off), stock and with the fix, plus the compound package's tests, the nightly TestFactories and :lucene:analysis:common:check -x test with the fix; a standalone run with -ea and -da; a 7-configuration warning and token matrix, stock and with the fix. Release jars 10.5.2 and 10.6.0 (the minimal test passes) and 10.1.0 and 10.5.2 (the matrix logs nothing); earlier 9.12.3, 10.0.0, 10.1.0, 10.2.0, 10.2.1, 10.2.2, 10.3.0, 10.4.0 and 10.5.0, and the introducing commit and its parent shadowed over main. Upstream main 2f424fd428d (2026-10-30) has the same lines (read).

Lucene and upstream sources

Finding record
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/compound/DictionaryCompoundWordTokenFilterFactory.java:36 (class-javadoc example onlyLongestMatch="true"), :46-47 (JUL logger), :69-72 (getBoolean(args, "onlyLongestMatch", true); if (onlyLongestMatch) LOG.warning(...)), :74 (onlyLongestMatchIgnoreSubwords default true), :76-78 ('Unknown parameters'), :97-103 (create() passes only onlyLongestMatchIgnoreSubwords)
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/compound/DictionaryCompoundWordTokenFilter.java:54-56 (@param onlyLongestMatchIgnoreSubwords '... Supersede parameter onlyLongestMatch'), :75
  • lucene/core/src/java/org/apache/lucene/analysis/AbstractAnalysisFactory.java:213-214 (getBoolean consumes the key with args.remove)
  • lucene/CHANGES.txt:1857-1860 (10.2.0 GITHUB#14278: names the option onlyLongestMatchNoSubwords, 'replaces the onlyLongestMatch option', 'Deprecate onlyLongestMatch')
  • lucene/MIGRATE.md:648-668 (GITHUB#14356: removal of the deprecated filter constructor only; nothing on the factory key or the warning)
  • lucene/analysis/common/src/test/org/apache/lucene/analysis/compound/TestDictionaryCompoundWordTokenFilterFactory.java (only testDecompounding and testBogusArguments; nothing asserts onlyLongestMatch handling or the warning)
The finding in 30 seconds
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/compound/DictionaryCompoundWordTokenFilterFactory.java:35-36, :46-47, :69-72, :74, :76-78, :97-103 (at 645d4fe14a20)
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/compound/DictionaryCompoundWordTokenFilter.java:54-56, :58-66, :74-75 (at 645d4fe14a20)
  • lucene/core/src/java/org/apache/lucene/analysis/AbstractAnalysisFactory.java:213-216 (at 645d4fe14a20)
Who is affected
  • lucene/analysis/common/src/java/module-info.java:171; lucene/analysis/common/src/resources/META-INF/services/org.apache.lucene.analysis.TokenFilterFactory:31 (SPI registration, at 645d4fe14a20)
  • lucene/analysis/common/src/test/org/apache/lucene/analysis/compound/TestDictionaryCompoundWordTokenFilterFactory.java (testDecompounding: dictionary only)
Minimal repro
  • build-tools/build-infra/src/main/java/org/apache/lucene/gradle/plugins/java/TestsAndRandomizationPlugin.java:240-248 (seed-chosen tests.asserts default, at 645d4fe14a20)
Why it looks unintended
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/compound/DictionaryCompoundWordTokenFilterFactory.java:69-72 (at 645d4fe14a20)
  • lucene/MIGRATE.md:648-668; lucene/CHANGES.txt:1890-1893 (at 645d4fe14a20)
  • lucene/analysis/common/src/test/org/apache/lucene/analysis/compound/TestDictionaryCompoundWordTokenFilterFactory.java (at 645d4fe14a20)
Suggested fix
  • lucene/MIGRATE.md:648-668; lucene/CHANGES.txt:1890-1893 (at 645d4fe14a20)
History
  • a Lucene clone: git show releases/lucene/10.6.0:lucene/analysis/common/src/java/org/apache/lucene/analysis/compound/DictionaryCompoundWordTokenFilterFactory.java (no LOG.warning)

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

Repro files

About the diagrams

Made in parallel with this doc from the code at 645d4fe14a20 and the executed configuration probe. The diagram's fix step shows the adjudication's containsKey + remove form of the presence check; the recommended option A writes the same check as one args.remove(...) != null call (same behaviour) and also updates the javadoc example.

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.