An interval query can filter a source by a reference such as or(york, "new york"). Because or() keeps only minimal matches, LUCENE-8477 (8.1.0) made several operators split such a reference into one filter per alternative. containedBy and within split it that way; overlapping, nonOverlapping and notWithin do not.
within splits R and finds "new york" right next to visit; notWithin reads R as one source, which keeps only york, two positions away, so it keeps visit too. Select the still to open it at full size. Generated with AI, checked against the code.- Index one document, "visit new york" (visit = position 0, new = 1, york = 2), and use the reference R =
or(york, "new york"), built with the defaultIntervals.or. within(visit, 1, R)finds 1 hit: visit is right before "new york". You expectnotWithin(visit, 1, R)to find 0, since the two are documented as opposites; with R = "new york" alone they give 1 and 0.- Instead
notWithin(visit, 1, R)also finds 1 hit, the same word [0,0]. Read as one source, the defaultor()keeps only minimal matches: "new york" [1,2] contains york [2,2] and is dropped, sonotWithinsees only york, 2 positions away.withinsplits R and checks each alternative. - The filters also disagree on "new york":
containedBy(new, R)= 1 butoverlapping(new, R)= 0 andnonOverlapping(new, R)= 1, whilemaxgaps(0, overlapping(new, R)), which can only remove intervals, gives 1. Withor(false, ...)every relation agrees.
Anyone calling overlapping, nonOverlapping or notWithin, or the fn: query-text forms, with a default or() reference whose alternatives contain one another, such as a word and a phrase ending in it. Default settings, every release since 8.1.0. overlapping misses hits; the two exclusion filters keep hits they should drop.
Mechanism, terms and numbers661 words
Intervals queries can filter a source by a reference that is an or() of alternatives. The default Intervals.or() keeps only minimal intervals, so an alternative such as "new york" [1,2] is dropped when it contains another one, york [2,2]. Since LUCENE-8477 (8.1.0), containedBy and therefore within pull such a reference apart and test every alternative; overlapping, nonOverlapping and notWithin were built without that rewrite and read the minimized or(). For one and the same reference the relations then contradict each other: on "visit new york", within(visit, 1, or(york, "new york")) and notWithin(visit, 1, or(york, "new york")) both return visit; on "new york", containedBy(new, R) matches but overlapping(new, R) does not, nonOverlapping(new, R) keeps new, and maxgaps(0, overlapping(new, R)) matches again because the nested form does split the reference. The same happens from fn: query text. With or(false, ...) every relation agrees. Every release from 8.1.0 to 10.5.2, 10.6.0 (its intervals sources at 4ae946e5a9b) and main is affected; 8.0.0 is consistent. Rewriting the disjunctions in the three factories (for overlapping on both operands, as its nested form already does) fixes it and passes the queries and queryparser test suites.
- Interval: the positions a source matches,
[start,end]. In "visit new york",yorkis [2,2] and the phrase "new york" is [1,2]. - Minimal intervals: the default
Intervals.or()drops an interval that contains another interval of the sameor(). Soor(york, "new york")yields onlyyork[2,2] on "visit new york". - Disjunction rewrite (LUCENE-8477, 8.1.0): for operators where that loses matches, Lucene builds one copy of the operator per alternative and ORs the copies (
Disjunctions.pullUp). Alternatives withminExtent()1 are grouped into oneor();extendraises the extent, sowithin/notWithinwith n >= 1 split even anor()of single words.or(false, ...)turns the rewrite off; the javadoc offers it for when you "care more about speed than about accuracy". - The relations:
within(x, n, r)=containedBy(x, extend(r, n, n));notWithin(x, n, r)=nonOverlapping(x, extend(r, n, n));overlapping(x, r)keeps x intervals that overlap an r interval.
// ContainedByIntervalsSource.java:28-30: the reference is split, one filter per alternative
return Intervals.or(Disjunctions.pullUp(big, s -> new ContainedByIntervalsSource(small, s)));
// Intervals.java:508-511
within(source, n, reference) -> containedBy(source, Intervals.extend(reference, n, n))
// Intervals.java:493-497, 467-470, 478-480: plain constructors, the reference is read as one or()
notWithin(minuend, n, subtrahend) -> new NonOverlappingIntervalsSource(minuend, Intervals.extend(subtrahend, n, n))
nonOverlapping(minuend, subtrahend) -> new NonOverlappingIntervalsSource(minuend, subtrahend)
overlapping(source, reference) -> new OverlappingIntervalsSource(source, reference)
// DisjunctionIntervalsSource.java:230-236: the or() iterator drops queued intervals that contain the current one
while (intervalQueue.size() > 0 && contains(intervalQueue.top(), start, end)) { ... pop ... }
// OverlappingIntervalsSource.java:78-83: the split exists, but only a parent (maxgaps, phrase, ...) calls it
public Collection<IntervalsSource> pullUpDisjunctions() {
return Disjunctions.pullUp(Arrays.asList(source, reference), ss -> new OverlappingIntervalsSource(ss.get(0), ss.get(1)));
}On "visit new york" with R = or(york, "new york"):
within(visit, 1, R)testsvisit[0,0] againstextend(york)= [1,3] andextend("new york")= [0,3]. The second contains it: 1 hit.notWithin(visit, 1, R)readsextend(R)as one source. The minimizedor()yields onlyyork, widened to [1,3], which does not touchvisit[0,0]:visitis kept, 1 hit.- On "new york",
overlapping(new, R)comparesnew[0,0] withyork[1,1] only: 0 hits, althoughnewoverlaps the alternative "new york" [0,1], whichcontainedBy(new, R)finds.nonOverlapping(new, R)andnotWithin(new, 0, R)keepnew. maxgaps(0, overlapping(new, R))asks its source for its alternatives (FilteredIntervalsSource.java:31-36), which splits the reference: 1 hit. A filter that can only remove intervals adds one.- With
or(false, ...)nothing is split anywhere, every relation seesyorkonly, and they agree:within0,notWithin1,containedBy0,overlapping0.
The same holds without a single-word alternative: with or("new york", "new york city") on "new york city hall", within(hall, 1, ...) and notWithin(hall, 1, ...) both match, because "new york city" [0,2] contains "new york" [0,1] and is dropped for notWithin.
Exposure and likelihood391 words
- Who. Callers of
Intervals.overlapping,Intervals.nonOverlappingandIntervals.notWithin, and offn:overlapping,fn:nonOverlappingandfn:notWithinin the flexibleStandardQueryParser, whose reference is a defaultIntervals.or()with alternatives that contain one another: a word and a phrase that starts or ends with it (york/ "new york"), or a phrase and a longer phrase ("new york" / "new york city"). Synonym or place-name lists can have this shape. - Settings. Default
IndexWriterConfigsettings and codec and a plainIndexSearcher, withStandardAnalyzeror the test framework'sMockAnalyzer; assertions on and off. Every run used one-document indexes; the mechanism works per document, so index size should not matter (reasoning, not run). The rewrite is on by default (Intervals.or(...)=or(true, ...)). - Effect. Silent wrong match sets in both directions:
overlappingmisses documents (0 instead of 1 on "new york");nonOverlappingandnotWithinused as exclusion filters keep documents they should drop (1 instead of 0 on "visit new york" and on "new york city hall"). Adding an alternative to the reference removesoverlappinghits and addsnonOverlapping/notWithinhits, and wrapping the filter inmaxgapsorphraseadds hits back. - Not affected. References with one alternative,
or()s of single words (two different single-position intervals never contain one another, so the minimizedor()drops nothing: 0 violations in 1,452 relation checks over all 363 documents of up to 5 words, executed),or(false, ...), andwithin/containedBythemselves. - How likely. Rated rare: it needs one of these three filters, a reference whose alternatives contain one another, and text where only the longer alternative decides. No report was found since 8.1.0 (2019). For such a query and document the wrong answer comes every time. The intervals package is marked experimental.
- Workaround on stock Lucene. Spell the alternatives out with the public factories, the structure the fix builds:
or(overlapping(x, york), overlapping(x, "new york"))(1 on "new york"),nonOverlapping(nonOverlapping(x, york), "new york")(0) andnotWithin(notWithin(visit, 1, york), 1, "new york")(0 on "visit new york"), all executed on main. Or useor(false, ...)everywhere for consistent minimized answers. - Downstream (code and documentation reading only, not executed). Elasticsearch's intervals query builds
any_ofwithIntervals.orand itsoverlapping/not_overlappingfilters withIntervals.overlapping/Intervals.nonOverlapping; Solr's intervals query parser exposeswithin,not_within,overlapping,not_overlappingandany_of.
The test shows that Intervals.within(visit, 1, ref) and Intervals.notWithin(visit, 1, ref) both match 'visit new york' when ref = Intervals.or(term(york), phrase(new, york)): within tests every alternative of the or(), notWithin reads the minimized or() that drops 'new york'.
/*
* 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;
import org.apache.lucene.tests.util.TestUtil;
public class TestWithinNotWithinDisjunction extends LuceneTestCase {
public void testWithinAndNotWithinAreComplements() throws Exception {
IndexWriterConfig iwc =
new IndexWriterConfig(new StandardAnalyzer()).setCodec(TestUtil.getDefaultCodec());
try (Directory dir = new ByteBuffersDirectory();
IndexWriter w = new IndexWriter(dir, iwc)) {
Document doc = new Document();
doc.add(new TextField("f", "visit new york", Field.Store.NO));
w.addDocument(doc);
w.commit();
try (DirectoryReader reader = DirectoryReader.open(dir)) {
IndexSearcher searcher = new IndexSearcher(reader);
IntervalsSource visit = Intervals.term("visit");
IntervalsSource ref = Intervals.or(Intervals.term("york"), Intervals.phrase("new", "york"));
int within = searcher.count(new IntervalQuery("f", Intervals.within(visit, 1, ref)));
int notWithin = searcher.count(new IntervalQuery("f", Intervals.notWithin(visit, 1, ref)));
// expected: 'visit' is either within 1 position of ref or not (1 hit); actual: both (2)
assertEquals("within=" + within + ", notWithin=" + notWithin, 1, within + notWithin);
}
}
}
}
# in a copy of this finding's repro folder (repro/LFV-828/, 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.TestWithinNotWithinDisjunction
- Expected vs actual
- Expected the one 'visit' interval to be within 1 position of ref or not (1 hit in total); actual: both queries match (
within=1, notWithin=1 expected:<1> but was:<2>), with-eaand-da.
More files, environment, full output35 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: within=1, notWithin=1 expected:<1> but was:<2>
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.TestWithinNotWithinDisjunction.testWithinAndNotWithinAreComplements(TestWithinNotWithinDisjunction.java:50)- The javadoc:
withinreturns intervals that "appear within" n positions of the reference,notWithinthose that "do not appear within" them; LUCENE-8629 addedwithinas the "inverse" ofnotWithin. - The default
or()is the mode the javadoc presents as accurate, yet onlyor(false, ...)answers consistently. - Under
maxgaps, which rewrites its child,overlapping(new, R)finds the match it misses alone.
The full argument429 words
- Documented as opposites.
Intervals.java:500:within"Returns intervals of the source that appear within a set number of positions of intervals from the reference";:485:notWithin"Returns intervals of the minuend that do not appear within a set number of positions of intervals from the subtrahend query".fn:notWithin: "Matches intervals of the source that do not appear within the provided number of positions from the intervals of the reference". LUCENE-8629 (#9675) proposedwithinandoverlappingas the "inverse of the already available NOT_WITHIN" and "NOT_OVERLAPPING". The repro uses single-position sources, so the separate question of a partly-near multi-position interval (LFV-830) does not arise. - Set logic.
containedBy("appear within intervals of the big query",:554) impliesoverlapping("overlap with intervals from another source",:473;fn:overlapping: "overlap with at least one interval of the reference") and excludesnonOverlapping("do not overlap with intervals from the subtrahend",:462). - The default is the mode the javadoc presents as accurate. The class note, written about gap-sensitive sources ("e.g. PHRASE and MAXGAPS"), says the rewrite ensures documents "are matched" and "if you care more about speed than about accuracy you can use"
or(false, ...)(:47-54);or(false, ...)"can miss valid hits due to minimization" (:131). LUCENE-8477's CHANGES entry: "Interval disjunctions could miss valid hits if some of the clauses of the disjunction are minimized away". Here the default is the inconsistent path. The note does not promise in so many words that every top-level relation expands its operands; a maintainer may read it that narrowly. - The same filter answers differently under a parent.
maxgaps(0, overlapping(new, R))andphrase(overlapping(new, R), york)match "new york" althoughoverlapping(new, R)alone has no interval there.maxgapsandphraserewrite their child at construction, as documented for gap-sensitive sources, so the difference itself is expected from the design; what it shows is that the module's own expansion finds a hit that the top-level filter misses. - Maintainers on the rewrite (#10244): romseygeek, "Intervals automatically rewrite disjunctions (and have done since LUCENE-8477) so that queries behave as your or I would expect them to"; jimczi, "If it's not correct, it's a bug, not a feature".
- Tests. No upstream test uses these filters with a reference whose alternatives contain one another:
TestDisjunctionRewriteshas no case for them, and the other uses (inTestIntervals,TestIntervalQuery, the match-highlighter tests andTestStandardQPEnhancements) have a single word, a phrase, anordered/unorderedpair, anextend, or anor()of single words (single words never contain one another, so minimization drops nothing there).
The devil's-advocate check334 words
Intervals use minimal-interval semantics ("These sources implement minimum-interval algorithms", Intervals.java:43), under which or(york, "new york") is york. The rewrite is documented for gap-sensitive sources ("e.g. PHRASE and MAXGAPS"); LUCENE-8477's commit message lists PHRASE, MAXGAPS and the four containment filters, not the overlap filters. In its review jimczi wrote "I am not sure if the rewriting should be the default option since minimum interval semantic is the basis of the paper", and earlier in #9523 "IMO we should document this unintuitive behavior rather than rewriting all queries in a non-optimal form". The fn: docs say "the algorithm is not required to return or highlight all matching interval alternatives", and the package is experimental. Two narrower readings follow: the nested-versus-top-level overlapping difference is the documented construction-time rewrite of gap-sensitive parents at work, and standalone overlapping / nonOverlapping are complementary over the minimized reference.
Why it fails: that explains overlapping or nonOverlapping taken alone, not the contradiction. within itself uses the non-minimized alternatives, so under either reading one of within and notWithin is wrong, and with or(false, ...) everything agrees, so the inconsistency lives only on the default path. romseygeek answered jimczi in the same thread ("I think we still need to make it possible to get accurate hits", "My suggestion is that we automatically rewrite things to match accurately"), and the rewrite was committed that way; #10244 later defends it. The fn: caveat is about which intervals a matching document returns or highlights; this bug changes which documents match, and notWithin returns an extra interval rather than missing one. The earlier cross-model reviews rated the argument moderate and rejected it, and the intended-behaviour checker built it and rejected it. The cross-model fact check (reading only) rated the intended reading weak for within / notWithin and strong for the two narrower cases taken alone. A maintainer could still choose either consistent fix (rewrite the overlap filters, or minimize in within / containedBy too, which would undo LUCENE-8477), which is why the adjudication's confidence is medium.
Recommended A2. Rewrite the reference in overlapping, nonOverlapping and notWithin, and the source too in overlapping
Rewrite the disjunctions in the three factories: overlapping is built as its nested form already is (one filter per pair of source and reference alternatives, ORed); nonOverlapping / notWithin chain one exclusion per reference alternative (+20/-4 in Intervals.java). The repro, 1,093 upstream tests and a nested-case guard test pass.
All options and trade-offs4 options ยท 1,172 words
Option A2 is recommended: validated with gradle on a9779ef3c193 (tests with assertions on, static checks) and with the repro runner (assertions off). The finder's sketch, option A, passes the same tests but loses a nested match that stock Lucene finds, which a cross-model fact check found and a later run confirmed. Options B and C were not run.
A2. Rewrite the reference in overlapping, nonOverlapping and notWithin, and the source too in overlapping (validated by the checks) (recommended)
In Intervals.java (+20/-4): overlapping is built exactly as its nested form already is (OverlappingIntervalsSource.pullUpDisjunctions): one OverlappingIntervalsSource per combination of a source alternative and a reference alternative, ORed. So top-level and nested overlapping agree on both operands. nonOverlapping and notWithin become a chain of one NonOverlappingIntervalsSource per reference alternative ("overlaps no alternative"), the AND that the LFV-291 fix also uses for notContainedBy. A reference with nothing to split gives one filter, as before, and or(false, ...) is never split. The guard test (1 test, public API) passes on stock main and with A2 and fails with option A.
public static IntervalsSource nonOverlapping(
IntervalsSource minuend, IntervalsSource subtrahend) {
- return new NonOverlappingIntervalsSource(minuend, subtrahend);
+ return nonOverlappingAll(minuend, subtrahend);
+ }
+
+ // A minuend interval must overlap no alternative of a disjunctive subtrahend. Like
+ // containedBy/containing (LUCENE-8477), look at every alternative instead of the minimized
+ // disjunction, and combine the exclusions as a conjunction.
+ private static IntervalsSource nonOverlappingAll(
+ IntervalsSource minuend, IntervalsSource subtrahend) {
+ IntervalsSource result = minuend;
+ for (IntervalsSource s : Disjunctions.pullUp(subtrahend, s -> s)) {
+ result = new NonOverlappingIntervalsSource(result, s);
+ }
+ return result;
}
...
public static IntervalsSource overlapping(IntervalsSource source, IntervalsSource reference) {
- return new OverlappingIntervalsSource(source, reference);
+ // overlap with any alternative of a disjunctive source or reference, as the nested form
+ // (OverlappingIntervalsSource.pullUpDisjunctions) already does
+ return Intervals.or(
+ Disjunctions.pullUp(
+ Arrays.asList(source, reference),
+ ss -> new OverlappingIntervalsSource(ss.get(0), ss.get(1))));
}
...
public static IntervalsSource notWithin(
IntervalsSource minuend, int positions, IntervalsSource subtrahend) {
- return new NonOverlappingIntervalsSource(
- minuend, Intervals.extend(subtrahend, positions, positions));
+ return nonOverlappingAll(minuend, Intervals.extend(subtrahend, positions, positions));
}Trade-offs
- Like every LUCENE-8477 rewrite it trades speed for accuracy (not benchmarked):
overlappingbuilds one filter per combination of source and reference alternatives, each with its own iterators; thenonOverlapping/notWithinchain builds the minuend iterator once, plus one iterator per reference alternative. - Expansion limit: A2's
overlappinguses the list pull-up, which throwsIllegalArgumentException("Too many disjunctions to expand")aboveIndexSearcher.getMaxClauseCount()combinations, as the nested form andphrasealready do. Top-leveloverlappingover anor()of 1,025 phrases now throws where stock main builds it (executed;maxgaps(0, ...)of it throws on stock main too). The chain uses the single-source pull-up, which has no limit, likecontainedByandnotContainedBytoday. - The chain's order is a hash set's iteration order, so
toString,equalsandhashCodeof the chain depend on it, though results do not (NON_OVERLAPPING(NON_OVERLAPPING(xx,EXTEND(bb,1,1)),EXTEND(aa,1,1)), executed); a PR could sort the alternatives (reasoning, not run). - It changes results for existing queries (that is the fix): top-level
overlappingwith a disjunctive source or reference,nonOverlapping/notWithinwith a disjunctive reference. The source side ofnonOverlapping/notWithinneeds no split at the document level: if a longer source interval overlaps nothing, the shorter one inside it overlaps nothing either (reasoning, not run).
What the checks validated
- Gradle on
a9779ef3c193in a scratch worktree, seed DEADBEEF, assertions on: the finder's 4 tests, the nested-filter probe and the 2 query-text tests pass, and so do the whole:lucene:queries(517 tests in 53 classes) and:lucene:queryparser(535 tests in 31 classes) test tasks and the 48 match-highlighter tests: 1,100 tests, 0 failures (1,093 upstream). - Gradle static checks of
:lucene:querieswith A2 and the guard test:checkGoogleJavaFormat,forbiddenApisMain,forbiddenApisTest,ecjLintMainandecjLintTestpass, and the guard test passes. - The repro runner, assertions off: the 4 + 2 repro tests, 117 upstream interval tests and 23 upstream query-parser tests pass. The guard test passes with assertions on and off.
- The probe with A2: every count in this doc consistent; the source-side case (
overlapping(or(new, "new york"), york)on "new york") is 1 at top level and 1 nested. - Bounded differential (10 x 10 operand pairs, 120 documents, top level and under
maxgaps(0, ...),maxgaps(1, ...)andphrase(..., cc)): A2 leaves all 300 nestedoverlappingexpressions as on stock main, while option A loses documents in 15 of them.
In Intervals.java (+17/-4): the same nonOverlapping / notWithin chain as A2, but overlapping = Intervals.or(Disjunctions.pullUp(reference, r -> new OverlappingIntervalsSource(source, r))), so only the reference is split. That loses a nested match that stock Lucene finds: maxgaps(0, overlapping(or(new, "new york"), or(york, "zzz qqq"))) on "new york" is 1 on stock main and with A2, but 0 with A, because the parent now maps over A's outer or() and no longer splits the source (executed, assertions on and off). In the bounded differential, A loses documents in 15 of the 300 nested overlapping expressions. It also leaves the source side of top-level overlapping alone.
public static IntervalsSource overlapping(IntervalsSource source, IntervalsSource reference) {
- return new OverlappingIntervalsSource(source, reference);
+ // overlap with any alternative of a disjunctive reference, as containedBy does
+ return Intervals.or(
+ Disjunctions.pullUp(reference, r -> new OverlappingIntervalsSource(source, r)));
}
(nonOverlapping and notWithin: as in A2)Trade-offs
Smaller than A2, but it regresses the nested case above, which none of the 1,100 tests covers (the guard test fails with A). It moves the clause limit: maxgaps(0, overlapping(new, or(1,025 phrases))) builds with A, where stock main throws (executed).
What the checks validated
- Gradle on
a9779ef3c193in a scratch worktree, seed DEADBEEF: the finder's 4 tests, the nested-filter probe and the 2 query-text tests pass, and so do the whole:lucene:queries(517 tests in 53 classes) and:lucene:queryparser(535 tests in 31 classes) test tasks and the 48 match-highlighter tests: 1,100 tests, 0 failures (1,093 upstream). - Assertions off: the intervals tests (122 in 9 classes) and the flexible standard query-parser tests (127 in 8 classes) pass.
- Static checks of
:lucene:querieswith A:forbiddenApisMainandecjLintMainpass;checkGoogleJavaFormatdoes not flagIntervals.java(only line wraps in the copied test files, now applied to the repro test, which still fails 4 of 4 on main and passes with A). - Earlier, with the standalone runner: the finder's, the independent reproducer's and both reviewers' own tests pass; in the law-checking harness the
within/notWithin"both" class and the inclusion-law failures disappear.
Make every relation read the minimized or(), as in 8.0.0.
Trade-offs
Consistent, but it undoes LUCENE-8477 for the containment filters and brings back the missed containedBy hits that LUCENE-8477 fixed. Not run.
State that the overlap filters read a disjunctive reference as minimized.
Trade-offs
Leaves within and notWithin both matching, and the same filter answering differently at top level and when nested.
Downstream: the Elasticsearch intervals query builds any_of and its overlapping / not_overlapping filters on these factories, and Solr's intervals query parser documents the same filters, so an exclusion rule written that way can let documents through (code and documentation reading only, not executed).
Semi-formal. An AI tool ran Lucene's real interval sources against relation laws that need no reference implementation (each interval is either within or not within a reference; containedBy implies overlapping) over every document of up to 5 positions, with the known interval fixes shadowed. The remaining violations were reduced by hand to a public-API JUnit.
The method in full186 words
Method: reference-free law checking (translation validation) of the real jar, the hunt's semi-formal tier: no model checker, solver or prover produced the signal.
- The harness ran real interval sources on an index of all 1,364 documents of up to 5 positions over a 4-letter alphabet and checked laws that need no reference semantics: each minimal interval of X is returned by exactly one of a relation and its negation;
containedBy(X, Y)is a subset ofoverlapping(X, Y);or()is commutative and idempotent. - Bounded-exhaustive over a 116-shape pool (116 x 116 pairs), plus 30,000 random depth-3 pairs per random leg, with the known interval fixes shadowed (the LFV-287 and LFV-291 sketches and seven earlier switches), so only new violation classes remained.
- The residual class "within and notWithin both return an alternative's interval" and the inclusion-law failures were reduced by hand to the public-API JUnit; with the fix added, both classes disappear from every leg.
- An earlier unit of the hunt had noted the
nonOverlappinghalf alone and judged it a design question; the complement and inclusion laws turn it into a contradiction between documented relations.
No prior report found: 224 read-only queries on 2026-10-28 over GitHub issues, PRs and commits, the Lucene lists, JIRA, Elasticsearch, OpenSearch and Solr, open-PR file lists and the web, re-checked later that day. Nearest: LUCENE-8477 itself, whose discussion never mentions the overlap filters.
Searches6 searches
No report, issue, PR, review comment, commit, list thread or downstream report of this bug was found. The closest item is the change that introduced it.
| Where | When | How | Found |
|---|---|---|---|
| GitHub apache/lucene and apache/lucene-solr issues, PRs and commits | 2026-10-28 | three scripted prior-report searches (identifiers; symptoms and the fn: functions; downstream) and 27 GraphQL searches (class and method names, pullUpDisjunctions, "new york") | no report; nearest #9523 / LUCENE-8477 and lucene-solr#620 (the rewrite; overlap filters never mentioned), #9243, #9675, #10244, #11442 / #11443 |
| Open and recent PRs | 2026-10-28 | the changed files of all 411 open apache/lucene PRs; the 73 issues and PRs updated since 2026-10-26; intervals commits on main, branch_10x and branch_10_6 | no PR touches the three factories; newest intervals commit #16899 (2026-10-26, unrelated) |
| Downstream (Elasticsearch, OpenSearch, Solr) | 2026-10-28 | scripted prior-report search downstream leg and own searches for not_overlapping, overlapping and any_of | no report; a generic Elasticsearch minimization caveat (its example is contained_by over a repeated phrase) |
| Mailing lists and JIRA | 2026-10-28 | dev@, java-user@, issues@ and commits@ (60 list queries over the three runs); 19 JIRA queries (12 by the tool, 7 manual) | no report |
| Web | 2026-10-28 | 11 queries (2 throttled) | no report |
| Re-check for this doc | 2026-10-28 | upstream main head and intervals commits; issues created since 2026-10-27; notWithin, nonOverlapping, Intervals.overlapping and pullUpDisjunctions searches | nothing new; upstream main 20ced71c1e6 has no later intervals commit |
Introduced by LUCENE-8477 (2019-03-27, first released in 8.1.0), which added the split to the containment filters but built the overlap filters without it. 8.0.0 is consistent; all 59 releases from 8.1.0 to 10.5.2, 10.6.0 (branch_10_6 4ae946e5a9b) and main are affected (executed), and a commit-level A/B pins it. Not fixed upstream.
Versions tested6 rows
Introduced by LUCENE-8477 "Automatically rewrite disjunctions when internal gaps matter" (#9523, apache/lucene-solr#620): f1782d0dd11 on master (released from 9.0.0) and 2571bf355f8 on branch_8x (released in 8.1.0), both 2019-03-27. It gave containedBy (and so within) the reference pull-up and extend() a pullUpDisjunctions(), but built overlapping and nonOverlapping as plain constructors and notWithin as NonOverlapping over extend. Before it, every relation read the same minimized or() and they agreed.
API history: nonOverlapping, notWithin and containedBy since 7.4.0, within and overlapping since 8.0.0, the fn: functions since 9.1.0. 8.1.0 and 8.1.1 shipped the package in lucene-sandbox; it is in lucene-queries from 8.2.0.
| Version or commit | Verdict | Result |
|---|---|---|
main a9779ef3c193 | affected | 4 of 4 fail (gradle and runner, assertions on and off); query-text test 2 of 2 |
branch_10_6 4ae946e5a9b (10.6.0, released 2026-10-29) | affected | 4 of 4 fail (its intervals sources over the 10.5.2 core) |
| 8.1.0 to 10.5.2 (59 releases) | affected | all four contradictions on each (history probe); the JUnit fails 4 of 4 on 10.5.2, 10.0.0 and 9.12.3 |
2571bf355f8 vs its parent b08566cacc7 | bisect | the commit's intervals sources fail 4 of 4 over the 8.1.0 jars; the parent's are consistent |
| 8.0.0 | not affected | consistent: every relation reads the minimized or() |
| 7.4.0, 7.7.3 | not affected | consistent on the one check that could run there, containedBy versus nonOverlapping (within and overlapping did not exist yet; notWithin did, but without within to compare against) |
The finder, an independent reproducer, an intended-behaviour devil's advocate and reviews from another model family (OpenAI GPT-5.6) said real; an adjudication confirmed it (medium confidence) and an audit kept it. Fact checks kept it and corrected the wording and the fix choice; this doc's gradle runs reproduced it and validated the fix.
Checks and verdicts535 words
Each check had its own lens.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | JUnit 4 of 4 fail on main (-ea, -da), 10.5.2, 9.12.3 and the branch_10_6 sources, still 4 of 4 with every other known interval fix shadowed; query-text test 2 of 2; the fix sketch passes them and 117 interval and 23 query-parser tests. |
| refute review (OpenAI GPT-5.6) | real, high | Own JUnit (within and notWithin over or(beta, phrase(alpha, beta)) both match) fails on main with two seeds and with -da; phrase-only control passes; passes with the fix. |
| independent reproducer | real | Own 7-test JUnit outside the intervals package: the 4 relation checks fail, the 3 controls pass, on main (-ea, -da, two seeds), 10.5.2 and 9.12.3; all pass with the fix. |
| works-as-designed review (OpenAI GPT-5.6, reading only) | real, medium | Intended argument rated moderate and rejected; a separate JUnit shows maxgaps(0, overlapping(...)) adding a match on main, fixed by the sketch. |
| intended-behaviour devil's advocate | real | Built the strongest minimal-interval case and rejected it; probes show maxgaps(0, overlapping(...)) and phrase(overlapping(...), york) matching where overlapping(...) has no interval (main -ea/-da, 10.5.2). |
| adjudication | confirmed, medium | Fresh seed 5EED1234: the reproducer's JUnit 4 of 7 fail with -ea and -da, controls pass. Not a duplicate (LFV-291 is the counterpart at another fix site); security none. |
| credibility audit | keep | Own StandardAnalyzer / plain IndexSearcher probe: within and notWithin both 1, containedBy 1 vs overlapping 0, maxgaps(0, overlapping) 1; or(false, ...) consistent; finder JUnit 4 of 4 with a fresh seed. |
| doc step (gradle) | reproduced; fix validated | Gradle in a scratch worktree at a9779ef3c193: stock 7 of 7 repro tests fail with -ea and -da; option A passes them and 1,093 upstream tests (queries, queryparser, match highlighter), also with -da for the interval and flexible-parser suites; option A2 the same with -ea. A plain-Java probe with default settings gives every count in this doc. |
| fact check | holds; 2 wording errors fixed | Re-ran the gradle repro (4 of 4 fail, -ea and -da), the runner on main, 10.5.2 and 9.12.3 with a fresh seed, and its own probe; bug, mechanism, history and security hold. Two wording errors (the repro's controls; why an or() of single words is unaffected) are fixed. |
| cross-model fact check (OpenAI GPT-5.6, reading only) | holds; 7 errors fixed | Confirmed the central observation and the history. Found by reading that option A loses a nested overlapping match; a later run confirmed it, so A2 is now recommended. The other six errors (no clause guard on A's expansion, the controls claim, the single-word reason, the analyzer, iterator reuse, the 7.x API) are fixed. |
| diagram check | pass | Every on-screen value, code line and trace matches the code at a9779ef3c193 and the executed probe; sizes, contrast and vendor scrub pass. |
| resolution runs | A regresses, A2 does not | Probe on stock main, A and A2 (assertions on and off): the nested case is 1 / 0 / 1; a bounded differential of 1,200 expressions finds 15 nested overlapping losses with A and none with A2; the guard test fails only with A; A2 passes the static checks and the interval and query-parser tests with assertions off. |
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 463 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,734 words
- Registry title
- Intervals.overlapping, nonOverlapping and notWithin read a disjunctive reference through the minimized or(), while containedBy and within test every alternative, so for the reference or(york, "new york") the same interval is reported both within and not within n positions of it, and containedBy(new, ref) matches 'new york' where overlapping(new, ref) does not
- Area
- queries / intervals (overlap filters Intervals.overlapping / nonOverlapping / notWithin vs the LUCENE-8477 disjunction rewrite that containedBy and within apply to their reference)
- Priority
- Low, rank 265: Needs an overlap filter with an or() reference whose alternatives contain one another, a rare query shape.
- Impact class
- silent wrong results
- Impact note
- Anyone who calls Intervals.overlapping, Intervals.nonOverlapping or Intervals.notWithin (or writes fn:overlapping / fn:nonOverlapping / fn:notWithin for the flexible StandardQueryParser) with a default Intervals.or() reference whose alternatives contain one another, such as or(york, "new york") or or("new york", "new york city"), with default writer, codec, analyzer and searcher settings, at any index size, with -ea and -da, in every release from 8.1.0 to 10.6.0 and on main, silently gets match sets that contradict the containment filters over the same reference: overlapping misses hits that containedBy and within find, and nonOverlapping / notWithin keep intervals that lie inside or next to the longer alternative, so an exclusion filter lets through documents it should drop; single-alternative references and or(false, ...) give consistent answers [exec: the controls]; downstream intervals queries that build any_of with Intervals.or and offer overlapping / not_overlapping filters reach the same factories [read by the finder in Elasticsearch's IntervalsSourceProvider; not executed].
- Summary
- You index 'visit new york' and run within(visit, 1, R) and notWithin(visit, 1, R) with R = or(york, "new york"), and you expect exactly one of them to return 'visit', because the javadoc defines notWithin as the intervals that do not appear within n positions of the reference. Instead both return the same interval [0,0]: within is built on containedBy, which pulls the or() apart (LUCENE-8477) and sees 'new york' right next to 'visit', while notWithin, overlapping and nonOverlapping read the default or() as one minimized source, which drops 'new york' because that interval contains 'york'. In the same way containedBy(new, R) matches 'new york' but overlapping(new, R) does not, and maxgaps(0, overlapping(new, R)) does match, because the nested form does pull the reference apart.
- Introduced
- f1782d0dd1195c823c79aab87529ebd7e8217b95 (master, released from 9.0.0) and its branch_8x backport 2571bf355f82e193d50ec2509f83ba698b262562 (the first released form, in 8.1.0), both 2019-03-27: 'LUCENE-8477: Automatically rewrite disjunctions when internal gaps matter' (apache/lucene-solr#620 by romseygeek; the JIRA issue is now apache/lucene#9523). It gave containedBy (ContainedByIntervalsSource.build) the reference pull-up and extend() a pullUpDisjunctions(), so within = containedBy(source, extend(reference)) tests every alternative, but rewrote overlapping and nonOverlapping as plain new OverlappingIntervalsSource / NonOverlappingIntervalsSource constructors (and notWithin as NonOverlapping over extend) without a top-level pull-up; OverlappingIntervalsSource.pullUpDisjunctions runs only when the filter is nested [exec: commit-level A/B with the intervals sources of the branch_8x parent b08566cacc7 and of 2571bf355f8 over the 8.1.0 jars: parent consistent, commit affected 4 of 4] [read: git blame on main; the nonOverlapping and overlapping bodies are f1782d0dd11, the notWithin body is that line reformatted by LUCENE-9570 (8ef6a0da568), within is unchanged since LUCENE-8629 (7d34bfdaad3)]. A regression of the relations' mutual consistency: 8.0.0 is consistent because every relation read the same minimized or() (2019-03-27 (author date of f1782d0dd11 and of the branch_8x backport 2571bf355f8))
- First affected version
- 8.1.0 [exec] (8.1.0 and 8.1.1 shipped the intervals package in lucene-sandbox as org.apache.lucene.search.intervals; LUCENE-8856 moved it to lucene-queries in 8.2.0). Last unaffected release 8.0.0 [exec]: there within, containedBy and the overlap filters all read the minimized or() and agree (all miss the 'new york' alternative). API age: nonOverlapping, notWithin and containedBy since 7.4.0 (LUCENE-8196), within and overlapping since 8.0.0 (LUCENE-8629), the fn: query-text functions since 9.1.0 (LUCENE-10223) [read]
- Affected versions
- every release from 8.1.0 through 10.5.2 and 10.6.0 (its intervals sources at branch_10_6 RC1 4ae946e5a9b [exec], below) [exec: the follow-up's history probe shows all four contradictions (within and notWithin both on 'visit new york' and on 'new york city hall', containedBy without overlapping, containedBy and nonOverlapping on 'new york'), with single-alternative controls passing, on each of the 59 releases 8.1.0, 8.1.1, 8.2.0-8.11.4, 9.0.0-9.12.3 and 10.0.0-10.5.2; the finder JUnit fails 4 of 4 on 10.5.2, 10.0.0 and 9.12.3 (it does not compile against the 9.0.0 test framework, so 9.0.0 is probe-only); the independent reproduction's own JUnit fails 4 of 7, its 3 controls passing, on 10.5.2 and 9.12.3; the intended-behaviour check's probes fail on 10.5.2]; branch_10_6 head 4ae946e5a9b [exec: its intervals sources over the 10.5.2 core, 4 of 4], so the released 10.6.0 has it; main a9779ef3c193 [exec, -ea and -da]; upstream main 20ced71c1e6 [read: no intervals commit after f4cc6ac2097 (#16899), which a9779ef3c193 contains]. Not affected: 8.0.0 [exec]; 7.4.0 and 7.7.3 for the only pair that existed then, containedBy / nonOverlapping [exec]. All release runs on JDK 25
- 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
- semi-formal: exhaustive check
- Upstream and prior reports
- none found (follow-up prior-report record verdict new, confidence high but not certain, searched 2026-10-28 plus the finder's scripted prior-report searches and web leg, read-only; 224 recorded queries): three scripted prior-report searches (identifiers; symptoms and the flexible query parser; downstream) over apache/lucene issues and PRs, commits, downstream repositories (Elasticsearch, OpenSearch, Solr), the dev@, java-user@, issues@ and commits@ lists and the JIRA archive; 27 GitHub GraphQL searches including apache/lucene-solr; 7 manual JIRA and 14 web queries (2 throttled); the changed files of all 411 open apache/lucene PRs; the 73 issues and PRs updated since 2026-10-26; the intervals commit history on main, branch_10x and branch_10_6 by author date. Not fixed upstream: no open PR touches Intervals.java, OverlappingIntervalsSource.java or NonOverlappingIntervalsSource.java, and the newest intervals commit on main is f4cc6ac2097 (#16899, author date 2026-10-26, contained in a9779ef3c193). Nearest items, not reports: #9523 / LUCENE-8477 and apache/lucene-solr#620 (the rewrite for phrase, maxgaps and the four containment operators; the overlap filters are never mentioned), #9243 / LUCENE-8196 (2018 comment: containedBy over an OR loses a match, the case LUCENE-8477 fixed), #9675 / LUCENE-8629 (added within and overlapping as inverses of notWithin and nonOverlapping), #10244 / LUCENE-9204 (maintainers on the rewrite as needed for correctness), #11442 / #11443 (a different, fixed or/extend/containing bug). Re-check (2026-10-28, gh read-only): no apache/lucene issue or PR created or updated since 2026-10-26 concerns intervals except the merged #16899; upstream main head 20ced71c1e6 and branch_10_6 have no later intervals commit
- Lucene commit tested
- main
a9779ef3c193(gradle in a scratch worktree, assertions on and off, and the standalone JUnit runner with several seeds); 59 release jars from 8.1.0 to 10.5.2 and the clean 8.0.0, 7.4.0 and 7.7.3 (history probe; the JUnit also on 10.5.2, 10.0.0 and 9.12.3); branch_10_64ae946e5a9b(its intervals sources over 10.5.2); the LUCENE-8477 branch_8x commit and its parent over the 8.1.0 jars; options A and A2 ona9779ef3c193. JDK 25.0.3, Linux aarch64.
- Finding record
lucene/queries/src/java/org/apache/lucene/queries/intervals/Intervals.java:459-470 (at a9779ef3c193) (nonOverlapping javadoc 'Returns intervals of the minuend that do not overlap with intervals from the subtrahend'; body new NonOverlappingIntervalsSource), :472-480 (overlapping: new OverlappingIntervalsSource, no pull-up), :482-497 (notWithin javadoc :485 'do not appear within a set number of positions of intervals from the subtrahend query'; body NonOverlapping over extend), :499-511 (within javadoc :500 'appear within a set number of positions of intervals from the reference'; body containedBy(source, extend(reference))), :559 (containedBy), :47 (class note on the rewrite), :121 and :130-131 (or() 'Automatically rewrites if wrapped by an interval source that is sensitive to internal gaps'; rewrite=false 'can miss valid hits due to minimization')lucene/queries/src/java/org/apache/lucene/queries/intervals/ContainedByIntervalsSource.java:28-29 (at a9779ef3c193) (build: Intervals.or(Disjunctions.pullUp(big, ...)))lucene/queries/src/java/org/apache/lucene/queries/intervals/OverlappingIntervalsSource.java:79-83 (at a9779ef3c193) (pullUpDisjunctions splits source and reference, used only when the filter is nested)lucene/queries/src/java/org/apache/lucene/queries/intervals/DisjunctionIntervalsSource.java:231 (at a9779ef3c193) (the iterator drops a queued interval that contains the current one)lucene/queries/src/java/org/apache/lucene/queries/intervals/package-info.java:21 (at a9779ef3c193) ('experimental classes')lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/intervalfn/package-info.java:542-566 (at a9779ef3c193) (fn:notWithin) and :673-690 (fn:overlapping)
- The finding in 30 seconds
lucene/queries/src/java/org/apache/lucene/queries/intervals/Intervals.java:467-470 (at a9779ef3c193) (nonOverlapping), :478-480 (overlapping), :493-497 (notWithin), :508-511 (within), :551-561 (containedBy)lucene/queries/src/java/org/apache/lucene/queries/intervals/ContainedByIntervalsSource.java:28-30 (at a9779ef3c193) (build: pull-up of big)lucene/queries/src/java/org/apache/lucene/queries/intervals/DisjunctionIntervalsSource.java:147-152 (at a9779ef3c193) (pullUpDisjunctions only when rewrite is on), :222-247 (nextInterval drops containing intervals, contains())lucene/queries/src/java/org/apache/lucene/queries/intervals/Disjunctions.java:66-95 (at a9779ef3c193) (pullUp of one source; splitDisjunctions groups alternatives with minExtent 1)lucene/queries/src/java/org/apache/lucene/queries/intervals/ExtendedIntervalsSource.java:126-143 (at a9779ef3c193) (minExtent; pullUpDisjunctions passes the split through)lucene/queries/src/java/org/apache/lucene/queries/intervals/OverlappingIntervalsSource.java:78-83 (at a9779ef3c193) (pullUpDisjunctions splits both operands); NonOverlappingIntervalsSource.java:37-39 (returns Collections.singletonList(this))lucene/queries/src/java/org/apache/lucene/queries/intervals/FilteredIntervalsSource.java:31-36 (at a9779ef3c193) (maxGaps maps over in.pullUpDisjunctions())
- Who is affected
lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/intervalfn/package-info.java:513-566 (at a9779ef3c193) (fn:within, fn:notWithin), :673-710 (fn:overlapping, fn:nonOverlapping)lucene/queries/src/java/org/apache/lucene/queries/intervals/Intervals.java:121 (at a9779ef3c193) (or() rewrites by default), :131 (or(false, ...) 'can miss valid hits due to minimization')lucene/queries/src/java/org/apache/lucene/queries/intervals/package-info.java:21 (at a9779ef3c193) ('experimental classes')lucene/queries/src/java/org/apache/lucene/queries/intervals/ExtendedIntervalsSource.java:126-131 (at a9779ef3c193) (extend raises minExtent, so within / notWithin split an or() of single words)
- Why it looks unintended
lucene/queries/src/java/org/apache/lucene/queries/intervals/Intervals.java:43, (at a9779ef3c193) 47-54, 131, 462, 473, 485, 500, 554 (javadoc quotes)lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/intervalfn/package-info.java:542-566 (at a9779ef3c193) (fn:notWithin), :673-690 (fn:overlapping)lucene/CHANGES.txt:6502-6506 (at a9779ef3c193) (LUCENE-8477 entry)GitHub apache/lucene#10244 comments, read 2026-10-28 (romseygeek and jimczi quotes)lucene/queries/src/test/org/apache/lucene/queries/intervals/TestIntervals.java:925-933, (at a9779ef3c193) TestIntervalQuery.java:150-158, 182-187; lucene/highlighter/src/test/org/apache/lucene/search/matchhighlight/TestMatchRegionRetriever.java:382-384, TestMatchHighlighter.java:423-455; lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/standard/TestStandardQPEnhancements.java:137-145lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/intervalfn/package-info.java:347-349, (at a9779ef3c193) 381-383 ('not required to return or highlight all matching interval alternatives')lucene/queries/src/java/org/apache/lucene/queries/intervals/FilteredIntervalsSource.java:31-36, (at a9779ef3c193) BlockIntervalsSource.java:30-35 (maxgaps and phrase rewrite their child at construction)
- Suggested fix
lucene/queries/src/java/org/apache/lucene/queries/intervals/Disjunctions.java:33-62 (at a9779ef3c193) (list pullUp with the max-clause guard at :43-44), :66-73 (single-source pullUp, no guard); DisjunctionIntervalsSource.java:46-48 (or of one source returns it), :58-67 (HashSet), :147-152 (or(false) never split); ExtendedIntervalsSource.java:140-142 (Set)lucene/queries/src/java/org/apache/lucene/queries/intervals/OverlappingIntervalsSource.java:78-83 (at a9779ef3c193) (nested pull-up of both operands); FilteredIntervalsSource.java:31-36 (maxGaps maps over in.pullUpDisjunctions())
- History
a Lucene clone: git tag --contains 2571bf355f8 (branch_8_1 onwards)GitHub apache/lucene commits API, read 2026-10-28 (newest intervals commit f4cc6ac2097; main head 20ced71c1e6)
- TestWithinNotWithinDisjunction.java (JUnit test):
repro/LFV-828/src/org/apache/lucene/queries/intervals/TestWithinNotWithinDisjunction.java(54 lines)
A step-by-step picture of the one-document example: how within and notWithin are built, how the or() read as one source drops "new york", and where both keep visit. The PNG is the summary still. Made in parallel with this doc from the code at a9779ef3c193 and the executed probe. The card's last line ("Fix sketch") describes the finder's sketch, option A; the recommended option A2 builds the same chain for nonOverlapping / notWithin and, for overlapping, also splits the source (see Suggested fix).
assets/LFV-828/bug.png(463 KB)
![The example on the document "visit new york" with R = or(york, "new york"): within(visit, 1, R) tests each alternative, and "new york" widened by 1 to positions 0-3 contains visit at position 0, so visit matches; notWithin(visit, 1, R) reads the or() as one source, which keeps only york at position 2 ("new york" contains it and is dropped), widened to positions 1-3, which misses visit, so visit matches again: both return visit [0,0].](../assets/LFV-828/bug.png)