- It is the classic query parser plus wildcards, fuzzy words and ranges inside quotes:
"jo* smith"becomes a span query that finds "john smith". - 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.
- 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.
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.- Parse with
ComplexPhraseQueryParseron default fieldname, over 2 documents: (name "john smith", id 1) and (name "jane doe", id 2)."jo* smith"finds john smith. - Put a clause for another field inside the quotes.
"jo* id:2?"is rejected byparse():Cannot have clause for field "id" nested in phrase for field "name". So areid:2~1,id:[1 TO 2]andid:2(forid:2the message comes as the cause of aRuntimeException), so you expect the prefix"jo* id:2*"to be rejected too. - Instead
parse()accepts"jo* id:2*"and"jo* id:/2/", andsearch()throwsIllegalArgumentException: Clauses must have same field."jo* id:9*", whose prefix matches no term, returns 0 hits and no error. - With one clause there is no error at all: the phrase
name:"id:2*"runs asid:2and returns jane doe;name:"id:/[12]/"returns both documents. - Why: the second pass runs the field check only in
newTermQuery,getWildcardQuery,getRangeQueryandgetFuzzyQuery. The classic parser sendsx*and/re/words togetPrefixQueryandgetRegexpQuery, which this class never overrides.
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 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) throwsParseException("Cannot have clause for field ... nested in phrase for field ..."). It is called only fromnewTermQuery(:141-151, wrapped in aRuntimeExceptionbecause that method cannot throw a checked exception),getWildcardQuery(:166-172),getRangeQuery(:174-182) andgetFuzzyQuery(:198-205).- The classic parser's
handleBareTokenQuerysendsx*words togetPrefixQueryand/re/words togetRegexpQuery(QueryParserBase.java:747-754). Both build their query directly (:709-715,:679-686), andComplexPhraseQueryParseroverrides neither, so their field is never checked. No commit ever added or removed such an override (git log -S).
- One clause: a lone
TermQuery,MultiTermQueryorSynonymQueryis returned unchanged (:261-265).id:2*is aMultiTermQuery, soname:"id:2*"runs asid:2*on fieldid. - Several clauses: each clause is rewritten (
:285). A foreign prefix or regexp that matches terms becomes a span clause on fieldid: a span term when it matches one term (id:2*rewrites toid:2,:317-318), a span OR when it matches several (:294-300,:371-414). Thennew SpanNearQuery(...)(:333) throwsIllegalArgumentException("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.
| query | result |
|---|---|
"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 |
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 exampletitle:"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 checkedParseException) toIndexSearcher.search(an uncheckedIllegalArgumentException). Code that catchesParseExceptionto 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 fieldid. 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:
-eaand-dabehave 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.
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.
/*
* 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*\""));
}
}
cd $LUCENE && ./gradlew :lucene:queryparser:test --tests TestComplexPhrasePrefixClauseOtherField -Ptests.seed=DEADBEEF -Ptests.asserts=true
- Expected
parse("\"jo* id:2*\"")throwsParseException, asparse("\"jo* id:2?\"")does.- Observed
Expected exception ParseException but no exception was thrown, with assertions on and off.
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.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT): fails; release 10.6.0: fails; with fix A or fix B: passes |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux aarch64 (the only platform run; plain Java, no native code) |
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
# 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- 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
getRegexpQueryoverride inMultiFieldQueryParserwas 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) expectsqp.parse(...)itself to throw. It dates from the class's creation (LUCENE-1486). - Every other clause kind is checked.
QueryParserBase.java:688-693callsgetPrefixQuery"similar to getWildcardQuery" and a prefix "a special case of generic wildcard term";:706-707documents "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?"andname:"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.
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.
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 onname:"id:2*"searching fieldidwould get aParseException(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 itsgetPrefixQueryoverride callssuperandgetRegexpQueryis 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, upstreamTestComplexPhraseQuery11) passes with seeds DEADBEEF and CAFEBABE; the whole:lucene:queryparser:testpasses 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*"andname:"id:2*", is rejected byparse()with the usualParseException; 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
TestComplexPhraseQueryall pass.
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 plainParseExceptioninstead of aRuntimeExceptionwrapping one; thenewTermQuerycheck 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.
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.
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).
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.
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.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene; Lucene.NET, OpenSearch, Elasticsearch, Solr) | 2026-10-27 | a scripted prior-report search (32 issue/PR, 6 commit, 4 downstream queries), manual gh searches, the changed files of all 409 open apache/lucene PRs | background only: #2560, #6593, #6594, #8665, #5943, #5842, #7408, #9712, #16820 |
| JIRA archive, mailing lists, Stack Overflow and the web | 2026-10-27 | every 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 API | usage questions and the pre-4.8 fielded-phrase rejection; nothing about this gap |
| Upstream source and open PRs | 2026-11-01 | last commits to ComplexPhraseQueryParser.java on upstream main (2f424fd428da); gh search prs --state open "ComplexPhrase"; gh search issues "complexphrase prefix field"; #16820 status | file 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 |
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 notgetPrefixQuery, although the classic grammar already sent prefix words there.838e19beae1"LUCENE-2604: Added RegexpQuery support to QueryParser" (2010-08-30) addedgetRegexpQuerywithout 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 (
52f2a77b78fon master,ac85a41cbefon branch_6x, 2017-01-05) maderewritereturn 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.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20; 10.6.0 | affected | minimal repro fails with assertions on and off |
| 10.5.2, 10.0.0, 9.12.3, 9.1.0 | affected | the 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 | affected | probe: 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 | affected | probe: 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 only | probe: multi-clause prefix accepted, fails at search; no regexp syntax yet |
| 2.4.1 | no class | ComplexPhraseQueryParser does not exist |
main 645d4fe14a20 with fix A or fix B | not affected | repro passes; complexPhrase tests and the queryparser module pass |
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.
| Check | Verdict | What it found |
|---|---|---|
| Finder | candidate | Public-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 | real | Own 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 impact | Probe 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 | confirmed | Re-ran the finder's JUnit on main with -da: 3 of 4 fail; the wrong-error class holds. |
| Minimal repro | verified | Fails on 645d4fe14a20 with -ea and -da; passes with fix A. |
| This doc | real; fixes A and B validated | Gradle in a scratch worktree of 645d4fe14a20; standalone runs on main and 10.6.0; a 13-query probe with and without the fixes. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 535 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,417 words
- 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 ona9779ef3c193, whose queryparser, spans and QueryBuilder code is identical.
- 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-121lucene/queryparser/src/test/org/apache/lucene/queryparser/complexPhrase/TestComplexPhraseQuery.java:81-82, (at 645d4fe14a20) :87, :127-135lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/QueryParserBase.java:688-693, (at 645d4fe14a20) :706-707lucene/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)
- TestComplexPhrasePrefixClauseOtherField.java (one test class):
repro/LFV-715/src/org/apache/lucene/queryparser/complexPhrase/TestComplexPhrasePrefixClauseOtherField.java(30 lines)
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.
assets/LFV-715/bug.png(535 KB)
