Two steps of the parser run in the wrong order: the step that normalizes Luc* runs before the step that gives a field-less clause its fields.
- Index one document titled "Lucene". The analyzer is a
PerFieldAnalyzerWrapper:StandardAnalyzer(lower-cases) fortitle,WhitespaceAnalyzer(keeps case) for other fields. CallsetMultiFields({"title"}), thenparse("Luc*", null). - You expect the same query as typing
title:Luc*, which the parser makestitle:luc*: 1 hit. FuzzyLucene~1through the same expansion does becometitle:lucene~1and finds it. - Instead you get
title:Luc*and 0 hits (Lu?enetoo). The parser normalizesLucone step before the step that gives the clause its field, so it asks the analyzer for fieldnull, and the wrapper answers with its default analyzer. - It also goes the other way. With the wrapper's own javadoc setup (
StandardAnalyzerdefault,KeywordAnalyzerforfirstnameandlastname),Jo*becomesfirstname:jo* lastname:jo*: 0 hits for "John". - With a per-field map from
Map.of, thenulllookup throws: every field-less prefix or wildcard term with a literal part (Luc*, not a bare*) makesparse()fail withNullPointerException. From 7.0.0 to 10.5.2 and on main; swapping two processors fixes it.
Applications that parse field-less queries with the flexible StandardQueryParser in multi-field mode (setMultiFields, parse(query, null)) over fields whose analyzers normalize differently, e.g. a PerFieldAnalyzerWrapper. One document is enough; results are silently wrong, or parse() throws with a Map.of map. The classic MultiFieldQueryParser, explicit fields and one shared analyzer are not affected.
Mechanism, terms and numbers580 words
The flexible StandardQueryParser runs a parsed query through a fixed list of processors. The first one, WildcardQueryNodeProcessor, normalizes the literal parts of prefix and wildcard terms with Analyzer#normalize(field, text); the second one, MultiFieldQueryNodeProcessor, gives a clause without a field the fields set by setMultiFields. So a field-less Luc* is normalized for field null, which a PerFieldAnalyzerWrapper answers with its default analyzer instead of the expanded field's. With a case-keeping default and a lower-cased title, Luc* becomes title:Luc* and misses the document titled "Lucene" that title:Luc* (parsed as title:luc*) finds; with the wrapper's javadoc setup (lower-casing default, keyword name fields) Jo* is lower-cased and misses "John"; with a Map.of per-field map every such term with a literal part throws NullPointerException from parse(). Fuzzy, regexp and range terms through the same expansion are normalized per field and plain terms analyzed per field, and the classic MultiFieldQueryParser normalizes wildcard terms per field. Introduced by LUCENE-7355 together with LUCENE-7429 (both 2016), first released in 7.0.0, still on main; moving MultiFieldQueryNodeProcessor before WildcardQueryNodeProcessor fixes it and passes the whole queryparser module.
- Multi-field mode:
StandardQueryParser.setMultiFields(fields)plusparse(query, null). A clause without a field is expanded into one clause per field (StandardQueryParser.java:102-105,:357-362). - Normalization:
Analyzer#normalize(field, text)maps a partial word to "the representation that it would have in the index" (lower-casing, folding) without tokenizing; query parsers use it for wildcard and fuzzy terms (Analyzer.java:202-213). It depends on the field. - Per-field analyzer:
PerFieldAnalyzerWrapper(or anyDelegatingAnalyzerWrapper) picks an analyzer by field name withfieldAnalyzers.get(fieldName), else the default (PerFieldAnalyzerWrapper.java:74-77).
- The pipeline adds
WildcardQueryNodeProcessorfirst andMultiFieldQueryNodeProcessorsecond (StandardQueryNodeProcessorPipeline.java:48-49); each processor walks the whole tree before the next one (QueryNodeProcessorPipeline.java:75-81). WildcardQueryNodeProcessorcallsanalyzeWildcard(analyzer, fqn.getFieldAsString(), text)(WildcardQueryNodeProcessor.java:107-111), which callsa.normalize(field, chunk)on each literal chunk (:66,:76). For a field-less clausegetFieldAsString()is stillnull(FieldQueryNode.java:99-102).- The wrapper is asked for field
null(AnalyzerWrapper.java:146):PerFieldAnalyzerWrapperfalls back to its default analyzer, and aMap.ofmap throwsNullPointerExceptiononget(null). - Only then does
MultiFieldQueryNodeProcessorset the field and clone the node per field (MultiFieldQueryNodeProcessor.java:70-105). Nothing normalizes the text again; the builder makesnew PrefixQuery(new Term(field, text), ...)(PrefixWildcardQueryNodeBuilder.java:48). - Fuzzy, regexp and range terms are normalized by later processors with the expanded field (
FuzzyQueryNodeProcessor.java:59,RegexpQueryNodeProcessor.java:43,TermRangeQueryNodeProcessor.java:138-139), plain terms are analyzed byAnalyzerQueryNodeProcessor(pipeline:58). Only prefix and wildcard terms seenull.
setup (setMultiFields, parse(q, null)) | query | main and 7 releases from 7.0.0 to 10.5.2 (executed) | with the field typed, or with the fix |
|---|---|---|---|
default WhitespaceAnalyzer, title -> StandardAnalyzer; doc title "Lucene" | Luc*, Lu?ene | title:Luc*, title:Lu?ene: 0 hits | title:luc*, title:lu?ene: 1 hit |
| same | Lucene~1, Lucene (controls) | title:lucene~1, title:lucene: 1 hit | same |
javadoc setup: default StandardAnalyzer, firstname/lastname -> KeywordAnalyzer; doc "John" | Jo*, J?hn | firstname:jo* lastname:jo*: 0 hits | firstname:Jo* lastname:Jo*: 1 hit |
per-field map from Map.of("title", ...) | Luc* | NullPointerException from parse() | title:luc* |
title lower-cased, id keyword, 14 query shapes (nested AND/OR/NOT, ^ boosts, field boosts, leading *, quotes, ranges) | e.g. (Luc* OR ABC*) AND NOT Zed* | 10 of 14 shapes differ from the explicit-field form; here the NOT misses "Zedcene": 2 hits | all 14 agree: 1 hit |
Exposure and likelihood274 words
- Who is hit. Callers of
StandardQueryParserwithsetMultiFieldsandparse(query, null)whose fields use analyzers that normalize differently, throughPerFieldAnalyzerWrapperor anotherDelegatingAnalyzerWrapper. Typical setups: the wrapper's javadoc example (lower-casing default, keyword name or ID fields), or a keyword or whitespace default with analyzed text fields. - Which queries. A field-less prefix or wildcard term is affected when the default analyzer and the field's analyzer normalize one of its literal chunks differently (whether hits change then depends on the indexed data): an upper-case letter in every hand-written repro and security case; in the finder's scan also accent and width folding (ASCII-folding and ICU-folding chains).
- Default vs opt-in. Multi-field mode and per-field analyzers are application choices; given those, no other setting is needed. With one field-independent analyzer nothing visible changes.
- Both directions. Missing normalization (
Luc*on a lower-cased field: 0 hits) and wrong normalization (Jo*lower-cased on keyword fields: 0 hits;ABC*selectsabc-2instead ofABC-1). - Crash variant. A per-field map that rejects
nullkeys (Map.of, executed; by the JDK contract alsoConcurrentHashMap, not executed) makes every such term with a literal part throw an uncheckedNullPointerExceptionfromparse()instead of aQueryNodeException. - Scale. None: one document, a one-word query. Silent: fewer or different hits.
- Reach. Solr, Elasticsearch and OpenSearch do not call
setMultiFields(read-only code search of their default branches, not exhaustive); Lucene's Luke uses the parser without it. Applications that embed the flexible parser directly can be affected; they were not surveyed. No user report was found in the nine years since 7.0.0; the likelihood is rated plausible. - Fix cost. Query side only: no reindexing.
The test shows that StandardQueryParser with setMultiFields({title}) normalizes the field-less prefix Luc* with field null (the wrapper's default analyzer) instead of the analyzer of title, the field it expands to.
/*
* 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.queryparser.flexible.standard;
import java.util.Collections;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.core.WhitespaceAnalyzer;
import org.apache.lucene.analysis.miscellaneous.PerFieldAnalyzerWrapper;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestMultiFieldWildcardNormalize extends LuceneTestCase {
public void testPrefixUsesExpandedFieldAnalyzer() throws Exception {
Analyzer a =
new PerFieldAnalyzerWrapper(
new WhitespaceAnalyzer(), Collections.singletonMap("title", new StandardAnalyzer()));
StandardQueryParser qp = new StandardQueryParser(a);
qp.setMultiFields(new CharSequence[] {"title"});
// expected title:luc* (as for "title:Luc*"); actual title:Luc* (normalized with field null)
assertEquals(qp.parse("title:Luc*", null), qp.parse("Luc*", null));
}
}
# in a copy of this finding's repro folder (repro/LFV-560/, which holds src/)
# Lucene checkout at the tested commit, built with: ./gradlew --no-daemon assemble (JDK 25)
LUCENE=/path/to/lucene; V=12.0.0-SNAPSHOT
CP=$LUCENE/lucene/core/build/libs/lucene-core-$V.jar
CP=$CP:$LUCENE/lucene/codecs/build/libs/lucene-codecs-$V.jar
CP=$CP:$LUCENE/lucene/test-framework/build/libs/lucene-test-framework-$V.jar
CP=$CP:$LUCENE/lucene/analysis/common/build/libs/lucene-analysis-common-$V.jar
CP=$CP:$LUCENE/lucene/facet/build/libs/lucene-facet-$V.jar
CP=$CP:$LUCENE/lucene/queries/build/libs/lucene-queries-$V.jar
CP=$CP:$LUCENE/lucene/queryparser/build/libs/lucene-queryparser-$V.jar
CP=$CP:$LUCENE/lucene/sandbox/build/libs/lucene-sandbox-$V.jar
CP=$CP:randomizedtesting-runner-2.9.2.jar:junit-4.13.2.jar:hamcrest-3.0.jar # test-framework dependencies (Maven Central)
CP=$CP:junit-platform-console-standalone-6.1.3.jar # JUnit Platform console launcher (Maven Central)
javac -d classes -cp "$CP" $(find src -name '*.java')
# run 1: assertions on, seed DEADBEEF
java -ea -esa --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED -Dtests.seed=DEADBEEF \
-cp "classes:$CP" org.junit.platform.console.ConsoleLauncher \
execute --select-class org.apache.lucene.queryparser.flexible.standard.TestMultiFieldWildcardNormalize
# the module(s) facet also need their third-party runtime jars
- Expected vs actual
- Expected:
parse("Luc*", null)equalsparse("title:Luc*", null), i.e.title:luc*. Actual:expected:<title:luc*> but was:<title:Luc*>(-ea and -da).
More files, environment, full output34 words
| lucene | Lucene main e1faeba08bde (2026-11-02) |
|---|---|
| jdk | 25.0.3+9-LTS |
# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
=> java.lang.AssertionError: expected:<title:luc*> but was:<title:Luc*>
org.junit.Assert.fail(Assert.java:89)
org.junit.Assert.failNotEquals(Assert.java:835)
org.junit.Assert.assertEquals(Assert.java:120)
org.junit.Assert.assertEquals(Assert.java:146)
org.apache.lucene.queryparser.flexible.standard.TestMultiFieldWildcardNormalize.testPrefixUsesExpandedFieldAnalyzer(TestMultiFieldWildcardNormalize.java:35)- Javadoc: a field-less clause is expanded over the
setMultiFieldsfields; typed with the field,title:Luc*becomestitle:luc*. - The same parser normalizes fuzzy, regexp and range terms, and analyzes plain terms, per expanded field; the classic
MultiFieldQueryParsernormalizes wildcards per field. - 7.0 removed
lowercaseExpandedTermsbecause expanded terms "are now normalized through Analyzer#normalize" (per field); no test pins the order.
The full argument230 words
- The contract.
StandardQueryParser: "If the field specification is omitted, the query parser will expand the clause over all fields specified by a call to setMultiFields(CharSequence[])" (:102-105);setMultiFields: "Set the fields a query should be expanded to when the field is null" (:357-362).Analyzer#normalize: "Normalize a string down to the representation that it would have in the index ... used by query parsers ... in case of a wildcard or fuzzy query" (Analyzer.java:202-213).PerFieldAnalyzerWrapper"can be used like any other analyzer, for both indexing and query parsing" (:42-43). - The 7.0 migration note. "CommonQueryParserConfiguration.setLowerCaseExpandedTerms removed (LUCENE-7355). This option has been removed as expanded terms are now normalized through Analyzer#normalize" (
lucene/MIGRATE.txt:94-97at the 7.0.0 tag). - The parser disagrees with itself. Fuzzy, regexp, range and plain clauses through the same expansion get per-field forms (
title:lucene~1); the classicMultiFieldQueryParserexpands prefix and wildcard terms per field before normalizing (MultiFieldQueryParser.java:251-274). - Tests and history. No upstream test combines multi fields, per-field normalization and wildcards; the whole queryparser module (533 upstream tests) passes with the fix. LUCENE-7355 removed
LowercaseExpandedTermsQueryNodeProcessor, which ran after the expansion and lower-cased without looking at the field, and put the newnormalize()call into the processor that already ran first; its other calls run after the expansion. None of the issue's 34 comments mentions the multi-field path.
The devil's-advocate check255 words
The strongest case for "intended" (a devil's-advocate verifier, reviews by OpenAI GPT-5.6 and a fact check by the same model family, all of which rated it weak): the pipeline javadoc says "The order they are assembled affects the results" (StandardQueryNodeProcessorPipeline.java:32); MultiFieldQueryNodeProcessor expands terms "so the query looks for the same term in different fields" (:31-33), which could mean one identical, already-normalized term; TestMultiFieldQPHelper.testAnalyzerReturningNull says bla*, bla~ and [a TO c] "are not affected as their terms are not analyzed anyway" (:308-315); the NullPointerException could be blamed on the map; and normalizing once before the expansion is cheaper than once per expanded field.
Why it fails: the order warning is generic; the same parser gives fuzzy, regexp, range and plain clones per-field forms, so "same term" means the same input text, analyzed per field; the test comment comes from LUCENE-1567 (2009), seven years before Analyzer#normalize, lumps in fuzzy and range terms that are now normalized per field, and passes in either order; with multi fields set no other clause type passes null; neither normalize nor getWrappedAnalyzer says whether null is allowed, so the API is silent, and the case rests on the disagreement with the explicit-field form; the classic MultiFieldQueryParser already normalizes once per field. Verdict: a defect. Before 7.0 this parser lower-cased expanded wildcard terms without looking at the field, so the regression is the null field (the field-less form disagrees with the explicit-field form, and Map.of maps throw), not the loss of a per-field feature.
Recommended Run the multi-field expansion before wildcard normalization (validated)
Swap two lines in StandardQueryNodeProcessorPipeline: add MultiFieldQueryNodeProcessor before WildcardQueryNodeProcessor, so each expanded clause is normalized for its own field. Query side only, no reindex. The repro passes, the queryparser module passes (538 tests, 0 failures), and nested syntax and boosts match the explicit-field form.
public StandardQueryNodeProcessorPipeline(QueryConfigHandler queryConfig) {
super(queryConfig);
add(new MultiFieldQueryNodeProcessor()); // was second: set the fields first
add(new WildcardQueryNodeProcessor()); // was first: now normalizes per field
add(new FuzzyQueryNodeProcessor());
// ... the rest unchangedAll options and trade-offs4 options · 678 words
The root cause is the order of two processors. The swap was built and run in a scratch worktree at ea57360912ed (removed afterwards) and with the standalone runner; the other options are not built.
Move add(new MultiFieldQueryNodeProcessor()) above add(new WildcardQueryNodeProcessor()). MultiFieldQueryNodeProcessor works on any FieldableNode (a FieldQueryNode is one), so it clones the raw Luc* per field, and WildcardQueryNodeProcessor then normalizes each clone for its own field. Results: Luc* gives title:luc*, Jo* gives firstname:Jo* lastname:Jo*, and the Map.of map no longer throws.
public StandardQueryNodeProcessorPipeline(QueryConfigHandler queryConfig) {
super(queryConfig);
add(new MultiFieldQueryNodeProcessor()); // was second: set the fields first
add(new WildcardQueryNodeProcessor()); // was first: now normalizes per field
add(new FuzzyQueryNodeProcessor());
// ... the rest unchangedTrade-offs
- Behaviour change for affected users (the point of the fix): field-less prefix and wildcard terms now match like the explicit-field form; queries that matched only through the default analyzer's normalization see different hits. Worth a CHANGES entry.
- Pipeline positions change.
StandardQueryNodeProcessorPipelineis a publicList; code that edits it by index (for exampleget(0)expectingWildcardQueryNodeProcessor) sees the two processors swapped. - Cost. A field-less wildcard term is then normalized once per expanded field instead of once, as the classic
MultiFieldQueryParseralready does; not measured, and only noticeable with many fields or an expensivenormalize(). - Failure order for invalid configurations. Without
setMultiFields,parse(query, null)makesMultiFieldQueryNodeProcessorthrowIllegalArgumentException(MultiFieldQueryNodeProcessor.java:79-82); after the swap it throws before the wildcard processor runs, so aMap.ofmap then gives that exception instead of theNullPointerException. With an empty multi-field list the field staysnullin either order. - Checked: nested
AND/OR/NOT, clause boosts, field boosts (setFieldsBoost), a leading wildcard, quoted text, ranges and fuzzy-on-wildcard all agree with the explicit expansion after the swap (14 shapes x 2 configurations). Not checked: custom pipelines built on this class.
What the checks validated
- gradle,
:lucene:queryparser:testwith this doc's test and the scope probe: stock 538 tests, 4 failures (exactly those 4 tests), 1 skipped; with the fix 538 tests, 0 failures, 2 skipped (the upstream skip and the workaround test, which skips itself once the order is fixed), for seed DEADBEEF with assertions on and off and a random seed36B0F102C8846336. That is the 533 upstream queryparser tests plus 5. :lucene:queryparser:check -x test(checkGoogleJavaFormat, ecjLint, forbiddenApis main and test) passes with the fix and the test.- Standalone runner with the fix shadowed: this test 3 of 3 (assertions on and off), the
PerFieldAnalyzerWrappervariant 3 of 3, the scope probe 28 of 28 pairs agree (stock: 20 of 28 differ). Earlier, the finder's test, verifiers' tests, the security probe and the 7.0.0 history probe passed with the same swap.
Leave WildcardQueryNodeProcessor first but skip analyzeWildcard while the field is null and multi fields are set, then normalize in a processor that runs after MultiFieldQueryNodeProcessor.
Trade-offs
- Keeps pipeline positions (and the current failure order) stable; more code, and two places that handle wildcard text. If maintainers want to keep the public pipeline positions, this option deserves equal consideration.
Removes the NullPointerException with Map.of maps; the prefix and wildcard terms stay wrong.
Trade-offs
- It would also bypass a
nullkey that a null-tolerant map holds on purpose (undocumented, but possible). Only useful as hardening next to the swap.
After creating the parser, move the wildcard processor behind the multi-field processor; check the order first, so the line does nothing on a fixed release (the probe makes the same check). Or write the fields explicitly, or use the classic MultiFieldQueryParser.
QueryNodeProcessorPipeline p = (QueryNodeProcessorPipeline) qp.getQueryNodeProcessor();
if (p.get(0) instanceof WildcardQueryNodeProcessor && p.get(1) instanceof MultiFieldQueryNodeProcessor) {
p.add(1, p.remove(0)); // WildcardQueryNodeProcessor now runs after MultiFieldQueryNodeProcessor
}What the checks validated
- On main, 10.5.2 and 9.12.3 the scope probe's 28 pairs all agree with the explicit-field form.
Applications that write restriction rules as field-less query strings parsed this way get the literal normalized by the analyzer for field null (the wrapper's default), not the field's: a rule can miss the values it names or catch differently-cased ones, and a deny-only rule such as *:* -Confid* returns every document. Writing the fields out avoids it.
Semi-formal. While building a scan of how query parsers normalize terms, an AI tool noticed the processor order; the bounded scan on the stock jars then showed it: with setMultiFields, 38-92% of prefix and wildcard checks missed for 15 of the 16 analyzers compared, and none with an explicit field. A JUnit test confirmed it.
The method in full237 words
Method: semi-formal (a hypothesis from code reading, then witnessed by a bounded tool run).
- A hunt unit (analyzer normalize consistency, 2026-10-26). The finder, an AI tool, checked a metamorphic relation: a word's
Analyzer#normalizeform should equal the token the index holds, and each parser should build its prefix, wildcard, fuzzy, regexp and range terms from that form. - While building the parser-reachability scan it read
StandardQueryNodeProcessorPipelineand suspected the order. The bounded scan on the stock main jars (2,921,246 query checks: 18 analyzers, words of length 1-3, 4 parser setups, 5 query kinds) used aPerFieldAnalyzerWrapperwith aKeywordAnalyzerdefault and the scanned analyzer on the expanded field. - Result: in multi-field mode the flexible parser missed 38-92% of the prefix and wildcard checks for 15 of the 16 analyzer configurations in the comparison, counted over the words for which the analyzer's own
normalize()form equals its indexed token (StandardAnalyzer 4,548 of 10,032, an ASCII-folding chain 7,772 of 10,032, an ICU-folding chain 11,167 of 12,198); the 16th, a Ukrainian chain, missed none. With an explicit default field there were no such misses, and fuzzy, regexp and range in multi-field mode had none either. The scan covers a finite set of analyzers and words. - A public-API JUnit test then failed on main, 10.5.2 and 9.12.3; an independent reviewer from another model family (OpenAI GPT-5.6) reviewed it, verifiers and an adjudication confirmed it, and the history, prior-report and security checks followed.
No prior report: 147 logged queries in three runs over GitHub issues, PRs and commits, the dev@, java-user@, issues@ and commits@ lists, the JIRA archive, downstream repositories and the web, plus manual searches, 2026-10-26. Closest: #8409 (the introducing change), #4428 (a different ordering defect), #9070 (similar symptom, other cause).
Searches4 searches
Verdict new. No issue, PR, commit, JIRA entry, list thread or web page reports it, by anyone.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene), downstream repositories, dev@, java-user@, issues@, commits@, the JIRA archive, the web | 2026-10-26 | three tool runs, 147 queries (the three processors, StandardQueryParser, PerFieldAnalyzerWrapper, setMultiFields, analyzeWildcard, lowercaseExpandedTerms, 'Analyzer normalize null field', the NullPointerException text); the finder's run and a verifier's run | 167, 292 and 544 distinct hits, 0 high overlap (finder: 1,927 hits, 0 high) |
| GitHub, manual | 2026-10-26 | issue and PR searches for the class names; all of GitHub for setMultiFields and WildcardQueryNodeProcessor | no report; one downstream PR (2018) noticed the new normalization after an upgrade, not this bug |
| Open PRs | 2026-10-26 | all 406 open apache/lucene PRs by touched files | none touches the pipeline, the three processors, StandardQueryParser or PerFieldAnalyzerWrapper |
| Related items (read) | 2026-10-26 | #8409, #4428, #9070, #8482 and about ten older flexible-parser issues | different bugs: #8409 introduced the call (34 comments, no multi-field discussion); #4428 is another ordering defect around the same processor (AND operator); #9070 is the classic parser on one field; #8482 made AnalyzerWrapper delegate normalize() by field name |
Introduced by LUCENE-7355 (e92a38af90d, 2016-07-12), which put wildcard normalization into the processor that runs before the multi-field expansion, together with LUCENE-7429 (af60048097a, 2016-10-27), which made analyzer wrappers normalize by field name. First release 7.0.0; main and 7 releases up to 10.5.2 were run and fail; 6.6.6 and older pass. Not fixed upstream.
Versions tested6 rows
Introduced by two 2016 commits, both first released in 7.0.0. e92a38af90d "LUCENE-7355: Add Analyzer#normalize() and use it in query parsers" (now #8409, by @jpountz; authored 2016-06-28, committed to master 2016-07-12; a JIRA patch, no PR) removed LowercaseExpandedTermsQueryNodeProcessor, which ran after the expansion and lower-cased without looking at the field, and added the normalize() call to WildcardQueryNodeProcessor, the first processor: this is where field null comes from. It did not touch AnalyzerWrapper, so at that commit a PerFieldAnalyzerWrapper did not yet apply any wrapped analyzer's normalization. af60048097a "LUCENE-7429: AnalyzerWrapper can now wrap the normalization analysis chain too" (now #8482, by @jpountz, 2016-10-27) made wrappers normalize through getWrappedAnalyzer(fieldName), which turns field null into the default analyzer's normalization or the Map.of exception; its 6.x backport (6.3.0) has no effect there because the 6.x flexible parser does not call normalize(). The processor order is older: LUCENE-1792 (565a6d22b5f, 2009) put the wildcard processor first; blame shows 6f66858572c (2010), which only renamed addProcessor to add; no commit message explains the order. The 6.x backport of LUCENE-7355 (6.2.0) did not touch the flexible parser. Not bisected at commit level and neither commit's tree was run: blame, the diffs and ancestry name the commits, and the release runs bound the change to 7.0.0.
What changed for users at 7.0.0: Luc* with a case-keeping default and a lower-cased field matched in 6.x and stops matching; the keyword case was lower-cased in 6.x too, but the same way for the field-less and the explicit form; the Map.of exception is new. Upstream main (a95c754ee003), branch_10x and branch_10_5 have the same processor files (unchanged since 2021). The releases between the ones run were not run; every release tag from 7.0.0 to 10.5.2 in the history clone contains both commits.
| Version or commit | Verdict | Result |
|---|---|---|
| 5.5.5, 6.1.0, 6.2.0, 6.6.6 | before 7.0 | the follow-up probe: 0 mismatches (expanded terms lower-cased field-independently, after the expansion) |
| 7.0.0 | first affected | the probe: 7 mismatches across Luc*, Lu?ene, ABC* and the Map.of case; 0 with the two processors swapped |
| 7.7.3, 8.11.4, 9.0.0 | affected | the probe: the same 7 mismatches |
| 9.12.3, 10.0.0, 10.5.2 | affected | this doc's test: 3 of 3 fail (10.5.2 with assertions on and off); the probe: 7 mismatches |
main ea57360912ed | affected | 3 of 3 fail with assertions on and off, gradle and runner |
| main + the fix | not affected | 3 of 3 pass; queryparser module 538 tests, 0 failures |
A reviewer from another model family (OpenAI GPT-5.6), AI verifiers, an adjudication and a credibility audit: all say real except one procedural "not real" from a review that could not run code. This doc's test was run on main, 10.5.2, 10.0.0, 9.12.3 and with the fix.
Checks and verdicts369 words
Every check ran on main ea57360912ed unless noted. The GPT reviews were reading-only (no code ran).
| Check | Verdict | What it found |
|---|---|---|
| Refute review (OpenAI GPT-5.6, read-only) | not real, low confidence, procedural | It could not run Java, and it needs a run of its own to vote real; it agreed with the root cause, judged the inputs in contract and rated the intended case weak. A separate run re-ran the repro: 2 of 2 fail. |
| Independent reproducer | real | Own 3-test JUnit fails 3 of 3 on main (assertions on and off), 10.5.2 and 9.12.3, passes with its own swap; added the keyword-field direction; 7.0.0 affected, 6.6.6 not. |
| "works as designed" review (OpenAI GPT-5.6, read-only) | real, medium; intended argument weak | Its verifier ran the finder's test with assertions off (2 of 2 fail) and the independent reproduction's test with the swap (3 of 3 pass). |
| Devil's advocate | real | No intended-behaviour argument survived; its probe shows only prefix and wildcard skip per-field normalization, plus the reverse case ABC* -> id:abc*. |
| Adjudication | confirmed, high | Re-ran the independent reproduction's test (3 of 3 fail, assertions on and off; 3 of 3 pass with the swap); corrected the first affected release to 7.0.0. |
| Security assessment | none (earlier "out of scope of SECURITY.md", retired 2026-10-27) | Rule probe: 6 differing checks on main and 10.5.2, all 5 controls agree, none with the fix. |
| Credibility audit | keep | Re-ran the finder's test with a new seed (2 of 2 fail) and an independent two-field probe, which fails on main and 10.5.2 and passes with the swap. |
| Fact check of this doc (reviewers, one from OpenAI GPT-5.6, read-only) | bug confirmed; text errors found and fixed | One reviewer re-ran the repro, the fix, the scope, workaround and security probes (all as stated) and found three text errors; the other, read-only, found 10 errors, 8 overclaims and 9 missing caveats, mostly wording. Each item is fixed or answered. |
| This doc | reproduced; fix validated | A module-native 3-test repro on main, 10.5.2, 10.0.0 and 9.12.3; the fix with the full queryparser suite, the static checks and a 14-shape scope probe; the user workaround on main, 10.5.2 and 9.12.3. |
Finding record, Lucene and upstream sources and repro files1,257 words
- Registry title
- StandardQueryParser with setMultiFields normalizes field-less prefix and wildcard terms with field null, before the multi-field expansion, so per-field analyzers are ignored for them (and a PerFieldAnalyzerWrapper over Map.of throws NullPointerException)
- Area
- queryparser / flexible StandardQueryParser (processor pipeline order: WildcardQueryNodeProcessor runs before MultiFieldQueryNodeProcessor)
- Priority
- Low, rank 264: Only the opt-in multi-field mode of the less common flexible StandardQueryParser with field-dependent normalizers is affected.
- Impact class
- silent wrong results
- Impact note
- Applications that parse field-less queries with the flexible StandardQueryParser in multi-field mode (setMultiFields plus parse(query, null)) over fields whose analyzers normalize differently (PerFieldAnalyzerWrapper or another wrapper) silently get the wrong prefix and wildcard terms, in either direction: under-normalized (Luc* stays Luc* on a lower-cased title field, 0 hits) or over-normalized (Jo* is lower-cased to jo* on KeywordAnalyzer fields, the PerFieldAnalyzerWrapper class-javadoc setup, 0 hits); with a per-field map that rejects null keys (Map.of) every such query throws NullPointerException from parse(); opt-in mode, no scale needed, every release from 7.0.0 to 10.5.2 and main.
- Summary
- You give StandardQueryParser a PerFieldAnalyzerWrapper (WhitespaceAnalyzer by default, StandardAnalyzer for 'title'), call setMultiFields({title}) and parse 'Luc*' with no field, expecting what the javadoc promises: the clause expanded over the multi fields, i.e. the same query as 'title:Luc*', which becomes title:luc* and finds the document titled 'Lucene'. Instead the pipeline normalizes the prefix in WildcardQueryNodeProcessor before MultiFieldQueryNodeProcessor has set the field, so Analyzer#normalize is called with field null (the wrapper's default analyzer), the query becomes title:Luc* and finds nothing, while plain, fuzzy, regexp and range clauses in the same setup are normalized with the expanded field. If the per-field map is a Map.of, the null lookup makes parse() throw NullPointerException for every field-less prefix or wildcard clause.
- Introduced
- e92a38af90d12e51390b4307ccbe0c24ac7b6b4e (master): LUCENE-7355 'Add Analyzer#normalize() and use it in query parsers' by @jpountz (now apache/lucene#8409; a JIRA patch, no PR) [read: blame on the analyzeWildcard/normalize lines, the commit diff and ancestry (an ancestor of 7.0.0, not of 6.6.6); exec: 6.6.6 passes, 7.0.0 fails and passes with the two processors swapped]. It removed LowercaseExpandedTermsQueryNodeProcessor, which ran after MultiFieldQueryNodeProcessor and lower-cased field-independently, and added Analyzer#normalize(fqn.getFieldAsString(), ...) to WildcardQueryNodeProcessor, the first processor of the pipeline; the calls it added to the fuzzy, regexp and range processors run after the expansion. The pipeline order itself dates from 6f66858572c (2010) and was harmless until then. The 6.x backport 7c2e7a0fb80 (6.2.0) did not touch the flexible parser. Not commit-bisected (2016 sources not built) (2016-07-12 (committed to master; authored 2016-06-28))
- First affected version
- 7.0.0 [exec: the follow-up probe shows 7 mismatches on 7.0.0 and 0 with the processors swapped; the independent reproduction's parse-only main() 3 of 3 mismatches]; not affected up to 6.6.6 [exec: 5.5.5, 6.1.0, 6.2.0, 6.6.6]. The 'present since 6.2.0 [read]' in the hunt's record and the intended-behaviour check is wrong: the 6.x backport of LUCENE-7355 did not change the flexible parser
- Affected versions
- every release 7.0.0 to 10.5.2 and main ea57360912ed. [exec] the follow-up's version-portable probe (MultiFieldWildcardHistoryMain, 7 mismatches each) on 7.0.0, 7.7.3, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and main (-ea and -da); the finder's JUnit (2 of 2 fail) on 9.12.3, 10.0.0, 10.5.2 and main (-ea and -da); the independent reproduction's JUnit (3 of 3 fail) on main (-ea and -da), 10.5.2 and 9.12.3, and its parse-only main() on 8.11.4 and 7.0.0. [read] git tag --contains e92a38af90d lists every 7.x to 10.x release tag (8.8.2 to 8.11.4 are not tagged in the history clone; 8.11.4 was executed). Not affected: every release up to 6.6.6 [exec 5.5.5, 6.1.0, 6.2.0, 6.6.6]. Upstream main a95c754ee003, branch_10x and branch_10_5 have the same three processor files [read, 2026-10-26]
- 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
- semi-formal: code reading
- Upstream and prior reports
- none found (2026-10-26, read-only; the prior-report record verdict new): three scripted prior-report searches (147 queries over GitHub issues and PRs, commits, downstream repositories, dev@, java-user@, issues@ and commits@, the JIRA archive and the web; 167, 292 and 544 distinct hits, 0 high overlap) plus the finder's run (1,927 hits, 0 high) and the intended-behaviour check's (21 hits); manual GitHub searches (including all of GitHub for setMultiFields and WildcardQueryNodeProcessor), JIRA JQL, lists.apache.org and about 20 web queries; no hit is the same bug. Closest: #8409 (LUCENE-7355, the introducing change; its 34 comments never mention the multi-field path), #4428 (LUCENE-3355, open, a different order defect around the same processor: the AND default operator), #9070 (LUCENE-8022, same symptom family but classic parser, one field, an analyzer without normalize()). No open PR touches the three processors, StandardQueryParser or PerFieldAnalyzerWrapper (406 open PRs checked by touched files)
- Lucene commit tested
- main
ea57360912ed(commit of 2026-10-24, tested 2026-10-26), assertions on and off, seeds DEADBEEF, 0BADC0DE (5 iterations) and one random seed, through gradle in a scratch worktree (removed afterwards) and a standalone JUnit runner; the Maven Central jars of 10.5.2, 10.0.0 and 9.12.3; the fix with the full queryparser module suite; the follow-up's portable probe on 5.5.5, 6.1.0, 6.2.0, 6.6.6, 7.0.0, 7.7.3, 8.11.4, 9.0.0, 9.12.3, 10.0.0 and 10.5.2.
- Finding record
lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/StandardQueryNodeProcessorPipeline.java:32 (at ea57360912ed) ('The order they are assembled affects the results'), :48-49 (WildcardQueryNodeProcessor added before MultiFieldQueryNodeProcessor)lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/WildcardQueryNodeProcessor.java:52-80 (at ea57360912ed) (analyzeWildcard calls a.normalize(field, chunk) at :66 and :76; ':53 best effort'), :107-111 (postProcessNode passes fqn.getFieldAsString())lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/nodes/FieldQueryNode.java:99-102 (at ea57360912ed) (getFieldAsString returns null while the field is null)lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/MultiFieldQueryNodeProcessor.java:31-33 (at ea57360912ed) (javadoc: 'expand terms so the query looks for the same term in different fields'), :70-104 (preProcessNode sets the field only when it is null)lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/FuzzyQueryNodeProcessor.java:59, (at ea57360912ed) RegexpQueryNodeProcessor.java:43, TermRangeQueryNodeProcessor.java:138-139 (normalize with the expanded field, after MultiFieldQueryNodeProcessor)lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/StandardQueryParser.java:102-105 (at ea57360912ed) (field-less clause expanded over the setMultiFields fields), :357-362 (setMultiFields: 'the fields a query should be expanded to when the field is null')lucene/core/src/java/org/apache/lucene/analysis/Analyzer.java:202-213 (at ea57360912ed) (normalize: 'the representation that it would have in the index', used by query parsers for wildcard and fuzzy terms)lucene/analysis/common/src/java/org/apache/lucene/analysis/miscellaneous/PerFieldAnalyzerWrapper.java:26-43 (at ea57360912ed) (class javadoc example: StandardAnalyzer default, KeywordAnalyzer firstname/lastname; 'for both indexing and query parsing'), :74-77 (getWrappedAnalyzer: fieldAnalyzers.get(fieldName))lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/MultiFieldQueryParser.java:251-275 (at ea57360912ed) (classic getPrefixQuery/getWildcardQuery recurse per field before normalizing)https://github.com/apache/lucene/issues/8409 (LUCENE-7355, introducing issue)https://github.com/apache/lucene/issues/4428 (LUCENE-3355, different order defect around the same processor)https://github.com/apache/lucene/issues/9070 (LUCENE-8022, same symptom family, different cause)
- The finding in 30 seconds
Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/StandardQueryNodeProcessorPipeline.java:32, :48-49Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/WildcardQueryNodeProcessor.java:52-80, :105-116Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/nodes/FieldQueryNode.java:99-102Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/processors/QueryNodeProcessorPipeline.java:75-81Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/MultiFieldQueryNodeProcessor.java:31-33, :70-105Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/builders/PrefixWildcardQueryNodeBuilder.java:48; WildcardQueryNodeBuilder.java:44-45Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/FuzzyQueryNodeProcessor.java:59; RegexpQueryNodeProcessor.java:43; TermRangeQueryNodeProcessor.java:138-139Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/StandardQueryParser.java:102-105, :357-362Lucene main ea57360912ed: lucene/core/src/java/org/apache/lucene/analysis/Analyzer.java:202-213; AnalyzerWrapper.java:146 (stack trace)Lucene main ea57360912ed: lucene/analysis/common/src/java/org/apache/lucene/analysis/miscellaneous/PerFieldAnalyzerWrapper.java:28-43, :74-77
- Who is affected
Lucene main ea57360912ed: lucene/analysis/common/src/java/org/apache/lucene/analysis/miscellaneous/PerFieldAnalyzerWrapper.java:28-43 (javadoc example)
- Why it looks unintended
Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/StandardQueryParser.java:102-105, :357-362Lucene main ea57360912ed: lucene/core/src/java/org/apache/lucene/analysis/Analyzer.java:202-213Lucene main ea57360912ed: lucene/analysis/common/src/java/org/apache/lucene/analysis/miscellaneous/PerFieldAnalyzerWrapper.java:42-43a Lucene clone: git show releases/lucene-solr/7.0.0:lucene/MIGRATE.txt (lines 94-97)Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/MultiFieldQueryParser.java:251-274Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/StandardQueryNodeProcessorPipeline.java:32Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/MultiFieldQueryNodeProcessor.java:31-33Lucene main ea57360912ed: lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/standard/TestMultiFieldQPHelper.java:300-315
- Suggested fix
Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/MultiFieldQueryNodeProcessor.java:70-105 (preProcessNode on FieldableNode)Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/nodes/FieldValuePairQueryNode.java:26 (extends FieldableNode)Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/processors/QueryNodeProcessorPipeline.java:39, :119-125, :244-250 (public List, add(int), remove(int))Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/MultiFieldQueryNodeProcessor.java:76-85 (IllegalArgumentException without multi fields; empty list leaves the field null); StandardQueryParser.java:362-368
- TestMultiFieldWildcardNormalize.java (JUnit test):
repro/LFV-560/src/org/apache/lucene/queryparser/flexible/standard/TestMultiFieldWildcardNormalize.java(37 lines)