CONTAINS with several points should match only documents that hold all of them. The points query does that with one MUST clause per point; the doc-values query accepts a document as soon as one of its points equals one query point.
answer for all (document point, query point) pairs is set by any match. Select the still to open it at full size. Generated with AI, checked against the code.- Index four documents with
LatLonPointandLatLonDocValuesFieldon one field: A = {P1}, B = {P2}, C = {P1, P2}, D = {P3}. - Ask for CONTAINS(P1, P2), the documents whose points include both P1 and P2. You expect [C], and
LatLonPoint.newGeometryQueryreturns [C]. LatLonDocValuesField.newSlowGeometryQuery, which its javadoc tells you to pair with that query, returns [A, B, C]: every document holding P1 or P2, exactly what INTERSECTS(P1, P2) returns.- So the documented pair
IndexOrDocValuesQuery(points, docValues)depends on which side runs: with 2,000 more one-point documents it returns [C] on its own, butid:A ANDit returns A, which holds only P1, andexplain()says A matches. - Why: the doc-values CONTAINS keeps one flag per document and sets it as soon as any document point equals any query point; nothing checks that every query point was found. The points query adds one MUST clause per point.
Anyone running CONTAINS with two or more points on a LatLonDocValuesField: on its own (for example a doc-values-only field), through the documented IndexOrDocValuesQuery pair under a selective clause, or via that pair's explain(). Default configuration, any index size; extra hits only. Rare: a niche query; no user report was found since 2021.
Mechanism, terms and numbers587 words
LatLonDocValuesField.newSlowGeometryQuery(field, CONTAINS, p1, p2, ...) should match the documents whose points include every query point, as LatLonPoint.newGeometryQuery(field, CONTAINS, p1, p2, ...) does with one MUST clause per point. Its javadoc says to wrap it in an IndexOrDocValuesQuery alongside exactly that query, and IndexOrDocValuesQuery requires that "Both provided queries must match the same documents". Instead LatLonDocValuesQuery.contains() keeps a single flag per document and sets it as soon as any document point equals any query point, so it computes an OR: on four documents A = {P1}, B = {P2}, C = {P1, P2}, D = {P3}, CONTAINS(P1, P2) returns [C] on the points path and [A, B, C] on the doc-values path, the INTERSECTS hit set. The wrapper's answer then depends on which side runs: with 2,000 more one-point documents it returns [C] alone, A under id:A AND, and its explain() reports a match for A. No error; default configuration; extra hits only. LUCENE-9641 (14009f4424d, 8.8.0) added the AND and the OR in the same commit; every release since is affected, and a one-method fix that requires every query point passes the repro and the closest existing suites.
- Point fields:
LatLonPointindexes a location in a points (BKD) index;LatLonDocValuesFieldstores it as doc values, read one document at a time. A document may hold several points on one field. - CONTAINS on points:
newGeometryQuery(field, CONTAINS, p1, ..., pk)andnewSlowGeometryQuery(field, CONTAINS, p1, ..., pk)accept onlyPointgeometries for CONTAINS (others giveMatchNoDocsQuery,LatLonDocValuesField.java:255-262). A query point is contained when a document point equals it on the encoding grid: the query point is rounded up to the grid (Point2D.java:168-181) and compared with exact==(Point2D.java:56-57). IndexOrDocValuesQuery: wraps a points query and an equivalent doc-values query and picks a side per segment: the doc-values one only when the points cost shifted right by 3 (divided by 8, rounded down) is greater than the lead clause's cost,cost() >>> 3 > leadCost(IndexOrDocValuesQuery.java:183-193), otherwise the points one;explain()andmatches()always use the doc-values one (:153-163). "Both provided queries must match the same documents" (:61-62).
LatLonDocValuesQuery.contains()(LatLonDocValuesQuery.java:244-273) builds one component per query point (:245-248), because a multi-component tree cannot answerwithinPoint(ComponentTree.java:284-289).- Per document it starts with
answer = DISJOINT(:252) and, for every (document point, query point) pair, setsanswerto any relation other thanDISJOINT(:261-262); it returnsanswer == CANDIDATE(:266). - A point component answers only
CANDIDATEorDISJOINT(Point2D.java:126-127), so theNOTWITHINexit never fires and the document matches as soon as one of its points equals one query point: an OR. LatLonPoint.newGeometryQuerysends CONTAINS tomakeContainsGeometryQuery, one MUSTLatLonPointQuery(CONTAINS, point)per point (LatLonPoint.java:337-354): an AND.LatLonShapeandXYShapealso AND one clause per geometry.
P1 = 22.5° N 45° E, P2 = 45° N 135° E, P3 = 22.5° S 90° W (exact on the grid); A = {P1}, B = {P2}, C = {P1, P2}, D = {P3}. CONTAINS(P1): [A, C] on both paths. CONTAINS(P1, P2): points [C], doc values [A, B, C], the same as INTERSECTS(P1, P2). With 2,000 more one-point documents (alternately at P1 and P2): points and the wrapper alone [C], doc values 2,003 hits instead of 1, FILTER(id:A) + FILTER(wrapper) [A], wrapper explain(A).isMatch() true. The same on main and 10.5.2 (assertions on and off), 10.0.0 and 9.12.3.
Exposure and likelihood303 words
- Who is exposed. Applications that call
newSlowGeometryQuery(field, CONTAINS, p1, p2, ...)directly (for example on a field with doc values but no points index), or the documented pairIndexOrDocValuesQuery(LatLonPoint.newGeometryQuery(...), newSlowGeometryQuery(...))when a selective lead clause makes it run the doc-values side, orexplain()/matches()of that pair, which always use the doc-values side, soexplain()can report a match thatsearch()does not return. - Default, not opt-in. Default
IndexWriterConfig, codec and searcher; both factories are public and not deprecated (IndexOrDocValuesQueryis@lucene.experimental). Four documents suffice for the doc-values query alone; the wrapper runs the doc-values side only under a lead clause more than 8 times cheaper than the points query (exactly:cost() >>> 3 > leadCost). - What you see. Extra hits, silently: documents that hold some but not all of the query points. The doc-values CONTAINS(P1, ..., Pk) returns the INTERSECTS hits. Documents that hold all points still match.
- Query cache. A cached entry is built from the points side (
LRUQueryCache.java:609withIndexOrDocValuesQuery.java:196-201), so where a query cache is in use the same query can change its answer once it is cached (read from the code, not executed). That needs a cache configured explicitly (main'sIndexSearcherhas none by default), a segment the cache accepts (the defaultLRUQueryCacheskips segments under 10,000 documents) and a query used often enough for the caching policy; neither repro index qualifies. - Not affected. Single-point CONTAINS, the other relations, the points-only query;
XYDocValuesFieldhas no relation-taking factory. - How likely. Rare: "documents that hold all of these locations" is a niche query, and the searches of 2026-10-27 found no user report since 8.8.0. A downstream remark (code reading only, not executed): one downstream search engine builds exactly this pair for its geo-point field and uses the doc-values query alone for doc-values-only fields.
The test shows that LatLonDocValuesField.newSlowGeometryQuery(CONTAINS, (0, 0), (10, 10)) matches a document whose only point is (0, 0), so the doc-values CONTAINS is an OR over the query points, while its documented IndexOrDocValuesQuery partner LatLonPoint.newGeometryQuery requires every point.
/*
* 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.document;
import java.io.IOException;
import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.Point;
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.search.Query;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestLatLonDocValuesContainsTwoPoints extends LuceneTestCase {
public void testDocWithOnlyOneOfTwoPointsDoesNotMatch() throws IOException {
try (Directory dir = new ByteBuffersDirectory()) {
try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
Document doc = new Document();
doc.add(new LatLonDocValuesField("p", 0, 0));
w.addDocument(doc);
}
try (DirectoryReader reader = DirectoryReader.open(dir)) {
Point[] both = {new Point(0, 0), new Point(10, 10)};
Query q = LatLonDocValuesField.newSlowGeometryQuery("p", QueryRelation.CONTAINS, both);
// expected 0 (doc lacks (10, 10); LatLonPoint.newGeometryQuery gives 0), actual 1
assertEquals(0, new IndexSearcher(reader).count(q));
}
}
}
}
# in a copy of this finding's repro folder (repro/LFV-701/, 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: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.document.TestLatLonDocValuesContainsTwoPoints
- Expected vs actual
- Expected: 0 hits (the document lacks (10, 10)). Actual on main with -ea and with -da: 1 hit (expected:<0> but was:<1>).
More files, environment, full output34 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: expected:<0> but was:<1>
org.junit.Assert.fail(Assert.java:89)
org.junit.Assert.failNotEquals(Assert.java:835)
org.junit.Assert.assertEquals(Assert.java:647)
org.junit.Assert.assertEquals(Assert.java:633)
org.apache.lucene.document.TestLatLonDocValuesContainsTwoPoints.testDocWithOnlyOneOfTwoPointsDoesNotMatch(TestLatLonDocValuesContainsTwoPoints.java:44)- The javadoc pairs it with
LatLonPoint#newGeometryQueryin anIndexOrDocValuesQuery, which requires: "Both provided queries must match the same documents". LatLonPoint,LatLonShapeandXYShapeall add one MUST clause per CONTAINS geometry; the commit that added both paths says "Equivalent to LatLonShape".- Read as OR, the doc-values CONTAINS is just INTERSECTS; no test sends more than one CONTAINS point.
The full argument193 words
- The pairing is documented.
LatLonDocValuesField.java:232-238: "It is best used wrapped in an {@link IndexOrDocValuesQuery} alongside a {@link LatLonPoint#newGeometryQuery(String, ShapeField.QueryRelation, LatLonGeometry...)}".IndexOrDocValuesQuery.java:61-62: "Both provided queries must match the same documents and give the same scores." The code relies on it: "Both index and dv query should return the same values" (:212-213). - CONTAINS over several geometries is an AND elsewhere.
LatLonPoint.java:343-354,LatLonShape.java:354-370andXYShape.java:263-267add one MUST clause per geometry. - One commit added both.
14009f4424d(LUCENE-9641, 2021-01-08): "Equivalent to LatLonShape, LatLonPoint can be queried now using spatial relationships." It added the AND and the OR together, with no comment on a difference; issue #10680 states the goal thatLatLonShapeandLatLonPointreturn the same results.contains()is unchanged since. - The author's intent elsewhere. The LUCENE-9641 author's downstream test expects 0 hits for CONTAINS(MULTIPOINT(a, b)) on a document holding only a (it runs the points side). Their open PR #12162 documents CONTAINS for
newSlowGeometryQueryas "one or more points of the field matches all the provided LatLonGeometry". - The test gap.
BaseSpatialTestCase.java:562-565sends onlyqueryPoints[0]for CONTAINS, so the two paths were never compared with several points. No test asserts the OR.
The devil's-advocate check233 words
The strongest case for "intended": LatLonPoint.newGeometryQuery's javadoc says "@return query matching points within at least one geometry" (LatLonPoint.java:322), which reads like OR; the pairing is only "best used"; a committer has said relations make little sense for points; and no javadoc spells out multi-point CONTAINS. Independent reviews by another model family (OpenAI GPT-5.6, refute lens and intended lens) rated this moderate; the devil's advocate rejected it point by point; the adjudication called it weak. A maintainer may also cite the IndexOrDocValuesQuery class note ("only works well with point range/exact queries and their equivalent doc values queries", IndexOrDocValuesQuery.java:51-52).
Why it fails: that @return line dates from LUCENE-9552, before relations existed, and the same method builds an AND for CONTAINS, so it does not follow it either. The javadoc names the exact counterpart with the same arguments, and the wrapper forbids operands that disagree. The committer's remark is about API design; both factories are public and shipped since 8.8.0. Under OR, CONTAINS would duplicate INTERSECTS. The IndexOrDocValuesQuery class note is about performance ("works well") and does not relax "must match the same documents"; newSlowGeometryQuery's own javadoc recommends this wrapping. #12162's wording is not literally the per-point AND (read literally, one document point equal to every query point, it would reject C too), but neither reading admits A or B, which hold only one of the two points. Verdict: a defect.
Recommended Require every query point in contains() (validated)
In LatLonDocValuesQuery.contains(), remember for each query point whether some document point equals it, and match only when all are found: one reused boolean[] and a counter, +11/-6. The new test passes 2 of 2, and 31 existing LatLon, XY and IndexOrDocValuesQuery suites (224 tests) pass with assertions on and off.
final boolean[] contained = new boolean[component2Ds.size()];
// in matches():
Arrays.fill(contained, false);
int numContained = 0;
// for each document point (lat, lon) and each query component c:
Component2D.WithinRelation relation = component2Ds.get(c).withinPoint(lon, lat);
if (relation == Component2D.WithinRelation.NOTWITHIN) {
return false;
} else if (relation == Component2D.WithinRelation.CANDIDATE && contained[c] == false) {
contained[c] = true;
numContained++;
}
// after the loop:
return numContained == contained.length;All options and trade-offs4 options · 557 words
One fix was built and validated through gradle; the hunt validated a variant of it; two alternatives were not built.
In LatLonDocValuesQuery.contains(): keep one flag per query component in a boolean[] allocated once per iterator, reset it per document, count the components found, and match only when all are found; NOTWITHIN still rejects at once. 1 file, +11/-6.
final boolean[] contained = new boolean[component2Ds.size()];
// in matches():
Arrays.fill(contained, false);
int numContained = 0;
// for each document point (lat, lon) and each query component c:
Component2D.WithinRelation relation = component2Ds.get(c).withinPoint(lon, lat);
if (relation == Component2D.WithinRelation.NOTWITHIN) {
return false;
} else if (relation == Component2D.WithinRelation.CANDIDATE && contained[c] == false) {
contained[c] = true;
numContained++;
}
// after the loop:
return numContained == contained.length;Trade-offs
Same loop and cost as today plus an Arrays.fill of k booleans per document (k = number of query points); no allocation per document. The array is created once per scorer supplier (per segment or intra-segment partition, and also when IndexOrDocValuesQuery then runs the points side, since it creates both suppliers first); the stock code already allocates a list and one Component2D per query point there. One-point CONTAINS behaves as before. The diff keeps the full loop; it could return true as soon as every point is found, since only Point geometries reach contains() and they never return NOTWITHIN. No benchmark was run.
What the checks validated
- Scratch worktree of
a9779ef3c193, gradle, JDK 25: stock + the new test fails 2 of 2 with assertions on and off; + fix it passes 2 of 2 with assertions on and off. - With the fix, the existing
TestLatLon*,TestXY*andTestIndexOrDocValuesQuery: 31 suites, 224 tests, 0 failures, 25 skipped (24@Nightly), assertions on and off (gradle's totals of 32 suites and 226 tests include a first version of the new test); the two doc-values point suites with-Ptests.nightly=true: 10 tests, 0 failures;:lucene:core:check -x testpasses. - Standalone runner with the fix: the finder's test, the independent reproducer's 4 tests, the devil's advocate's probe and the cross-model reviewers' own tests pass.
- Not run: the full Lucene test suite, and any benchmark.
Functionally the same as the recommended option, but allocates the array for every candidate document.
Trade-offs
One small allocation per candidate document; the recommended option avoids it.
What the checks validated
Through the standalone runner: the finder's, the independent reproducer's, the devil's advocate's and the cross-model reviewers' tests pass, and 49 of 49 tests of four existing doc-values and point suites.
In newSlowGeometryQuery, for CONTAINS with several points, return a ConstantScoreQuery over one MUST LatLonDocValuesQuery(CONTAINS, point) per point, mirroring makeContainsGeometryQuery.
Trade-offs
Symmetric and obviously equivalent, but each clause reads the document's doc values again (k reads per document), the query's structure changes (toString, equals), and contains() stays an OR for any other caller.
Declare that CONTAINS with several points on the doc-values path matches any of them.
Trade-offs
Makes it identical to INTERSECTS and still breaks the IndexOrDocValuesQuery contract unless the points CONTAINS changes too, which would break parity with LatLonShape, the goal of LUCENE-9641.
An application that uses a multi-point CONTAINS on a point field as a restriction ("only documents that hold all of these locations") also gets documents that hold just one of them whenever the doc-values side runs.
A bounded-exhaustive differential check: the same grid-exact documents were indexed as shapes, as points and as doc-values points, queried with all four relations, and compared with an exact geometry reference (JTS). Only the doc-values point path disagreed, on multi-point CONTAINS; the cause was then read from the code.
The method in full183 words
Method: bounded-exhaustive differential testing against an exact reference (the hunt's semi-formal tier).
- The hunt unit compared Lucene's shape and point relation queries with JTS 1.20 DE-9IM relations on the decoded coordinates, with a strict and a permissive boundary band per relation, so a boundary convention cannot show up as a bug.
- Inputs: lattices of 6 x 6 grid-exact coordinates in nine families (around the origin, the poles and the antimeridian, and XY), every point, segment and small polygon of the lattice plus random ones, multi-part and multi-point documents, and 2-3-point CONTAINS queries, each document indexed as
LatLonShape,LatLonPointandLatLonDocValuesField. - At scale 1 the doc-values point path disagreed with the reference and with both index paths in 606 (document, query) pairs in six of the seven LatLon families (490 with a multi-point document, 116 with a one-point document) and 585 in the dateline family, all multi-point CONTAINS; the index paths agreed with the reference. The root cause was read from
LatLonDocValuesQuery.contains()and reduced to a public-API JUnit. - The same harness also explained every other mismatch by known bugs or documented conventions.
No prior report found (searched 2026-10-27, re-checked the same day): about 140 queries over GitHub issues, PRs and commits, the JIRA archive, the mailing lists, downstream repositories and the web, plus the changed files of all 409 open Lucene PRs. Nearest: open PR #12162, which documents an all-geometries CONTAINS and pairs both queries without changing the code.
Searches4 searches
No issue, PR, JIRA ticket, list thread, downstream report or web page describes the doc-values CONTAINS matching documents that hold only some of the query points, or the two paths disagreeing.
Closest, not this bug: #12162 (open since 2023-02, last human comment 2023-03; adds the all-geometries CONTAINS javadoc and a LatLonField that wraps both factories; leaves contains() alone, so the new field would inherit the bug), #10680 (LUCENE-9641, the introducing change), #12154 (closed unmerged; moved contains() unchanged), #10635 (LUCENE-9595, a different withinPoint defect) and #16847 (open; touches the file only for maxDoc).
| Where | When | How | Found |
|---|---|---|---|
| GitHub, commits, downstream repositories, mailing lists, JIRA archive, web | 2026-10-27 | two scripted prior-report search runs (52 and 45 queries; 436 and 398 distinct hits, the 3 rated high match by term overlap only); the finder's run (51 queries, 6 web queries left pending; 35 hits) | only the related items above |
| GitHub (apache/lucene, apache/lucene-solr and downstream repositories) | 2026-10-27 | 41 manual issue and PR searches and 4 code searches (read-only) | no report |
| Open PRs | 2026-10-27 | the changed files of all 409 open apache/lucene PRs | only #16847 touches LatLonDocValuesQuery.java (maxDoc only); #12162 touches the two field classes |
| GitHub, re-check for this doc | 2026-10-27 | 6 read-only searches by class, factory and symptom; every issue and PR created since 2026-10-26 | nothing new |
Introduced with the feature in 8.8.0 by LUCENE-9641 (14009f4424d, 2021-01-08), which made the points CONTAINS an AND and the doc-values CONTAINS an OR in the same commit. A version-portable main() fails on all 18 releases tried from 8.8.0 to 10.5.2 and on main; contains() is unchanged since.
Versions tested8 rows
Introduced by LUCENE-9641 "Support for spatial relationships in LatLonPoint" (@iverase, 14009f4424d, 2021-01-08; apache/lucene-solr#2155, issue #10680), backported to branch_8x as f73f6b11f0e (apache/lucene-solr#2188) and released in 8.8.0 (CHANGES: "LatLonPoint query support for spatial relationships"). Before it, 8.7.0 had no relation-taking factories. git log -L and git blame attribute every line of contains() to that commit; later commits to the file are scorer plumbing. Not fixed upstream.
| Version or commit | Verdict | Result |
|---|---|---|
main a9779ef3c193 | affected | this doc's test 2 of 2 fail (gradle and the standalone runner, assertions on and off); the main() port fails (-ea and -da) |
| 10.5.2, 9.12.3 | affected | this doc's test 2 of 2 fail (10.5.2 with assertions on and off) |
| 10.2.2, 10.3.0, 10.4.0 | affected | main() port: points 0, doc values 1000, wrapper under a lead 1 |
| 10.0.0, 10.1.0, 10.2.0 | affected | main() port: doc values 1000; the MUST-lead wrapper form returned 0 because a separate, fixed planner bug (#14542) ran the points side; the FILTER-lead form returns 1; this doc's test (FILTER lead) 2 of 2 fail on 10.0.0 |
| 9.0.0, 9.1.0, 9.11.1, 9.12.0 | affected | main() port: points 0, doc values 1000, wrapper under a lead 1 |
| 8.8.0, 8.8.1, 8.8.2, 8.9.0, 8.10.1, 8.11.4 | affected | main() port: points 0, doc values 1000, wrapper under a lead 1 |
| 8.7.0 | API absent | the relation-taking factories do not exist; the port does not compile |
| main + fix | not affected | 0 everywhere; this doc's test 2 of 2 pass |
An independent reproducer, an intended-behaviour devil's advocate and reviews by another model family (OpenAI GPT-5.6, each running its own tests) call it real; the adjudication re-ran the tests and confirmed it with high confidence; a registry re-run and an audit reproduced it again.
Checks and verdicts387 words
Every check ran tests on the stock jars, with assertions on and off. The cross-model reviews executed their own tests on main ea57360912ed (no geo, LatLon, XY, spatial or IndexOrDocValuesQuery source differs from a9779ef3c193); This doc's own runs are on a9779ef3c193.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | Differential check against JTS (606 mismatched pairs in six LatLon families and 585 in the dateline family, all multi-point CONTAINS on the doc-values path), then a public-API JUnit that fails on main (-ea, -da), 10.5.2 and 9.12.3 and passes with the fix. |
| cross-model review, refute lens | real (high); intended argument moderate | OpenAI GPT-5.6, executed: own parity test (indexed 1, doc values 3) and own wrapper test (target matched through the wrapper) fail on main with three seeds; the fix passes both and 49 of 49 existing tests. |
| Independent reproducer | real | Own 4-test JUnit outside the Lucene package: 3 of 4 fail on main (-ea, -da, two seeds), 10.5.2 and 9.12.3 (101 documents instead of 1; explain() disagrees with search()); 4 of 4 pass with the fix. |
| Cross-model review, intended lens | real (high); intended argument moderate | OpenAI GPT-5.6, executed: traced the "at least one geometry" wording to LUCENE-9552; own test (indexed 1, doc values 257) fails on main and 10.5.2, passes with the fix. |
| Intended-behaviour devil's advocate | real (high) | No javadoc, test, CHANGES entry or discussion endorses the OR; the doc-values CONTAINS hit set equals INTERSECTS on main, 10.5.2 and 9.12.3; passes with the fix. |
| adjudication | confirmed (high) | Re-ran the independent reproduction's test with a third seed (3 of 4 fail, -ea and -da; 4 of 4 with the fix) and the finder's repro. |
| registry re-run and audit | keep | The finder's JUnit re-run fails with seed 5EEDF00D (-ea and -da) and A0D17F00 (-ea) and passes with the fix (5EEDF00D, -ea); an independent probe gives the same hit sets and the explain() mismatch with -ea and -da. |
| this doc | reproduced; fix validated | New 2-test JUnit on the diagram's four documents fails 2 of 2 on main (gradle and standalone, -ea and -da), 10.5.2, 10.0.0 and 9.12.3; passes with the fix, as do the earlier reproducers and the closest existing suites. |
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 470 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,327 words
- Registry title
- LatLonDocValuesField.newSlowGeometryQuery with CONTAINS and two or more Point geometries matches documents that hold only some of the query points, while its documented IndexOrDocValuesQuery partner LatLonPoint.newGeometryQuery requires all of them
- Area
- core / document: LatLon point geometry queries (LatLonDocValuesQuery CONTAINS on the doc-values path vs LatLonPoint.newGeometryQuery, and their IndexOrDocValuesQuery pairing)
- Priority
- Medium, rank 198: The core LatLonDocValuesField CONTAINS query over two or more points silently computes OR instead of AND since 8.8.0, but multi-point CONTAINS on a point field is niche and no user report was found, so it follows the spatial3d wrong-result row LFV-247 and precedes the spatial-extras rows.
- Impact class
- silent wrong results
- Impact note
- Applications that run a CONTAINS geometry query with two or more Point geometries against a LatLonDocValuesField (default IndexWriterConfig, codec and searcher, any index size, -ea and -da, every release from 8.8.0 to 10.5.2 and main) get extra hits, documents that hold some but not all of the query points, whenever the doc-values side runs: newSlowGeometryQuery on its own (for example on a doc-values-only field), IndexOrDocValuesQuery(LatLonPoint.newGeometryQuery, newSlowGeometryQuery) under a selective lead clause, and IndexOrDocValuesQuery's explain(); the points-only query is correct, documents that hold all of the points still match, and single-point CONTAINS and the other relations are not affected.
- Summary
- You index points with LatLonPoint and LatLonDocValuesField and ask for the documents that contain both P1 and P2 (CONTAINS with two Point geometries); a document that holds only P1 must not match, and LatLonPoint.newGeometryQuery, which builds one MUST clause per query point, indeed matches none. LatLonDocValuesField.newSlowGeometryQuery, which its javadoc says to pair with that query in an IndexOrDocValuesQuery (whose two queries 'must match the same documents'), accepts a document as soon as any one of its points equals any one query point, so it computes an OR instead of an AND. In the repro (1000 one-point documents, half at P1 and half at P2) the points query returns 0 hits, the doc-values query returns all 1000, and 'id:7 AND IndexOrDocValuesQuery(points, docValues)' returns document 7, with no error.
- Introduced
- 14009f4424d847ce84fa23a71d8066dfe6c70da0 (LUCENE-9641 'Support for spatial relationships in LatLonPoint', apache/lucene-solr#2155, issue now apache/lucene#10680; author iverase; branch_8x backport f73f6b11f0ed4bcbfce445152d813adb3327d249, apache/lucene-solr#2188, shipped in 8.8.0) [read: git log -L and git blame attribute every line of LatLonDocValuesQuery.contains() to it; its parent has no relation-taking factories, so the defect cannot be expressed there; exec: the repro does not compile against 8.7.0 because the API is absent]. The same commit made LatLonPoint.newGeometryQuery(CONTAINS) a conjunction (makeContainsGeometryQuery; commit message 'Equivalent to LatLonShape'), so the bug is as old as the feature. (2021-01-08)
- First affected version
- 8.8.0 [exec] (the relation-taking factories first appear there; 8.7.0 lacks them [exec: compile error])
- Affected versions
- Every release that has the API: 8.8.0-8.11.4, 9.0.0-9.12.3, 10.0.0-10.5.2, and main a9779ef3c193; never fixed. [exec] the version-portable main() port fails on 8.8.0, 8.8.1, 8.8.2, 8.9.0, 8.10.1, 8.11.4, 9.0.0, 9.1.0, 9.11.1, 9.12.0, 9.12.3, 10.0.0, 10.1.0, 10.2.0, 10.2.2, 10.3.0, 10.4.0, 10.5.2 and main (-ea and -da), with LatLonPoint CONTAINS(P1,P2) = 0 and LatLonDocValuesField CONTAINS(P1,P2) = 1000 (expected 0) on every one; the finder's JUnit fails on main (-ea, -da), 10.5.2, 10.0.0, 9.12.3 and 9.1.0; the independent reproduction's and the intended-behaviour check's own tests fail on main, 10.5.2 and 9.12.3. On 10.0.0-10.2.0 the MUST(id)+FILTER(IndexOrDocValuesQuery) form returned 0 because a separate, fixed planner bug (apache/lucene#14542, fixed by #14543 in 10.2.1 and 10.3.0) made IndexOrDocValuesQuery run the points side; the doc-values query alone and the FILTER(id)+FILTER(IndexOrDocValuesQuery) form still fail there. The LatLonDocValuesQuery.contains() source is identical in the 35 local release tags from 8.8.0 to 10.5.2 and on main, and the LatLonDocValuesQuery class files of the 8.8.0 and 8.11.4 jars are byte-identical [read]. All runs with JDK 25 and SHA-1-checked release jars.
- 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 (the prior-report record, verdict new, searched 2026-10-27, read-only: two scripted prior-report searches with 52 and 45 queries over GitHub issues, PRs and commits, downstream repositories, the mailing lists, JIRA and the web (436 and 398 distinct hits; the 3 rated high match by term overlap only); 41 more GitHub issue and PR searches in apache/lucene, apache/lucene-solr and downstream repositories, 4 code searches, the file lists of all 409 open apache/lucene PRs, and the full history of LatLonDocValuesQuery.java; the finder's unit-level run (51 queries, 6 web queries left pending; 35 hits, 0 high) and the independent reproduction's and the intended-behaviour check's searches agree). Nearest items, none of them this bug: open PR apache/lucene#12162 (iverase, 'Add LatLonField class to index both LatLonPoint and LatLonDocValues', open since 2023-02-20, last human comment 2023-03-28, stale-bot labels since) adds CONTAINS javadoc 'Matches a documents if one or more points of the field matches all the provided LatLonGeometry' (an all-geometries wording) and pairs both factories in a new LatLonField without changing contains(), so the new field would inherit the path-dependent result; #10680 (LUCENE-9641) introduced both behaviours with the stated goal that LatLonShape and LatLonPoint return the same results; #12154 (closed unmerged) moved contains() unchanged; #16847 (open) touches the file only for maxDoc; #10635 (LUCENE-9595) is a different withinPoint defect.
- Lucene commit tested
- main
a9779ef3c193(commit of 2026-10-26; tested 2026-10-27) through gradle in a scratch worktree and through the standalone JUnit runner, with assertions on and off; the 10.5.2 and 9.12.3 release jars. Earlier checks: a version-portablemain()on 18 releases from 8.8.0 to 10.5.2 and on main (follow-up); the cross-model reviews ran on mainea57360912ed, where none of the geo, LatLon, XY, spatial orIndexOrDocValuesQuerysources differ froma9779ef3c193. The fix was validated in the scratch worktree: the new test with assertions on and off, 31 of the closest existing suites (224 tests) with assertions on and off, the two doc-values point suites in nightly mode, and:lucene:core:check -x test.
- Finding record
lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesQuery.java:244-273 (contains(): a single 'answer' set to any non-DISJOINT relation of any (doc value, query point) pair; line 266 'return answer == Component2D.WithinRelation.CANDIDATE')lucene/core/src/java/org/apache/lucene/document/LatLonPoint.java:322 (generic '@return query matching points within at least one geometry'), :337-338 (CONTAINS goes to makeContainsGeometryQuery) and :343-354 (one MUST LatLonPointQuery(CONTAINS, point) per point in a ConstantScoreQuery)lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesField.java:232-247 (newSlowGeometryQuery javadoc: 'It is best used wrapped in an IndexOrDocValuesQuery alongside a LatLonPoint#newGeometryQuery(String, ShapeField.QueryRelation, LatLonGeometry...)') and :248-264 (CONTAINS accepts Point geometries only, else MatchNoDocsQuery)lucene/core/src/java/org/apache/lucene/search/IndexOrDocValuesQuery.java:61-62 ('Both provided queries must match the same documents and give the same scores.')lucene/core/src/java/org/apache/lucene/geo/Point2D.java:126-127 (withinPoint returns only CANDIDATE or DISJOINT, so the NOTWITHIN early exit never fires for points)lucene/core/src/java/org/apache/lucene/document/LatLonShape.java:337 and :354-366, XYShape.java:263-267 (the shape factories also AND one CONTAINS clause per geometry)lucene/core/src/test/org/apache/lucene/document/BaseSpatialTestCase.java:563-564 (the randomized test sends only queryPoints[0] for CONTAINS)
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesQuery.java:244-273 (contains(); :252, :261-262, :266)lucene/core/src/java/org/apache/lucene/geo/Point2D.java:56-57, :126-127 (exact equality; withinPoint returns CANDIDATE or DISJOINT)lucene/core/src/java/org/apache/lucene/geo/ComponentTree.java:284-289 (withinPoint throws for more than one component)lucene/core/src/java/org/apache/lucene/document/LatLonPoint.java:337-354 (CONTAINS -> makeContainsGeometryQuery, one MUST clause per point)lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesField.java:232-263 (javadoc pairing; CONTAINS accepts Point geometries only)lucene/core/src/java/org/apache/lucene/search/IndexOrDocValuesQuery.java:61-62, :153-163, :183-193
- Who is affected
lucene/core/src/java/org/apache/lucene/search/IndexOrDocValuesQuery.java:54, :153-163, :183-193, :196-201lucene/core/src/java/org/apache/lucene/search/LRUQueryCache.java:609 (cache built from scorerSupplier.bulkScorer())lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:84 (no default query cache); LRUQueryCache.java:252 (MinSegmentSizePredicate(10000))
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesField.java:232-238, :244lucene/core/src/java/org/apache/lucene/search/IndexOrDocValuesQuery.java:61-62, :212-213lucene/core/src/java/org/apache/lucene/document/LatLonPoint.java:322, :343-354; LatLonShape.java:354-370; XYShape.java:263-267lucene/core/src/test/org/apache/lucene/document/BaseSpatialTestCase.java:562-565lucene/core/src/java/org/apache/lucene/search/IndexOrDocValuesQuery.java:51-52 (class note)
- History
lucene/CHANGES.txt:5448 (8.8.0 New Features)
- TestLatLonDocValuesContainsTwoPoints.java (JUnit test):
repro/LFV-701/src/org/apache/lucene/document/TestLatLonDocValuesContainsTwoPoints.java(48 lines)
Made in parallel with this doc; every element was checked against main a9779ef3c193 and an executed diagram probe on the same four documents, plus 2,000 one-point documents for the IndexOrDocValuesQuery lead case (assertions on and off). Its fix step shows the hunt's variant of the fix (a boolean[] per call), with a note to avoid that allocation.
assets/LFV-701/bug.png(470 KB)
