An ordered interval query must not let its parts overlap. When two adjacent parts are equal, Lucene takes a shortcut that skips that rule, which matters only when the part's own intervals can share a position: it spans several positions and can overlap itself, or it is a term stacked twice at one position.
ordered(ab, ab) on a b a: the two intervals of ab share the one b, the repeat shortcut joins them into [0,2] and reports a hit, while the unequal copy or(ab, zzz) finds none. Select the still to open it at full size. Generated with AI, checked against the code.- You search for "
anearb, twice, in order":Intervals.ordered(ab, ab)withab = Intervals.unordered(a, b), default settings, on a one-document index holdinga b a(positions 0, 1, 2). - You expect 0 hits: the text has one
b, and the two copies may not share it. The query parser'sfn:ordereddocs say "Source intervals must not overlap". - Instead you get 1 hit, interval [0,2]. The two arguments are equal, so Lucene merges them into one repeat source that joins two consecutive matches of a single
abiterator, [0,1] and [1,2], without checking that both use thebat position 1. - Write the second copy as an equivalent but unequal source,
or(ab, zzz)withzzzin no document, and you get the correct 0; Lucene 8.4.1 also gave 0. The phrasea atwice,ordered(phrase(a, a), phrase(a, a)), likewise matchesa a a, andordered(cat, cat)matches the one wordcatwhen the analysis chain indexes it twice at one position. - Matching documents get wrong intervals: on
a b a bthe query reports [0,2] and [1,3] instead of [0,3], andWeight#matchesreports the same. Affected: every release from 8.5.0 to 10.6.0, and main.
Anyone whose Intervals.ordered / unorderedNoOverlaps query (or fn: string) repeats back to back a piece whose own matches can share a position: a near pair, a phrase with a repeated word, a nested operator, or a word stacked twice at one position. Default settings, any index size. Single words with one token per position are not affected.
Mechanism, terms and numbers823 words
Intervals.ordered(X, X) asks for X and then X again, and as in every ordered interval query the two parts must not overlap. When the two arguments are equal (by equals(), so also two identically built sources), OrderedIntervalsSource.build replaces them with one RepeatingIntervalsSource, the repeat handling LUCENE-9099 added in 8.5.0. Its iterator joins consecutive intervals of a single X iterator, [first start, last end], and never checks that they are disjoint. For a single term with one token per position that is harmless, since its consecutive intervals are different positions; but when X spans several positions and can overlap itself, the copies share a position: ordered(unordered(a, b), unordered(a, b)) matches a b a (one b), ordered(phrase(a, a), phrase(a, a)) matches a a a, and the same happens through Intervals.unorderedNoOverlaps(X, X) and the query parser's fn:ordered and fn:unorderedNoOverlaps. A term stacked twice at one position (position increment 0) overlaps the same way: ordered(cat, cat) matches the one-word document cat indexed through KeywordRepeatFilter and a stemmer that leaves the word unchanged. Matching documents get wrong intervals too ([0,2], [1,3] instead of [0,3]), and Weight#matches reports them. With an equivalent but unequal second argument the query correctly finds nothing, and so did 8.4.1. Every release from 8.5.0 to 10.6.0 and main are affected with default settings. Dropping the deduplication from OrderedIntervalsSource.build fixes it; the repro and the queries, queryparser and highlighter suites pass with it.
- Interval: a span of positions
[start, end]in a field where a sub-query matches.Intervals.term("a")ona b agives[0,0]and[2,2];Intervals.unordered(a, b)("aandbin any order, as close as possible") gives[0,1]and[1,2], which share position 1;Intervals.phrase("a", "a")ona a agives[0,1]and[1,2]. - Stacked token: two tokens at one position (position increment 0), for example a word and its unchanged stem from
KeywordRepeatFilterand a stemmer. If both area, the postings keep both andIntervals.term("a")gives[0,0]twice. - ordered(X, Y): an interval that contains an X interval followed by a Y interval that starts after the X interval ends.
fn:orderedinStandardQueryParsermaps to it;unorderedNoOverlaps(X, Y)is built asor(ordered(X, Y), ordered(Y, X)).
OrderedIntervalsSource.buildcallsdeduplicate(OrderedIntervalsSource.java:29-38), which turns each run of consecutiveequals()sub-sources intoRepeatingIntervalsSource.build(source, runLength)(:40-57); a single run is returned on its own, renamed "ORDERED" (:53-55).RepeatingIntervalsSource.intervalswraps one iterator of the sub-source in aDuplicateIntervalIterator(RepeatingIntervalsSource.java:66-72). It cachesnconsecutive intervals of that iterator and reports the first cached start and the last cached end (:145-154,:172-200). Nothing compares one cached interval's start with the previous one's end.- For
ab = unordered(a, b)ona b athe cache holds[0,1]and[1,2], soordered(ab, ab)reports[0,2]: 1 hit with a singleb. - For unequal sub-sources each gets its own iterator, and
OrderedIntervalIteratoradvances each one whilecurrentStart <= prevEnd(OrderedIntervalsSource.java:144-151), so the copies can never share a position. That is whyordered(ab, or(ab, zzz))gives 0. RepeatingIntervalsSource.matchescombines the sub-matches the same way (:75-86), soWeight#matchesreports the same wrong interval (executed); highlighters built on it would show it too (read, not run).- A single term is safe only while each position holds it once.
TermIntervalsSourcereturns every posting position (TermIntervalsSource.java:114-120, 127-135), so a term stacked twice at position 0 yields[0,0]twice, the cache joins them into[0,0], andordered(a, a)matches a document with a single position.
One-document index, main a9779ef3c193, assertions on and off (the tests use the test framework's randomized config; the probes use a plain IndexWriterConfig and give the same answers):
| query | text | expected | actual |
|---|---|---|---|
ordered(ab, ab), ab = unordered(a, b) | a b a | 0 hits | 1 hit, [0,2] |
ordered(ab, ab) | a b a b | [0,3] | [0,2], [1,3] |
ordered(P, P), P = phrase(a, a) | a a a | 0 | 1, [0,2] |
ordered(P, P) | a a a a | [0,3] | [0,2], [1,3] |
unorderedNoOverlaps(P, P) | a a a | 0 | 1 |
ordered(ordered(a, a), ordered(a, a)) | a a a | 0 | 1 |
fn:ordered("a a" "a a"), fn:unorderedNoOverlaps(fn:phrase(a a) fn:phrase(a a)) | a a a | 0 | 1 |
ordered(a, a), unorderedNoOverlaps(a, a), a stacked | a twice at position 0 | 0 | 1, [0,0] |
ordered(a, a), a stacked at 0, then a at 1 | three tokens, two positions | [0,1] | [0,0], [0,1] |
ordered(cat, cat), KeywordRepeatFilter + PorterStemFilter | cat | 0 | 1, [0,0] |
controls: ordered(ab, or(ab, zzz)), ordered(P, or(P, zzz)), ordered(a, a, a, a), the documented fn:ordered(fn:phrase(brown fox) fn:phrase(fox jumps)), ordered(a, or(a, zzz)) on the stacked a, ordered(cat, cat) with RemoveDuplicatesTokenFilter | the same texts; the fox sentence | 0 | 0 |
Two separately built unordered(a, b) sources are equal, so they take the same path; fn:ordered("a a" "a a") builds two equal phrases from query text.
Exposure and likelihood337 words
- Who. Users of
Intervals.orderedorIntervals.unorderedNoOverlaps(Java API since 8.5.0) or ofStandardQueryParser'sfn:ordered/fn:unorderedNoOverlaps(query text since 9.1.0) whose query gives the same sub-source twice in a row, where that sub-source spans several positions and its consecutive intervals can share one: a near pair (unordered(a, b)), a phrase whose words repeat (phrase(a, a);phrase(ha, ha)onha ha haalso matches), or a nestedordered/unordered.ordered(X, X)is a direct way to ask for "X twice". Also anyone who repeats a single word that their analysis chain stacks twice at one position, for exampleKeywordRepeatFilterbefore a stemmer that leaves the word unchanged, withoutRemoveDuplicatesTokenFilter:ordered(cat, cat)then matches the one-word documentcat. - Effect. Silent extra hits, and wrong intervals on documents that do match.
Weight#matchesreturns the same wrong positions (executed), which can carry over into match-based highlighting and enclosing interval filters; of those onlymaxgapswas tested directly (containing,containedBy,notContainingfollow from the intervals, read). No exception, no hang, nothing written to the index. - Default vs opt-in. Nothing to enable: a plain
IndexWriterConfigwith the default codec, the default searcher, any index size, assertions on and off (executed by the probes); the JUnit tests use the test framework's randomized config and fail the same way. - Not affected. Repeats of single words when each position holds the word once (
ordered(a, a)ona a a), phrases that cannot overlap themselves (phrase(new, york): not by this overlap, though their gaps are still wrong through LFV-306), andunordered(P, P)matchinga a a, which is correct because unordered sources "may overlap" (Intervals.java:432). - How likely. It needs that query shape and a document where two of its intervals share a position; for such a pair the false hit happens every time. The trigger is structurally narrow and no report was found although it has shipped since 8.5.0 (2020); how often it happens in practice is unknown.
The test shows that Intervals.ordered(phrase(a, a), phrase(a, a)) matches the document a a a, although its only two a a intervals, [0,1] and [1,2], overlap (the two equal sub-sources are merged into a RepeatingIntervalsSource).
/*
* 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.queries.intervals;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestOrderedRepeatedPhraseOverlap extends LuceneTestCase {
public void testOrderedCopiesOfPhraseMustNotOverlap() throws Exception {
try (Directory dir = new ByteBuffersDirectory()) {
try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig(new StandardAnalyzer()))) {
Document doc = new Document();
doc.add(new TextField("f", "a a a", Field.Store.NO));
w.addDocument(doc);
}
try (DirectoryReader reader = DirectoryReader.open(dir)) {
IntervalsSource aa = Intervals.phrase("a", "a");
IntervalQuery q = new IntervalQuery("f", Intervals.ordered(aa, aa));
// "a a" occurs in 'a a a' only at [0,1] and [1,2], which overlap: expected 0 hits, actual 1
assertEquals(q + " on 'a a a'", 0, new IndexSearcher(reader).count(q));
}
}
}
}
# in a copy of this finding's repro folder (repro/LFV-807/, 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/queries/build/libs/lucene-queries-$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.queries.intervals.TestOrderedRepeatedPhraseOverlap
- Expected vs actual
- Expected: 0 hits, as for any ordered query whose sub-intervals would have to overlap. Actual: 1 hit (
expected:<0> but was:<1>).
More files, environment, full output38 words
| lucene | Lucene main e1faeba08bde (2026-11-02) |
|---|---|
| jdk | 25.0.3+9-LTS |
# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
=> java.lang.AssertionError: f:ORDERED(BLOCK(a,a),BLOCK(a,a)) on 'a a a' expected:<0> but was:<1>
org.junit.Assert.fail(Assert.java:89)
org.junit.Assert.failNotEquals(Assert.java:835)
org.junit.Assert.assertEquals(Assert.java:647)
org.apache.lucene.queries.intervals.TestOrderedRepeatedPhraseOverlap.testOrderedCopiesOfPhraseMustNotOverlap(TestOrderedRepeatedPhraseOverlap.java:44)- The
fn:ordereddocs say "Source intervals must not overlap";unorderedNoOverlapssays its sources "do not overlap". - An equivalent but unequal second copy,
or(X, zzz)withzzzin no document, gives 0 hits; only equal arguments take the overlapping path. - A regression: 8.4.1 gives 0, 8.5.0 gives 1. LUCENE-9099 added the shortcut for repeated single-position sources.
The full argument266 words
- The contract.
fn:ordered: "Source intervals must not overlap", with the examplefn:ordered(fn:phrase(brown fox) fn:phrase(fox jumps))"(Sources overlap, no matches.)" (intervalfn/package-info.java:328-356, LUCENE-10223, 9.1.0); upstreamTestMatchHighlighterasserts no hit for it and forfn:unorderedNoOverlapsof the same phrases (TestMatchHighlighter.java:396-397).Intervals.unorderedNoOverlaps: "Returns intervals in which both the subsources appear and do not overlap" (Intervals.java:440-447). - The cited algorithm. The
Intervalsclass javadoc says "These sources implement minimum-interval algorithms taken from the paper Efficient Optimally Lazy Algorithms for Minimal-Interval Semantics" (Boldi and Vigna,Intervals.java:43-45); there the ordered operator is the "ordered non-overlapping conjunction operator" and "the intervals must be nonoverlapping". - Design note. LUCENE-8828 (#9872, 2019, by the later author of LUCENE-9099): "NO_OVERLAPS(a, a) will end up as ORDERED(a, a)".
- Self-consistency.
ordered(ab, or(ab, zzz))(0) andordered(ab, ab)(1) differ ona b athough their sub-intervals are the same;ordered(a, a, a, a)gives 0 ona a a; the span analogue (SpanNearQueryin order over twonear(a a)) gives 0. - History. No evidence was found that LUCENE-9099 (#10141, lucene-solr#1097,
7c1ba1aebee) considered this case. It fixed two term cases:ORDERED(a, a, b)returning an extra interval andUNORDERED(a, a)matching a singlea. Its CHANGES entry says "repeated subterms", the class javadoc "repeated terms within an unordered interval", and every test it added repeats a single-position source: a term, or intestMaxGapsWithOnlyRepeatsthe disjunctionor(pease, hot)(TestIntervals.java:739-749). One approving review, no inline comments; nothing relaxes the no-overlap rule. 8.4.1 gives 0, 8.5.0 gives 1, and 8.5.0 with only thededuplicate()call removed gives 0 again.
The devil's-advocate check137 words
The intended-behaviour checker, the second cross-model review and the adjudication built the strongest "intended" case: the Intervals.ordered javadoc alone says only "intervals in which the subsources all appear in the given order" (Intervals.java:415-421), not "non-overlapping"; LUCENE-9099 sent equal sources through RepeatingIntervalsSource on purpose, which could mean "N consecutive occurrences of X" with its own semantics; and the package is experimental.
Why it fails: the paper the class javadoc cites, the fn:ordered docs, the unorderedNoOverlaps javadoc and LUCENE-8828 all state the no-overlap rule; the equal and unequal forms of one query disagree; no record shows LUCENE-9099 discussing sub-sources whose intervals can overlap each other; and "experimental" is about API stability, not wrong results. Verdict: not intended, a regression. Expect the bare ordered javadoc to come up first; answer with the fn:ordered sentence and the unequal-copy control.
Recommended Drop the deduplication in OrderedIntervalsSource.build (validated)
Drop the deduplication from OrderedIntervalsSource.build (+4/-24): the ordered iterator already gives every sub-source its own iterator and never lets two of them overlap. With it the 7 repro tests pass (assertions on and off, 10 iterations), as do the queries, queryparser and highlighter suites (1,774 tests) and the lint, format and javadoc checks.
static IntervalsSource build(List<IntervalsSource> sources) {
if (sources.size() == 1) {
return sources.get(0);
}
// No deduplication: OrderedIntervalIterator gives every sub-source its own iterator and never
// lets two sub-intervals overlap, so equal sub-sources need no special handling. Merging them
// into a RepeatingIntervalsSource would let the copies of a multi-position source overlap.
return new OrderedIntervalsSource(new ArrayList<>(sources));
}All options and trade-offs5 options · 711 words
One fix was applied in a scratch worktree at a9779ef3c193 and run through gradle; the alternatives were not run. Land it before, or together with, the fix for the repeat width sign (LFV-306): that one-line fix alone makes maxgaps(0, ordered(P, P)) match a a a, because the overlapping interval's gaps turn from 3 to -1.
deduplicate() goes away; build returns new OrderedIntervalsSource(new ArrayList<>(sources)) for two or more sources. Equal sub-sources then get separate iterators like unequal ones. UnorderedIntervalsSource keeps its own use of RepeatingIntervalsSource. The copy keeps what the old code passed (a fresh list, not the caller's varargs view). The hunt's version passed sources directly: the same results in every run, but it keeps the Arrays.asList view of the caller's array (Intervals.java:422-424), so changing that array later would change the query. The code comment's "multi-position source" could also name a term stacked at one position.
static IntervalsSource build(List<IntervalsSource> sources) {
if (sources.size() == 1) {
return sources.get(0);
}
// No deduplication: OrderedIntervalIterator gives every sub-source its own iterator and never
// lets two sub-intervals overlap, so equal sub-sources need no special handling. Merging them
// into a RepeatingIntervalsSource would let the copies of a multi-position source overlap.
return new OrderedIntervalsSource(new ArrayList<>(sources));
}Trade-offs
ordered(t, t)of a term now opens two postings iterators instead of one (small, not benchmarked).toStringstaysORDERED(...), andTestSimplifications(unorderedNoOverlaps(term, term)equalsordered(term, term)) still passes.- LUCENE-9099's own examples keep their answers:
ordered(a, a, b)ona a bgives only[0,2], andcontaining(ordered(x, y), ordered(a, a, b))ona x a b ygives 0. - It also removes the
ordered()half of LFV-306 (repeat width sign) and the ordered manifestations of three sibling findings in the same class (minExtent, nested width, disjunction rewrite); theirunordered()halves need their own fixes. - A PR also needs a CHANGES entry; no benchmark was run.
What the checks validated
- In a scratch worktree at
a9779ef3c193, gradle: the 7 repro tests pass with assertions on and off, and 50 of 50 plus 20 of 20 over 10 iterations (seedC0FFEE34);lucene/queries517 tests (includingTestIntervals43 andTestSimplifications12),lucene/queryparser535 andlucene/highlighter722 (includingTestMatchHighlighter), 0 failures, the same skips as stock; on stock the same suites fail only on the 7 repro tests;ecjLint,checkGoogleJavaFormatandrenderJavadocpass;tidychanges nothing. - Earlier, by shadowing the class on main: the hunt's repro, the independent reproducer's 11 tests and upstream
TestIntervals43 of 43 pass; in the hunt's bounded-exhaustive harness the ordered-repeat mismatches disappear only with this fix. - The stacked-term probe with the patched class shadowed on main: 0 of 12 checks bad, assertions on and off.
- The full validation is on main only; the 8.5.0 A/B ran only the four bug checks, so a backport to an older branch needs its own test run.
A serious alternative a committer may prefer: deduplicate only sub-sources whose intervals are always one position wide, such as terms, keeping the one-iterator saving for ordered(a, a). On its own it does not fix the stacked-term case, whose intervals are one position wide: the repeat iterator would also have to skip a cached interval that starts at or before the previous one ends.
Trade-offs
Needs a reliable "single-position" test; minExtent() is only a lower bound (or(a, phrase(b, c)) has minExtent 1 and two-position intervals), plus the same-position skip. More code for a small saving.
Advance the shared iterator until the next cached interval starts after the previous one ends.
Trade-offs
With one shared iterator it cannot go back to an interval it skipped, which the next match may need, so it would need a buffer: a re-implementation of what OrderedIntervalIterator already does with separate iterators.
A javadoc note on Intervals.ordered.
Trade-offs
Contradicts the fn:ordered docs and the unorderedNoOverlaps javadoc, and keeps the equal-vs-unequal disagreement.
Make the second copy unequal but equivalent: ordered(X, or(X, term-that-never-occurs)) (executed as the control in every test).
Trade-offs
Relies on an implementation detail (equality); an extra disjunction per query.
Products that build Intervals.ordered or unorderedNoOverlaps from user-written rules inherit this whenever a rule repeats the same self-overlapping piece back to back (a multi-position piece, or a word their analysis chain stacks at one position); no downstream product was read or run for this doc.
A bounded-exhaustive comparison ran Lucene's real interval iterators against an independently written reference semantics on every short document and thousands of interval expressions with repeated arguments. Extra intervals for ordered queries over equal arguments pointed at the deduplication; public-API JUnit tests on the stock jar confirmed it.
The method in full258 words
Method: bounded-exhaustive translation validation on the real jar (semi-formal tier), in two hunt units on interval queries.
- First signal (2026-10-26): a unit on span and interval queries ran the stock main jar's interval iterators against an independently written reference semantics on every small document and recorded extra and missing intervals for
orderedwith repeated arguments as a lead: "ordered(X, X)is built as aRepeatingIntervalsSource, which lets the two X intervals overlap". - Reduction (2026-10-27, main
a9779ef3c193): a second unit wrote its own reference and harness: 6,856 expressions of depth up to 3 over term, ordered, unordered, or, phrase, maxgaps(0|1) and atLeast(2 of 3) with repeated arguments, on every document of up to 6 positions over a 3-letter alphabet, with switchable fixes for every suspected fix site. At 4 positions (about 1.1 million checks per run) the mismatches fell from 3,596 to 1,098 when this fix was added to the others, and to 0 with all of them; at 6 positions (11,287,592 checks) 0 remained with all seven fixes. - Then a public-API JUnit test on the stock jar, controls first; checked by an independent reproducer, an intended-behaviour devil's advocate, reading-only reviews by a model from another family (OpenAI GPT-5.6) and an adjudication, then a history sweep and a prior-report search.
- The stacked-term case (2026-10-28) came from a cross-model fact check of this doc, which inferred it from the code; a public-API probe then executed it on main, 8.4.1, 8.5.0 and 10.5.2. The harness's documents hold one token per position, so it could not reach this case.
No prior report found (2026-10-27: 223 recorded queries across GitHub issues and PRs, all 409 open PRs, JIRA, the mailing lists, downstream repositories and the web). Closest: LUCENE-9099 (#10141), which introduced the shortcut for single-position repeats.
Searches6 searches
Verdict new (high confidence, not certain): 29 hits judged, none the same bug. Nearest: #10141 / LUCENE-9099 and lucene-solr#1097 (introduced it; single-position repeats only); #11177 / LUCENE-10140 (Matches sub-positions for repeated sub-iterators, fixed in 9.0, another fix site); #9872 / LUCENE-8828, #9347 / LUCENE-8300, #9243 / LUCENE-8196 (contract evidence from before the deduplication); #13819, #13871, #13937 (flattening and speedups that left it alone). Not fixed upstream: main 20ced71c1e6 and branch_10_6 still deduplicate, and no open PR touches either class. Keyword search cannot prove absence.
| Where | When | How | Found |
|---|---|---|---|
| apache/lucene issues and PRs, open and closed | 2026-10-27 | 70 queries (the hunt's prior-report tool in three runs plus 22 manual phrasings: class, method, symptom and contract) | no report; origin and contract items only (#10141, #9872, #9347, #9243) |
| Open apache/lucene PRs; recent activity | 2026-10-27 | changed files of all 409 open PRs; PRs updated since 2026-10-25 (51) and issues since 2026-10-24 (16) | none touches OrderedIntervalsSource or RepeatingIntervalsSource; #16899 (expansion limits) and #16913 (sloppy phrase) unrelated |
| Commits and branches | 2026-10-27 | 11 history checks on main, branch_10x and branch_10_6 | the deduplication is unchanged since LUCENE-9099 |
| JIRA archive (LUCENE); mailing lists | 2026-10-27 | 20 JIRA queries; 69 list queries over dev@, java-user@, issues@, commits@ and two Solr lists | no report |
| Downstream repositories (Elasticsearch, OpenSearch, Solr); the web | 2026-10-27 | 14 repository queries; 36 web queries recorded (18 run) | precursors of LUCENE-9099 and unrelated features only |
| apache/lucene issues and PRs: the stacked-term case | 2026-10-28 | 10 read-only queries on stacked tokens, synonyms, KeywordRepeatFilter and RemoveDuplicatesTokenFilter with intervals, and RepeatingIntervalsSource | no report; only #10141 names RepeatingIntervalsSource |
Introduced in 8.5.0 by LUCENE-9099 (7c1ba1aebee, 2020-02-06), which added the repeat shortcut. Executed: 8.2.0 to 8.4.1 pass, all 52 releases from 8.5.0 to 10.5.2 fail, and 8.5.0 with only the deduplicate() call removed passes.
Versions tested6 rows
Introduced by LUCENE-9099 "Correctly handle repeats in ORDERED and UNORDERED intervals" (now #10141; PR apache/lucene-solr#1097 by romseygeek, one approving review by jimczi): master 7c1ba1aebee and the branch_8x backport aa916bac3c3, both 2020-02-06; the backport is the form first released, in 8.5.0 (7c1ba1aebee itself first ships in 9.0.0). It created RepeatingIntervalsSource and OrderedIntervalsSource.deduplicate(). Later changes (LUCENE-9418, LUCENE-9570, LUCENE-10140, #12214, #13819, #13871, #13937, #15889) left the deduplication unchanged. All three shapes (the repeated phrase, the repeated unordered pair and the repeated nested ordered) fail from 8.5.0 on, including the nested one: before #13819 (10.1.0) flatten() inlined only OrderedIntervalsSource children, and ordered(a, a) was a RepeatingIntervalsSource. Reachable from query text since 9.1.0 (fn: functions, LUCENE-10223). Releases before 8.2.0 (sandbox package) were not run; they have no deduplication. The stacked-term probe gives the same boundary: 8.4.1 passes, 8.5.0, 10.5.2 and main fail.
| Version or commit | Verdict | Result |
|---|---|---|
main a9779ef3c193 | affected | this doc's 7 tests fail via gradle, assertions on and off; the hunt's test 4 of 4 with four seeds |
branch_10_6 head 4ae946e5a9b | affected | a hybrid run, not a full build of the branch: its intervals classes shadowed over the 10.5.2 jars, 4 of 4 fail; the two files involved are blob-identical in the branch and 10.5.2, so the released 10.6.0 has it |
| 8.5.0 to 10.5.2 (all 52 releases) | affected | probe: the 4 bug checks fail, every control passes; the JUnit fails 4 of 4 on 8.5.0, 8.11.4, 9.0.0, 9.1.0, 9.12.3, 10.0.0, 10.1.0 and 10.5.2 |
8.5.0 without the deduplicate() call | root-cause A/B | 4 of 4 pass |
| 8.2.0, 8.3.0, 8.3.1, 8.4.0, 8.4.1 | not affected | 0 hits for every repeated-copy query; 8.4.1 also with assertions off and on the stacked-term probe |
fix (fix.diff) on main | not affected | this doc's 7 tests pass; queries, queryparser and highlighter suites pass; the stacked-term probe passes |
An independent reproducer (7 of 11 own tests fail, 4 controls pass) and an intended-behaviour devil's advocate reproduced it; reading-only reviews by a model from another family (OpenAI GPT-5.6) agreed on the root cause; an adjudication confirmed it with a fresh seed, and this doc's gradle runs reproduce it on a9779ef3c193.
Checks and verdicts412 words
Every executing check wrote or re-ran its own test on stock main; every control passed.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | Exhaustive comparison pinned to one fix site; JUnit 4 of 4 fail on main with assertions on and off, and on 10.5.2 and 9.12.3; the fix passes. |
| independent reproducer | real | Own tests from the claim only: 7 of 11 fail on main (assertions on and off), 10.5.2 and 9.12.3; query-parser leg 3 of 5 fail; 8.4.1 passes; 11 of 11 pass with the fix; confirmed the LFV-306 coupling. |
| cross-model reviews (refute, intended) | not real (low, procedural); real (high) | An independent reviewer from another model family (OpenAI GPT-5.6) reviewed by reading only, since it could not run Java. The lenses agreed on the root cause; the refute lens pointed to the fn:ordered contract, which was added. A separate run ran code: the finder's test fails 4 of 4 on main with assertions on and off and on 10.5.2 and passes with the fix; a probe shows 4 bad and 6 good cases on stock and none bad with the fix. Its LUCENE-9099 probe gives identical output with and without the fix, but one of its five expectations is wrong (ordered(a, a, b) on a b a b legitimately matches [0,3]). |
| intended-behaviour devil's advocate | real | Built the strongest intended case and showed why it fails (the cited paper, the fn:ordered docs, LUCENE-8828, the span analogue); the finder's test fails on main and 10.5.2; LUCENE-9099's examples keep their answers with the fix. |
| adjudication | confirmed (high), silent wrong results | Re-ran the finder's test with a fresh seed (1A2B3C4D): 4 of 4 fail with assertions on and off. |
| Follow-up and audit | keep | History sweep over 57 releases and the 8.5.0 A/B, which also corrected the nested shape's first affected version to 8.5.0; prior-report search (new); an audit re-ran the test with seed A0D17F4 (4 of 4 fail). |
| this doc | reproduced; fix validated | 7 tests fail on stock main via gradle (assertions on and off); with the fix they pass, and so do the queries, queryparser and highlighter suites. Fact checks of this doc: one re-ran the tests on a fresh extraction of main and its own public-API probe on 8.4.1, 8.5.0, 10.5.2 and main; the other, by a model from another family, read the code and the recorded runs and raised the stacked-term case, which was then executed (probes/stacked-term/). |
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 492 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,381 words
- Registry title
- Intervals.ordered(X, X) with two equal sub-sources that span several positions lets the two copies of X overlap, so ordered(phrase(a, a), phrase(a, a)) matches 'a a a' and ordered(unordered(a, b), unordered(a, b)) matches 'a b a' (one b), while the same query with an equivalent but unequal second source finds nothing
- Area
- queries / intervals (Intervals.ordered and unorderedNoOverlaps with equal consecutive sub-sources: OrderedIntervalsSource.deduplicate, RepeatingIntervalsSource)
- Priority
- Medium, rank 193: A new silent over-match in every release since 8.5.0 that needs ordered or unorderedNoOverlaps to get the same multi-position self-overlapping source twice.
- Impact class
- silent wrong results
- Impact note
- Anyone whose interval query gives Intervals.ordered (or unorderedNoOverlaps, which is built from ordered) the same sub-source twice in a row, where that sub-source spans several positions and its consecutive intervals can share a position (a phrase with a repeated word such as phrase(ha, ha), an unordered or ordered pair such as unordered(a, b), or a nested ordered), through the Java factories or StandardQueryParser fn:ordered / fn:unorderedNoOverlaps strings, with default writer, codec, analyzer and searcher settings, at any index size, with -ea and -da, in every release since 8.5.0, gets extra matching documents and wrong match intervals (also seen by Weight#matches, highlighting and enclosing interval filters); repeats of single terms, the common case, and phrases that cannot overlap themselves are not affected, and nothing is written to the index.
- Summary
- You ask for two occurrences of X in order with Intervals.ordered(X, X), for example X = unordered(a, b) ('a near b', twice) or X = phrase(a, a), and expect the usual ordered rule that the sub-intervals do not overlap (the fn:ordered documentation says 'Source intervals must not overlap'). Instead, because the two arguments are equal, OrderedIntervalsSource.build merges them into a RepeatingIntervalsSource, which joins consecutive intervals of a single sub-iterator without checking that they are disjoint, so ordered(unordered(a, b), unordered(a, b)) matches 'a b a', which has only one b, and ordered(phrase(a, a), phrase(a, a)) matches 'a a a'. The same query with an equivalent but unequal second source, ordered(X, or(X, a term that occurs nowhere)), correctly finds nothing, and so did Lucene 8.4.1.
- Introduced
- 7c1ba1aebeea540b67ae304deee60162baee2e12 (master) and its branch_8x backport aa916bac3c3369a461afa06e384e070657c32973 (the same day; the first released form, in 8.5.0): 'LUCENE-9099: Correctly handle repeats in ORDERED and UNORDERED intervals' (apache/lucene-solr#1097 by romseygeek, one approving review by jimczi, no inline review comments; the JIRA issue is now apache/lucene#10141). It created RepeatingIntervalsSource and added OrderedIntervalsSource.deduplicate(), called from build() for every ordered() source [read: git log -S 'deduplicate(' (only hit), git log --follow RepeatingIntervalsSource.java (created there), git log -L of deduplicate, git blame at 7cecf795236] [exec: 8.4.1 passes, 8.5.0 fails, and 8.5.0 with only the deduplicate() call removed passes]. Later commits to the two files (LUCENE-9418, LUCENE-10140, #12214, #13819, #13871, #13937, #15889) left the deduplication unchanged [read] (2020-02-06 (7c1ba1aebee and the branch_8x backport aa916bac3c3))
- First affected version
- 8.5.0 [exec] (8.4.1, the last release before LUCENE-9099, passes [exec]; 8.5.0 with only the deduplicate() call removed passes [exec])
- Affected versions
- every release from 8.5.0 through 10.5.2 (52 releases: 8.5.0 to 8.11.4, all 9.x and all 10.x) [exec: a portable main() probe with the JUnit's assertions fails 4 of 4 bug checks with every control passing on all 52, -da; the unchanged JUnit fails 4 of 4 on 9.1.0, 9.12.3 (-ea and -da), 10.0.0, 10.1.0 and 10.5.2 (-ea and -da), and the same JUnit with its two test-framework imports moved to the pre-9.1 packages fails 4 of 4 on 8.5.0 (-ea and -da), 8.11.4 and 9.0.0]; main a9779ef3c193 [exec, -ea and -da]; branch_10_6 head 4ae946e5a9b [exec: its intervals package, shadowed on the 10.5.2 jars, fails 4 of 4], so the released 10.6.0 has it; upstream main 20ced71c1e6 [read: intervals package identical to a9779ef3c193]. Not affected [exec]: 8.2.0, 8.3.0, 8.3.1, 8.4.0 and 8.4.1. Reachable from query text (fn:ordered, fn:unorderedNoOverlaps) since 9.1.0 (LUCENE-10223); 8.5.0 to 9.0.0 through the Java Intervals API only. All three shapes (repeated phrase, repeated unordered pair, repeated nested ordered) fail from 8.5.0 on [exec]
- 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: exhaustive check
- Upstream and prior reports
- none found (follow-up prior-report record verdict new, confidence high, searched 2026-10-27 plus the finder's run and its web leg, read-only): 223 recorded queries, 205 executed: 70 GitHub issue and PR queries over apache/lucene (scripted prior-report searches plus 22 manual phrasings), a changed-file scan of all 409 open apache/lucene PRs, the PRs updated since 2026-10-25 (51) and issues updated since 2026-10-24 (16), 11 commit and branch history checks (main, branch_10x, branch_10_6), 20 LUCENE JIRA-archive queries, 69 mailing-list queries (dev@, java-user@, issues@, commits@ and two Solr lists), 14 downstream GitHub queries (Elasticsearch, OpenSearch, Solr) and 36 web queries. Nearest items, none the same bug: #10141 / LUCENE-9099 and lucene-solr#1097 (they introduced the deduplication; single-term repeats only), #11177 / LUCENE-10140 (Matches sub-positions for repeated sub-iterators, fixed in 9.0, other fix site), #9872 / LUCENE-8828, #9347 / LUCENE-8300 and #9243 / LUCENE-8196 (contract evidence from before the deduplication), #13819, #13871 and #13937 (flattening and speedups, deduplication unchanged), #16899 (merged, interval expansion limits) and #16913 (SloppyPhraseMatcher). Not fixed upstream: upstream main 20ced71c1e6 and branch_10_6 head still deduplicate in OrderedIntervalsSource.build, and no open PR touches OrderedIntervalsSource or RepeatingIntervalsSource. Re-check (2026-10-27, read-only GitHub API): the 51 apache/lucene PRs updated since 2026-10-25 and the 8 issues created since 2026-10-24 were listed and none is about this (the changed files of #16904, #16913, #16919 and #16923 touch no intervals code); searches for RepeatingIntervalsSource (only #10141), 'OrderedIntervalsSource deduplicate' (none), unorderedNoOverlaps (only #9872) and 'fn:ordered overlap' (none).
- Lucene commit tested
- main
a9779ef3c193(commit of 2026-10-26): this doc's two public-API test classes (7 tests) through gradle in a scratch worktree, stock and with the fix, assertions on and off and 10 iterations, plus the whole queries, queryparser and highlighter modules with the fix and, as a baseline, without it (2026-10-28). Hunt runs on maina9779ef3c193and the 10.5.2 and 9.12.3 jars; a history sweep over the 57 releases from 8.2.0 to 10.5.2 and thebranch_10_6head4ae946e5a9b(its intervals classes over the 10.5.2 jars); upstream main20ced71c1e6has a blob-identical intervals package (read). A stacked-termmain()probe on main (assertions on and off, stock and with the fix), 8.4.1, 8.5.0 and 10.5.2 (2026-10-28).
- Finding record
lucene/queries/src/java/org/apache/lucene/queries/intervals/OrderedIntervalsSource.java:29-38 (build calls deduplicate), :40-57 (deduplicate folds equal consecutive sub-sources into RepeatingIntervalsSource.build), :140-153 (OrderedIntervalIterator advances while currentStart <= prevEnd, so unequal sub-sources never overlap); RepeatingIntervalsSource.java:33-41 (class javadoc: 'useful for repeated terms within an unordered interval ... ensuring that multiple iterators do not match on a single term'), :172-200 (nextInterval/cacheNextInterval cache consecutive intervals of one sub-iterator with no disjointness check); Intervals.java:415-424 (ordered: 'intervals in which the subsources all appear in the given order'), :440-447 (unorderedNoOverlaps: 'appear and do not overlap', built as or(ordered(a, b), ordered(b, a))); the two source files are identical at Lucene main ea57360912edlucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/intervalfn/package-info.java:328-356 (fn:ordered: 'Source intervals must not overlap'; example fn:ordered(fn:phrase(brown fox) fn:phrase(fox jumps)) '(Sources overlap, no matches.)'); Ordered.java:35-41 (fn:ordered maps to Intervals.ordered); lucene/highlighter/src/test/org/apache/lucene/search/matchhighlight/TestMatchHighlighter.java:396-397 (no hits expected for the overlapping fn:ordered and fn:unorderedNoOverlaps examples)
- The finding in 30 seconds
lucene/queries/src/java/org/apache/lucene/queries/intervals/OrderedIntervalsSource.java:29-57, 140-153 @ a9779ef3c193lucene/queries/src/java/org/apache/lucene/queries/intervals/RepeatingIntervalsSource.java:33-86, 129-200 @ a9779ef3c193lucene/queries/src/java/org/apache/lucene/queries/intervals/MinimizingConjunctionIntervalsSource.java:41-51; Intervals.java:422-424, 445-447lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/intervalfn/Ordered.java:36-41lucene/queries/src/java/org/apache/lucene/queries/intervals/TermIntervalsSource.java:114-120, 127-135; lucene/core/src/java/org/apache/lucene/analysis/tokenattributes/PositionIncrementAttribute.java:30-34
- Who is affected
lucene/queries/src/java/org/apache/lucene/queries/intervals/Intervals.java:426-438 (unordered: 'The subsources may overlap')
- Why it looks unintended
lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/intervalfn/package-info.java:328-356lucene/highlighter/src/test/org/apache/lucene/search/matchhighlight/TestMatchHighlighter.java:396-397lucene/queries/src/java/org/apache/lucene/queries/intervals/Intervals.java:43-45, 415-447; RepeatingIntervalsSource.java:33-36lucene/queries/src/test/org/apache/lucene/queries/intervals/TestIntervals.java:739-749 (testMaxGapsWithOnlyRepeats repeats or(pease, hot))
- Suggested fix
lucene/queries/src/test/org/apache/lucene/queries/intervals/TestSimplifications.java:63-68lucene/queries/src/java/org/apache/lucene/queries/intervals/Intervals.java:422-424 (ordered passes Arrays.asList(subSources))
- TestOrderedRepeatedPhraseOverlap.java (JUnit test):
repro/LFV-807/src/org/apache/lucene/queries/intervals/TestOrderedRepeatedPhraseOverlap.java(48 lines)
Made in parallel with this doc from a probe run on main a9779ef3c193 (assertions on and off); the values on screen come from the probe's log. Line numbers on the diagram are at a9779ef3c193. Re-rendered on 2026-10-28 after the fact checks: the stacked-term trigger (from the stacked-term probe) was added and "not affected" narrowed to a term with one token per position.
assets/LFV-807/bug.png(492 KB)
![Diagram of the bug. Panel 1: the text 'a b a' at positions 0 to 2; the intervals of ab = Intervals.unordered(a, b) are [0,1] and [1,2] (grey), and Intervals.ordered(ab, ab) returns [0,2] (blue), using the one b in both copies: expected 0 hits, actual 1. The control Intervals.ordered(ab, Intervals.or(ab, zzz)) gives 0 hits. Panel 2, inside Lucene: deduplicate() sees that the second ab equals the first and builds one RepeatingIntervalsSource(ab, 2) named ORDERED; its single iterator caches [0,1] and [1,2] and returns [0,2] with no check that they are disjoint, while for unequal copies the ordered iterator makes the second copy start after the first ends. Panel 3: it happens for a phrase with a repeated word, a near pair, a nested ordered or a term stacked twice at one position; a repeated term with one token per position is not affected; a matching document gets wrong intervals ([0,2] and [1,3] instead of [0,3] on 'a b a b'); silent, also from query text (fn:ordered, fn:unorderedNoOverlaps) since 9.1.0.](../assets/LFV-807/bug.png)