The factory checks the removed key's value, whose default is true, instead of checking whether you set the key.
DictionaryCompoundWordTokenFiltersplits compound words with a word list: with {donau, dampf, dampfschiff, schiff}, the worddonaudampfschiffalso yieldsdonauanddampfschiff.- Its factory, SPI name
dictionaryCompoundWord, builds it from string settings, asCustomAnalyzer,TokenFilterFactory.forNameand Solr-style schemas do. - 10.2.0 added
onlyLongestMatchIgnoreSubwords(defaulttrue) to replace the olderonlyLongestMatch. On main the old key does nothing any more; the factory still accepts it and logs a warning that it was removed.
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.- 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 settingonlyLongestMatchIgnoreSubwords="true". - 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},donaudampfschiffgives[donaudampfschiff, donau, dampfschiff]on both. - 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 tofalse: within the factory settings, only passing the ignored key with a value other than"true"(ignoring case) silences it. - Why: the factory reads the removed key with
getBoolean(args, "onlyLongestMatch", true)and warnsif (onlyLongestMatch). A missing key returns the defaulttrue, so it looks set. The value feeds nothing else, so only the log is wrong.
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.
AbstractAnalysisFactory.getBoolean(args, name, defaultVal): removes the setting from the map and returns its value, ordefaultValwhen 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-216at645d4fe14a20).onlyLongestMatchIgnoreSubwords: the replacement setting, read with defaulttrue(DictionaryCompoundWordTokenFilterFactory.java:74). The filter javadoc says it supersedesonlyLongestMatch(DictionaryCompoundWordTokenFilter.java:54-56).
- 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");- A missing key and
"true"(in any letter case) givetrue."false"givesfalse, and so does any other present value such as"yes"or"", becauseBoolean.parseBooleanreturnstrueonly for"true"(executed: a six-value probe on main). The defaulttruegoes back to the first factory version (2008), when the key still had a meaning. - The value is used for nothing else:
create()passes only the dictionary, the size limits andonlyLongestMatchIgnoreSubwordsto the filter (:97-103), whose 6-argument constructor handsfalseto the base class (DictionaryCompoundWordTokenFilter.java:58-66). - 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 writesonlyLongestMatch="true"(:35-36), so following it also gives the warning.
Dictionary {donau, dampf, dampfschiff, schiff}, word donaudampfschiff, 7 configurations through the SPI name on main 645d4fe14a20:
settings besides dictionary | warning on main | tokens on main | same tokens on 10.5.2? |
|---|---|---|---|
| none (the default) | yes | [donaudampfschiff, donau, dampfschiff] | yes |
onlyLongestMatchIgnoreSubwords=true | yes | [donaudampfschiff, donau, dampfschiff] | yes |
onlyLongestMatchIgnoreSubwords=false | yes | [donaudampfschiff, donau, dampf, dampfschiff, schiff] | no (10.5.2 has no dampf) |
onlyLongestMatch=true | yes | [donaudampfschiff, donau, dampfschiff] | yes |
onlyLongestMatch=true, onlyLongestMatchIgnoreSubwords=false | yes | the 5 tokens with dampf | no (10.5.2 has no dampf) |
onlyLongestMatch=false | no | [donaudampfschiff, donau, dampfschiff] | yes |
onlyLongestMatch=false, onlyLongestMatchIgnoreSubwords=false | no | the 5 tokens with dampf | yes |
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
dictionaryCompoundWordfactory: its publicMapconstructor,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.testDecompoundingbuilds, or the replacement setting the warning itself recommends. - What happens. One constant
WARNINGper 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), conventionallyfalse(otherwise only the logging configuration hides it); a configuration that still setsonlyLongestMatch="false", an ignored key, is not told. - Scale. Minor: log diagnostics only, on unreleased code. It would ship in 11.0.0 unless fixed.
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.
/*
* 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);
}
}
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.
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.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT); 10.6.0 and earlier releases do not have the code |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 |
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)
# 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- 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
prefixLengthit 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.txtentry,MIGRATE.mdentry or test says when the factory logs.MIGRATE.md:648-668(GITHUB#14356) covers only the removed filter constructor.TestDictionaryCompoundWordTokenFilterFactoryhas onlytestDecompoundingandtestBogusArguments. - Lucene's canonical use is warned.
testDecompoundingbuilds the factory with onlydictionary, 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.
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 @@
* <filter class="solr.DictionaryCompoundWordTokenFilterFactory" dictionary="dictionary.txt"
- * minWordSize="5" minSubwordSize="2" maxSubwordSize="15" onlyLongestMatch="true"/>
+ * minWordSize="5" minSubwordSize="2" maxSubwordSize="15"
+ * onlyLongestMatchIgnoreSubwords="true"/>
@@ -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 @@
* <filter class="solr.DictionaryCompoundWordTokenFilterFactory" dictionary="dictionary.txt"
- * minWordSize="5" minSubwordSize="2" maxSubwordSize="15" onlyLongestMatch="true"/>
+ * minWordSize="5" minSubwordSize="2" maxSubwordSize="15"
+ * onlyLongestMatchIgnoreSubwords="true"/>
@@ -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(trueorfalse) 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 (dampfis now emitted fordonaudampfschiff), because the 10.5.2 factory passed the defaultonlyLongestMatch=truethrough the constructor that #14356 removed. That change follows from the intended removal and is not this bug, butMIGRATE.mddoes 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 nightlyTestFactories(every factory with random arguments): 6 classes, 27 tests, 0 failures with assertions on;:lucene:analysis:common:check -x testpasses (google-java-format, ecjLint, forbiddenApis). - The same change shadowed into the stock main jars: the minimal test passes with
-eaand-da, and the matrix shows the effect above.
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.
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.
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.
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.
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.
| Where | When | How | Found |
|---|---|---|---|
| apache/lucene issues, PRs (incl. the migrated JIRA issues) and commits | 2026-10-26 and 2026-11-01 | scripted 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 keys | no report; the exact warning text only in the #14356 mail mirror |
| Open PRs and the factory's history | 2026-11-01 | changed 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 timeline | no open PR changes the warning; #14356's unanswered review question (near-miss) |
| Mailing lists, JIRA, downstream, web | 2026-10-26 and 2026-11-01 | dev@, java-user@, issues@ and commits@ (Lucene); dev@, users@ and issues@ (Solr); LUCENE and SOLR JIRA; Elasticsearch, OpenSearch and Solr trackers; 6 then 10 web queries | context only (filter-semantics threads, Elasticsearch adapting to the constructor removal) |
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.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | minimal test fails with assertions on and off (through Gradle and standalone); passes with fix A |
main ea57360912ed, a9779ef3c193 | affected | the history test fails 2 of 3 (absent key warned; true warned, false silent) |
ebc63504bd6 (#14356), shadowed over main | introducing commit | minimal test fails |
38f81b84ba4 (parent), shadowed over main | not affected | minimal test passes |
| 10.6.0 | not affected | minimal 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 affected | minimal test passes (release jars) |
| 10.0.0, 10.3.0, 10.4.0, 10.5.0 | not affected | the history test passes 3 of 3 |
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.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate, later re-scoped | 3-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, narrower | Its 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, procedural | Root 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 real | Overall 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, high | Re-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 defect | Minimal 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. |
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 464 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,185 words
- 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 nightlyTestFactoriesand:lucene:analysis:common:check -x testwith the fix; a standalone run with-eaand-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 main2f424fd428d(2026-10-30) has the same lines (read).
- 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'), :75lucene/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)
- TestDictionaryCompoundFactoryRemovedKeyWarning.java (JUnit, one test):
repro/LFV-439/src/org/apache/lucene/analysis/compound/TestDictionaryCompoundFactoryRemovedKeyWarning.java(58 lines)
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.
assets/LFV-439/bug.png(464 KB)
