Sort fields with a custom comparator source are compared by that source, and every getSortField call makes a new one that equals only itself.
SortandSortFieldimplementequalsandhashCodeso they can be used as keys, for example in a result cache keyed by query plus sort. That was why they were added (LUCENE-1481, 2008).- A
SortFieldthat sorts with a customFieldComparatorSourceequals another only if the two comparator sources are equal. Its javadoc says such a source "must properly implement equals (unless a singleton is always used)". - A
DoubleValuesSourceorLongValuesSourcecomputes one value per document, andgetSortField(reverse)turns it into a sort field. The sources themselves must implement valueequalsandhashCode.
getSortField(false) calls on one DoubleValuesSource build two sort fields, each around its own new comparator source; SortField.equals compares those sources, so the sort fields and their Sorts are unequal and a cache keyed on the Sort misses. Select the still to open it at full size. Generated with AI, checked against the code.- You sort by a values source,
price = DoubleValuesSource.fromDoubleField("price"), and cache results in aHashMap<Sort, TopDocs>, buildingnew Sort(price.getSortField(false))for each request. - You expect the second identical request to hit the cache: two such sorts should be equal, with equal hash codes, as two
new SortField("price", SortField.Type.DOUBLE)are. - Instead three identical requests give miss, miss, miss and leave 3 entries in the map. Both sort fields print
<double(price)>and come from the same source object, yetequalsis false and the hash codes differ. The results are sorted correctly (prices 10, 20, 30). - Why: each
getSortFieldcall wraps a new private comparator source that has noequalsorhashCode, andSortField.equalscompares comparator sources, so a sort field equals only itself.LongValuesSourceandExpression#getSortFieldwork the same way.
Code that compares, deduplicates or caches on a Sort or SortField built by DoubleValuesSource, LongValuesSource, Expression#getSortField or ValueSource.fromDoubleValuesSource, when the Sort is built per request: every lookup misses, and an unbounded map gains an entry per request. Only these opt-in sort types; relevance and plain field sorts are fine, and results stay correct.
Mechanism, terms and numbers523 words
DoubleValuesSource#getSortField and LongValuesSource#getSortField (and Expression#getSortField, which delegates to the first) build a new SortField around a new private comparator source on every call, and that comparator source has no equals or hashCode. SortField#equals compares comparator sources, and its javadoc requires a provided one to "properly implement equals (unless a singleton is always used)". So two sort fields built identically, even from the same source object, are unequal (their identity-based hash codes differed in every run that checked them), and so are Sorts built from them.
Sort order is not affected. The effect is on code that compares, deduplicates or caches on a Sort or SortField it builds anew for each request, such as a result cache keyed by query plus sort: it always misses for these sorts (reusing one Sort instance does hit). It has been so since 6.4.0. For expression sorts it is a regression: the ExpressionSortField they replaced had value equality, and the same expression sort built twice is equal on 6.3.0 (executed).
getSortField(reverse)returnsnew DoubleValuesSortField(this, reverse, 0)(DoubleValuesSource.java:111-113); the overload with a missing value does the same (:121-123).- The constructor hands
SortFielda new comparator source:super(producer.toString(), new DoubleValuesComparatorSource(producer, missingValue), reverse)(:633).DoubleValuesSortField(:628-661) overridesneedsScores,toStringandrewrite, notequals. DoubleValuesComparatorSource(:667-703) holdsproducerandmissingValueand declares neitherequalsnorhashCode, so two instances are equal only if they are the same object.SortField#equalscomparesObjects.equals(this.comparatorSource, other.comparatorSource)(SortField.java:459) andhashCodehashes it (:469). Everything else it compares is the same for the two fields: field (producer.toString()), typeCUSTOM, reverse flag, andSortField's own missing value,nullbecause the missing value lives in the comparator source (SortField.java:195-200).Sort#equalsandhashCodedelegate to the fields (Sort.java:119,:125).LongValuesSourcehas the same shape (LongValuesSource.java:77-79,:87-89,:289,:323-359).Expression#getSortField(bindings, reverse)isgetDoubleValuesSource(bindings).getSortField(reverse)(Expression.java:107-109), and since #14654 (9.12.2, 10.2.2)ValueSource.fromDoubleValuesSource(dvs).getSortFieldreturnsin.getSortField(reverse)(ValueSource.java:332-334). Executed by the fact check: two such sort fields overfromDoubleField("price")are unequal on main and 9.12.2 and equal on 9.12.1, where that path still returned the queries module's ownValueSourceSortField, so #14654 also removed equality there.
The values sources must implement equals/hashCode themselves (abstract on both, DoubleValuesSource.java:125-129, LongValuesSource.java:55-59); the field sources used here are value-equal (two fromDoubleField("price") compare equal, probe), and the repro uses one source object for both calls anyway, so only the wrapper is at fault.
On main, with assertions on and off: a.equals(b) is false and the hash codes differ (identity-based), although field, type, reverse flag, missing value and toString (<double(price)>) are all equal; new Sort(a).equals(new Sort(b)) is false. A HashMap<Sort, TopDocs> over three identical requests (3 documents, prices 30, 10, 20) records miss, miss, miss and holds 3 entries; with the fix it records miss, hit, hit and holds 1. Both sort fields order the documents 10, 20, 30. Control: new SortField("price", SortField.Type.DOUBLE) built twice is equal, with equal hash codes.
Exposure and likelihood251 words
- Who is hit. Code that compares, deduplicates, hashes or caches on
Sort/SortFieldobjects built byDoubleValuesSource#getSortField,LongValuesSource#getSortField,Expression#getSortFieldor (since 9.12.2 / 10.2.2)ValueSource.fromDoubleValuesSource(...).getSortField. The typical case is a search-result cache keyed by query plus sort, with theSortbuilt anew for each request: every lookup for such a sort misses, deterministically. Code that reuses oneSortinstance, or keys its cache on its own request description, is not hit. - Opt-in. The default relevance sort and plain field sorts (
new SortField(field, type),SortedNumericSortField, ...) are not affected; function, expression and values-source sorts are. - Seen downstream. On SOLR-18006 (2025-12-10) David Smiley noted that Solr's change to its distance sort "adds queryResultCache cache-ability since the prior code didn't implement equals/hashCode on all components of the SortField". The prior Solr code returned
super.getSortField(true), which isDoubleValuesSource#getSortField(Solr's own values source has valueequals). Solr worked around it in its own code (apache/solr#3923; SOLR-18006 lists fix versions 9.10.1, 9.11 and 10.0). - What it does not do. Sort order is correct (checked on the probe's 3-document index), with assertions on and off. These fields cannot be index sorts (
IndexWriterConfig#setIndexSortrejects a field without an index sorter,IndexWriterConfig.java:505-510). We found no non-test Lucene code that keys a map or set onSort/SortField. - Scale. Every lookup for an affected sort; the cost is lost cache hits and, for an unbounded map, one duplicate entry per request. Unreported in Lucene for about ten years.
One test class on the public API with two one-line methods, one per class because each has its own fix site: build the sort field twice from one source and assertEquals the two. No index, no randomness, single thread; it fails with assertions on and off.
/*
* 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.search;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestValuesSourceSortFieldEquals extends LuceneTestCase {
// expected: equal SortFields, per the SortField#equals javadoc; actual: they are not equal
public void testDoubleValuesSourceSortField() {
DoubleValuesSource source = DoubleValuesSource.fromDoubleField("price");
assertEquals(source.getSortField(false), source.getSortField(false));
}
public void testLongValuesSourceSortField() {
LongValuesSource source = LongValuesSource.fromLongField("ts");
assertEquals(source.getSortField(false), source.getSortField(false));
}
}
cp $REPRO/TestValuesSourceSortFieldEquals.java $LUCENE/lucene/core/src/test/org/apache/lucene/search/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.search.TestValuesSourceSortFieldEquals -Ptests.seed=DEADBEEF -Ptests.asserts=true
# same failure with -Ptests.asserts=false; the seed does not matter (no randomness)
- Expected
- The two sort fields are equal, as two
new SortField("price", SortField.Type.DOUBLE)are. - Observed
expected: ...DoubleValuesSortField<<double(price)>> but was: ...DoubleValuesSortField<<double(price)>>, and the same for<long(ts)>.
More files, environment, full output164 words
The test sits in org.apache.lucene.search by upstream convention but uses only public API (DoubleValuesSource.fromDoubleField, LongValuesSource.fromLongField, getSortField(boolean)). Two methods, not one, because they are two separate fix sites and a failed assertEquals in one would hide the other. The same test is the suggested regression test: it passes with the fix below and passes the core lint checks as written.
Not asserted here (shown by the verification runs): hashCode, Sort equality, the HashMap<Sort, ?> miss, Expression#getSortField, and that sorting is correct.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT): fails; every release from 6.4.0 to 10.6.0: fails; with the fix: passes |
|---|---|
| jdk | OpenJDK 25.0.3 |
| os | Linux, aarch64 |
# stock main 645d4fe14a20, gradle, seed DEADBEEF (same with -Ptests.asserts=false)
TestValuesSourceSortFieldEquals > testLongValuesSourceSortField FAILED
java.lang.AssertionError: expected: org.apache.lucene.search.LongValuesSource$LongValuesSortField<<long(ts)>> but was: org.apache.lucene.search.LongValuesSource$LongValuesSortField<<long(ts)>>
at org.apache.lucene.search.TestValuesSourceSortFieldEquals.testLongValuesSourceSortField(TestValuesSourceSortFieldEquals.java:31)
TestValuesSourceSortFieldEquals > testDoubleValuesSourceSortField FAILED
java.lang.AssertionError: expected: org.apache.lucene.search.DoubleValuesSource$DoubleValuesSortField<<double(price)>> but was: org.apache.lucene.search.DoubleValuesSource$DoubleValuesSortField<<double(price)>>
at org.apache.lucene.search.TestValuesSourceSortFieldEquals.testDoubleValuesSourceSortField(TestValuesSourceSortFieldEquals.java:26)
2 tests completed, 2 failed
# with the fix (asserts true and false)
BUILD SUCCESSFUL: 2 tests, 0 failuresSortField#equalsjavadoc: a provided comparator source "must properly implement equals (unless a singleton is always used)". These are Lucene's own, new on every call.- A regression for expressions: the same expression sort built twice is equal on 6.3.0 and unequal from 6.4.0 (executed).
- The ten other
SortFieldsubclasses in the tree compare by value.
The full argument298 words
- Contract on
SortField(SortField.java:446-449): "Returns true ifois equal to this. If aFieldComparatorSourcewas provided, it must properly implement equals (unless a singleton is always used)." The same forhashCode(:463-466). - Stated intent for values sources in sorts. LUCENE-7723 (7.0; CHANGES "DoubleValuesSource enforces implementation of equals() and hashCode()") gave the reason: "Given that we embed values sources in queries and sorts, from which we expect that they have working equals/hashCode, we should also implement equals/hashCode on LongValuesSource/DoubleValuesSource impls." The sort-field wrapper was not updated.
- Why
SortFieldhas equality. LUCENE-1481 (2008, Uwe Schindler): a query-result cache "uses the Sort/SortField as key into the cache". LUCENE-7798 (6.6) gaveToParentBlockJoinSortFieldequality because it "is going to be used as query result key". LUCENE-6946 (5.4.1, Bug Fixes) treated an incompleteSortField.equalsas a bug. - Regression for expressions. LUCENE-5220 (2013) gave
ExpressionSortFieldvalue equality; LUCENE-7609 (6.4.0) deleted it and routedExpression#getSortFieldthroughDoubleValuesSource#getSortField. Release jars: equal on 6.3.0, unequal on 6.4.0. TheValueSource.fromDoubleValuesSourcepath lost equality the same way in 9.12.2 (#14654): equal on 9.12.1, unequal on 9.12.2 (executed by the fact check). - Consistency. The ten other non-test
SortFieldsubclasses all compare by value. Nine overrideequalsandhashCode: BinarySortField, SortedSetSortField, SortedNumericSortField, LatLonPointSortField, XYPointSortField, FeatureSortField, ToParentBlockJoinSortField, Geo3DPointSortField and Geo3DPointOutsideSortField. The tenth,ValueSource.ValueSourceSortFieldin the queries module (ValueSource.java:373-384), overrides neither but holds no comparator source until rewrite, soSortField's own comparison (field = the source's description, type, reverse) makes two identically built ones equal, with equal hash codes (executed by the fact check, assertions on and off). - Tests.
TestSort#testEqualsasserts that separately built sorts are equal (TestSort.java:73-92).TestExpressionSortField#testEquals(:41-71) checks only inequality cases andassertEquals(sf1, sf1)on one instance; it dates from LUCENE-5220, so it stayed green through LUCENE-7609.
The devil's-advocate check156 words
The strongest case for "intended": FieldComparatorSource is @lucene.experimental and its javadoc says nothing about equality; no Lucene code needs equal sorts; on LUCENE-7736 Adrien Grand (jpountz) accepted identity-only equality for some reader-bound values sources ("ok imo as long as a.equals(b) returns true if a == b"); on LUCENE-9451 they said "Reference equality is the right check"; and the expressions test asserts only inequality.
Why it fails: the requirement is written on SortField, which is not experimental, and the comparator source is Lucene's own, not the user's. The LUCENE-7736 bar is that a == b implies equal, and here both sort fields come from the same source object and are still unequal. The LUCENE-9451 remark is about rewrite's "did it change" check, not equals. The test gap is a missing positive case in a test written for a class that had value equality, and expression sorts regressed from 6.3.0 to 6.4.0. Verdict: not intended.
Recommended Value equals/hashCode on the two comparator sources
Give the two private comparator sources value equals and hashCode over the producer and the missing value (+24 lines in two files). Validated on main: the repro passes, and 2,341 core search tests, the expressions module and the queries function tests pass, with lint clean.
// DoubleValuesSource.DoubleValuesComparatorSource
@Override
public boolean equals(Object obj) {
return obj instanceof DoubleValuesComparatorSource other
&& Double.compare(missingValue, other.missingValue) == 0
&& producer.equals(other.producer);
}
@Override
public int hashCode() {
return Objects.hash(producer, missingValue);
}All options and trade-offs3 options · 494 words
One option was built and validated in a scratch checkout of main 645d4fe14a20; the other two are alternatives with their trade-offs.
Add equals and hashCode to DoubleValuesSource.DoubleValuesComparatorSource and LongValuesSource.LongValuesComparatorSource (the Long one compares missingValue == other.missingValue). This is the object SortField's javadoc says must implement equals, and the only place that holds the missing value, so equality must include it. SortField, Sort and the sort-field classes stay unchanged.
// DoubleValuesSource.DoubleValuesComparatorSource
@Override
public boolean equals(Object obj) {
return obj instanceof DoubleValuesComparatorSource other
&& Double.compare(missingValue, other.missingValue) == 0
&& producer.equals(other.producer);
}
@Override
public int hashCode() {
return Objects.hash(producer, missingValue);
}Trade-offs
- Equality is as good as the producer's
equals. A values source with identity equality (accepted for reader-bound sources on LUCENE-7736) still makes two sort fields from the same source object equal, and sort fields over unequal sources stay unequal. - For expressions this is the equality of the sources the bindings return (
ExpressionValueSourcecompares them,ExpressionValueSource.java:146-171): custom bindings that return a new identity-equal source on each lookup keepExpression#getSortFieldunequal. - Equal sort fields do not make a cached result valid for another index reader; a cache still has to be scoped to the reader, as with query keys.
Double.comparetreats0.0and-0.0as different andNaNas equal to itself, consistent withDouble.hashCode.Objects.hashboxes its arguments; this is not a per-document path.- Not covered: the queries module's own
ValueSourceSortFieldis equal before rewrite, and itsrewritereturns a plainSortFieldaround a newValueSourceComparatorSourceeach time (ValueSource.java:379-383). Neither option changes that; it is outside this report.
What the checks validated
- Scratch worktree of
645d4fe14a20, gradle--offline, seed DEADBEEF. Stock: the repro fails with-Ptests.asserts=trueandfalse. With the fix: the repro passes with both. :lucene:core:test --tests 'org.apache.lucene.search.*': 215 suites, 2,341 tests, 0 failures, with asserts true (49 skipped) and false (51 skipped).:lucene:expressions:test(170 tests, includingTestExpressionSortFieldandTestExpressionSorts) and:lucene:queries:test --tests 'org.apache.lucene.queries.function.*'(111 tests, includingTestFunctionQuerySort): 0 failures.- Lint (
checkGoogleJavaFormat,ecjLintMain,ecjLintTest,forbiddenApisMain,forbiddenApisTest,renderJavadocon core, the repro test included): BUILD SUCCESSFUL. - Not run: the rest of the core suite, other modules, x86.
Compare the producer, the reverse flag and the missing value in DoubleValuesSortField and LongValuesSortField. Not validated.
Trade-offs
Fixes SortField and Sort equality, but the comparator sources stay identity-equal (code that compares getComparatorSource() still sees unequal objects), the missing value must be read back through a cast, and it is more code than the recommended option.
Say in the getSortField javadoc that the returned sort fields are equal only to themselves. Not recommended.
Trade-offs
Keeps SortField's written contract broken for Lucene's own sort fields, keeps the 6.4.0 regression for expression sorts, and leaves every caching user to work around it, as Solr did.
Solr ran into this: its reverse distance sort, built with DoubleValuesSource#getSortField(true), could not be cached in Solr's query result cache, as David Smiley noted on SOLR-18006, and Solr worked around it in its own code.
Static analysis: during the 2026-10-22 study, SpotBugs flagged the two values-source sort-field classes, which do not override SortField.equals (EQ_DOESNT_OVERRIDE_EQUALS). An independent verifier then reproduced it with a 9-test public-API JUnit, probed release jars and checked a fix; the hunt later cut the repro to the two-method test.
The method in full126 words
Method: non-formal baseline, static analysis. In the study's static-analysis pass (2026-10-22, main 7cecf79523), SpotBugs reported EQ_DOESNT_OVERRIDE_EQUALS for the values-source sort-field classes.
- An independent verifier wrote a 9-test JUnit on the public API: 5 contract tests (same source twice, equal sources,
Sortequality and hash, aHashMap<Sort, ?>lookup,Expression#getSortFieldtwice) fail and 4 controls (plainSortField,SortedNumericSortField,FeatureField.newFeatureSort,LatLonDocValuesField.newDistanceSortare equal; different reverse, missing value or field stay unequal) pass, with several seeds. - The same verifier ran a probe on the 6.3.0 and 6.4.0 release jars (expression sort equal, then unequal) and checked that value
equals/hashCodeon the two comparator sources makes all 9 tests pass. - On 2026-10-29 the hunt cut the repro to one class with two one-line methods.
No Lucene issue, PR, JIRA entry or list thread reports it (searched 2026-10-31 to 2026-11-01). The only public trace is the Solr comment in the downstream note above; nobody traced it to Lucene or filed it there.
Searches4 searches
No report in Lucene's trackers. The closest item is downstream: SOLR-18006 (a Solr issue; fix versions 9.10.1, 9.11, 10.0), where David Smiley noted that Solr's previous reverse distance sort, Lucene's DoubleValuesSource#getSortField(true), lacked equals/hashCode on a SortField component and so could not be cached; Solr replaced it with its own comparator source (apache/solr#3923). Nobody named the Lucene classes or filed it upstream. On apache/solr#3935 they wrote "There is no bug to be fixed in Lucene" about an unrelated NPE.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene, apache/lucene-solr) | 2026-10-31 to 2026-11-01 | the scripted prior-report search (40 issue/PR and 4 commit queries) plus 64 own searches: class names (DoubleValuesSortField, DoubleValuesComparatorSource, LongValuesComparatorSource), "getSortField equals", "SortField equals hashCode", sort-cache phrasings | no report; related: #8774 (LUCENE-7723), #6284 (LUCENE-5220), #2555 (LUCENE-1481), #8849 (LUCENE-7798), #14654, #15480, #16857 |
| Open and recently closed PRs | 2026-10-31 | changed-file lists of all 410 open PRs and of the 1,249 closed PRs updated since 2025-10-01 | none adds equals/hashCode to these classes |
| JIRA archive and mailing lists | 2026-10-31 | 8 + 16 JQL queries (LUCENE and SOLR), 32 + 33 list queries (dev@, java-user@, issues@, commits@ and Solr's lists) | SOLR-18006 comment (downstream, quoted above); nothing in LUCENE |
| Downstream projects and the web | 2026-10-31 | searches over Solr and two other downstream search servers; 15 web queries | apache/solr#3923 (Solr's workaround); nothing else |
Introduced in 6.4.0 by LUCENE-5325 (3f24fd81c83, 2016-12-28), which created both classes with identity-equal sort fields; LUCENE-7609 (8b055382d6c) then moved expression sorts onto them, losing the equality LUCENE-5220 had given. Every release from 6.4.0 to 10.6.0 and main fail; not fixed upstream.
Versions tested6 rows
Introduced by 3f24fd81c83 "LUCENE-5325: Add LongValuesSource and DoubleValuesSource in core" (Alan Woodward, 2016-12-28; 6.x backport e2aa2b63846), which created both classes; the sort-field and comparator-source classes still blame to it. Spread to expressions by 8b055382d6c "LUCENE-7609: Refactor expressions module to use DoubleValuesSource" (2017-01-05; backport 776087eef48), which deleted ExpressionSortField (value equality since LUCENE-5220, 95629b4f65a). Both first shipped in 6.4.0. #15483 (2025) added the missing-value parameter and kept the new-source-per-call pattern.
Not fixed upstream: on main 2f424fd428da (2026-10-30) both files are byte-identical to 645d4fe14a20.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | minimal test fails, assertions on and off (standalone runner and gradle); passes with the fix |
| 10.6.0 | affected | fails, assertions on and off |
| 6.4.0 to 10.5.2 (86 releases) | affected | fail, assertions on and off |
| 6.4.0, expression sort | affected | Expression#getSortField twice: unequal, different hash codes |
| 6.3.0, expression sort | not affected | ExpressionSortField twice: equal, same hash code |
| 6.3.0, values sources | absent | DoubleValuesSource and LongValuesSource do not exist yet (the test does not compile) |
The study's verifier and a hunt tie-breaker reproduced it with their own tests; a read-only OpenAI GPT-5.6 refute review could not run Java (a procedural "not real") but agreed on the root cause; the fix was validated on 645d4fe14a20.
Checks and verdicts218 words
Every executing check ran on Linux aarch64 with OpenJDK 25.0.3, with assertions on and off where stated.
| Check | Verdict | What it found |
|---|---|---|
| Study verifier | reproduced, low severity | 9-test JUnit: 5 contract tests fail, 4 controls pass, several seeds; 6.3.0 equal vs 6.4.0 unequal on release jars; the fix makes all 9 pass. |
| Re-verification on 645d4fe14a20 | real | Minimal test fails on main and 10.5.2 with assertions on and off, and on 9.12.3 with assertions on; the fix check passes. |
| Refute review (OpenAI GPT-5.6, read-only) | not real, low (procedural) | Could not run Java, so it voted not real on procedure only; agreed on the root cause, inputs in contract, intended case weak. A separate re-run on 9.12.3 with assertions off failed 2/2. |
| Tie-breaker | real, high confidence | Own 9-test JUnit on main fails exactly the 5 contract tests (including the HashMap<Sort, ?> miss), 4 controls pass, the fix passes 9/9; a framework-free main() shows the same. |
| History run | 6.4.0 onwards | 87 releases from 6.4.0 to 10.6.0 fail with assertions on and off; 6.3.0 expression sort equal. |
| Fix validation (this doc) | fix passes | Gradle on 645d4fe14a20: the repro fails stock and passes with the fix; 2,341 core search tests, 170 expressions and 111 queries function tests pass; lint clean. |
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 477 KB
Finding record, Lucene and upstream sources, repro files and diagram notes533 words
- Registry title
- DoubleValuesSource and LongValuesSource sort fields never equal identical ones
- Area
- search / sorting
- Priority
- Low, rank 1023: Values-source sort fields lack equals/hashCode, so caches keyed on Sort always miss for them, performance only for an opt-in sort type and unreported for about ten years.
- Impact class
- minor
- Impact note
- Only code that compares SortFields built from values sources (e.g. caches keyed on Sort, Expression#getSortField); search results are unaffected; released since 6.4.0.
- Summary
- You create two identical SortFields from the same DoubleValuesSource or LongValuesSource and compare them. You expect them to be equal. Instead equals returns false; search results are unaffected.
- Introduced
- 3f24fd81c83 (LUCENE-5325 'Add LongValuesSource and DoubleValuesSource in core', Alan Woodward; branch_6x backport e2aa2b63846), which created DoubleValuesSource and LongValuesSource with sort fields around a new comparator source that has no equals/hashCode [read: git blame; exec: 6.3.0, the release before, has no such classes]; LUCENE-7609 (8b055382d6c, 2017-01-05; branch_6x 776087eef48) then routed Expression#getSortField through DoubleValuesSource#getSortField and deleted ExpressionSortField, so expression sorts lost their value equality [exec: equal on 6.3.0, unequal on 6.4.0]; both first released in 6.4.0 (2016-12-28 (3f24fd81c83 author date; committed 2016-12-29; LUCENE-7609 authored 2017-01-05))
- First affected version
- 6.4.0
- Affected versions
- every lucene-core release from 6.4.0 to 10.6.0 (87 releases) and main 645d4fe14a20 [exec: the minimal test fails on each, with -ea and -da, Maven Central jars SHA-1 checked]; 6.3.0 correct [exec: the classes do not exist; its expression sort fields compare equal]
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed in the earlier formal verification study by independent AI checks and a rerun on Lucene main; also a cross-model review that could only read the code (its low-confidence “not real” is procedural); not yet triaged by a person.
- Found by
- non-formal baseline: static analysis
- Upstream and prior reports
- none found (study searches 2026-10-22; public-list check 2026-10-20..23)
- Lucene commit tested
- main
645d4fe14a20: the minimal test through the standalone runner and through Lucene's gradle build, assertions on and off, and the fix validation (core search package, expressions module, queries function tests, core lint); every release from 6.4.0 to 10.6.0 (87 Maven Central jars, SHA-1 checked), assertions on and off; 6.3.0 (the classes do not exist yet; its expression sort field is equal); the study's 9-test check on7cecf79523anda9779ef3c193, and the tie-breaker's own 9-test check on645d4fe14a20(and 10.5.2).
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/search/DoubleValuesSource.java:111-129, 628-703lucene/core/src/java/org/apache/lucene/search/LongValuesSource.java:55-59, 77-89, 284-359lucene/core/src/java/org/apache/lucene/search/SortField.java:195-200, 446-469; lucene/core/src/java/org/apache/lucene/search/Sort.java:113-126lucene/expressions/src/java/org/apache/lucene/expressions/Expression.java:107-109lucene/queries/src/java/org/apache/lucene/queries/function/ValueSource.java:332-334github.com/apache/lucene/issues/2555 (LUCENE-1481 description: the cache "uses the Sort/SortField as key")
- Who is affected
lucene/core/src/java/org/apache/lucene/index/IndexWriterConfig.java:505-510
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/search/SortField.java:446-449, 463-466lucene/core/src/java/org/apache/lucene/search/FieldComparatorSource.java:22-24 (@lucene.experimental)lucene/CHANGES.txt:8086 (LUCENE-7723), :9868 (LUCENE-6946)lucene/core/src/test/org/apache/lucene/search/TestSort.java:73-92lucene/expressions/src/test/org/apache/lucene/expressions/TestExpressionSortField.java:41-71github.com/apache/lucene/issues/2555 (LUCENE-1481 description, read-only)
- Suggested fix
lucene/expressions/src/java/org/apache/lucene/expressions/ExpressionValueSource.java:146-171; lucene/queries/src/java/org/apache/lucene/queries/function/ValueSource.java:379-383
- TestValuesSourceSortFieldEquals.java (one class, two one-line methods, public API):
repro/LFV-035/src/org/apache/lucene/search/TestValuesSourceSortFieldEquals.java(33 lines)
Made in parallel with this doc, from the code at 645d4fe14a20 and an executed probe of the scenario.
assets/LFV-035/bug.png(477 KB)
