All findings · Lucene finding · LFV-035 · valuessource-sortfield-equals

Sort fields built twice from one DoubleValuesSource or LongValuesSource compare unequal

Call getSortField(false) twice on the same DoubleValuesSource or LongValuesSource and the two sort fields are not equal (their hash codes differed too), so a cache keyed on a Sort built per request never hits. Sort order is not affected; every release since 6.4.0 is affected, and so is Expression#getSortField.

Summary

Priority
Low, rank 1023Values-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
minor
Since
6.4.0 (LUCENE-5325 and LUCENE-7609)introduced 2016-12-28 · about 9 years
Affected
6.4.0 to 10.6.0 (all 87 releases) and main
Status
documented
Found by
static analysis (SpotBugs), then a public-API JUnit

AI-assisted page: this finding was first signalled by a testing baseline (randomized, fuzz, property-based or stress testing, or static analysis) and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

Sort fields with a custom comparator source are compared by that source, and every getSortField call makes a new one that equals only itself.

Background: how sort fields are compared

  1. Sort and SortField implement equals and hashCode so 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).
  2. A SortField that sorts with a custom FieldComparatorSource equals 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)".
  3. A DoubleValuesSource or LongValuesSource computes one value per document, and getSortField(reverse) turns it into a sort field. The sources themselves must implement value equals and hashCode.
Diagram of the bug: one DoubleValuesSource for the field price; two calls of getSortField(false) each create a new DoubleValuesSortField that wraps a new DoubleValuesComparatorSource. The comparator sources have no equals method, so SortField.equals finds them unequal, the two sort fields and the two Sorts built from them are unequal with different hash codes, and a cache keyed on the Sort misses on every identical request although both sorts print <double(price)> and sort the documents correctly.
Two 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.

The finding, step by step

  1. You sort by a values source, price = DoubleValuesSource.fromDoubleField("price"), and cache results in a HashMap<Sort, TopDocs>, building new Sort(price.getSortField(false)) for each request.
  2. 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.
  3. 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, yet equals is false and the hash codes differ. The results are sorted correctly (prices 10, 20, 30).
  4. Why: each getSortField call wraps a new private comparator source that has no equals or hashCode, and SortField.equals compares comparator sources, so a sort field equals only itself. LongValuesSource and Expression#getSortField work the same way.

Who is affected

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).

The mechanism (main 645d4fe14a20)

  1. getSortField(reverse) returns new DoubleValuesSortField(this, reverse, 0) (DoubleValuesSource.java:111-113); the overload with a missing value does the same (:121-123).
  2. The constructor hands SortField a new comparator source: super(producer.toString(), new DoubleValuesComparatorSource(producer, missingValue), reverse) (:633). DoubleValuesSortField (:628-661) overrides needsScores, toString and rewrite, not equals.
  3. DoubleValuesComparatorSource (:667-703) holds producer and missingValue and declares neither equals nor hashCode, so two instances are equal only if they are the same object.
  4. SortField#equals compares Objects.equals(this.comparatorSource, other.comparatorSource) (SortField.java:459) and hashCode hashes it (:469). Everything else it compares is the same for the two fields: field (producer.toString()), type CUSTOM, reverse flag, and SortField's own missing value, null because the missing value lives in the comparator source (SortField.java:195-200). Sort#equals and hashCode delegate to the fields (Sort.java:119, :125).
  5. LongValuesSource has the same shape (LongValuesSource.java:77-79, :87-89, :289, :323-359). Expression#getSortField(bindings, reverse) is getDoubleValuesSource(bindings).getSortField(reverse) (Expression.java:107-109), and since #14654 (9.12.2, 10.2.2) ValueSource.fromDoubleValuesSource(dvs).getSortField returns in.getSortField(reverse) (ValueSource.java:332-334). Executed by the fact check: two such sort fields over fromDoubleField("price") are unequal on main and 9.12.2 and equal on 9.12.1, where that path still returned the queries module's own ValueSourceSortField, 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.

The numbers

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/SortField objects built by DoubleValuesSource#getSortField, LongValuesSource#getSortField, Expression#getSortField or (since 9.12.2 / 10.2.2) ValueSource.fromDoubleValuesSource(...).getSortField. The typical case is a search-result cache keyed by query plus sort, with the Sort built anew for each request: every lookup for such a sort misses, deterministically. Code that reuses one Sort instance, 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 is DoubleValuesSource#getSortField (Solr's own values source has value equals). 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#setIndexSort rejects a field without an index sorter, IndexWriterConfig.java:505-510). We found no non-test Lucene code that keys a map or set on Sort/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.

Minimal repro

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.

TestValuesSourceSortFieldEquals.java (one class, two one-line methods, public API)33 lines
/*
 * 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));
  }
}
Command
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)>.

All files of this repro: repro/LFV-035/

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.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT): fails; every release from 6.4.0 to 10.6.0: fails; with the fix: passes
jdkOpenJDK 25.0.3
osLinux, aarch64

Observed output

# 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 failures

Why it looks unintended

  • SortField#equals javadoc: 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 SortField subclasses in the tree compare by value.
The full argument298 words
  • Contract on SortField (SortField.java:446-449): "Returns true if o is equal to this. If a FieldComparatorSource was provided, it must properly implement equals (unless a singleton is always used)." The same for hashCode (: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 SortField has equality. LUCENE-1481 (2008, Uwe Schindler): a query-result cache "uses the Sort/SortField as key into the cache". LUCENE-7798 (6.6) gave ToParentBlockJoinSortField equality because it "is going to be used as query result key". LUCENE-6946 (5.4.1, Bug Fixes) treated an incomplete SortField.equals as a bug.
  • Regression for expressions. LUCENE-5220 (2013) gave ExpressionSortField value equality; LUCENE-7609 (6.4.0) deleted it and routed Expression#getSortField through DoubleValuesSource#getSortField. Release jars: equal on 6.3.0, unequal on 6.4.0. The ValueSource.fromDoubleValuesSource path 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 SortField subclasses all compare by value. Nine override equals and hashCode: BinarySortField, SortedSetSortField, SortedNumericSortField, LatLonPointSortField, XYPointSortField, FeatureSortField, ToParentBlockJoinSortField, Geo3DPointSortField and Geo3DPointOutsideSortField. The tenth, ValueSource.ValueSourceSortField in the queries module (ValueSource.java:373-384), overrides neither but holds no comparator source until rewrite, so SortField'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#testEquals asserts that separately built sorts are equal (TestSort.java:73-92). TestExpressionSortField#testEquals (:41-71) checks only inequality cases and assertEquals(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.

Suggested fix

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.

Value equals/hashCode on the two comparator sources (validated by the checks) (recommended)

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 (ExpressionValueSource compares them, ExpressionValueSource.java:146-171): custom bindings that return a new identity-equal source on each lookup keep Expression#getSortField unequal.
  • 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.compare treats 0.0 and -0.0 as different and NaN as equal to itself, consistent with Double.hashCode.
  • Objects.hash boxes its arguments; this is not a per-document path.
  • Not covered: the queries module's own ValueSourceSortField is equal before rewrite, and its rewrite returns a plain SortField around a new ValueSourceComparatorSource each 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=true and false. 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, including TestExpressionSortField and TestExpressionSorts) and :lucene:queries:test --tests 'org.apache.lucene.queries.function.*' (111 tests, including TestFunctionQuerySort): 0 failures.
  • Lint (checkGoogleJavaFormat, ecjLintMain, ecjLintTest, forbiddenApisMain, forbiddenApisTest, renderJavadoc on core, the repro test included): BUILD SUCCESSFUL.
  • Not run: the rest of the core suite, other modules, x86.

Override equals/hashCode in the two sort-field classes

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.

Document identity equality

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.

Downstream note

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.

How it was found

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, Sort equality and hash, a HashMap<Sort, ?> lookup, Expression#getSortField twice) fail and 4 controls (plain SortField, SortedNumericSortField, FeatureField.newFeatureSort, LatLonDocValuesField.newDistanceSort are 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/hashCode on 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.

Prior reports, history and checks

Prior reports

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.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene, apache/lucene-solr)2026-10-31 to 2026-11-01the 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 phrasingsno report; related: #8774 (LUCENE-7723), #6284 (LUCENE-5220), #2555 (LUCENE-1481), #8849 (LUCENE-7798), #14654, #15480, #16857
Open and recently closed PRs2026-10-31changed-file lists of all 410 open PRs and of the 1,249 closed PRs updated since 2025-10-01none adds equals/hashCode to these classes
JIRA archive and mailing lists2026-10-318 + 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 web2026-10-31searches over Solr and two other downstream search servers; 15 web queriesapache/solr#3923 (Solr's workaround); nothing else

History

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.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedminimal test fails, assertions on and off (standalone runner and gradle); passes with the fix
10.6.0 affectedfails, assertions on and off
6.4.0 to 10.5.2 (86 releases) affectedfail, assertions on and off
6.4.0, expression sort affectedExpression#getSortField twice: unequal, different hash codes
6.3.0, expression sort not affectedExpressionSortField twice: equal, same hash code
6.3.0, values sources absentDoubleValuesSource and LongValuesSource do not exist yet (the test does not compile)

Independent checks

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.

Each independent check and its verdict
CheckVerdictWhat it found
Study verifier reproduced, low severity9-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 realMinimal 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 confidenceOwn 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 onwards87 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 passesGradle 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.

Diagram file

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.

Sources and provenance

Finding record, Lucene and upstream sources, repro files and diagram notes533 words

Finding record

LFV-035, snapshot of 2026-11-06

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 on 7cecf79523 and a9779ef3c193, and the tie-breaker's own 9-test check on 645d4fe14a20 (and 10.5.2).

Lucene and upstream sources

The finding in 30 seconds
  • lucene/core/src/java/org/apache/lucene/search/DoubleValuesSource.java:111-129, 628-703
  • lucene/core/src/java/org/apache/lucene/search/LongValuesSource.java:55-59, 77-89, 284-359
  • lucene/core/src/java/org/apache/lucene/search/SortField.java:195-200, 446-469; lucene/core/src/java/org/apache/lucene/search/Sort.java:113-126
  • lucene/expressions/src/java/org/apache/lucene/expressions/Expression.java:107-109
  • lucene/queries/src/java/org/apache/lucene/queries/function/ValueSource.java:332-334
  • github.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-466
  • lucene/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-92
  • lucene/expressions/src/test/org/apache/lucene/expressions/TestExpressionSortField.java:41-71
  • github.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

Other statements trace to the hunt's working files (models, logs, reviews and briefs), which are not published.

Repro files

About the diagrams

Made in parallel with this doc, from the code at 645d4fe14a20 and an executed probe of the scenario.

Built 2026-11-06 from the hunt's record of this finding and its doc file. Lucene main under test in this hunt: ea57360912ed. Every listed finding was re-run on Lucene main e1faeba08bde on 2026-11-02.