All findings · Lucene finding · LFV-715 · complexphrase-prefix-regexp-skip-same-field-check

ComplexPhraseQueryParser skips its same-field check for prefix and regexp clauses inside a phrase

ComplexPhraseQueryParser rejects "jo* id:2?" at parse time because id is not the phrase's field, but it accepts "jo* id:2*", and the search then throws IllegalArgumentException; name:"id:2*" even searches field id. The class checks the field of term, wildcard, fuzzy and range clauses, but never of prefix or regexp clauses.

Summary

Priority
Low, rank 1055An unusual field-qualified prefix or regexp clause inside a complex phrase fails loudly at search time instead of at parse time in the opt-in parser.
Impact
wrong error
Since
2.9.0 (2009, prefix forms); regexp forms from 4.0.0-alpha (first final release 4.0.0); one-clause search on the other field from 6.4.0introduced 2009-07-06 · over 17 years
Affected
2.9.0 to 10.6.0 and main (21 releases and main reproduced)
Status
documented
Found by
non-formal: code reading of the parser's second-pass checks

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

The finding in 30 seconds

ComplexPhraseQueryParser in three steps

  1. It is the classic query parser plus wildcards, fuzzy words and ranges inside quotes: "jo* smith" becomes a span query that finds "john smith".
  2. It parses twice. Pass 1 treats each quoted phrase as one unit; pass 2 parses the text inside the quotes again, with the phrase's field as the default field.
  3. A span query can only combine clauses of one field, so pass 2 must reject any clause that names another field. The class javadoc says it does, and a test checks that "jo* id:1 smith" is rejected.
How ComplexPhraseQueryParser handles a clause for another field inside a phrase on field name: "jo* id:2?" goes through the same-field check and parse() rejects it; "jo* id:2*" goes to getPrefixQuery, which has no check, so parse() accepts it and the search throws IllegalArgumentException: Clauses must have same field.
The second pass of ComplexPhraseQueryParser: term, wildcard, fuzzy and range clauses go through the same-field check, prefix and regexp clauses go around it, so "jo* id:2*" reaches the search and fails there. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. Parse with ComplexPhraseQueryParser on default field name, over 2 documents: (name "john smith", id 1) and (name "jane doe", id 2). "jo* smith" finds john smith.
  2. Put a clause for another field inside the quotes. "jo* id:2?" is rejected by parse(): Cannot have clause for field "id" nested in phrase for field "name". So are id:2~1, id:[1 TO 2] and id:2 (for id:2 the message comes as the cause of a RuntimeException), so you expect the prefix "jo* id:2*" to be rejected too.
  3. Instead parse() accepts "jo* id:2*" and "jo* id:/2/", and search() throws IllegalArgumentException: Clauses must have same field. "jo* id:9*", whose prefix matches no term, returns 0 hits and no error.
  4. With one clause there is no error at all: the phrase name:"id:2*" runs as id:2 and returns jane doe; name:"id:/[12]/" returns both documents.
  5. Why: the second pass runs the field check only in newTermQuery, getWildcardQuery, getRangeQuery and getFuzzyQuery. The classic parser sends x* and /re/ words to getPrefixQuery and getRegexpQuery, which this class never overrides.

Who is affected

Applications that run end-user query text through the opt-in ComplexPhraseQueryParser (Solr's {!complexphrase} builds on it), default settings, when a user field-qualifies a prefix or regexp word inside quotes. They get an unchecked exception from search instead of a ParseException, silently 0 hits, or a search on the other field. Nothing is corrupted. Rare.

Mechanism, terms and numbers541 words

ComplexPhraseQueryParser parses the text inside quotes a second time and, as its javadoc says, checks that every clause in it is for the phrase's own field. The check runs for term, wildcard, fuzzy and range clauses but not for prefix (id:2*) or regular-expression (id:/2/) clauses, because the class never overrides getPrefixQuery or getRegexpQuery. So "jo* id:2*" parses and the search then throws IllegalArgumentException: Clauses must have same field., "jo* id:9*" silently matches nothing, and the one-clause phrase name:"id:2*" searches field id. The prefix gap is as old as the class (2.9.0, 2009), the regexp gap dates from 4.0.0-alpha, and both are still on main; two small overrides fix it.

The check and where it is missing

  • The class javadoc: the second pass parses phrase content "checking all embedded clauses are referring to the same field and therefore can be rewritten as Span queries" (ComplexPhraseQueryParser.java:51-53).
  • checkPhraseClauseIsForSameField (:153-164) throws ParseException("Cannot have clause for field ... nested in phrase for field ..."). It is called only from newTermQuery (:141-151, wrapped in a RuntimeException because that method cannot throw a checked exception), getWildcardQuery (:166-172), getRangeQuery (:174-182) and getFuzzyQuery (:198-205).
  • The classic parser's handleBareTokenQuery sends x* words to getPrefixQuery and /re/ words to getRegexpQuery (QueryParserBase.java:747-754). Both build their query directly (:709-715, :679-686), and ComplexPhraseQueryParser overrides neither, so their field is never checked. No commit ever added or removed such an override (git log -S).

What happens at search time (ComplexPhraseQuery.rewrite)

  • One clause: a lone TermQuery, MultiTermQuery or SynonymQuery is returned unchanged (:261-265). id:2* is a MultiTermQuery, so name:"id:2*" runs as id:2* on field id.
  • Several clauses: each clause is rewritten (:285). A foreign prefix or regexp that matches terms becomes a span clause on field id: a span term when it matches one term (id:2* rewrites to id:2, :317-318), a span OR when it matches several (:294-300, :371-414). Then new SpanNearQuery(...) (:333) throws IllegalArgumentException("Clauses must have same field.") (SpanNearQuery.java:119-126).
  • A foreign clause that matches nothing is replaced by a placeholder term on the phrase's field (:299-315, "Dummy clause because no terms found - must match nothing"): 0 hits, no error.

The numbers (probe on main, StandardAnalyzer, 2 documents; same outcomes with -ea and -da)

queryresult
"jo* smith", "jo* name:smi*"1 hit, john smith
"jo* id:2?", "jo* id:2~1", "jo* id:[1 TO 2]", name:"id:2?"parse() throws ParseException
"jo* id:2", name:"id:2"parse() throws RuntimeException wrapping a ParseException
"jo* id:2*", "jo* id:/2/"parse() ok; search throws IllegalArgumentException: Clauses must have same field.
"jo* id:9*"parse() ok; 0 hits, no error
name:"id:2*"parse() ok; rewritten id:2, 1 hit, jane doe
name:"id:/[12]/"parse() ok; rewritten id:1 id:2, 2 hits

Same-root sibling

With a synonym analyzer, a plain word for another field becomes a SynonymQuery through QueryBuilder.newSynonymQuery instead of newTermQuery, so "smith id:dogs" (with dogs -> dogs|dog) also parses and fails at search, while "smith id:cats" is rejected. The two overrides do not cover it; fix B below does.

Exposure and likelihood170 words
  • Who is hit: code that parses user queries with ComplexPhraseQueryParser, default parser settings, documented classic syntax, public API. The trigger is a field qualifier on a prefix or regexp word inside a quoted phrase, for example title:"jo* author:sm*". No index size, setting or concurrency is involved.
  • Several clauses, the foreign clause matches a term: the rejection moves from parse() (a checked ParseException) to IndexSearcher.search (an unchecked IllegalArgumentException). Code that catches ParseException to report a bad query does not see it.
  • Several clauses, the foreign clause matches nothing: no error and 0 hits.
  • One clause: no error; name:"id:2*" searches field id. That field is one the user typed, and the classic syntax lets them search it at top level anyway (id:2*), so nothing new is reachable.
  • Assertions: -ea and -da behave the same.
  • Likelihood: rare. A malformed query shape in an opt-in parser; no report was found in the 17 years since 2.9.0. An open proposal (#16820) would deprecate this parser for Lucene 11.

Minimal repro

One JUnit class, one test method, one query string, no index, public API only; it runs in the queryparser module's tests with Lucene's test framework. It parses "jo* id:2*" on default field name and expects the ParseException that "jo* id:2?" gets.

TestComplexPhrasePrefixClauseOtherField.java (one test class)30 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.complexPhrase;

import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.tests.analysis.MockAnalyzer;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestComplexPhrasePrefixClauseOtherField extends LuceneTestCase {

  public void testPrefixClauseForOtherFieldIsRejected() {
    ComplexPhraseQueryParser qp = new ComplexPhraseQueryParser("name", new MockAnalyzer(random()));
    // expected: ParseException, as for the wildcard clause in "jo* id:2?"; actual: parse() succeeds
    expectThrows(ParseException.class, () -> qp.parse("\"jo* id:2*\""));
  }
}
Command
cd $LUCENE && ./gradlew :lucene:queryparser:test --tests TestComplexPhrasePrefixClauseOtherField -Ptests.seed=DEADBEEF -Ptests.asserts=true
Expected
parse("\"jo* id:2*\"") throws ParseException, as parse("\"jo* id:2?\"") does.
Observed
Expected exception ParseException but no exception was thrown, with assertions on and off.

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

More files, environment, full output231 words

new ComplexPhraseQueryParser("name", new MockAnalyzer(random())) parses "jo* id:2*"; the test expects a ParseException, the type the wildcard, fuzzy and range checks already throw and the type both fixes throw here. Deterministic, single thread; it needs Lucene's test framework (LuceneTestCase as the base class, MockAnalyzer, expectThrows) and no package-private API.

The test shows the accepted parse only. The search-time IllegalArgumentException, the regexp form, the silent 0 hits and the one-clause search on field id are in the table above (a separate probe, not part of the repro).

Regression test: this same class (or its one method added to TestComplexPhraseQuery, next to the checkBadQuery lines); it passes with fix A and with fix B.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT): fails; release 10.6.0: fails; with fix A or fix B: passes
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux aarch64 (the only platform run; plain Java, no native code)
Command (environment)
cp $REPRO/TestComplexPhrasePrefixClauseOtherField.java $LUCENE/lucene/queryparser/src/test/org/apache/lucene/queryparser/complexPhrase/
cd $LUCENE && ./gradlew :lucene:queryparser:test --tests TestComplexPhrasePrefixClauseOtherField -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off fails the same way: -Ptests.asserts=false

Observed output

# gradle, stock 645d4fe14a20, -Ptests.seed=DEADBEEF -Ptests.asserts=true (stack trimmed; -Ptests.asserts=false: same failure)
   >     junit.framework.AssertionFailedError: Expected exception ParseException but no exception was thrown
   >         at __randomizedtesting.SeedInfo.seed([DEADBEEF:65C37B6BF24F3147]:0)
   >         at app/org.apache.lucene.test_framework@11.0.0-SNAPSHOT/org.apache.lucene.tests.util.LuceneTestCaseParent.expectThrows(LuceneTestCaseParent.java:1432)
   >         at app/org.apache.lucene.test_framework@11.0.0-SNAPSHOT/org.apache.lucene.tests.util.LuceneTestCaseParent.expectThrows(LuceneTestCaseParent.java:1418)
   >         at app//org.apache.lucene.queryparser.complexPhrase.TestComplexPhrasePrefixClauseOtherField.testPrefixClauseForOtherFieldIsRejected(TestComplexPhrasePrefixClauseOtherField.java:28)

:lucene:queryparser:test (FAILURE): 1 test, 1  failure

# standalone, seed DEADBEEF: main 645d4fe14a20 and 10.6.0, -ea and -da, fail with the same message

Why it looks unintended

  • The class javadoc says pass 2 checks that "all embedded clauses are referring to the same field"; the original test expects parse() to reject "jo* id:1 smith".
  • Term, wildcard, fuzzy and range clauses are checked: "jo* id:2?" is rejected, "jo* id:2*" is not.
  • The same missing getRegexpQuery override in MultiFieldQueryParser was fixed as a bug (#5943).
The full argument251 words
  • The class promises the check. ComplexPhraseQueryParser.java:51-53 (quoted above); the pass-1 comment says pass 2 runs "with different set of syntax restrictions (i.e. all fields must be same)" (:119-121).
  • The original test pins it at parse time. TestComplexPhraseQuery.java:87: checkBadQuery("\"jo* id:1 smith\""); // mixing fields in a phrase is bad; checkBadQuery (:127-135) expects qp.parse(...) itself to throw. It dates from the class's creation (LUCENE-1486).
  • Every other clause kind is checked. QueryParserBase.java:688-693 calls getPrefixQuery "similar to getWildcardQuery" and a prefix "a special case of generic wildcard term"; :706-707 documents "throw in overridden method to disallow" as the way to reject input.
  • Later changes kept the rule. LUCENE-5530 (#6593, 4.8.0) made author:"fred* smith" work and kept the rejection of other-field clauses. LUCENE-7614 (#8665, 6.4.0) added the one-clause fast path but did not lift the check: name:"id:2?" and name:"id:2" are still rejected (probe).
  • Precedent. In #5943 (LUCENE-4878) the issue body quotes Simon Willnauer: "This is in-fact a bug in the MultiFieldQueryParser [...] MultifieldQueryParser should override getRegexpQuery but it doesn't"; it was fixed with the missing override in 4.2.1.
  • The recorded searches found nothing that says otherwise. No javadoc, test, CHANGES or MIGRATE entry, and no issue, PR or list thread among the recorded searches, presents prefix or regexp clauses as exempt. The adjudication, the verifiers and the cross-model reviews rated the intended-behaviour argument weak; a cross-model fact check agreed for intent but rated it moderate as a "not worth changing" compatibility argument, so a behaviour-changing fix is something to discuss.
The devil's-advocate check181 words

Strongest case for "intended": (1) the classic parser's docs say wildcard searches work "within single terms (not within phrase queries)" (classic/package-info.java:162-163); (2) the input is malformed, and the multi-clause form still fails, only later; (3) the parser always validated loosely: in #2560 (LUCENE-1486) its author, Mark Harwood, introduced the list of supported in-phrase operators with "these are not necessarily definitive answers or guarantees of correctly implemented support", and "john -percival" is a known non-working case commented out in TestComplexPhraseQuery.java:81-82; (4) LUCENE-7614's CHANGES entry says the parser "ignores double quotes around single token prefix, wildcard, range queries", so name:"id:2*" could be read as id:2*; (5) #16820 proposes deprecating the parser.

Why it fails: (1) is the generic limitation that ComplexPhraseQueryParser exists to lift, and its own javadoc states the same-field rule. (2), (3) and (5) are about severity and priority, not intent; (3) is about operators, while the field rule has a dedicated check and a test. (4) proves too much: the same entry names wildcard and range queries, yet name:"id:2?" and name:"id:[1 TO 2]" are still rejected.

Suggested fix

Recommended A: run the existing check in getPrefixQuery and getRegexpQuery (validated)

Override getPrefixQuery and getRegexpQuery in ComplexPhraseQueryParser to run the existing checkPhraseClauseIsForSameField in the second pass, then call super (16 lines). Validated on main: the repro passes with assertions on and off, as do the complexPhrase tests (two seeds) and the whole queryparser test module. A third override (fix B) also closes the synonym path.

All options and trade-offs3 options ยท 613 words

Fix A is the finder's patch; an independent verifier wrote the same two overrides without reading it. Fix B adds one more override for the same-root synonym path. Both were validated with gradle in a scratch worktree of 645d4fe14a20. Both change behaviour (queries that parse today are rejected), so they are a choice: the narrow fix A, which leaves the synonym path open, or the complete fix B, which also turns the other-field term-clause error from a wrapped RuntimeException into a plain ParseException.

A: run the existing check in getPrefixQuery and getRegexpQuery (validated) (recommended)

Two overrides next to the four existing checks, 1 file, +16 lines.

  @Override
  protected Query getPrefixQuery(String field, String termStr) throws ParseException {
    if (isPass2ResolvingPhrases) {
      checkPhraseClauseIsForSameField(field);
    }
    return super.getPrefixQuery(field, termStr);
  }

  @Override
  protected Query getRegexpQuery(String field, String termStr) throws ParseException {
    if (isPass2ResolvingPhrases) {
      checkPhraseClauseIsForSameField(field);
    }
    return super.getRegexpQuery(field, termStr);
  }

Trade-offs

  • The smallest change, in the style of the existing checks. A query that used to parse and then fail (or return nothing, or search the other field) now fails in parse(); anyone relying on name:"id:2*" searching field id would get a ParseException (worth a CHANGES line).
  • Same-field prefixes and regexps ("jo* smi*", "jo* name:smi*", "jo* /smi.h/") keep working.
  • Leaves the synonym sibling ("smith id:dogs" with a synonym analyzer) open; fix B closes it.
  • Solr's {!complexphrase} would get the check: on apache/solr main its getPrefixQuery override calls super and getRegexpQuery is inherited (read, not run).

What the checks validated

  • Gradle, scratch worktree of 645d4fe14a20, OpenJDK 25.0.3: the repro fails on stock with assertions on and off and passes with the fix, on and off.
  • org.apache.lucene.queryparser.complexPhrase.* (12 tests: the repro 1, upstream TestComplexPhraseQuery 11) passes with seeds DEADBEEF and CAFEBABE; the whole :lucene:queryparser:test passes with seed DEADBEEF (31 test classes, 534 tests, 1 skipped, 0 failures).
  • An independent verifier's 9-test probe: 8 of 9 pass; only the synonym case ("smith id:dogs") still fails, as expected.
  • A probe of 13 queries with the fix: every other-field prefix and regexp clause, including "jo* id:9*" and name:"id:2*", is rejected by parse() with the usual ParseException; the same-field controls still find john smith.
  • Earlier, with the fix shadowed: the finder's, the independent reproduction's and separate tests and upstream TestComplexPhraseQuery all pass.

B: A plus a check in getFieldQuery(String, String, boolean), which also covers synonyms (validated)

Fix A plus one more override, +9 lines; every plain word passes through it before QueryBuilder builds a term, synonym, boolean or phrase query.

  @Override
  protected Query getFieldQuery(String field, String queryText, boolean quoted)
      throws ParseException {
    if (isPass2ResolvingPhrases) {
      checkPhraseClauseIsForSameField(field);
    }
    return super.getFieldQuery(field, queryText, quoted);
  }

Trade-offs

  • Also rejects "smith id:dogs" with a synonym analyzer at parse time.
  • Side effect: a term clause for another field ("jo* id:2") is then rejected with a plain ParseException instead of a RuntimeException wrapping one; the newTermQuery check becomes redundant on that path but is harmless.
  • A broader change than the bug strictly needs; the synonym path's history was not traced.

What the checks validated

  • Gradle, same worktree: the repro passes; the independent verifier's 9-test probe, including the synonym case, passes 9 of 9; the complexPhrase package (12 tests) and the whole :lucene:queryparser:test (31 classes, 534 tests, 1 skipped, 0 failures) pass with seed DEADBEEF.
  • The 13-query probe with fix B: every other-field clause, term clauses included, is rejected with ParseException; the same-field controls still find john smith.

C: document the gap (not recommended)

State that prefix and regexp clauses are not checked.

Trade-offs

Leaves the late unchecked exception, the silent 0 hits and the other-field search, and contradicts the class javadoc and the existing checkBadQuery test.

Downstream note

Solr's {!complexphrase} parser subclasses this class and adds no field check of its own: on apache/solr main (read 2026-11-01) its getPrefixQuery override calls super and getRegexpQuery is inherited; older Solr (8.8.1) inherits both. So by reading the source it has the same gap and would pick up the upstream fix (not run).

How it was found

Found by reading the parser's second-pass overrides while working out its same-field rule: four factory methods run the check, and the prefix and regexp factories are not overridden. Probes and a public-API JUnit then confirmed it on main, 10.5.2 and 9.12.3.

The method in full66 words

Non-formal: a hunt unit on the complex-phrase and extendable query parsers read ComplexPhraseQueryParser's pass-2 overrides. Its bounded-exhaustive harness did not generate clauses for another field and missed the gap; code reading found it, probes witnessed it, and a 4-test public-API JUnit (3 fail, the control passes) reproduced it. Independent checks, two of them by a reviewer from another model family (OpenAI GPT-5.6), then confirmed it.

Prior reports, history and checks

Prior reports

No prior report: 192 logged queries on 2026-10-27 over GitHub (Lucene and downstream), the Lucene and Solr lists, JIRA, Stack Overflow and the web, plus a re-check on 2026-11-01. The closest item is the same missing override in MultiFieldQueryParser (#5943), fixed in 4.2.1.

Searches3 searches

Related, not this bug: #2560 (LUCENE-1486, the parser's origin and its same-field test); #6593 (LUCENE-5530, 4.8.0, made fielded phrases work and kept the rejection; #6594 is its duplicate); #8665 (LUCENE-7614, 6.4.0, the one-clause fast path); #5943 (LUCENE-4878, the MultiFieldQueryParser precedent; #5842 and #7408 are the same story there); #9712 (LUCENE-8666, open, a different rewrite defect); #16820 (open, a proposal to deprecate this parser for Lucene 11). Searches cannot prove that no report exists.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene; Lucene.NET, OpenSearch, Elasticsearch, Solr)2026-10-27a scripted prior-report search (32 issue/PR, 6 commit, 4 downstream queries), manual gh searches, the changed files of all 409 open apache/lucene PRsbackground only: #2560, #6593, #6594, #8665, #5943, #5842, #7408, #9712, #16820
JIRA archive, mailing lists, Stack Overflow and the web2026-10-27every LUCENE (36) and SOLR (30) issue mentioning the parser; 51 list queries (40 through the scripted search on dev@, java-user@, issues@ and commits@; 11 by hand, including solr-user@, users@solr and dev@solr); 60 web queries (40 scripted, 20 by hand); Stack Overflow APIusage questions and the pre-4.8 fielded-phrase rejection; nothing about this gap
Upstream source and open PRs2026-11-01last commits to ComplexPhraseQueryParser.java on upstream main (2f424fd428da); gh search prs --state open "ComplexPhrase"; gh search issues "complexphrase prefix field"; #16820 statusfile unchanged since 2026-06-14 (#16255, a build commit; its only change to the file is a one-word comment typo fix); no open PR or issue found; #16820 open with 0 comments

History

Prefix forms are as old as the class: since LUCENE-1486 (f7fa5799719, 2009, first released in 2.9.0); regexp forms since 4.0.0-alpha, first final release 4.0.0 (LUCENE-2604 added regexp syntax); and the one-clause search on the other field since 6.4.0 (LUCENE-7614). All 21 releases run, from 2.9.0 to 10.6.0, and main are affected.

Versions tested7 rows
  • f7fa5799719 "Initial commit of LUCENE-1486" (2009-07-06, SVN era, no PR) put the check into four factory methods but not getPrefixQuery, although the classic grammar already sent prefix words there.
  • 838e19beae1 "LUCENE-2604: Added RegexpQuery support to QueryParser" (2010-08-30) added getRegexpQuery without an override here; first release 4.0.0-alpha (not run; its class is byte-identical to 4.0.0's), first final release 4.0.0.
  • LUCENE-7614 (52f2a77b78f on master, ac85a41cbef on branch_6x, 2017-01-05) made rewrite return a lone multi-term query unchanged. Before it, the one-clause form parsed and threw "Unknown query type" at search.
  • Attribution check: replacing only this class, the 2.9.0 and 4.0.0 sources with the overrides added pass and the unmodified ones fail; the 6.3.0 class on 6.4.0 jars brings back the 6.3.0 one-clause behaviour.
  • Releases not run have the same code (git tag --contains, code identity). Runs before 9.12.3 were made by the history step only.
Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20; 10.6.0 affectedminimal repro fails with assertions on and off
10.5.2, 10.0.0, 9.12.3, 9.1.0 affectedthe finder's JUnit fails 3 of 4 (multi-clause prefix and regexp, one-clause prefix)
9.0.0, 8.11.4, 7.7.3, 6.6.6, 6.4.0 affectedprobe: multi-clause forms fail at search; one-clause form searches field id
6.3.0, 5.5.5, 5.0.0, 4.10.4, 4.0.0 affectedprobe: multi-clause prefix and regexp accepted, fail at search; one-clause form accepted, "Unknown query type" at search
3.6.2, 3.1.0, 3.0.3, 3.0.0, 2.9.4, 2.9.0 prefix onlyprobe: multi-clause prefix accepted, fails at search; no regexp syntax yet
2.4.1 no classComplexPhraseQueryParser does not exist
main 645d4fe14a20 with fix A or fix B not affectedrepro passes; complexPhrase tests and the queryparser module pass

Independent checks

Every check found it real: an independent reproducer, an intended-behaviour devil's advocate, reviews by a reviewer from another model family (both executed), an adjudication and a credibility audit. This doc's gradle runs validated both fixes on main.

Checks and verdicts257 words

All verdicts and re-runs are listed in the collapsed table; each row names what it ran.

Each independent check and its verdict
CheckVerdictWhat it found
Finder candidatePublic-API JUnit fails 3 of 4 on main (-ea, -da), 10.5.2 and 9.12.3; the control test passes; with fix A the repro and upstream TestComplexPhraseQuery pass 15 of 15.
Cross-model refute review real (high)An independent reviewer from another model family (OpenAI GPT-5.6), executed: root cause agrees, fix fixes it, intended argument weak; its own tests fail on main and pass with the fix.
Independent reproducer realOwn 11-test JUnit: 5 fail, 6 controls pass, on main (-ea, -da), 10.5.2 and 9.12.3; its own two-override fix passes 11 of 11.
Cross-model intended-lens review real (high)OpenAI GPT-5.6, executed: intended argument weak; its contract test fails on main and passes with the fix.
Intended-behaviour devil's advocate real (high), low impactProbe fails 5 of 9 on main and 10.5.2; found the synonym sibling path.
Adjudication confirmed (high)Re-ran the independent reproduction's JUnit on main -ea and -da: 5 of 11 fail; 11 of 11 pass with the fix.
Credibility audit confirmedRe-ran the finder's JUnit on main with -da: 3 of 4 fail; the wrong-error class holds.
Minimal repro verifiedFails on 645d4fe14a20 with -ea and -da; passes with fix A.
This doc real; fixes A and B validatedGradle in a scratch worktree of 645d4fe14a20; standalone runs on main and 10.6.0; a 13-query probe with and without the fixes.

Diagram file

The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.

Sources and provenance

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

Finding record

LFV-715, snapshot of 2026-11-06

Registry title
ComplexPhraseQueryParser skips its same-field check for prefix and regular-expression clauses: "jo* id:2*" parses and then fails at search time with IllegalArgumentException, and name:"id:2*" searches field id
Area
queryparser / complexPhrase: ComplexPhraseQueryParser (pass-2 same-field check for clauses nested in a phrase)
Priority
Low, rank 1055: An unusual field-qualified prefix or regexp clause inside a complex phrase fails loudly at search time instead of at parse time in the opt-in parser.
Impact class
wrong error
Impact note
Hits applications that use the opt-in ComplexPhraseQueryParser (default parser settings, documented classic syntax) only when a user writes a prefix or regular-expression clause that names another field inside a quoted phrase: in a multi-clause phrase ("jo* id:2*", "jo* id:/2/") parse() accepts what it rejects for term, wildcard, fuzzy and range clauses, and the search then throws an unchecked IllegalArgumentException 'Clauses must have same field.' (or silently matches nothing when the foreign clause matches no term: "jo* id:9*" returns 0 hits and no error [exec: main 645d4fe14a20, -ea and -da, 2026-11-01]), while a one-clause phrase (name:"id:2*", name:"id:/[23]/") raises no error and searches field id; -ea and -da behave the same, no scale is needed, and nothing is corrupted.
Summary
You parse a quoted phrase such as "jo* id:2*" or "jo* id:/2/" with ComplexPhraseQueryParser on the default field name; its class javadoc says the second pass checks that all embedded clauses refer to the same field, and it rejects "jo* id:2", "jo* id:2?", "jo* id:2~1" and "jo* id:[1 TO 2]" at parse time, so you expect a parse error. Instead parse() succeeds, because the check is wired into newTermQuery, getWildcardQuery, getRangeQuery and getFuzzyQuery but the class never overrides getPrefixQuery or getRegexpQuery, and the search then throws IllegalArgumentException 'Clauses must have same field.'. A one-clause phrase such as name:"id:2*" also parses, and because rewrite returns a lone multi-term query unchanged, it searches field id and returns the document whose id is 2.
Introduced
f7fa57997191 (LUCENE-1486, GitHub #2560, the initial commit of ComplexPhraseQueryParser, 2009-07-06; Subversion era, no pull request) for the prefix forms: it applied checkPhraseClauseIsForSameField in newTermQuery, getWildcardQuery, getRangeQuery and getFuzzyQuery but never overrode getPrefixQuery, although the classic grammar already sent prefix tokens there (QueryParser.jj at f7fa5799719). The regexp forms come from 838e19beae1f (LUCENE-2604, #3678, 2010-08-30), which added regexp syntax and QueryParser.getRegexpQuery without an override in this class. The one-clause search on the other field comes from LUCENE-7614 (#8665; 52f2a77b78fc on master, backport ac85a41cbefa on branch_6x, 2017-01-05), whose rewrite fast path returns a lone MultiTermQuery unchanged; before it, the one-clause form threw 'Unknown query type' at search. Not a regression: the gap is as old as the class [read: git log -S getPrefixQuery and -S getRegexpQuery over every path of the file return no commit; executed: shadowing only this class, the 2.9.0 and 4.0.0 sources with the missing overrides pass while the unmodified sources fail, and the 6.3.0 class on 6.4.0 jars restores the 6.3.0 one-clause behaviour] (2009-07-06 (prefix forms, LUCENE-1486); 2010-08-30 (regexp forms, LUCENE-2604); 2017-01-05 (one-clause search on the other field, LUCENE-7614))
First affected version
2.9.0 [exec] (first release with ComplexPhraseQueryParser; 2.4.1 has no such class [exec]); the regexp forms from 4.0.0-alpha [read: same class blob as 4.0.0; 4.0.0 exec]; the one-clause search on the other field from 6.4.0 [exec] (2.9.0 to 6.3.0 also accept the one-clause form, but its search throws 'Unknown query type' [exec])
Affected versions
Every release from 2.9.0 through 10.6.0, and main a9779ef3c193 and 645d4fe14a20 (unreleased 11.0); never fixed. [exec] the minimal repro fails on main 645d4fe14a20 and on the 10.6.0 release jars (SHA-1 checked against Maven Central), -ea and -da (2026-11-01). [exec, version-portable probe of the history step] 2.9.0, 2.9.4, 3.0.0, 3.0.3, 3.1.0, 3.6.2, 4.0.0, 4.10.4, 5.0.0, 5.5.5, 6.3.0, 6.4.0, 6.6.6, 7.7.3, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and main (-ea and -da); [exec, JUnit] 9.1.0, 9.12.3, 10.0.0, 10.5.2 and main (finder's test), 10.5.2 and 9.12.3 (the independent reproducer's test), 10.5.2 (the intended-behaviour check's test). In 2.9.0-3.6.2 only the prefix forms exist (no regexp syntax: '/2/' is an ordinary term there and is rejected [exec]). Releases before 9.12.3 were run only by the history step, not re-run by the verifiers. Other point releases [read: git tag --contains and code identity]. Upstream main a95c754ee003, branch_10x 1309534296e and branch_10_6 2763db11b0a still have no getPrefixQuery/getRegexpQuery override [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 agreed, 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 (searched 2026-10-27, read-only): 192 queries (two scripted prior-report search specs over GitHub issues, PRs and commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists and the JIRA archive, plus web and Stack Overflow searches; manual gh searches over apache/lucene, Lucene.NET, OpenSearch, Elasticsearch and Solr), the changed-file lists of all 409 open apache/lucene PRs (none touches complexPhrase/, QueryParserBase.java or QueryParser.java), every LUCENE (36) and SOLR (30) JIRA issue mentioning the parser, and solr-user@, users@solr and dev@solr. No item describes prefix or regexp clauses skipping the same-field check. Related, not this bug: #2560 (LUCENE-1486, the parser's origin; its first test pins the same-field rule for a term clause), #6593 (LUCENE-5530, 4.8.0: made fielded phrases such as author:"fred* smith" work and kept the rejection of other-field clauses; #6594 is its duplicate), #8665 (LUCENE-7614, 6.4.0: the one-clause fast path), #5943 (LUCENE-4878, 4.2.1: the same kind of missing getRegexpQuery override, fixed as a bug in MultiFieldQueryParser; #5842 and #7408 are the same story there), #9712 (LUCENE-8666, open: a different rewrite defect), #16820 (open, 2026-10-12, no comments: a proposal to deprecate the complexPhrase parser for Lucene 11; context, not a report). Downstream: Solr's {!complexphrase} parser subclasses this class, and its getPrefixQuery override adds no field check and it has no getRegexpQuery override, so by code reading it inherits the gap (not executed). Not fixed upstream: main a95c754ee003 has no override.
Lucene commit tested
main 645d4fe14a20 (2026-10-28): the minimal test fails with assertions on and off, through gradle in a scratch worktree and standalone; with fix A or fix B it passes, as do the complexPhrase tests and the whole queryparser test module. Release 10.6.0 fails the same way; the history step reproduced it on 20 releases from 2.9.0 to 10.5.2. The hunt's evidence was gathered on a9779ef3c193, whose queryparser, spans and QueryBuilder code is identical.

Lucene and upstream sources

Finding record
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/complexPhrase/ComplexPhraseQueryParser.java:46-60 (at a9779ef3c193) (class javadoc; :51-53 the second pass checks 'all embedded clauses are referring to the same field and therefore can be rewritten as Span queries'), :136-151 (newTermQuery: check wrapped in RuntimeException), :153-163 (checkPhraseClauseIsForSameField, 'Cannot have clause for field ... nested in phrase'), :166-171 (getWildcardQuery), :174-181 (getRangeQuery), :198-204 (getFuzzyQuery); no getPrefixQuery or getRegexpQuery override; :257-265 (rewrite returns a lone TermQuery, MultiTermQuery or SynonymQuery unchanged), :299-316 (placeholder clause on the phrase field when a clause has no terms), :331-333 (new SpanNearQuery over all clauses)
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/QueryParserBase.java:679 (at a9779ef3c193) (getRegexpQuery), :690-716 (getPrefixQuery javadoc: 'a special case of generic wildcard term', '@exception ParseException throw in overridden method to disallow'), :735-761 (handleBareTokenQuery sends prefix tokens to getPrefixQuery and regexp tokens to getRegexpQuery)
  • lucene/queryparser/src/test/org/apache/lucene/queryparser/complexPhrase/TestComplexPhraseQuery.java:87 (at a9779ef3c193) (checkBadQuery("\"jo* id:1 smith\""), 'mixing fields in a phrase is bad')
The finding in 30 seconds
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/complexPhrase/ComplexPhraseQueryParser.java:46-60 (at 645d4fe14a20) (class javadoc, :51-53), :93-135 (two passes), :137-151 (newTermQuery), :153-164 (checkPhraseClauseIsForSameField), :166-172, :174-182, :198-205, :234-250, :257-333 (rewrite: :261-265 fast path, :285, :294-300 span OR via addComplexPhraseClause :371-414, :317-318 span term, :299-315 placeholder, :333 SpanNearQuery)
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/QueryParserBase.java:679-686 (at 645d4fe14a20) (getRegexpQuery), :688-715 (getPrefixQuery and javadoc), :735-761 (handleBareTokenQuery)
  • lucene/queries/src/java/org/apache/lucene/queries/spans/SpanNearQuery.java:119-126 (at 645d4fe14a20)
  • lucene/core/src/java/org/apache/lucene/util/QueryBuilder.java:395, (at 645d4fe14a20) :603 (newSynonymQuery)
Who is affected
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/package-info.java:129-130 (at 645d4fe14a20) ('You can search any field by typing the field name followed by a colon')
  • https://github.com/apache/lucene/issues/16820 (open, 0 comments, checked 2026-11-01)
Why it looks unintended
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/complexPhrase/ComplexPhraseQueryParser.java:51-53, (at 645d4fe14a20) :119-121
  • lucene/queryparser/src/test/org/apache/lucene/queryparser/complexPhrase/TestComplexPhraseQuery.java:81-82, (at 645d4fe14a20) :87, :127-135
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/QueryParserBase.java:688-693, (at 645d4fe14a20) :706-707
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/package-info.java:162-163 (at 645d4fe14a20)
  • https://github.com/apache/lucene/issues/5943 (issue body, read 2026-11-01)
Prior reports
  • gh api repos/apache/lucene/commits?path=lucene/queryparser/src/java/org/apache/lucene/queryparser/complexPhrase/ComplexPhraseQueryParser.java (2026-11-01: 54283150a816 newest)
  • gh search prs/issues, gh issue view 16820 (2026-11-01)

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

Repro files

About the diagrams

Drawn from an executed trace of the same queries on main (assertions on and off) and with fix A, and checked frame by frame against the code at 645d4fe14a20.

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.