Both factories widen the reference by n positions on each side. within is containedBy that window (Intervals.java:510): the whole source interval must lie inside. notWithin is non-overlap with it (Intervals.java:495-496): no position may touch it. A phrase that overlaps the window without lying inside fails both tests; a single term never can.
- An interval is a run of token positions in a field. A term is one position; in "the new york times" the phrase "new york" is the interval 1-2.
Intervals.within(source, n, reference)andIntervals.notWithin(source, n, reference)keep or drop the source's intervals depending on how near they are to the reference's intervals; anIntervalQuerymatches the documents where at least one interval is left.- The flexible
StandardQueryParseroffers the same two functions asfn:within(source n reference)andfn:notWithin(source n reference).
within (containment) and notWithin (no overlap) both drop it. Select the still to open it at full size. Generated with AI, checked against the code.- You index "the new york times" (positions: the 0, new 1, york 2, times 3) and run
within(phrase(new, york), 1, times)andnotWithin(phrase(new, york), 1, times). - The docs describe the two as opposites ("appear within" / "do not appear within a set number of positions"), so you expect exactly one to match, most likely
within: "new york" ends right before "times". - Instead both count 0. Both widen "times" by 1 to positions 2-4;
withinwants all of "new york" (1-2) inside that window,notWithinwants none of it there. withinneeds n = 2 for this phrase, although the documented single-term examples match a term right next to the reference at n = 1. The query syntaxfn:within/fn:notWithinbehaves the same. A single term cannot touch the window without lying inside it, so it never falls in this gap.
Users of within / notWithin (Java API, or fn:within / fn:notWithin in StandardQueryParser since 9.1.0) with a multi-position source such as a phrase. A phrase right next to the reference is missed by within(..., 1, ...), and notWithin(X, n, Y) and MUST(X) MUST_NOT(within(X, n, Y)) return different documents. Default settings; opt-in experimental package.
Mechanism, terms and numbers757 words
Intervals.within(source, n, reference) and Intervals.notWithin(source, n, reference) are documented as opposites: one returns source intervals that "appear within a set number of positions" of the reference, the other those that "do not appear within" it, and the fn:within / fn:notWithin query-parser docs give both the same "maximum positions" parameter. Both widen each reference interval by n positions on both sides, but within keeps a source interval only if it lies entirely inside that window (containedBy), while notWithin drops it if it touches the window at all (non-overlap).
A single term cannot touch the window without lying inside it, so it never falls in this gap (against a single-term reference the pair splits it exactly; with a nested disjunctive reference the hunt's separate LFV-828 can make it match both). For a phrase or near query, an interval that overlaps the window without lying inside it fails both: on "the new york times", within(phrase(new, york), 1, times) and notWithin(...) both count 0, and within needs n = 2. The code has done this since within was added in 8.0.0 and looks deliberate, so this is a documentation / API-contract defect: the proposed fix states the two relations and the gap in the javadoc and the fn: docs, with a one-line behaviour change only as an option.
- Both factories widen the reference the same way.
withinreturnscontainedBy(source, Intervals.extend(reference, positions, positions))(Intervals.java:508-511);notWithinreturnsnew NonOverlappingIntervalsSource(minuend, Intervals.extend(subtrahend, positions, positions))(Intervals.java:493-497).extendmoves each reference interval's start back by n (clamped at 0,ExtendedIntervalIterator.java:61) and its end forward by n. For "times" at position 3 and n = 1 the window is 2-4. withintests whole containment.ContainedByIntervalsSourcereturns a source interval a only when the window b it finds hasb.end() >= a.end()(ContainedByIntervalsSource.java:53) andb.start() <= a.start()(:59). "new york" is 1-2 and the window starts at 2, so it is dropped.notWithintests no contact.NonOverlappingIteratorreturns a only when the next window withb.end() >= a.start()(NonOverlappingIntervalsSource.java:72) hasb.start() > a.end()(:78). The window starts at 2 and "new york" ends at 2, so it is dropped too.- Partly inside means neither. A single position is either inside a window or apart from it, so a single-term source never falls in this gap; against "times" the pair splits it exactly (executed: the, new, york and times, n = 0 to 3,
within + notWithin = 1every time). Over the same window of a single-term reference,overlapping/nonOverlappingandcontainedBy/notContainedByare each exact complements; on the repro document overlapping = 1, nonOverlapping = 0, containedBy = 0, notContainedBy = 1. Only the mixed pair thatwithin/notWithinuse leaves a gap. With a disjunctive reference, separate defects change this: with nesting alternatives (LFV-828)notWithinreads fewer reference intervals thanwithin, so a single term can match both (executed: on "visit new york",within(visit, 1, or(york, phrase(new, york)))= 1 andnotWithin(...)= 1), and for someor()references LFV-291 breakscontainedBy/notContainedBy.
- A source that straddles the window edge, as above: a phrase directly before or after the reference at n = 1.
within(phrase(new, york), 2, times)= 1, sowithinneeds n = 2 where the documented single-term examples use n = 1 for a term right next to the reference ("dog" after "lazy",intervalfn/package-info.java:533-535). - A source that contains the reference and sticks out more than n positions past it. On "new york",
within(phrase(new, york), 0, york)andnotWithin(phrase(new, york), 0, york)both count 0, andMUST(phrase) MUST_NOT(within(...))counts 1 whilenotWithincounts 0, so two common ways of writing "X but not near Y" disagree. A containing source that sticks out by at most n iswithin(within(phrase(new york), 0, phrase(new york))= 1). So, for one occurrence of the reference, a source that contains it falls in the gap exactly when it starts more than n positions before the reference interval or ends more than n positions after it; any source wider than 2n + 1 around a single-position reference is one such case.
fn:within and fn:notWithin call the same factories (Within.java:38, NotWithin.java:38). On the docs' own example sentence, "The quick brown fox jumps over the lazy dog", fn:within(fn:phrase(brown fox) 1 jumps) and fn:notWithin(fn:phrase(brown fox) 1 jumps) both count 0; at n = 0 only fn:notWithin matches and at n = 2 only fn:within.
Exposure and likelihood243 words
- Who. Callers of
Intervals.within/Intervals.notWithinwithIntervalQuery, and users offn:within/fn:notWithinin the flexibleStandardQueryParser(since 9.1.0), who pass a multi-position source: a phrase, or an ordered or unordered near. A single-term source never falls in this gap; the upstream tests use single-term sources and thefn:doc examples a disjunction of single terms, so none has a multi-position source. - Downstream. The Solr 10.1 intervals query parser exposes
within/not_withinwith the same opposite-pair wording; the Elasticsearch and OpenSearch intervals queries do not offer these two filters. - Default vs opt-in. Default analyzer, codec and searcher; the relation is decided per source interval inside one document, so index size plays no part (the repro uses one document). The intervals package is opt-in (the queries module) and labelled "experimental classes" (
intervals/package-info.java:21). - What goes wrong. Fewer hits than the docs describe; splitting a source's matches into "near Y" and "not near Y" silently loses the partly-near ones; an exclusion written two common ways returns different documents. No error, deterministic, nothing written to the index.
- Age and likelihood. The gap is present from 8.0.0 (2019) through 10.6.0 and on main; the recorded searches found no public report, which suggests that accidental hits are rare (a judgement, not measured).
- Scope of the evidence. The repro uses a single-term reference with one occurrence; disjunctive, nested or repeated references are not part of this claim (for nesting
or()alternatives see the separate LFV-828).
One JUnit test, one method, one document, with a fixed document and fixed queries: "the new york times" with the default analyzer and codec. It asserts what the docs suggest, that exactly one of within(phrase(new, york), 1, times) and notWithin(...) matches. Public API only, in the intervals test package.
/*
* 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.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 TestWithinNotWithinPartlyNear extends LuceneTestCase {
public void testPhraseNextToReferenceIsWithinOrNotWithin() throws Exception {
try (Directory dir = new ByteBuffersDirectory();
IndexWriter w =
new IndexWriter(dir, new IndexWriterConfig().setCodec(TestUtil.getDefaultCodec()))) {
Document doc = new Document();
doc.add(new TextField("f", "the new york times", Field.Store.NO));
w.addDocument(doc);
try (DirectoryReader r = DirectoryReader.open(w)) {
IndexSearcher searcher = new IndexSearcher(r);
IntervalsSource newYork = Intervals.phrase("new", "york");
IntervalsSource times = Intervals.term("times");
int within = searcher.count(new IntervalQuery("f", Intervals.within(newYork, 1, times)));
int notWithin =
searcher.count(new IntervalQuery("f", Intervals.notWithin(newYork, 1, times)));
// expected: exactly one of within / notWithin matches the doc; actual: neither (0 + 0)
assertEquals("within + notWithin", 1, within + notWithin);
}
}
}
}
cp $REPRO/TestWithinNotWithinPartlyNear.java $LUCENE/lucene/queries/src/test/org/apache/lucene/queries/intervals/
cd $LUCENE
./gradlew :lucene:queries:test --tests org.apache.lucene.queries.intervals.TestWithinNotWithinPartlyNear -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off: -Ptests.asserts=false (same failure)
- Expected
- Exactly one of
within(phrase(new, york), 1, times)andnotWithin(phrase(new, york), 1, times)matches "the new york times". - Observed
- Neither matches:
within + notWithin expected:<1> but was:<0>, with assertions on and off.
More files, environment, full output340 words
- Seed-independent. One fixed document and fixed queries, a plain
IndexSearcher, the default codec pinned, so the result does not depend on the seed: the test framework still randomizes locale, time zone and similarity, and the tie-break's seeds7EB4EA11and0DA0DA01give the same failure asDEADBEEF. Assertions on and off fail the same way. - It transcribes the complement reading of the docs. No Java API text promises that complement in so many words, so the test demonstrates the documentation ambiguity, not a code defect. Under the recommended javadoc fix it becomes obsolete, because the documented contract becomes "a partly-near interval matches neither". A test is unusual for a javadoc-only change; if upstream wants one, keep this same test as a characterization test with the expectation the clarified docs give,
assertEquals("within + notWithin", 0, within + notWithin), and the comment "new york [1,2] overlaps the widened times [2,4] but is not inside it, so it matches neither". It passes with the javadoc fix, with assertions on and off. Under the behaviour change (option B) the test as written passes unchanged. - Releases. The same file fails unchanged on the 9.1.0, 9.12.3, 10.0.0, 10.5.2 and 10.6.0 release jars; with only the package line and the test-framework imports adapted it fails on 8.0.0, 8.1.1, 8.2.0, 8.11.4 and 9.0.0. 7.7.3 has no
within.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT); also fails on the 9.1.0, 9.12.3, 10.0.0, 10.5.2 and 10.6.0 release jars, and (package and imports adapted) on 8.0.0 to 9.0.0 |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 |
cp $REPRO/TestWithinNotWithinPartlyNear.java $LUCENE/lucene/queries/src/test/org/apache/lucene/queries/intervals/
cd $LUCENE
./gradlew :lucene:queries:test --tests org.apache.lucene.queries.intervals.TestWithinNotWithinPartlyNear -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off: -Ptests.asserts=false (same failure)
# gradle, main 645d4fe14a20, seed DEADBEEF, -Ptests.asserts=true
TestWithinNotWithinPartlyNear > testPhraseNextToReferenceIsWithinOrNotWithin FAILED
java.lang.AssertionError: within + notWithin expected:<1> but was:<0>
at __randomizedtesting.SeedInfo.seed([DEADBEEF:3A90972F4189603B]:0)
at junit@4.13.2/org.junit.Assert.fail(Assert.java:89)
at junit@4.13.2/org.junit.Assert.failNotEquals(Assert.java:835)
at junit@4.13.2/org.junit.Assert.assertEquals(Assert.java:647)
at org.apache.lucene.queries.intervals.TestWithinNotWithinPartlyNear.testPhraseNextToReferenceIsWithinOrNotWithin(TestWithinNotWithinPartlyNear.java:48)
:lucene:queries:test (FAILURE): 1 test, 1 failure
# -Ptests.asserts=false: the same failure
# documentation fix (option A): changes no code; the same test expecting 0 passes
# behaviour change (option B): BUILD SUCCESSFUL, 1 test- The docs present the pair as opposites: "appear within" / "do not appear within a set number of positions", and
fn:within/fn:notWithinshare one parameter text. - #9675, which added
within, calls it the "inverse of the already available NOT_WITHIN". - No doc or test mentions the partly-near case; the code looks deliberate, so the fix is the docs.
The full argument316 words
- The summaries use the file's own complement vocabulary. In
Intervals.java, "appear within" / "do not appear within" describecontainedBy(:554) andnotContainedBy(:541-542), an exact complement pair for a non-disjunctive reference (the separate LFV-291 breaks it for someor()references);withinandnotWithinuse the same words (:500-501,:485-486), and so does the package overview (intervals/package-info.java:68-70). - The query-parser docs differ only by an emphasized not.
fn:within: "Matches intervals of the source that appear within the provided number of positions from the intervals of the reference";fn:notWithin: the same with "do <em>not</em> appear within" (intervalfn/package-info.java:515-516,:544-545). Both take "an integer number of maximum positions between source and reference" (:526,:555), and the source is a "term or other function" (:524,:553), so a phrase is a documented input. - Intent at introduction. #9675 / LUCENE-8629 lists "a WITHIN b - inverse of the already available NOT_WITHIN" and, in the same list, "a OVERLAPPING b - inverse of the already available NOT_OVERLAPPING" (as @romseygeek wrote there). Over a non-disjunctive reference,
overlappingandnonOverlappingare exact complements. - Nothing documents the gap. No javadoc, overview,
fn:doc, CHANGES entry or test says thatwithinneeds the whole source inside the widened window, thatnotWithinrejects any contact, or that a partly-near interval matches neither. The upstream tests use single-term sources only (TestIntervals.java:915-922,TestIntervalQuery.java:182-186), and thefn:doc examples a disjunction of single terms (fn:or(fox dog),intervalfn/package-info.java:533-538), so every exercised source is single-position. - The parameter text is ambiguous for a phrase. By the docs' single-term convention ("dog" right after "lazy" matches at 1), "brown fox" right before "jumps" would match at 1; it needs 2. That follows only if the distance is measured to the farthest position of the source, which no doc says.
The devil's-advocate check338 words
Strongest case for "intended" (the study's devil's advocate and a cross-model reviewer, OpenAI GPT-5.6): read pointwise, the Java @param texts describe the code: within's "the maximum distance that intervals of the source may occur from intervals of the reference" (Intervals.java:504-505) means every position at most n away, i.e. containment; notWithin's "the minimum distance ..." (:489-490) is a bound on every position, i.e. no contact (the code keeps a position only if it is more than n away). LUCENE-8629 changed notWithin's @param from "maximum" to "minimum" in the same commit that added within. notWithin equals SpanNotQuery(include, exclude, n, n) (executed, n = 0 to 3). The package is experimental, the tests are single-term, and the recorded searches found no complaint since 2019. The reviewer: "within and notWithin are deliberately different endpoint-distance filters, not Boolean negations."
Why it fails: it shows that the code is intended, which is why this is classed as documentation and not as wrong results; it does not show that the documentation describes the behaviour. The gap follows from one reading of the Java @params only, and even under that reading the notWithin text is off by one: "dog" right after "lazy" is 1 position away, yet fn:notWithin(dog 1 fn:or(quick lazy)) drops it (executed in the tie-break; the fn:notWithin doc example shows the same), so the smallest distance notWithin keeps is n + 1, not the stated minimum n. The fn: docs, which document the query-string route, have no maximum / minimum distinction. Before LUCENE-8629, notWithin's @param said "the maximum distance" (LUCENE-8196, 2018); read literally, that describes what within keeps, not what notWithin keeps. The reviewer itself wrote that the parser docs' "symmetric 'maximum positions' wording could reasonably be clarified to state whole containment for within, no contact for notWithin, and that partial intersections match neither": that clarification is the proposed fix. Confidence: high on the behaviour and the documentation gap, medium on how upstream will want to resolve it (a javadoc clarification is the most likely outcome).
Recommended A. Document the two relations and the gap (recommended)
Document what the code does: the reference is widened by n positions on both sides, within needs the whole source interval inside, notWithin needs no overlap, and an interval only partly inside matches neither. No behaviour change. Validated with gradle: the format and javadoc checks pass, and the intervals and flexible-parser suites pass.
All options and trade-offs2 options ยท 1,180 words
Two options; both were applied in a scratch worktree of main 645d4fe14a20 and run with gradle. The documentation fix is the proposal, because the code looks deliberate and has behaved this way since 8.0.0.
In the javadoc of Intervals.within / notWithin and in the fn:within / fn:notWithin docs, say that the reference is widened by n positions on both sides, that within needs the whole source interval inside, that notWithin needs no overlap, and that an interval only partly inside matches neither. Replace the "distance" / "maximum positions between" parameter text.
diff --git a/lucene/queries/src/java/org/apache/lucene/queries/intervals/Intervals.java b/lucene/queries/src/java/org/apache/lucene/queries/intervals/Intervals.java
index f79d8bf0e10..33fe265df05 100644
--- a/lucene/queries/src/java/org/apache/lucene/queries/intervals/Intervals.java
+++ b/lucene/queries/src/java/org/apache/lucene/queries/intervals/Intervals.java
@@ -485,9 +485,15 @@ public final class Intervals {
* <p>Returns intervals of the minuend that do not appear within a set number of positions of
* intervals from the subtrahend query
*
+ * <p>Each subtrahend interval is widened by {@code positions} on both sides, and a minuend
+ * interval is returned only if it does not overlap any widened interval. A minuend interval that
+ * overlaps a widened interval but lies entirely inside none of them, for example a two-term
+ * phrase directly before the subtrahend when {@code positions} is 1, is returned neither by this
+ * source nor by {@link #within(IntervalsSource, int, IntervalsSource)}.
+ *
* @param minuend the {@link IntervalsSource} to filter
- * @param positions the minimum distance that intervals from the minuend may occur from intervals
- * of the subtrahend
+ * @param positions the number of positions by which each subtrahend interval is widened on both
+ * sides
* @param subtrahend the {@link IntervalsSource} to filter by
*/
public static IntervalsSource notWithin(
@@ -500,9 +506,15 @@ public final class Intervals {
* Returns intervals of the source that appear within a set number of positions of intervals from
* the reference
*
+ * <p>Each reference interval is widened by {@code positions} on both sides, and a source interval
+ * is returned only if it lies entirely inside a widened interval. A source interval that overlaps
+ * a widened interval but lies entirely inside none of them, for example a two-term phrase
+ * directly before the reference when {@code positions} is 1, is returned neither by this source
+ * nor by {@link #notWithin(IntervalsSource, int, IntervalsSource)}.
+ *
* @param source the {@link IntervalsSource} to filter
- * @param positions the maximum distance that intervals of the source may occur from intervals of
- * the reference
+ * @param positions the number of positions by which each reference interval is widened on both
+ * sides
* @param reference the {@link IntervalsSource} to filter by
*/
public static IntervalsSource within(
diff --git a/lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/intervalfn/package-info.java b/lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/intervalfn/package-info.java
index e8abe61644b..038cc1c3d34 100644
--- a/lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/intervalfn/package-info.java
+++ b/lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/intervalfn/package-info.java
@@ -513,7 +513,10 @@
* <h3>fn:within</h3>
*
* <p>Matches intervals of the source that appear within the provided number of positions from the
- * intervals of the reference.
+ * intervals of the reference. Each reference interval is widened by that number of positions on
+ * both sides, and a source interval matches only if it lies entirely inside a widened interval. A
+ * source interval that only partly overlaps a widened interval (with 1 position, the phrase "brown
+ * fox" directly before "jumps") matches neither fn:within nor fn:notWithin.
*
* <dl class="dl-horizontal narrow">
* <dt>Arguments
@@ -523,7 +526,8 @@
* <dt><code>source</code>
* <dd>source sub-interval (term or other function)
* <dt><code>positions</code>
- * <dd>an integer number of maximum positions between source and reference
+ * <dd>an integer number of positions by which each reference interval is widened on both
+ * sides
* <dt><code>reference</code>
* <dd>reference sub-interval (term or other function)
* </dl>
@@ -542,7 +546,9 @@
* <h3>fn:notWithin</h3>
*
* <p>Matches intervals of the source that do <em>not</em> appear within the provided number of
- * positions from the intervals of the reference.
+ * positions from the intervals of the reference. Each reference interval is widened by that number
+ * of positions on both sides, and a source interval matches only if it does not overlap any widened
+ * interval (see <code>fn:within</code> for a source that only partly overlaps one).
*
* <dl class="dl-horizontal narrow">
* <dt>Arguments
@@ -552,7 +558,8 @@
* <dt><code>source</code>
* <dd>source sub-interval (term or other function)
* <dt><code>positions</code>
- * <dd>an integer number of maximum positions between source and reference
+ * <dd>an integer number of positions by which each reference interval is widened on both
+ * sides
* <dt><code>reference</code>
* <dd>reference sub-interval (term or other function)
* </dl>Trade-offs
- No behaviour change, so no existing query moves.
- It replaces parameter wording that is ambiguous for a phrase source with what the code does, and states the partly-near case once per function.
- Optional, not run: a sentence in the intervals package overview (
intervals/package-info.java:68-70) and a CHANGES entry. - Limits of the wording: the examples assume one occurrence of the reference (a second occurrence whose widened window contains the phrase would make
withinreturn it), and with a disjunctive reference whose alternatives nest the separate LFV-828 currently makesnotWithinread fewer reference intervals thanwithin, so "does not overlap any widened interval" does not hold there today.
What the checks validated
- Gradle, scratch worktree of
645d4fe14a20, seedDEADBEEF::lucene:queries:checkGoogleJavaFormat,:lucene:queryparser:checkGoogleJavaFormat,:lucene:queries:renderJavadocand:lucene:queryparser:renderJavadocpass, and the renderedIntervals.htmlandintervalfn/package-summary.htmlshow the new text. - The intervals suite (
org.apache.lucene.queries.intervals.*: the 117 upstream tests plus the minimal test in its documented form, expecting 0) passes, 118 tests, and the flexible-parser suite (org.apache.lucene.queryparser.flexible.standard.*) passes, 125 tests with 1 skipped as on stock main, assertions on; the documented-form test also passes with assertions off. - The minimal test as written was not re-run with it: the fix changes only comments, so it keeps failing as on stock main.
One line: within tests overlap with the widened reference instead of containment, so within and notWithin become overlapping / nonOverlapping over the same window, an exact complement pair for a non-disjunctive reference.
diff --git a/lucene/queries/src/java/org/apache/lucene/queries/intervals/Intervals.java b/lucene/queries/src/java/org/apache/lucene/queries/intervals/Intervals.java
index f79d8bf0e10..f186518343a 100644
--- a/lucene/queries/src/java/org/apache/lucene/queries/intervals/Intervals.java
+++ b/lucene/queries/src/java/org/apache/lucene/queries/intervals/Intervals.java
@@ -507,7 +507,7 @@ public final class Intervals {
*/
public static IntervalsSource within(
IntervalsSource source, int positions, IntervalsSource reference) {
- return containedBy(source, Intervals.extend(reference, positions, positions));
+ return overlapping(source, Intervals.extend(reference, positions, positions));
}
/**Trade-offs
- Changes results for every existing
withinquery with a multi-position source:within(phrase(brown fox), 1, jumps)goes from 0 to 1, and a source that merely touches the window counts as within. It needs a CHANGES entry as a behaviour change, probably on main only. - The converse,
notWithin = notContainedBy(minuend, extend(subtrahend, n, n)), is worse: todaynotWithinmatchesSpanNotQuery(include, exclude, n, n), and the open #16820 proposes moving span users to intervals. Not run. - With a disjunctive reference (
or(...)), the hunt's separate overlap-filters finding (LFV-828, another root cause) also applies tooverlapping.
What the checks validated
- Gradle, the same worktree and seed: the minimal test passes with assertions on and off (1 test each); the intervals suite (117 tests) and the flexible-parser suite (125 tests, 1 skipped) pass, and
:lucene:queries:checkGoogleJavaFormatpasses. - The result change is real: with the change,
within(phrase(brown fox), 1, jumps)on the docs' example sentence goes from 0 to 1 (tie-break probe). No upstream test pins either behaviour for multi-position sources.
Products that hand these functions to their own users, such as Solr's intervals query parser (within / not_within), repeat the opposite-pair wording, so their users inherit the gap.
Semi-formal. A hunt unit checked whether interval filters documented as opposites really split every match, running the real jar over every pair of small interval sets (32,695,520 filter evaluations). Apart from a separately registered notContaining bug, only within / notWithin failed; the case was then reduced by hand to the one-document test.
The method in full210 words
- Step 1, the documented relation. The unit first settled from the docs which pairs are presented as opposites:
within/notWithin("appear within" / "do not appear within"),containedBy/notContainedBy,overlapping/nonOverlapping. - Bounded-exhaustive leg.
FilterLawsExhaustivefed every pair of minimal interval sets inside positions 0-6 (1,430 x 1,430 pairs, 32,695,520 filter evaluations) through the real filter classes via a public customIntervalsSource.containedBy/notContainedByandoverlapping/nonOverlappingover a widened reference were exact complements; apart from a separately registerednotContainingdefect, the only violations werewithin/notWithin"neither" (2,530,558 / 645,023 / 106,025 intervals for n = 0 / 1 / 2). That put the gap in the two factories, not in the filter iterators. - Real-source harness.
ComplementLawsTV(116 x 116 source shapes over all 1,364 documents of up to 5 positions over a 4-letter alphabet, plus 60,000 random pairs), with every other known interval fix shadowed, left the same class as the only residual besidesextend()clamp artifacts. - Reduction and verification. Reduced by hand to the public-API JUnit; then an independent reproducer, cross-model reviews (OpenAI GPT-5.6), an intended-behaviour devil's advocate, an adjudication, a credibility audit, a re-verification and a second adjudication. No model checker, SMT solver or prover produced the signal.
No prior report found (searched 2026-10-28 and 2026-11-01): GitHub issues and PRs incl. the JIRA archive, commits, mailing lists, Solr, Elasticsearch and OpenSearch, the web and all open PRs. Closest: #9675, which added within as the "inverse" of notWithin.
Searches4 searches
Read, not the same bug: #9675 / LUCENE-8629 (added within; no discussion of multi-position sources); #9658 / LUCENE-8612 (built notWithin on extend()); #429 / LUCENE-10223 (wrote the fn: docs; no semantics discussion); #16820 (open, proposes deprecating span queries in favour of intervals; does not mention within); the Solr 10.1 intervals guide (repeats the opposite-pair wording; downstream exposure, not a report); elastic/elasticsearch#27134 (span_not docs, single-term, spans). Open PRs #16847, #16851 and #16 (LUCENE-9634, open since 2021) touch the intervals package but not within / notWithin. Limits: GitHub search reads at most 100 results per query and tokenizes fn:within loosely; one web query did not complete; private reports cannot be searched.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene, incl. migrated JIRA) | 2026-10-28 and 2026-11-01 | a scripted prior-report search (3 query sets: mechanism, doc wording, downstream) and 26 manual queries: notWithin, "Intervals.within", "fn:within", "fn:notWithin", within notWithin, intervals within inverse / complement, "maximum distance" intervals, NonOverlappingIntervalsSource, ContainedByIntervalsSource; the 2026-10-28 study search ran 44 more and a regex scan of 171 threads | no report |
| JIRA archive | 2026-10-28 and 2026-11-01 | 15 queries; full reads of LUCENE-8629, LUCENE-8612, LUCENE-8196, LUCENE-8477, LUCENE-10223, LUCENE-10229 | no report |
| Open PRs and recent activity | 2026-11-01 | changed files of all 410 open PRs; every apache/lucene item updated since 2026-10-22 with its comments | none changes within / notWithin or their docs |
| Mailing lists, downstream projects and the web | 2026-10-28 and 2026-11-01 | dev@, java-user@, issues@, commits@ and the Solr lists; Solr, Elasticsearch, OpenSearch issues and code; 20 web queries | no report; the Solr 10.1 guide repeats the opposite-pair wording |
Introduced with within itself by LUCENE-8629 (#9675, 7d34bfdaad3 / branch_8x 3593af2ac55, 2019-01-07), first released in 8.0.0; the fn: docs followed in 9.1.0. Every release from 8.0.0 to 10.6.0 and main shows the gap; 7.4.0 to 7.7.3 have no within. Not fixed upstream.
Versions tested6 rows
- Introduced: LUCENE-8629 "New interval functions: before, after, within, overlapping" (@romseygeek, 2019-01-07; a JIRA patch, the issue is now #9675):
7d34bfdaad3on master and3593af2ac55on branch_8x, first released in 8.0.0. It addedwithin = containedBy(source, extend(reference, n, n))next to the existingnotWithin, which LUCENE-8612 (2532a5d31c8, 2019-01-01) had built as no overlap withextend(subtrahend, n, n), and changed notWithin's@paramfrom "maximum" to "minimum distance". - The
fn:docs with the shared "maximum positions between source and reference" text came with LUCENE-10223 (#429, 2021-11-11):f725b27e12aon main (first released in 10.0.0) and its branch_9x twinff9ee28c603(first released in 9.1.0). - Not a regression: the gap is present from 8.0.0 through 10.6.0 and on main;
within's body is unchanged in every revision since;notWithinmoved toNonOverlappingIntervalsSource(LUCENE-8477, 8.1.0) with the same relation. - Upstream: main
f815e679bf6(2026-11-01) has the sameIntervals.java, filter classes and package docs as645d4fe14a20.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | minimal test fails, gradle and standalone, assertions on and off; passes with option B |
| 10.6.0, 10.5.2 | affected | minimal test fails, assertions on and off |
| 10.0.0, 9.12.3, 9.1.0 | affected | minimal test fails |
| 9.0.0, 8.11.4, 8.2.0, 8.1.1 | affected | minimal test fails (package and test imports adapted) |
| 8.0.0 | first release | minimal test fails, assertions on and off; an earlier sweep showed the gap on all 60 releases 8.0.0 to 10.5.2 |
| 7.4.0 to 7.7.3 | no API | no within (compile error); notWithin already drops the phrase |
Every check agrees on the behaviour and the root cause. The study's reproducer, devil's advocate, judge and audit, a re-verification and a second adjudication call it a documentation / API-contract defect; cross-model reviews (OpenAI GPT-5.6) said not real as a code-level complement law, while conceding that the docs' wording invites a complement reading and could be clarified.
Checks and verdicts343 words
The study ran on main a9779ef3c193, whose intervals and intervalfn sources equal those at 645d4fe14a20. The executed cross-model review ran on ea57360912ed, where within / notWithin, the filter classes and the intervalfn docs are the same as at 645d4fe14a20 (Intervals.java differs only in unrelated @throws javadoc, #16899); its test was later re-run on 645d4fe14a20 with the same result.
| Check | Verdict | What it found |
|---|---|---|
| study finder | reproduced, category doc | Bounded-exhaustive complement-law leg and real-source harness; public-API JUnit fails 2 of 2 on main (assertions on and off), 10.5.2 and 9.12.3. |
| independent reproducer | real, documentation defect | Own test fails 3 of 5 with both controls passing, on main, 10.5.2 and 9.12.3, including the fn: route. |
| cross-model adversary | not real, medium (reading only) | Agreed with the root cause and that the fn: prose invites a complement reading; its counterexample, run by a separate follow-up check, narrowed the claim (a containing source can be within a multi-position reference). |
| devil's advocate | real, doc, Low | Built the intended-behaviour case (pointwise @params, SpanNotQuery equality) and rejected it as a refutation of the documentation defect. |
| judge and audit | confirmed, doc, minor; audit agrees | Re-ran the tests; the audit expects a committer to call the code working as designed and to welcome a javadoc clarification. |
| re-verification (2026-11-01) | real, doc | Minimal test fails on main and 10.5.2, assertions on and off; controls pass; passes with option B. |
| cross-model review, refute lens | not real, high (executed on ea57360912ed) | On main ea57360912ed, reproduced with its own test (3 run, 1 failed: the partition assertion; re-run by the hunt on 645d4fe14a20 with the same result, assertions on and off), root cause agrees, inputs in contract; rules out a code-level complement law, concedes the docs could state containment, no contact and the partial case. |
| Second adjudication | real, doc | New seeds on main and 10.6.0; the fn: route on the docs' example sentence gives 0 + 0; the 117 upstream intervals tests pass with and without option B. |
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 419 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,521 words
- Registry title
- Intervals.within and notWithin are documented as opposites, but a multi-position source interval only partly inside the n-position window around the reference matches neither (documentation / API-contract defect)
- Area
- queries / intervals (Intervals.within / Intervals.notWithin and their javadoc; queryparser fn:within / fn:notWithin docs)
- Priority
- Low, rank 1230: A documentation and API-contract defect where within and notWithin are documented as opposites but are not exact complements for multi-position sources in this little-used pair.
- Impact class
- minor
- Impact note
- Anyone who calls Intervals.within / Intervals.notWithin (directly, through IntervalQuery, or as fn:within / fn:notWithin in the flexible StandardQueryParser since 9.1.0) with a multi-position source (a phrase, ordered or unordered near) and reads the docs, which present the pair as opposites, with default writer, codec, analyzer and searcher settings, at any index size, with -ea and -da, in every release from 8.0.0 to 10.6.0 and on main: a source interval that straddles the edge of the n-position window around the reference, or that contains a single-position reference and is wider than 2n+1 positions, is returned by neither query, so 'X but not within n of Y' written as notWithin(X, n, Y) and as MUST(X) MUST_NOT(within(X, n, Y)) return different documents; single-position sources (the shape of every upstream test and doc example) are exact complements [exec: controls]; the code does what each @param can be read to say, so the defect is classed as documentation, and nothing is written to the index.
- Summary
- You index 'the new york times' and run within(phrase(new, york), 1, times) and notWithin(phrase(new, york), 1, times), and you expect exactly one of them to match, because the javadoc says within returns source intervals that 'appear within a set number of positions' of the reference and notWithin those that 'do not appear within' it, and the fn:within / fn:notWithin docs give both the same 'maximum positions' parameter. Instead neither matches: within keeps an interval only if all of it lies inside the window [r.start-n, r.end+n] (here [2,4]), while notWithin drops it if any part touches that window, so 'new york' at [1,2], which touches the window without lying inside it, is in neither result (within needs n = 2 for a phrase directly next to the reference). In the same way, on 'new york' both within(phrase(new, york), 0, york) and notWithin(phrase(new, york), 0, york) return nothing, and no doc says that a partly-near interval matches neither.
- Introduced
- 3593af2ac55f12a2ce2cfa76fd58407aa3fafa67 (branch_8x, first released in 8.0.0) and its master twin 7d34bfdaad37a25bfda5df2bf2c2bd1a31540bc2 (same Intervals.java, first released in 9.0.0), both 'LUCENE-8629: New interval functions: before, after, within, overlapping' (@romseygeek; a JIRA patch, no GitHub pull request; the issue is now apache/lucene#9675, whose description lists 'a WITHIN b - inverse of the already available NOT_WITHIN'). It added within = containedBy(source, extend(reference, n, n)) next to the existing notWithin = DifferenceIntervalsSource(minuend, extend(subtrahend, n, n), NON_OVERLAPPING) (LUCENE-8612, 2532a5d31c8, 2019-01-01, also first released in 8.0.0), and in the same commit changed notWithin's @param from 'the maximum distance' to 'the minimum distance' [read: git show 7d34bfdaad3; 3593af2ac55 has the same Intervals.java] [exec: 7.7.3 has no within; 8.0.0 shows the gap]. The gap needs both factories: notWithin's no-overlap rule predates within (7.4.0-7.7.3 already return no hit for notWithin on both repro documents [exec]). The fn:within / fn:notWithin docs with the shared text 'an integer number of maximum positions between source and reference' came later, with LUCENE-10223 (apache/lucene#429, @dweiss, f725b27e12a, first released in 9.1.0) [read]. Not a regression: within's body is identical in all 30 master revisions of Intervals.java since 7d34bfdaad3 and in main, and notWithin only moved to NonOverlappingIntervalsSource (LUCENE-8477), the same relation [read] (2019-01-07 (author date of 7d34bfdaad3 and of the branch_8x twin 3593af2ac55); the fn:within / fn:notWithin parser docs 2021-11-11 (f725b27e12a))
- First affected version
- 8.0.0 [exec] (the first release with Intervals.within, shipped in the lucene-sandbox artifact as org.apache.lucene.search.intervals; the package moved to lucene-queries in 8.2.0). 7.4.0 to 7.7.3 have notWithin but no within [exec: probe reports the API absent, the JUnit does not compile]; 7.3.1 and earlier have no intervals package [exec 7.3.1]. The fn:within / fn:notWithin query-string route exists from 9.1.0 [read]
- Affected versions
- every release from 8.0.0 through 10.5.2, 10.6.0 (its intervals sources at branch_10_6 RC1 4ae946e5a9b [exec], below) and main a9779ef3c193 [exec: the follow-up history sweep ran a public-API probe (5 controls that hold on every version) and the finder JUnit (package-adapted for 8.0.0-9.0.0) on all 60 releases 8.0.0, 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 with -ea, and also with -da on 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0 and 10.5.2: 82 of 90 runs show the gap, 8 report the API absent (7.3.1-7.7.3), no control failed, nothing threw; the independent reproduction and the intended-behaviour check also on 10.5.2 and 9.12.3]; branch_10_6 head 4ae946e5a9b [exec: its intervals sources shadowed over the 10.5.2 core, 2 of 2 fail], so the released 10.6.0 has it; main a9779ef3c193 [exec, -ea and -da, also with every other known interval fix and the sibling overlap-filters fix shadowed]; upstream main 20ced71c1e6 [read: the 4 commits after a9779ef3c193 touch no intervals or query-parser file]. Not affected: 7.4.0-7.7.3 (no within) and 7.3.1 (no intervals package) [exec]. All 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 disputed it (the adjudication ruled it real), 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 (searched 2026-10-28, plus the finder's, the independent reproduction's, the intended-behaviour check's and the adjudication's searches and a re-check the same day): four scripted prior-report searches (mechanism; documentation wording; downstream; the finder's run) over apache/lucene issues and PRs, commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists and the JIRA archive; 44 manual GitHub searches, including apache/solr, elastic/elasticsearch and opensearch-project/OpenSearch; 11 JIRA queries with full reads of LUCENE-8629, LUCENE-8612, LUCENE-8196, LUCENE-8477, LUCENE-10223, LUCENE-10229, SOLR-13764 and SOLR-18585; 8 web queries (three more did not complete); a regex scan of the bodies and comments of 171 issues and PRs; the changed files of all 411 open apache/lucene PRs. Not fixed upstream: no open PR touches within / notWithin or their docs; the newest Intervals.java commit is f4cc6ac2097 (#16899, author date 2026-10-26, contained in a9779ef3c193, unrelated); the intervals and intervalfn package-info files last changed in f725b27e12a (LUCENE-10223, author date 2021-11-11); the 4 upstream main commits after a9779ef3c193 touch neither. Re-check: no apache/lucene issue or PR created since 2026-10-26 mentions within / notWithin, and none of the 12 PRs opened since then touches intervals. Nearest items, not reports: #9675 / LUCENE-8629 (introduced within as the 'inverse of the already available NOT_WITHIN'), #9658 / LUCENE-8612 (notWithin on extend() and NON_OVERLAPPING), apache/lucene#429 / LUCENE-10223 (wrote the fn: docs; no semantics discussion), elastic/elasticsearch#64329 (a user asked for a span_not equivalent in intervals; closed as a question), the Solr 10.1 Intervals Query Parser guide (SOLR-13764, apache/solr#4582: exposes within / not_within with the same opposite-pair wording and single-term examples; downstream exposure, not a report)
- Lucene commit tested
- main
645d4fe14a20(2026-10-28): the minimal test through gradle with assertions on and off, before and after the javadoc fix and the code alternative, with the intervals (117 tests) and flexible-parser (125 tests) suites, in a scratch worktree (2026-11-01); standalone on 10.6.0, 10.5.2, 10.0.0, 9.12.3, 9.1.0, 9.0.0, 8.11.4, 8.2.0, 8.1.1 and 8.0.0 (an earlier sweep covered all 60 releases from 8.0.0 to 10.5.2). Upstream mainf815e679bf6(2026-11-01) has the same code and docs.
- Finding record
lucene/queries/src/java/org/apache/lucene/queries/intervals/Intervals.java:482-497 (at a9779ef3c193) (notWithin: summary :485-486 'Returns intervals of the minuend that do not appear within a set number of positions of intervals from the subtrahend query'; @param :489-490 'the minimum distance that intervals from the minuend may occur from intervals of the subtrahend'; body :495-496 new NonOverlappingIntervalsSource(minuend, Intervals.extend(subtrahend, positions, positions))), :499-511 (within: summary :500-501 'Returns intervals of the source that appear within a set number of positions of intervals from the reference'; @param :504-505 'the maximum distance that intervals of the source may occur from intervals of the reference'; body :510 containedBy(source, Intervals.extend(reference, positions, positions)))lucene/queries/src/java/org/apache/lucene/queries/intervals/package-info.java:21 (at a9779ef3c193) ('experimental classes'), :68-69 (notWithin 'Returns intervals that do not appear within a set number of positions of another interval')lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/intervalfn/package-info.java:513-536 (at a9779ef3c193) (fn:within: 'appear within the provided number of positions', :526 positions 'an integer number of maximum positions between source and reference', single-term examples), :542-566 (fn:notWithin: 'do not appear within', :555 the same positions text)lucene/queries/src/java/org/apache/lucene/queries/intervals/ContainedByIntervalsSource.java (at a9779ef3c193) and NonOverlappingIntervalsSource.java (the two relations the factories use)
- The finding in 30 seconds
lucene/queries/src/java/org/apache/lucene/queries/intervals/Intervals.java:482-511 (at 645d4fe14a20)lucene/queries/src/java/org/apache/lucene/queries/intervals/ContainedByIntervalsSource.java:46-66; lucene/queries/src/java/org/apache/lucene/queries/intervals/NonOverlappingIntervalsSource.java:60-84; lucene/queries/src/java/org/apache/lucene/queries/intervals/ExtendedIntervalIterator.java:53-61 (at 645d4fe14a20)lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/intervalfn/package-info.java:513-566; lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/intervalfn/Within.java:38; lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/intervalfn/NotWithin.java:38 (at 645d4fe14a20)
- Who is affected
lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/intervalfn/Within.java:38; lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/intervalfn/NotWithin.java:38; lucene/queries/src/java/org/apache/lucene/queries/intervals/package-info.java:21 (at 645d4fe14a20)
- Why it looks unintended
lucene/queries/src/java/org/apache/lucene/queries/intervals/Intervals.java:485-490, 500-505, 541-542, 554; lucene/queries/src/java/org/apache/lucene/queries/intervals/package-info.java:68-70 (at 645d4fe14a20)lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/intervalfn/package-info.java:513-566 (at 645d4fe14a20)lucene/queries/src/test/org/apache/lucene/queries/intervals/TestIntervals.java:915-922; TestIntervalQuery.java:182-186 (at 645d4fe14a20)
- History
lucene/CHANGES.txt:6797-6798 (LUCENE-8629 under 8.0.0), :4415 (LUCENE-10223 under 9.1.0)
- TestWithinNotWithinPartlyNear.java:
repro/LFV-830/src/org/apache/lucene/queries/intervals/TestWithinNotWithinPartlyNear.java(52 lines)
assets/LFV-830/bug.png(419 KB)
