All findings · Lucene finding · LFV-560 · flexible-multifield-wildcard-normalized-with-null-field

StandardQueryParser with setMultiFields normalizes field-less wildcards with the wrong analyzer

With setMultiFields and per-field analyzers, the flexible StandardQueryParser normalizes a field-less Luc* before it knows the field, so it searches title:Luc* (0 hits) where typing title:Luc* searches title:luc* (1 hit, the document "Lucene"); with a Map.of per-field map, parse() throws NullPointerException. Every release since 7.0.0 is affected.

Summary

Priority
Low, rank 264Only the opt-in multi-field mode of the less common flexible StandardQueryParser with field-dependent normalizers is affected.
Impact
silent wrong results
Since
7.0.0 (LUCENE-7355 e92a38af90d with LUCENE-7429 af60048097a, both 2016)introduced 2016-07-12 · over 10 years
Affected
7.0.0 to 10.5.2 and main; 6.6.6 and older are not affected
Status
documented
Found by
semi-formal: a bounded scan of the parsers on the stock jars, then a JUnit test

AI-assisted page: this finding was first signalled by a semi-formal check (an exact check of the real jar, or a modelling hypothesis that a tool then witnessed) 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

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.

  1. Index one document titled "Lucene". The analyzer is a PerFieldAnalyzerWrapper: StandardAnalyzer (lower-cases) for title, WhitespaceAnalyzer (keeps case) for other fields. Call setMultiFields({"title"}), then parse("Luc*", null).
  2. You expect the same query as typing title:Luc*, which the parser makes title:luc*: 1 hit. Fuzzy Lucene~1 through the same expansion does become title:lucene~1 and finds it.
  3. Instead you get title:Luc* and 0 hits (Lu?ene too). The parser normalizes Luc one step before the step that gives the clause its field, so it asks the analyzer for field null, and the wrapper answers with its default analyzer.
  4. It also goes the other way. With the wrapper's own javadoc setup (StandardAnalyzer default, KeywordAnalyzer for firstname and lastname), Jo* becomes firstname:jo* lastname:jo*: 0 hits for "John".
  5. With a per-field map from Map.of, the null lookup throws: every field-less prefix or wildcard term with a literal part (Luc*, not a bare *) makes parse() fail with NullPointerException. From 7.0.0 to 10.5.2 and on main; swapping two processors fixes it.

Who is affected

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.

Terms

  • Multi-field mode: StandardQueryParser.setMultiFields(fields) plus parse(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 any DelegatingAnalyzerWrapper) picks an analyzer by field name with fieldAnalyzers.get(fieldName), else the default (PerFieldAnalyzerWrapper.java:74-77).

The bug (code at ea57360912ed)

  1. The pipeline adds WildcardQueryNodeProcessor first and MultiFieldQueryNodeProcessor second (StandardQueryNodeProcessorPipeline.java:48-49); each processor walks the whole tree before the next one (QueryNodeProcessorPipeline.java:75-81).
  2. WildcardQueryNodeProcessor calls analyzeWildcard(analyzer, fqn.getFieldAsString(), text) (WildcardQueryNodeProcessor.java:107-111), which calls a.normalize(field, chunk) on each literal chunk (:66, :76). For a field-less clause getFieldAsString() is still null (FieldQueryNode.java:99-102).
  3. The wrapper is asked for field null (AnalyzerWrapper.java:146): PerFieldAnalyzerWrapper falls back to its default analyzer, and a Map.of map throws NullPointerException on get(null).
  4. Only then does MultiFieldQueryNodeProcessor set the field and clone the node per field (MultiFieldQueryNodeProcessor.java:70-105). Nothing normalizes the text again; the builder makes new PrefixQuery(new Term(field, text), ...) (PrefixWildcardQueryNodeBuilder.java:48).
  5. 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 by AnalyzerQueryNodeProcessor (pipeline :58). Only prefix and wildcard terms see null.

The numbers

setup (setMultiFields, parse(q, null))querymain 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?enetitle:Luc*, title:Lu?ene: 0 hitstitle:luc*, title:lu?ene: 1 hit
sameLucene~1, Lucene (controls)title:lucene~1, title:lucene: 1 hitsame
javadoc setup: default StandardAnalyzer, firstname/lastname -> KeywordAnalyzer; doc "John"Jo*, J?hnfirstname:jo* lastname:jo*: 0 hitsfirstname: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 hitsall 14 agree: 1 hit
Exposure and likelihood274 words
  • Who is hit. Callers of StandardQueryParser with setMultiFields and parse(query, null) whose fields use analyzers that normalize differently, through PerFieldAnalyzerWrapper or another DelegatingAnalyzerWrapper. 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* selects abc-2 instead of ABC-1).
  • Crash variant. A per-field map that rejects null keys (Map.of, executed; by the JDK contract also ConcurrentHashMap, not executed) makes every such term with a literal part throw an unchecked NullPointerException from parse() instead of a QueryNodeException.
  • 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.

Minimal repro

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.

TestMultiFieldWildcardNormalize.java (JUnit test)37 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.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));
  }
}
Command
# 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) equals parse("title:Luc*", null), i.e. title:luc*. Actual: expected:<title:luc*> but was:<title:Luc*> (-ea and -da).

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

More files, environment, full output34 words

Environment

luceneLucene main e1faeba08bde (2026-11-02)
jdk25.0.3+9-LTS

Observed output

# 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)

Why it looks unintended

  • Javadoc: a field-less clause is expanded over the setMultiFields fields; typed with the field, title:Luc* becomes title:luc*.
  • The same parser normalizes fuzzy, regexp and range terms, and analyzes plain terms, per expanded field; the classic MultiFieldQueryParser normalizes wildcards per field.
  • 7.0 removed lowercaseExpandedTerms because 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-97 at 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 classic MultiFieldQueryParser expands 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 new normalize() 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.

Suggested fix

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 unchanged
All 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.

Run the multi-field expansion before wildcard normalization (validated) (recommended)

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 unchanged

Trade-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. StandardQueryNodeProcessorPipeline is a public List; code that edits it by index (for example get(0) expecting WildcardQueryNodeProcessor) sees the two processors swapped.
  • Cost. A field-less wildcard term is then normalized once per expanded field instead of once, as the classic MultiFieldQueryParser already does; not measured, and only noticeable with many fields or an expensive normalize().
  • Failure order for invalid configurations. Without setMultiFields, parse(query, null) makes MultiFieldQueryNodeProcessor throw IllegalArgumentException (MultiFieldQueryNodeProcessor.java:79-82); after the swap it throws before the wildcard processor runs, so a Map.of map then gives that exception instead of the NullPointerException. With an empty multi-field list the field stays null in 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:test with 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 seed 36B0F102C8846336. 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 PerFieldAnalyzerWrapper variant 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.

Keep the order and normalize wildcard terms after the expansion (not built)

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.

Make PerFieldAnalyzerWrapper return its default analyzer for a null field (not built, partial)

Removes the NullPointerException with Map.of maps; the prefix and wildcard terms stay wrong.

Trade-offs

  • It would also bypass a null key that a null-tolerant map holds on purpose (undocumented, but possible). Only useful as hardening next to the swap.

Workaround for users today: reorder the parser's own pipeline (validated)

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.

Downstream note

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.

How it was found

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#normalize form 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 StandardQueryNodeProcessorPipeline and 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 a PerFieldAnalyzerWrapper with a KeywordAnalyzer default 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.

Prior reports, history and checks

Prior reports

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.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene), downstream repositories, dev@, java-user@, issues@, commits@, the JIRA archive, the web2026-10-26three 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 run167, 292 and 544 distinct hits, 0 high overlap (finder: 1,927 hits, 0 high)
GitHub, manual2026-10-26issue and PR searches for the class names; all of GitHub for setMultiFields and WildcardQueryNodeProcessorno report; one downstream PR (2018) noticed the new normalization after an upgrade, not this bug
Open PRs2026-10-26all 406 open apache/lucene PRs by touched filesnone 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 issuesdifferent 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

History

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.

Versions and branches executed, with results
Version or commitVerdictResult
5.5.5, 6.1.0, 6.2.0, 6.6.6 before 7.0the follow-up probe: 0 mismatches (expanded terms lower-cased field-independently, after the expansion)
7.0.0 first affectedthe 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 affectedthe probe: the same 7 mismatches
9.12.3, 10.0.0, 10.5.2 affectedthis doc's test: 3 of 3 fail (10.5.2 with assertions on and off); the probe: 7 mismatches
main ea57360912ed affected3 of 3 fail with assertions on and off, gradle and runner
main + the fix not affected3 of 3 pass; queryparser module 538 tests, 0 failures

Independent checks

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).

Each independent check and its verdict
CheckVerdictWhat it found
Refute review (OpenAI GPT-5.6, read-only) not real, low confidence, proceduralIt 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 realOwn 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 weakIts 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 realNo intended-behaviour argument survived; its probe shows only prefix and wildcard skip per-field normalization, plus the reverse case ABC* -> id:abc*.
Adjudication confirmed, highRe-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 keepRe-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 fixedOne 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 validatedA 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.

Sources and provenance

Finding record, Lucene and upstream sources and repro files1,257 words

Finding record

LFV-560, snapshot of 2026-11-06

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.

Lucene and upstream sources

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-49
  • Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/WildcardQueryNodeProcessor.java:52-80, :105-116
  • Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/nodes/FieldQueryNode.java:99-102
  • Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/processors/QueryNodeProcessorPipeline.java:75-81
  • Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/MultiFieldQueryNodeProcessor.java:31-33, :70-105
  • Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/builders/PrefixWildcardQueryNodeBuilder.java:48; WildcardQueryNodeBuilder.java:44-45
  • Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/FuzzyQueryNodeProcessor.java:59; RegexpQueryNodeProcessor.java:43; TermRangeQueryNodeProcessor.java:138-139
  • Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/StandardQueryParser.java:102-105, :357-362
  • Lucene 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-362
  • Lucene main ea57360912ed: lucene/core/src/java/org/apache/lucene/analysis/Analyzer.java:202-213
  • Lucene main ea57360912ed: lucene/analysis/common/src/java/org/apache/lucene/analysis/miscellaneous/PerFieldAnalyzerWrapper.java:42-43
  • a 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-274
  • Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/StandardQueryNodeProcessorPipeline.java:32
  • Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/MultiFieldQueryNodeProcessor.java:31-33
  • Lucene 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

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

Repro files

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.