ShapeAreaValueSource gives each document the area of its shape times a multiplier you choose, for example to change units. Lucene simplifies a BooleanQuery by merging clauses that equals calls the same, and this source's equals ignores the multiplier.
- A
DoubleValuesSourcecomputes one number per document;FunctionScoreQuery(query, source)scores each match with that number. new ShapeAreaValueSource(shapeSource, ctx, geoArea, multiplier)returns the shape's area times the multiplier: a 20 x 40 rectangle gives 800 with multiplier 1 and 2400 with multiplier 3.- While rewriting,
BooleanQuerytreats equal clauses as duplicates: equal SHOULD or MUST clauses become one clause with a summed boost, equal FILTER or MUST_NOT clauses are kept once.FunctionScoreQueryis equal when its query and its source are equal.
BooleanQuery merges the two clauses into area x 1 with boost 2 and scores 1600 instead of 3200. Select the still to open it at full size. Generated with AI, checked against the code.- Index one 20 x 40 rectangle (planar area 800) with
BBoxStrategy. Score all documents withFunctionScoreQueryovernew ShapeAreaValueSource(strategy.makeShapeValueSource(), ctx, false, m): with m = 1 the document scores 800, with m = 3 it scores 2400. - Put both as SHOULD clauses into one
BooleanQuery. A disjunction scores the sum of its matching clauses, so you expect 800 + 2400 = 3200. - You get 1600, and no error.
equalsandhashCodecompare only the shape source andgeoArea, never the multiplier, so the two clauses are "equal" and the rewrite merges them into the first one with boost 2: 2 x 800. Swap the clauses and you get 4800. - Area filters are merged the same way. In a 20,000-document test, FILTER (area x 2 >= 500) plus FILTER (area x 1 >= 500) returns 3800 hits instead of 2550, and FILTER x 2 plus MUST_NOT x 1 becomes
MatchNoDocsQuery: 0 hits instead of 1250.
Only applications that construct ShapeAreaValueSource themselves (@lucene.experimental; Lucene builds it only in tests, no query parser does) and use two sources that differ only in the multiplier, such as one area in two units, in one BooleanQuery or behind one query cache. Default settings, any index size. Solr's BBoxField fixes the multiplier per field type and cannot build the pair.
Mechanism, terms and numbers763 words
ShapeAreaValueSource returns, per document, the area of its indexed shape times a multiplier passed to its public constructor. Its equals compares only geoArea and the shape source, and its hashCode hashes only those two, so area x 1 and area x 3 over the same field are equal and hash alike, and so are the FunctionScoreQuery objects built on them.
BooleanQuery.rewrite removes "duplicate" clauses by equality. Two equal SHOULD (or MUST) clauses become the first one with boost 2, so a disjunction of area x 1 and area x 3 on a rectangle of area 800 scores 1600 instead of 3200, or 4800 with the clauses swapped. Two equal FILTER or MUST_NOT clauses are kept once, so an area filter or exclusion is silently dropped, and FILTER x 2 with MUST_NOT x 1 rewrites to MatchNoDocsQuery. An equality-keyed query cache serves one multiplier's match set for the other.
The multiplier was added by SOLR-6797 (2015-01-03, first released in 5.0.0) without touching equals, hashCode or the printed form; the wrong scores appear from 7.1.0, when BooleanQuery started merging equal clauses. Affected: 5.0.0 to 10.6.0 and main. Reach is narrow: the class is @lucene.experimental, Lucene builds it only in tests, and the only downstream caller found (Solr) cannot build the colliding pair. Fix: compare and hash the multiplier (2 lines).
- The multiplier scales the value. The public constructor stores it in a non-final field (
ShapeAreaValueSource.java:40,:42-48), anddoubleValue()returnsshapeValues.value().getArea(geoArea ? ctx : null) * multiplier(:63). - Equality ignores it.
equalsreturns false only for a differentgeoAreaor shape source (:89-100);hashCodecombines only those two (:102-107). The field comment exempts onlyctxfrom identity ("not part of identity; should be associated with shapeValueSource indirectly",:37).toStringisarea(<shape source>,geo=<geoArea>)(:51-53), so the merged query prints as if nothing was lost. - The query takes its equality from the source.
FunctionScoreQuery.equalsisObjects.equals(in, that.in) && Objects.equals(source, that.source)andhashCodeisObjects.hash(in, source)(FunctionScoreQuery.java:143-154). BooleanQuerymerges equal clauses. SHOULD and MUST clauses live inMultisets, FILTER and MUST_NOT clauses inHashSets (BooleanQuery.java:129-142). SHOULD clauses are de-duplicated whenminimumNumberShouldMatch <= 1(:438-447), MUST clauses always (:449-455), indeduplicateClauses(:714-758):boosts.merge(query, boost, Double::sum)(:726) keeps the first equal key; an unboosted pair getscomputeQueryTermWeight(2)= 2 from the defaultBM25Similarity(:743-746;IndexSearcher.java:123;BM25Similarity.java:121-136), a boosted pair the linear sum (10.x releases always sum linearly). So area x 1 + area x 3 becomes(area x 1)^2= 1600. Duplicate FILTER/MUST_NOT clauses are dropped (:343-362), and a FILTER clause equal to a MUST_NOT clause returnsMatchNoDocsQuery("FILTER clause also in MUST_NOT")(:364-369).- Filters and caches inherit the same equality.
FunctionRangeQuerycompares its value source (FunctionRangeQuery.java:119-125),FunctionMatchQueryits source and predicate (FunctionMatchQuery.java:119-124), and the facet drill-down query thatDoubleRange.getQuerybuilds (DoubleRange.java:335) its range, fast-match query and value source (DoubleRange.java:130-143; the probe passes no fast-match query). A query cache looks entries up by equality; caching is off by default on main (IndexSearcher.java:82-84), on in 10.x releases.
Adding the multiplier to equals/hashCode, and nothing else, makes the repro and every other executed form pass, so the equality is the cause.
Score forms (the independent reproduction, main ea57360912ed, 10.5.2, 9.12.3): x1 OR x3 scores 1600 forward and 4800 reverse instead of 3200, rewritten to (FunctionScoreQuery(id:x, scored by area(bboxShape(bbox),geo=false)))^2.0; x1^1.5 OR x3^1.5 scores 2400 instead of 4800. Controls pass: equal parameters give equal sources, and constant(800) OR constant(2400) scores 3200.
Filter and cache forms (security probe, main with assertions on and off, and 10.5.2): 20,000 documents, each a rectangle of width w = 1..100 and height 10 (area 10w); filters are FunctionRangeQuery over ValueSource.fromDoubleValuesSource(area x m) with "value >= 500"; counts within one 5,000-document slice, expected values from a brute-force oracle.
| query | expected | actual |
|---|---|---|
area(x1).equals(area(x2)) | false | true |
| FILTER x2 + FILTER x1 | 2550 | 3800 |
| MUST_NOT x1 + MUST_NOT x2 | 1200 | 2450 |
| FILTER x2 + MUST_NOT x1 | 1250 | 0 (MatchNoDocsQuery) |
facet DoubleRange drill-down on area x1 after the x2 query warmed the cache (stock 10.5.2 searcher cache; an installed LRUQueryCache on main) | 2550 | 3800 (2550 instead of 3800 in the other order) |
Each filter alone is right (2550 and 3800). The intended-behaviour check also ran FunctionMatchQuery over area x1 and x2 with one shared predicate behind an always-cache LRUQueryCache: 0 hits instead of 1.
Exposure and likelihood298 words
- Silent. Wrong scores that depend on clause order (1600 or 4800 for 3200), and, for area filters, missing or extra hits, with no exception.
- Default settings, any size. The score form needs one document, the default
IndexWriterConfig, the default similarity and a plainIndexSearcher; the filter forms need no cache. The test extendsLuceneTestCase, which still randomizes the default codec, locale and time zone and installs a test query cache; the search needs scores, so the cache is not used (IndexSearcher.java:979-980), and the result is the same under both seeds run. Only the cache form needs a query cache: the default in 10.x releases (it caches segments of at least 10,000 documents after repeated use), opt-in on main. The probe used one 20,000-document segment and ran the warming query 10 times. A cache stores match sets only for searches without scores, so it can change hits only through a filter consumer such asFunctionRangeQueryorFunctionMatchQuery, never for a bareFunctionScoreQuery; and the tenantTermQueryin the same query stayed enforced. - Which queries. Two area clauses over the same shape field that differ only in the multiplier, in one
BooleanQueryas SHOULD (withminimumNumberShouldMatch <= 1), MUST, FILTER or MUST_NOT clauses, or in two queries behind one query cache. - Who is exposed. Applications that call the public constructor directly. Lucene constructs the class only in
TestBBoxStrategy, and no query parser builds it. Solr'sBBoxField(score=area and area2D), the only downstream caller found, passes a unit factor fixed per field type and uses the source only as a score; a read-only code search (2026-10-26) found no use in Elasticsearch or OpenSearch. - Likelihood: rare. No report found since 5.0.0 (search gaps under Prior reports); the class is
@lucene.experimental(ShapeAreaValueSource.java:33).
The test shows that two ShapeAreaValueSource instances that differ only in the multiplier compare equal, so a SHOULD disjunction of FunctionScoreQuery clauses over them is merged into one clause and loses the second multiplier.
/*
* 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.spatial.util;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.queries.function.FunctionScoreQuery;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.spatial.bbox.BBoxStrategy;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.locationtech.spatial4j.context.SpatialContext;
public class TestShapeAreaValueSourceMultiplier extends LuceneTestCase {
public void testDisjunctionOfTwoMultipliersScoresTheSum() throws Exception {
SpatialContext ctx = SpatialContext.GEO;
BBoxStrategy strategy = BBoxStrategy.newInstance(ctx, "bbox");
try (Directory dir = new ByteBuffersDirectory()) {
try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
Document doc = new Document();
for (Field field :
strategy.createIndexableFields(ctx.getShapeFactory().rect(0, 20, 40, 80))) {
doc.add(field);
}
w.addDocument(doc);
}
try (DirectoryReader reader = DirectoryReader.open(dir)) {
Query area1 =
new FunctionScoreQuery(
MatchAllDocsQuery.INSTANCE,
new ShapeAreaValueSource(strategy.makeShapeValueSource(), ctx, false, 1));
Query area3 =
new FunctionScoreQuery(
MatchAllDocsQuery.INSTANCE,
new ShapeAreaValueSource(strategy.makeShapeValueSource(), ctx, false, 3));
Query query =
new BooleanQuery.Builder().add(area1, Occur.SHOULD).add(area3, Occur.SHOULD).build();
// expected: 800 x 1 + 800 x 3 = 3200; actual: 1600 (the clauses are equal and merged)
assertEquals(3200f, new IndexSearcher(reader).search(query, 1).scoreDocs[0].score, 0f);
}
}
}
}
# in a copy of this finding's repro folder (repro/LFV-302/, 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:$LUCENE/lucene/spatial-extras/build/libs/lucene-spatial-extras-$V.jar
CP=$CP:$LUCENE/lucene/spatial3d/build/libs/lucene-spatial3d-$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.spatial.util.TestShapeAreaValueSourceMultiplier
# the module(s) spatial-extras also need their third-party runtime jars
- Expected vs actual
- Expected: one rectangle of area 800 scores 800 x 1 + 800 x 3 = 3200. Actual (-ea and -da): 1600.
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:<3200.0> but was:<1600.0>
org.junit.Assert.fail(Assert.java:89)
org.junit.Assert.failNotEquals(Assert.java:835)
org.junit.Assert.assertEquals(Assert.java:577)
org.junit.Assert.assertEquals(Assert.java:701)
org.apache.lucene.spatial.util.TestShapeAreaValueSourceMultiplier.testDisjunctionOfTwoMultipliersScoresTheSum(TestShapeAreaValueSourceMultiplier.java:62)- Lucene's own test requires
FunctionScoreQueryoverconstant(1)andconstant(2)to be unequal (TestFunctionScoreQuery.java:91): changing scores must change equality. - All three sibling spatial sources with a multiplier compare it in
equals; the class comment exempts onlyctxfrom identity. - SOLR-6797 added the multiplier in 2015 without touching
equals, and a disjunction whose score depends on clause order is no design choice.
The full argument260 words
- Value-source equality is meant to track values. LUCENE-7723 (#8774, filed by Adrien Grand; committed by Alan Woodward as
1a278ae89be, 2017) madeDoubleValuesSource.equals/hashCodeabstract (DoubleValuesSource.java:125-129); its description reads: "Given that we embed values sources in queries and sorts, from which we expect that they have working equals/hashCode". In LUCENE-7736 (#8787) Adrien Grand wrote "I think it's important for equals to behave correctly and never return true for values sources that could return different values", and Alan Woodward agreed. - Lucene's own tests require it.
TestFunctionScoreQueryasserts thatFunctionScoreQueryover the same term query withDoubleValuesSource.constant(1)andconstant(2)are unequal (QueryUtils.checkUnequal(q2, q3),TestFunctionScoreQuery.java:78-91). - The siblings do it.
DistanceToShapeValueSourcecompares and hashes its multiplier (:110,:123), andvector/DistanceValueSource(:110) andShapeFieldCacheDistanceValueSource(:113) compare it too. LUCENE-5118 (#6182, 2013) added the multipliers ofDistanceValueSourceandShapeFieldCacheDistanceValueSourceand compared them;DistanceToShapeValueSourcecompared and hashed its multiplier from its creation (LUCENE-5408, 2014). - History points to an oversight. LUCENE-5714 created the class in 2014 with no multiplier, and its
equalscovered every field exceptctx, which the comment exempts. SOLR-6797 (8fd247cc0e5, 2015-01-03) added the field, the constructor argument and* multiplier, and did not touchequals,hashCodeor the printed form (description()then,toStringtoday); none of its 25 comments mentions equality. That it was an oversight is an inference; nobody said so. - Nothing documents it. No javadoc or test says the multiplier is outside identity;
TestBBoxStrategy.testAreaValueSourcechecks only that multiplier 2 doubles the value (TestBBoxStrategy.java:311-329).
The devil's-advocate check181 words
The strongest case for "intended": Query#equals says "Typically a query will be equal to another only if it's an instance of the same class and its document-filtering properties are identical to those of the other instance" (Query.java:95-107), and a multiplier only scales a score; the class is @lucene.experimental; the "not part of identity" comment shows identity was considered on purpose; and the only real caller, Solr, uses one fixed multiplier per field type.
Why it fails: "typically" hedges, and the javadoc does not say that state which changes scores may be left out of equals, while FunctionScoreQuery's own test requires score-only differences to make queries unequal, and BooleanQuery sums the boosts of equal clauses, which is right only if they score the same; the multiplier does change which documents an area filter matches (executed); the comment predates the multiplier and names only ctx; @lucene.experimental warns about API changes, not wrong results. Verdict: not intended, a defect (the independent reproduction, the cross-model review and the intended-behaviour check returned real; the adjudication confirmed it with high confidence).
Recommended A: compare and hash the multiplier
Compare the multiplier in equals with Double.compare and add Double.hashCode(multiplier) to hashCode, as DistanceToShapeValueSource does: +2 lines. The repro passes, and so do all 118 lucene/spatial-extras tests and the module's lint checks. Optionally also make the field final and print the multiplier in toString (validated too).
if (geoArea != that.geoArea) return false;
if (Double.compare(multiplier, that.multiplier) != 0) return false;
if (!shapeValueSource.equals(that.shapeValueSource)) return false;
...
result = 31 * result + (geoArea ? 1 : 0);
result = 31 * result + Double.hashCode(multiplier);All options and trade-offs2 options · 313 words
Two variants were built and validated through gradle on 645d4fe14a20, both with the test-only build.gradle line that the regression test needs.
+2 lines in ShapeAreaValueSource.java. Double.compare is what DistanceToShapeValueSource.java:110 uses, and it is consistent with Double.hashCode (NaN equals NaN, -0.0 differs from 0.0).
if (geoArea != that.geoArea) return false;
if (Double.compare(multiplier, that.multiplier) != 0) return false;
if (!shapeValueSource.equals(that.shapeValueSource)) return false;
...
result = 31 * result + (geoArea ? 1 : 0);
result = 31 * result + Double.hashCode(multiplier);Trade-offs
None known for correctness: queries that differ only in the multiplier are no longer merged or shared in a cache, which is the point. No in-tree code outside tests builds the class. The regression test needs lucene/queries on the spatial-extras test classpath (one line); a maintainer who prefers to avoid that could ask for an equality-only assertion in the existing TestQueryEqualsHashCode instead (not built).
What the checks validated
- Gradle, scratch worktree of
645d4fe14a20, seed DEADBEEF: the repro fails on stock and passes with the fix, assertions on and off. - The whole
:lucene:spatial-extras:test(27 suites, includingTestBBoxStrategy,TestQueryEqualsHashCodeand the repro): 118 tests, 5 skipped, 0 failures. :lucene:spatial-extras:check -x test(checkGoogleJavaFormat,ecjLintMain,ecjLintTest,forbiddenApisMain,forbiddenApisTest): passes.- Earlier, the same change made the finder's tests, the independent reproduction's three tests, the intended-behaviour check's two tests and the security probe's 9 checks pass on main
ea57360912ed.
private final double multiplier; and toString gains ",multiplier=" + multiplier, so rewritten queries show what they score by.
Trade-offs
Changes the printed query, so a downstream test that matches the old string would need an update (none in Lucene).
What the checks validated
- Same module run: 118 tests, 5 skipped, 0 failures;
checkGoogleJavaFormatpasses.
Not a Lucene security issue, but an application that builds area filters with different multipliers on one field would silently have one area FILTER or MUST_NOT clause replaced by the other (executed); restriction clauses that are not area clauses are unaffected.
A bytecode scan of query components compared the fields their value code reads with the fields equals/hashCode read, and flagged ShapeAreaValueSource.multiplier. An AI tool then wrote a public-API JUnit, and independent reproducers confirmed it on a real BBoxStrategy index. No model checker or solver was involved.
The method in full158 words
Method: semi-formal (a hunt unit, query object contracts).
- A bytecode scan of the stock main jars (JDK 25 ClassFile API), over the unit's inventory of 184 query-component classes (value sources, intervals sources, rewrite methods), compared for each class the fields read by its value code with the fields read by
equals/hashCode. It reportedEQ-MISS ShapeAreaValueSource.multiplier, read byShapeAreaValueSource$1.doubleValuebut not byequals/hashCode(itsctxentry is intended, per the class comment). - The unit's bounded-exhaustive equality-congruence harness does not cover this class, so the finder, an AI tool, witnessed it with a public-API JUnit on the stock jar. The independent reproduction then wrote its own test on a real
BBoxStrategyindex from the claim alone; the one-test repro was cut from it on 2026-10-29. - The finder tagged the find "formal"; the registry records it as semi-formal because the signal came from a custom bytecode analysis, not from a model checker, solver or proof.
No prior report: 125 recorded searches over GitHub, the mailing lists, JIRA, downstream repositories and the web, plus the files of all 413 open PRs (2026-10-26), rechecked on 2026-10-31. Closest: LUCENE-5714, which created the class, and SOLR-6797, which added the multiplier; neither discusses equality.
Searches4 searches
Verdict new: 13 hits read, none the same bug. Related, not reports of this bug: #6776 / LUCENE-5714 (created the class; its "equals/hashcode bug" remark is about 2014 BBox code), SOLR-6797 (introduced the multiplier), #8787 / LUCENE-7736 and #8774 / LUCENE-7723 (the equality rule for value sources), #6182 / LUCENE-5118 (the sibling precedent), LUCENE-10026 (same bug class in CombinedFieldQuery). Gaps: GitHub's search rate limit was saturated during the first runs; keyword search only; private reports are not visible.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits | 2026-10-26 | the scripted prior-report search in three runs (33 GitHub queries plus commit and downstream queries) and the finder's 16 queries; manual searches of apache/lucene, the archived lucene-solr repository, Solr, Lucene.NET, Elasticsearch and OpenSearch; the file's commit history | only the origin issues (#6776, #6841) and related equality items |
| Open PRs | 2026-10-26 | all 413 open apache/lucene PRs, with the file lists of the 6 spatial-labelled ones | none touches ShapeAreaValueSource.java |
| Lists, JIRA, web | 2026-10-26 | dev@, java-user@, issues@, commits@ and the Solr lists (48 tool queries plus a direct ShapeAreaValueSource query on 8 lists); LUCENE and SOLR JIRA (full reads of LUCENE-5714, SOLR-6797, LUCENE-7737); 19 web queries | no report |
| Recheck (GitHub) | 2026-10-31 | ShapeAreaValueSource; ShapeAreaValueSource multiplier; spatial area value source equals multiplier; BBoxStrategy area score multiplier; the upstream file history | only #6776 and #6841; the file's last commit is still the 2021 reformat |
Introduced by SOLR-6797 (2015-01-03, first released in 5.0.0), which added the multiplier but not to equals/hashCode. Wrong BooleanQuery scores since 7.1.0, when LUCENE-7925 started merging equal clauses. Executed from 4.10.4 (no multiplier) to 10.6.0 and main; not fixed upstream as of 2026-10-31.
Versions tested5 rows
Introduced by SOLR-6797 "Add score=degrees|kilometers|miles for AbstractSpatialFieldType" (dsmiley, svn era, no GitHub PR): trunk 8fd247cc0e5 and branch_5x c2b1b11a72f, both 2015-01-03. It added the non-final field, the fourth constructor argument and * multiplier, and left equals/hashCode and the printed form (description() then, toString today) as LUCENE-5714 (7e0aff8dabc, 2014) wrote them. Wrong scores need LUCENE-7925 (ab793e7ab56, 2017-08-21, first released in 7.1.0), which made BooleanQuery merge equal SHOULD/MUST clauses. A tag scan finds the same equals/hashCode in every tag from 5.0.0 to 10.5.2, and 10.6.0 has it too. The older releases were checked for the equality and the disjunction score; the filter and cache forms ran on 10.5.2 and main only. Upstream main 2f424fd428da (2026-10-30): the file's last commit is still 2cbf261032d (LUCENE-9570 reformat, 2021), checked on 2026-10-31.
| Version or commit | Verdict | Result |
|---|---|---|
| 4.10.4 | no multiplier | the class has a 3-argument constructor and no multiplier field |
| 5.0.0, 5.5.5, 6.0.0, 6.6.6, 7.0.1 | equality only | sources with values 800 and 2400 compare equal with the same hashCode; on 7.0.1 the disjunction still scores 3200 (no clause merging yet) |
| 7.1.0, 8.11.4, 9.0.0, 9.12.3 | first wrong scores: 7.1.0 | the disjunction scores 1600 instead of 3200; equality also fails on 8.8.2 and 8.9.0 to 8.11.3 |
| 10.5.2, 10.6.0 | affected | this doc's test fails (1600 instead of 3200, assertions on); on 10.5.2 the filter and cache forms fail too |
| main 645d4fe14a20 | affected | this doc's test fails through gradle and the standalone runner, assertions on and off; passes with fix A |
An independent reproducer, an intended-behaviour devil's advocate and a reading-only review by another model family (OpenAI GPT-5.6) rated it real, and an adjudication confirmed it with high confidence. The repros still fail on the later main commits a9779ef3c193 and 645d4fe14a20.
Checks and verdicts289 words
Before the finding was recorded, a reading-only refute review by OpenAI GPT-5.6 agreed on the root cause and rated the intended-behaviour argument weak; its "not real" was procedural, because it could not run Java, and a separate re-run failed.
| Check | Verdict | What it found |
|---|---|---|
| Finder (a hunt unit) | candidate | Bytecode scan; own JUnit (synthetic shape source, area 100) fails 2 of 2 on main (assertions on and off), 10.5.2 and 9.12.3; the fix passes, as do 12 upstream tests. |
| Independent reproducer | real | Own JUnit on a real BBoxStrategy index fails 3 of 3 on main (assertions on and off), 10.5.2 and 9.12.3: equal sources, 1600 or 4800 for 3200, 2400 for 4800 with boosts; controls pass; its fix passes 3 of 3. |
| Intended lens (OpenAI GPT-5.6, reading only) | real (medium) | Intended argument weak; the reviewer ran no Java. A separate run for this review re-ran the finder's test (2 of 2 fail) and ran its own BBoxStrategy check (2 of 3 fail on main with assertions on and off, the control passes; 0 fail with the fix). |
| Devil's advocate | real | Built the strongest intended case and rejected it; own test fails 2 of 2 on main and 10.5.2 and adds the FunctionMatchQuery plus query-cache wrong-hits form; passes with the fix. |
| Adjudication | confirmed (high) | Re-ran the independent reproduction's JUnit (3 run, 3 failed, assertions on and off) and with the fix (3 of 3 pass). |
| Repro minimization and main refresh | verified; still reproduces | The one-test repro fails on 645d4fe14a20 (seed DEADBEEF, and 5 of 5 with seed 1234ABCD) and passes with the fix; the finder's and the independent reproduction's tests and the probe still fail on a9779ef3c193. |
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 537 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,631 words
- Registry title
- ShapeAreaValueSource leaves its multiplier out of equals/hashCode, so area sources with different multipliers compare equal and BooleanQuery merges or drops their clauses: wrong, clause-order-dependent scores, and wrong hits for area filters
- Area
- spatial-extras / value sources (ShapeAreaValueSource equality, consumed by FunctionScoreQuery, area filters and BooleanQuery duplicate-clause handling)
- Priority
- Low, rank 853: ShapeAreaValueSource equality ignores its multiplier, which affects only applications that build two such experimental sources themselves (Lucene does so only in tests).
- Impact class
- silent wrong results
- Impact note
- Applications that construct the @lucene.experimental ShapeAreaValueSource themselves (Lucene builds it only in tests) and use two sources over the same shape field that differ only in the multiplier get wrong, clause-order-dependent scores when both feed FunctionScoreQuery clauses of one BooleanQuery (SHOULD with minimumNumberShouldMatch <= 1, or MUST), and wrong hits when both feed area FILTER or MUST_NOT clauses of one BooleanQuery (no cache needed) or a shared query cache, with default writer, codec, similarity and searcher, under -ea and -da, in every release since 7.1.0 (the equality violation itself since 5.0.0); Solr's BBoxField, the only downstream caller found, fixes the multiplier per field type and so cannot build the colliding pair.
- Summary
- You index one rectangle with BBoxStrategy (planar area 800) and score it with two FunctionScoreQuery SHOULD clauses whose sources differ only in the multiplier, new ShapeAreaValueSource(strategy.makeShapeValueSource(), ctx, false, 1.0) and the same with 3.0, expecting the sum 800 + 2400 = 3200. Instead ShapeAreaValueSource equals/hashCode compare only geoArea and the shape source, so the two clauses are equal, BooleanQuery.rewrite merges them into one clause with boost 2 that keeps only the first multiplier, and the document scores 1600 (4800 with the clauses swapped). Area filters built on such sources are de-duplicated the same way, so a FILTER or MUST_NOT area clause can be silently dropped and the hit set changes (2550 hits expected, 3800 returned).
- Introduced
- 8fd247cc0e5fded62a5b349ba82966a0c86bd61e (trunk, svn r1649243) and its branch_5x twin c2b1b11a72fb08a508a6f0f6d0d0dd75e7db5ea1 (svn r1649245, same day, byte-identical file; 5.0.0 gets the bug from it), 'SOLR-6797: spatial distanceUnits=degrees|kilometers|miles units=degrees is now deprecated.' (dsmiley; JIRA SOLR-6797 'Add score=degrees|kilometers|miles for AbstractSpatialFieldType', no GitHub PR): it added the non-final multiplier field, the fourth constructor parameter and '* multiplier' in the value, but did not touch equals, hashCode or toString, which still compare only geoArea and shapeValueSource as written in 7e0aff8dabc (LUCENE-5714/LUCENE-5779, 2014-07-08), when the only other field was ctx, which the class comment places outside identity [read: git log, git blame -w -M -C and git show in the history clone]. The merge that turns it into wrong scores, BooleanQuery's duplicate SHOULD/MUST collapse (LUCENE-7925, ab793e7ab56 / branch_7x 325263a9398, 2017-08-21, first released in 7.1.0), came later. (2015-01-03)
- First affected version
- 5.0.0 [exec] for the equality violation (values 800 and 2400, yet the two sources and the FunctionQuery objects built on them compare equal with the same hashCode); 4.10.4 is not affected [exec: the class has no multiplier and the 4-argument constructor does not exist]. Wrong scores from a plain BooleanQuery start in 7.1.0 [exec]; 7.0.1 still keeps both clauses and scores 3200 correctly [exec].
- Affected versions
- every release from 5.0.0 through 10.6.0 and main ea57360912ed has the equality violation; wrong BooleanQuery scores from 7.1.0 on; the one-test repro fails on the 10.6.0 jars with -ea (2026-10-31) [exec]. [exec] the follow-up history sweep (main() repros with -da on SHA-1-verified release jars; API-adapted variants for 4.x, 5.x, 6.x and 7.0+): equality fails on 5.0.0, 5.5.5, 6.0.0, 6.6.6, 7.0.1, 7.1.0, 8.8.2, 8.9.0, 8.10.0, 8.10.1, 8.11.0, 8.11.1, 8.11.2, 8.11.3, 8.11.4, 9.0.0, 9.12.3, 10.5.2 and main; the disjunction scores 1600 instead of 3200 on 7.1.0, 8.11.4, 9.0.0, 9.12.3, 10.5.2 and main, and correctly on 7.0.1; 4.10.4 not affected (API absent). The finder's JUnit and the independent reproduction's JUnit fail on main (-ea, -da), 10.5.2 and 9.12.3; the intended-behaviour check's on main (-ea, -da) and 10.5.2; the security probe (filter and cache paths) on main (-ea, -da) and 10.5.2. [read] a scan of every release tag in the history clone finds the multiplier present and absent from equals/hashCode in every tag from 5.0.0 to 10.5.2; the equals/hashCode bodies are unchanged since 2014. Releases between 5.0.0 and 7.0.1 were checked for the equality violation only, and the wrong-hit paths (filter de-duplication, query cache) only on 10.5.2 and main. Not fixed upstream. All runs on aarch64 with JDK 25.0.3.
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code (its low-confidence “not real” is procedural), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal
- Upstream and prior reports
- none found (2026-10-26, follow-up prior-report record verdict new, read-only: 125 logged scripted prior-report search queries in three runs (54 identifier and symptom, 38 contract, 33 downstream-trigger queries over GitHub issues, PRs and commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists, the JIRA archive and 19 web queries), the finder's 16 earlier queries, and manual searches (GitHub on apache/lucene, apache/lucene-solr, apache/solr, apache/lucenenet, elastic/elasticsearch and opensearch-project/OpenSearch; the commit list of the file; all 413 open apache/lucene PRs, with the file lists of the 6 spatial-labelled ones; lists.apache.org counts for 'ShapeAreaValueSource' over 8 Lucene and Solr lists; LUCENE and SOLR JIRA text queries; full reads of LUCENE-5714, SOLR-6797 and LUCENE-7737; git log in the history clone); 13 hits read, none the same bug): nobody reports ShapeAreaValueSource's multiplier missing from equals/hashCode, or any symptom of it (merged BooleanQuery clauses, wrong area scores, dropped area filters, query-cache confusion). Not fixed: upstream main HEAD equaled the tested ea57360912ed at search time, the file was last changed upstream by the 2021 LUCENE-9570 reformat (2cbf261032d), and no open PR touches it (#16847, the only open PR in spatial-extras/util, changes other files). Related, not reports of this bug: #6776 (LUCENE-5714, created the class; its 'equals/hashcode bug' remark concerns 2014 BBox code, before the multiplier existed), SOLR-6797 (introduced the multiplier; no equality discussion in its 25 comments), LUCENE-7737 (ported the class to DoubleValuesSource, equals carried over), #8787 (LUCENE-7736: the reviewer states that equals must never return true for value sources that could return different values), LUCENE-7723 (value-source equals/hashCode made abstract), #6182 (LUCENE-5118: a multiplier added to the distance value sources and compared in their equals) and LUCENE-10026 (same bug class in CombinedFieldQuery). Gaps: GitHub's search rate limit was saturated (reduced per-source maxima in two runs, one web query pending after HTTP 429), generic 'equals'/'hashCode' queries were triaged on their first page only, private reports to security@lucene.apache.org are not visible, and a refresh hit the GitHub rate limit and returned nothing
- Lucene commit tested
- main
645d4fe14a20(commit of 2026-10-28): this doc's one-method test through gradle with assertions on and off (seed DEADBEEF) and on the standalone runner; the same test on the 10.6.0 and 10.5.2 release jars with assertions on (fails). The hunt's tests ran on mainea57360912edand again ona9779ef3c193; the classes involved are unchanged up to645d4fe14a20. The history sweep executed main() repros on 19 releases from 4.10.4 to 10.5.2. Fixes A and B were run through gradle in a scratch worktree of645d4fe14a20on 2026-10-31. Upstream main2f424fd428da(2026-10-30) still has the same code (checked on 2026-10-31).
- Finding record
lucene/spatial-extras/src/java/org/apache/lucene/spatial/util/ShapeAreaValueSource.java:29-34 (class javadoc, @lucene.experimental), :36-40 (fields; :37 'not part of identity' comment on ctx only; :40 non-final multiplier), :42-48 (public constructor), :51-53 (toString omits the multiplier), :63 (value = area * multiplier), :80-82 (isCacheable delegates to the shape source), :89-100 (equals compares geoArea and shapeValueSource only), :102-107 (hashCode over the same two fields)lucene/spatial-extras/src/java/org/apache/lucene/spatial/util/DistanceToShapeValueSource.java:110, :123; lucene/spatial-extras/src/java/org/apache/lucene/spatial/vector/DistanceValueSource.java:110; lucene/spatial-extras/src/java/org/apache/lucene/spatial/util/ShapeFieldCacheDistanceValueSource.java:113 (sibling sources compare their multiplier)lucene/queries/src/java/org/apache/lucene/queries/function/FunctionScoreQuery.java:143-154 (equals/hashCode delegate to the wrapped query and the DoubleValuesSource)lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java:132-141 (SHOULD/MUST Multisets, FILTER/MUST_NOT HashSets), :343-361 (duplicate FILTER/MUST_NOT removal), :363-368 (FILTER clause also in MUST_NOT -> MatchNoDocsQuery), :438-455 (SHOULD de-duplication when minimumNumberShouldMatch <= 1, and MUST), :706-758 (deduplicateClauses: HashMap.merge keeps the first equal key and gives it a combined boost)lucene/core/src/java/org/apache/lucene/search/Query.java:95-104 (equals 'is required so that QueryCache works properly'; 'Typically' equal 'only if' the 'document-filtering properties are identical')lucene/queries/src/java/org/apache/lucene/queries/function/FunctionRangeQuery.java:119-125, :177-178; lucene/queries/src/java/org/apache/lucene/queries/function/ValueSource.java:267-272; lucene/queries/src/java/org/apache/lucene/queries/function/FunctionMatchQuery.java:119-124; lucene/facet/src/java/org/apache/lucene/facet/range/DoubleRange.java:335; lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:82-84 (filter consumers whose equality comes from the value source; no default cache on main)lucene/spatial-extras/src/test/org/apache/lucene/spatial/bbox/TestBBoxStrategy.java:311-329 (testAreaValueSource: multiplier 2.0 doubles the value; the only in-tree constructor calls); lucene/queries/src/test/org/apache/lucene/queries/function/TestFunctionScoreQuery.java:74-92 (checkUnequal(q2, q3) at :91: FunctionScoreQuery over the same term query with constant(1) and constant(2))
- The finding in 30 seconds
lucene/spatial-extras/src/java/org/apache/lucene/spatial/util/ShapeAreaValueSource.java:33, (at 645d4fe14a20) :37, :40, :42-48, :51-53, :63, :89-107lucene/queries/src/java/org/apache/lucene/queries/function/FunctionScoreQuery.java:143-154 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java:129-142, (at 645d4fe14a20) :343-369, :438-455, :714-758lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:82-84, (at 645d4fe14a20) :123; lucene/core/src/java/org/apache/lucene/search/similarities/BM25Similarity.java:121-136lucene/queries/src/java/org/apache/lucene/queries/function/FunctionRangeQuery.java:119-125; (at 645d4fe14a20) FunctionMatchQuery.java:119-124; lucene/facet/src/java/org/apache/lucene/facet/range/DoubleRange.java:335 (getQuery), :130-143 (ValueSourceQuery equality: range, fast-match query, value source)a Lucene clone releases/lucene/10.5.2 and releases/lucene/10.6.0 lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java (no computeQueryTermWeight)
- Who is affected
lucene/spatial-extras/src/test/org/apache/lucene/spatial/bbox/TestBBoxStrategy.java:318, (at 645d4fe14a20) :322, :326 (the only in-tree constructor calls); ShapeAreaValueSource.java:33lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:979-980; (at 645d4fe14a20) lucene/test-framework/src/java/org/apache/lucene/tests/util/SetupAndRestoreStaticEnv.java:214, :224, :229; SetupAndRestoreQueryCache.java:41-43
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/search/DoubleValuesSource.java:125-129; (at 645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/Query.java:95-107lucene/queries/src/test/org/apache/lucene/queries/function/TestFunctionScoreQuery.java:78-91 (at 645d4fe14a20)lucene/spatial-extras/src/java/org/apache/lucene/spatial/util/DistanceToShapeValueSource.java:110, (at 645d4fe14a20) :123; lucene/spatial-extras/src/java/org/apache/lucene/spatial/vector/DistanceValueSource.java:110; lucene/spatial-extras/src/java/org/apache/lucene/spatial/util/ShapeFieldCacheDistanceValueSource.java:113lucene/spatial-extras/src/test/org/apache/lucene/spatial/bbox/TestBBoxStrategy.java:311-329; (at 645d4fe14a20) ShapeAreaValueSource.java:37gh api repos/apache/lucene/issues/8774 (LUCENE-7723 description) and issues/8787/comments (LUCENE-7736, Adrien Grand and Alan Woodward), read-only, 2026-10-31gh api repos/apache/lucene/issues/8774 (migrated from LUCENE-7723 by Adrien Grand), read-only, 2026-10-31
- Suggested fix
lucene/spatial-extras/src/java/org/apache/lucene/spatial/util/DistanceToShapeValueSource.java:110, (at 645d4fe14a20) :123; lucene/spatial-extras/src/test/org/apache/lucene/spatial/TestQueryEqualsHashCode.java
- Prior reports
gh api search/issues and repos/apache/lucene/commits?path=lucene/spatial-extras/src/java/org/apache/lucene/spatial/util/ShapeAreaValueSource.java (read-only, 2026-10-31)
- History
a Lucene clone releases/lucene/10.6.0 lucene/spatial-extras/src/java/org/apache/lucene/spatial/util/ShapeAreaValueSource.java:89-107 (same equals/hashCode)gh api repos/apache/lucene/commits?path=lucene/spatial-extras/src/java/org/apache/lucene/spatial/util/ShapeAreaValueSource.java (read-only, 2026-10-31)
- Diagrams
lucene/spatial-extras/src/java/org/apache/lucene/spatial/util/ShapeAreaValueSource.java:63, (at 645d4fe14a20) :89-107; lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java:714-758
- TestShapeAreaValueSourceMultiplier.java (JUnit test):
repro/LFV-302/src/org/apache/lucene/spatial/util/TestShapeAreaValueSourceMultiplier.java(66 lines)
A picture of the one-document repro and the equality check that merges the two clauses, made in parallel with this doc from the code at 645d4fe14a20; the PNG is the summary still.
assets/LFV-302/bug.png(537 KB)
