These value sources compute similarityFunction.compare(v1, v2) for each document. Their base class stores three fields, but equals() compares only the two vector sources, while hashCode() and description() include the function. So two sources that differ only in the function are equal, yet their hash codes differ (with equal operands they could only match if the two enum constants' identity hash codes collided).
- A
ValueSource(modulelucene/queries) computes one value per document.FunctionQueryscores each document by it, andFunctionRangeQuerymatches the documents whose value lies in a range. FloatVectorSimilarityFunction(function, v1, v2)(alsoByteVectorSimilarityFunctionand, on main and branch_11x,Float16VectorSimilarityFunction) computesfunction.compare(v1, v2), for example a document's vector field against a constant query vector, with any of EUCLIDEAN, DOT_PRODUCT, COSINE or MAXIMUM_INNER_PRODUCT.ValueSourcedeclaresequalsandhashCodeabstract, and the query classes delegate their equality to it. Java requires that objects that areequalsreturn the same hash code.
a.equals(b) is true while their hash codes differ; a List treats them as equal, a HashSet does not. One more condition in equals fixes it. Select the still to open it at full size. Generated with AI, checked against the code.- Build two value sources over the vector field
vand the query vector[1, 2]:new FloatVectorSimilarityFunction(EUCLIDEAN, new FloatKnnVectorFieldSource("v"), new ConstKnnFloatValueSource(new float[] {1, 2})), and the same withCOSINE. - They compute different things: for a document with
v = [2, 1]the EUCLIDEAN source gives 0.333 and the COSINE source 0.9. You expectequals()to return false;hashCode()anddescription()already tell them apart. - Instead
equals()returns true, with assertions on and off, in every release since 9.7.0, while the hash codes differ (1652613436 vs 613814086 in one run). Java requires equal objects to have equal hash codes. - The queries built on them inherit it: two such
FunctionQueryobjects are equal, soList.containssays yes while aHashSetsays no, and aFunctionRangeQueryover [0.5, 1] that matches 0 documents equals one that matches 1. - Why:
VectorSimilarityFunction.equalscompares onlyvector1andvector2, whilehashCodeisObjects.hash(similarityFunction, vector1, vector2). A review commit in the PR that added the class (#12253) dropped the function fromequals.
Only callers that compare two of these sources (or the queries around them) with different functions over the same operands by equals alone: List.contains, equality-based caches or de-duplication, tests. Inside Lucene no search result changes in practice: these value-source weights are never cached, and query de-duplication is hash-based (barring an identity-hash collision of the two enum constants).
Mechanism, terms and numbers484 words
FloatVectorSimilarityFunction, ByteVectorSimilarityFunction and (on main and branch_11x) Float16VectorSimilarityFunction are lucene/queries value sources that score each document by similarityFunction.compare(v1, v2), for example a document's vector field against a constant query vector. Their shared base class valuesource.VectorSimilarityFunction holds three fields, but its equals() compares only the two vector sources, while hashCode() (Objects.hash(similarityFunction, vector1, vector2)) and description() include the function. So a EUCLIDEAN and a COSINE source over the same field and query vector, which score a document [2, 1] 0.333 and 0.9, compare equal with different hash codes, breaking the java.lang.Object contract; FunctionQuery, FunctionRangeQuery and FunctionScoreQuery pass the false equality on. Inside Lucene no search result changes in practice, because these value-source weights are never cached and query de-duplication is hash-based (only a collision of the two enum constants' identity hash codes could defeat that); only callers that compare with equals alone are misled. It has been in every release since the classes were added in 9.7.0 (#12253): an earlier commit of that PR compared the function, and a review commit dropped it. The fix is one added condition.
- Three fields:
similarityFunction,vector1,vector2(:29-31), set by the public constructor (:33-41). - The function decides the values. Each subclass's
funcends inreturn similarityFunction.compare(v1, v2);(FloatVectorSimilarityFunction.java:46,ByteVectorSimilarityFunction.java:47,Float16VectorSimilarityFunction.java:46). No subclass overridesequalsorhashCode. equalsleaves it out (:79-85): after thethis == o, null andgetClass()checks it returnsObjects.equals(vector1, ...vector1) && Objects.equals(vector2, ...vector2).hashCodeanddescription()include it:Objects.hash(similarityFunction, vector1, vector2)(:89) andsimilarityFunction.name() + "(" + ...(:94).- The operands compare by value (
ConstKnnFloatValueSourcewithArrays.equals,FloatKnnVectorFieldSourceby field name), so separately built EUCLIDEAN and COSINE sources over "fieldv, vector[1, 2]" are equal. - The wrappers pass it on.
FunctionQuery.equalsissameClassAs(other) && func.equals(...)(FunctionQuery.java:180-182);FunctionRangeQuerycompares its bounds andObjects.equals(valueSource, other.valueSource)(FunctionRangeQuery.java:115-125);FunctionScoreQuerycompares its source (FunctionScoreQuery.java:144-149), andValueSource.asDoubleValuesSource()wraps withObjects.equals(in, that.in)(ValueSource.java:249-253).FunctionScoreQueryuse became possible in 10.4.0 (#15328).
One document with v = [2, 1] and the query vector [1, 2]: EUCLIDEAN gives 1 / (1 + 2) = 0.333, COSINE (1 + 0.8) / 2 = 0.9. equals is true and the hash codes are 1652613436 and 613814086 (the function's part of the hash is the enum constant's identity hash, so the numbers can change between JVM runs: both runs of this probe printed this pair, the probe behind the diagram printed -1860021253 and -1987185915; they differed in every recorded run). List.of(euclideanQuery).contains(cosineQuery) is true, a HashSet of the same query says false. FunctionRangeQuery(source, 0.5f, 1f, true, true) matches 0 documents with EUCLIDEAN and 1 with COSINE, and the two range queries are equals.
Exposure and likelihood360 words
- Who is exposed. Callers that compare these value sources or the queries around them with
equalsalone, for exampleList.contains,indexOforremove, an equality-based cache or request de-duplicator, or a test that asserts two queries differ, and that see two functions over the same operands. - What does not break inside Lucene. The weights are never cached:
isCacheablereturns false inFunctionQuery(FunctionQuery.java:82-83),FunctionRangeQuery(FunctionRangeQuery.java:177-178) and theasLongValuesSourceandasDoubleValuesSourcewrappers (ValueSource.java:150-151,:230-231), soLRUQueryCachenever keys on them.FunctionScoreQuery's scoring weight is cacheable only when both its inner weight and its source are (FunctionScoreQuery.java:280-282), so never with these sources; when scores are not needed it returns the inner query's weight itself (:117-120), which may be cached but whose matches do not depend on the function.BooleanQuerykeeps its clauses in aMultiset/HashSet(BooleanQuery.java:132-140) andDisjunctionMaxQueryits disjuncts in aMultiset(DisjunctionMaxQuery.java:47); these compare hash codes first, which differ. Executed: aBooleanQuerywithSHOULDEUCLIDEAN +SHOULDCOSINE keeps both clauses (score 0.333 + 0.9 = 1.2333). No full sweep of in-tree consumers was run. One theoretical exception: with equal operands the two sources' hash codes are equal exactly when the two enum constants' identity hash codes are (Objects.hashmultiplies the function's hash by 961, an odd number), and identity hash codes are not unique; in a JVM run where they collided, these sets andBooleanQuery.deduplicateClauses(BooleanQuery.java:714-716) would reach the wrongequalsand could merge the two clauses. Never observed; not claimed. - Downstream. Solr's
vectorSimilarity()builds these classes, and its 4-argument form takes the function from the request. Whether a Solr cache could mix them up was not tested (the devil's-advocate verifier only re-did Solr's key hash arithmetic by hand), so it is not claimed. - Default vs opt-in. Opt-in
lucene/queriesclasses, not on the default search path. No index, codec or setting matters; assertions on or off behave the same. - How likely. Rare: it needs an equals-only caller that meets two functions over the same operands, and no report of it was found since 9.7.0 (June 2023).
One JUnit test for lucene/queries, public API only, no index: build the EUCLIDEAN and the COSINE source over the same two operands and assert they are not equal. Deterministic and single-threaded; it fails the same way 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.queries.function;
import org.apache.lucene.index.VectorSimilarityFunction;
import org.apache.lucene.queries.function.valuesource.ConstKnnFloatValueSource;
import org.apache.lucene.queries.function.valuesource.FloatKnnVectorFieldSource;
import org.apache.lucene.queries.function.valuesource.FloatVectorSimilarityFunction;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestVectorSimilarityFunctionEquals extends LuceneTestCase {
public void testSimilarityFunctionIsPartOfEquals() {
ValueSource field = new FloatKnnVectorFieldSource("v");
ValueSource target = new ConstKnnFloatValueSource(new float[] {1f, 2f});
// Expected: EUCLIDEAN and COSINE sources are not equal; actual: equals() returns true.
assertNotEquals(
new FloatVectorSimilarityFunction(VectorSimilarityFunction.EUCLIDEAN, field, target),
new FloatVectorSimilarityFunction(VectorSimilarityFunction.COSINE, field, target));
}
}
cp $REPRO/TestVectorSimilarityFunctionEquals.java $LUCENE/lucene/queries/src/test/org/apache/lucene/queries/function/
cd $LUCENE && ./gradlew :lucene:queries:test --tests TestVectorSimilarityFunctionEquals -Ptests.seed=5EED1079 -Ptests.asserts=true
# also with -Ptests.asserts=false; nothing depends on the seed
- Expected
- The EUCLIDEAN and the COSINE source are not equal, so
assertNotEqualspasses. - Observed
equals()returns true:AssertionError: Values should be different. Actual: COSINE(FloatKnnVectorFieldSource(v), ConstKnnFloatValueSource([1.0, 2.0])), with assertions on and off.
More files, environment, full output236 words
The test is byte-identical to the hunt's minimal repro and passes :lucene:queries:check -x test. It runs unchanged on the 10.6.0 and 9.7.0 release jars; 9.6.0 has none of these classes.
Executed: gradle on main e1faeba08bde with assertions on (seed 5EED1079) and off (5EED107A), both fail; the standalone runner on the 10.6.0 release jars with assertions on and off and on 9.7.0, all fail; the hunt's runs on main fail the same way. With the one-condition fix the test passes. JUnit's message prints only the second argument.
Use the same test as the regression test. It checks only the equals expectation: FunctionRangeQuery is exercised only by the illustration probe, and FunctionScoreQuery, the Float16 subclass, the query cache and downstream callers are not exercised.
| lucene | main e1faeba08bde (12.0.0-SNAPSHOT); also the 10.6.0 and 9.7.0 release jars, test unchanged |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 (plain Java; only this platform was run) |
cp $REPRO/TestVectorSimilarityFunctionEquals.java $LUCENE/lucene/queries/src/test/org/apache/lucene/queries/function/
cd $LUCENE && ./gradlew :lucene:queries:test --tests TestVectorSimilarityFunctionEquals -Ptests.seed=5EED1079 -Ptests.asserts=true
# also with -Ptests.asserts=false; nothing depends on the seed
TestVectorSimilarityFunctionEquals > testSimilarityFunctionIsPartOfEquals FAILED
java.lang.AssertionError: Values should be different. Actual: COSINE(FloatKnnVectorFieldSource(v), ConstKnnFloatValueSource([1.0, 2.0]))
at junit@4.13.2/org.junit.Assert.fail(Assert.java:89)
at junit@4.13.2/org.junit.Assert.failEquals(Assert.java:187)
at junit@4.13.2/org.junit.Assert.assertNotEquals(Assert.java:163)
at junit@4.13.2/org.junit.Assert.assertNotEquals(Assert.java:177)
at org.apache.lucene.queries.function.TestVectorSimilarityFunctionEquals.testSimilarityFunctionIsPartOfEquals(TestVectorSimilarityFunctionEquals.java:31)
1 test completed, 1 failed
# the same with -Ptests.asserts=false, and on the 10.6.0 (assertions on and off) and 9.7.0 release jars;
# with the fix: 1 test completed, 0 failed- The function decides the values, and
hashCode()includes it: equal objects with different hash codes are wrong under any equality definition. - The PR that added the class compared the function in
equals; a review change addinggetClass()and null checks dropped it, unremarked. - LUCENE-7736:
equalsmust "never return true for values sources that could return different values".
The full argument188 words
- Contract.
ValueSourcedeclaresequalsandhashCodeabstract (ValueSource.java:51-55); coreQuery.java:95-100asks subclasses to implement equivalence "properly ... so that QueryCache works properly".java.lang.Object#hashCoderequires objects that are equal byequalsto return the same hash code; here they are equal and their hash codes differ. - Maintainer rule. On LUCENE-7736 (#8787) jpountz wrote: "I think it's important for equals to behave correctly and never return true for values sources that could return different values." romseygeek agreed in the next comment ("I'll change things back again").
- Siblings. Other value sources put every value-deciding parameter in
equals, for exampleLinearFloatFunctioncomparesslope,interceptandsource(LinearFloatFunction.java:85-91). - Tests.
TestKnnVectorSimilarityFunctionshas noequals,hashCodeorQueryUtilsassertions; it passes unchanged with the fix. - History. In PR #12253, commit
901febe400(2023-06-13) comparedsimilarityFunctioninequals. uschindler's review asked for agetClass()check and athis == other/ null check; commit7219987d52("Addressing review") added them, and the function term disappeared, whilehashCodekept the field. No review or conversation comment on the PR mentions leaving it out. The PR was squash-merged as14c18d86245(2023-06-14, 9.7.0).
The devil's-advocate check134 words
The strongest "intended" case (built by the devil's-advocate verifier; the cross-model lenses rated it weak): equality identifies only the subclass and its two operands, the function is "implied" by the vector field, and Query.equals' javadoc speaks of "document-filtering properties", which a FunctionQuery (it matches every document) does not change.
Why it fails: it cannot explain why hashCode() and description() include the function, and equal objects with different hash codes are wrong under any equality definition. The function is an independent constructor argument (func never reads the field's own similarity, and Solr's 4-argument form lets a request choose it). For FunctionRangeQuery the function does decide which documents match (0 vs 1 hits in the probe, still equals). And the PR history is consistent with an accidental omission. Verdict: a defect, Low.
Recommended Compare the similarity function in equals (fix A)
Compare the function in equals: add similarityFunction == ((VectorSimilarityFunction) o).similarityFunction && before the two operand checks, one line. The repro then passes, and so do TestKnnVectorSimilarityFunctions, the whole lucene/queries suite (assertions on) and :lucene:queries:check -x test.
--- a/lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/VectorSimilarityFunction.java
+++ b/lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/VectorSimilarityFunction.java
@@ -80,7 +80,8 @@ public abstract class VectorSimilarityFunction extends ValueSource {
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
- return Objects.equals(vector1, ((VectorSimilarityFunction) o).vector1)
+ return similarityFunction == ((VectorSimilarityFunction) o).similarityFunction
+ && Objects.equals(vector1, ((VectorSimilarityFunction) o).vector1)
&& Objects.equals(vector2, ((VectorSimilarityFunction) o).vector2);
}All options and trade-offs2 options ยท 369 words
One fix was built and validated in a scratch worktree of e1faeba08bde; the alternative is listed with its trade-off.
In VectorSimilarityFunction.equals, compare similarityFunction too, so equals uses the same three fields as hashCode. The field is an enum, so == is exact (and null-safe: the constructor does not null-check the function, VectorSimilarityFunction.java:33-41, though a null one already fails in func and description()). Sources with the same function and operands stay equal, and the three subclasses and every wrapper are fixed at once. A reviewer may prefer casting once (VectorSimilarityFunction other = (VectorSimilarityFunction) o;); same behaviour.
--- a/lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/VectorSimilarityFunction.java
+++ b/lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/VectorSimilarityFunction.java
@@ -80,7 +80,8 @@ public abstract class VectorSimilarityFunction extends ValueSource {
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
- return Objects.equals(vector1, ((VectorSimilarityFunction) o).vector1)
+ return similarityFunction == ((VectorSimilarityFunction) o).similarityFunction
+ && Objects.equals(vector1, ((VectorSimilarityFunction) o).vector1)
&& Objects.equals(vector2, ((VectorSimilarityFunction) o).vector2);
}Trade-offs
A deliberate behaviour change for callers that compare two such sources (or the queries around them) with different functions over the same operands: equals now tells them apart, which is the intended correction. Pairs with the same function and operands stay equal, with the same hash code.
What the checks validated
- Gradle, JDK 25, scratch worktree of
e1faeba08bde: the test fails 1 of 1 on stock with assertions on and off; with the fix, the test plusTestKnnVectorSimilarityFunctionspass (2 suites, 12 tests, 0 failures) with assertions on and off (one upstream test skips itself without assertions); the whole:lucene:queries:testwith assertions on passes (52 suites, 513 tests, 5 skipped, 0 failures);:lucene:queries:check -x testpasses (checkGoogleJavaFormat,ecjLint,forbiddenApis). - The hunt: the repro passes with the fix shadowed, and so do the independent reproducer's own test (Byte subclass inside
FunctionQuery, with its identical fix) and the intended-lens reviewer's contract test.
Make hashCode match the current equals.
Trade-offs
Restores equals/hashCode consistency but makes the false equality permanent: sources that compute different values stay equal, against the LUCENE-7736 rule. Not validated.
Solr's vectorSimilarity() function builds these classes and its 4-argument form takes the similarity function from the request; whether any downstream cache confuses the two was not tested.
An AI finder scanning query and value-source classes for fields that hashCode reads but equals does not flagged similarityFunction in all three subclasses; its equality harness then saw equal FunctionQuery objects with different hashes and scores. A later hunt unit reduced it to one public-API test.
The method in full122 words
Method: semi-formal (an automated bytecode field-set scan, an equality-congruence harness, then executed JUnit tests).
- A hunt unit (query-object contracts), 2026-10-26: a bytecode scan of the stock jars reported
VectorSimilarityFunction.similarityFunctionas read byhashCode,description()and each subclass'sfuncbut not byequals, for the Float, Byte and Float16 subclasses. The unit's equality harness witnessed it: aFunctionQueryover EUCLIDEAN equals the one over DOT_PRODUCT while their hash codes and scores differ. - A later hunt unit (2026-11-02) re-ran the lead on main
e1faeba08bde(unchanged) and reduced it to one public-API JUnit test, which fails with assertions on and off and passes with the one-condition fix. - Independent verifiers, cross-model reviews, an adjudication and an audit then re-ran it (see the checks below).
No public report found in the sources searched (2026-11-02): GitHub issues, PRs and commits (incl. the migrated JIRA archive), the mailing lists, Solr, Elasticsearch, OpenSearch, the web and the open PRs. The nearest item is the introducing PR #12253, whose review touched equals without noticing the missing function.
Searches4 searches
None of the hits is the same bug. The class postdates the JIRA migration, so no JIRA report can exist. Search limits: keyword search only; private reports are not visible.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene incl. the migrated JIRA archive; apache/solr, Elasticsearch, OpenSearch) | 2026-11-02 | four scripted prior-report searches (each about 16 GitHub issue/PR, 3 commit and 2 downstream queries) and manual searches: VectorSimilarityFunction equals, VectorSimilarityFunction hashCode, similarityFunction equals, FunctionQuery equals vector, knn function query equals, the three class names and others | no report; the only targeted hit is #12253, the introducing PR |
| Open PRs and recent activity | 2026-11-02 | the changed files of the open apache/lucene PRs matching VectorSimilarityFunction, valuesource or FunctionQuery; every issue and PR updated since 2026-10-31; the commit history of the four class files on main, branch_10x and branch_10_6 | none touches queries/function/valuesource/; only #12253, #15329 (doubleVal) and #16383 (Float16 subclass) changed the files, none of them equals |
| Mailing lists, JIRA and the web | 2026-11-02 | about 20 queries per run over dev@, java-user@, issues@ and commits@; JIRA-archive and Solr JIRA queries; web queries | no report |
| Nearest items (read) | 2026-11-02 | #12253 review and comments, #15328 / #15329, #16383, Solr QueryEqualityTest, LUCENE-2092, LUCENE-10026, LUCENE-10552, #8861 (LUCENE-7810) | the introducing PR (its review asked for getClass() and null checks), a different bug in the same class (doubleVal), the Float16 subclass, a Solr test that checks only equal inputs, and the same defect class fixed in other classes |
Present since the classes were added in 14c18d86245 (#12253, 2023-06-14), so in every release from 9.7.0 to 10.6.0, in main and in branch_11x; not a regression. 9.6.0 has no such classes. Main, 10.6.0, 10.5.2, 10.0.0, 9.12.3 and 9.7.0 were executed and all fail. Not fixed upstream.
Versions tested8 rows
Introduced by 14c18d86245 (main, author date 2023-06-14, #12253 "GITHUB-12252: Add function queries for computing similarity scores between knn vectors", eliaporciani; co-authored and merged by alessandrobenedetti) and its same-day branch_9x backport 433aa49b2ec; CHANGES lists it under 9.7.0. The equals block has the same md5 at every release tag from 9.7.0 to 10.6.0 and on main, and hashCode is Objects.hash(similarityFunction, vector1, vector2) at all of them. The only later commit to the file, 05af6514226 (#15329, 2025-10-12, doubleVal), does not touch equals or hashCode. The Float16 subclass (#16383, CHANGES 11.0.0) inherits the same equals on main and on the 11.0.0 release branch branch_11x (both unreleased; branch_11x has the same VectorSimilarityFunction.java as main); it is not on branch_10_6 or in any release. So 11.0.0 would ship the defect unless the fix also lands on branch_11x, a backport target. Upstream main head was e1faeba08bd at the 2026-11-02 search.
| Version or commit | Verdict | Result |
|---|---|---|
main e1faeba08bde | affected | this doc's test fails 1 of 1 through gradle with assertions on and off |
| branch_11x (11.0.0, unreleased) | read | the same VectorSimilarityFunction.java blob as main (860f0c05617), with the Float16 subclass; not executed |
| 10.6.0 | affected | this doc's test fails 1 of 1 with assertions on and off (release jars) |
| 10.5.2, 10.0.0, 9.12.3 | same pair | the hunt's earlier test of the same EUCLIDEAN/COSINE pair fails 1 of 1 |
| 9.7.0 | first release | this doc's test and the hunt's earlier test fail 1 of 1 (release jars) |
| 9.7.0 to 10.6.0 (25 releases) | read | the equals block is byte-identical and hashCode includes the function at every release tag |
| 9.6.0 | no API | the classes do not exist; the test does not compile |
| main with the fix | fix | the test passes, as do TestKnnVectorSimilarityFunctions and the whole lucene/queries suite (assertions on) |
An independent reproducer, cross-model reviews (OpenAI GPT-5.6, executed), a devil's-advocate verifier, an adjudication and an audit agree on the root cause, and each re-ran a failing test. This doc's gradle runs reproduced it on main e1faeba08bde and validated the fix.
Checks and verdicts383 words
Every executed check saw equals return true for two sources that differ only in the function, from the same lines, VectorSimilarityFunction.java:79-85, and every one that ran the fix saw it pass. The cross-model reviews ran on an earlier main checkout, ea57360912ed, whose VectorSimilarityFunction.java is identical to e1faeba08bde.
| Check | Verdict | What it found |
|---|---|---|
| first sighting | flagged | The bytecode scan flagged the field; the equality harness saw equal FunctionQuery objects with different hashes and scores; its 3-test JUnit failed on main, 10.5.2 and 9.12.3. Its cross-model reviews (OpenAI GPT-5.6, a reviewer that could not run code) returned a procedural "not real" at low confidence while agreeing with the root cause; intended argument weak. |
| finder | reproduced | Re-ran the lead on main e1faeba08bde and reduced it to the one test: fails with assertions on and off, passes with the fix. |
| independent reproducer | real | Own test written from the claim (Byte subclass, DOT_PRODUCT vs EUCLIDEAN, inside FunctionQuery): fails on main (assertions on and off), 10.6.0, 10.5.2, 9.12.3 and 9.7.0; does not compile on 9.6.0; passes with the fix. |
| cross-model review, refute lens | real (high) | OpenAI GPT-5.6, executed: its own test fails with assertions on and off while same-function and changed-operand controls pass. Intended argument weak. |
| cross-model review, intended lens | real (high) | OpenAI GPT-5.6, executed: its contract test fails unpatched and passes with the fix. "Equality covers only the subclass and its operands" cannot explain equal objects with unequal hash codes; intended argument weak. |
| devil's advocate | real, Low | Replay fails on main (assertions on and off). Found the PR history (the function was compared, then dropped in a review commit) and the LUCENE-7736 rule. |
| adjudication | confirmed (high), Low | Re-ran with a new seed: fails with assertions on and off, passes with the fix. Rated it a contract break, not a wrong-results bug. |
| credibility audit | confirmed (Low) | The audit re-ran the test on main (assertions on and off) and rated the impact minor; a separate re-run also checked the one-test repro (no index, deterministic, assertions on and off) and the fix. |
| this doc | reproduced | Gradle on main (assertions on and off) and the standalone runner on 10.6.0 (both) and 9.7.0 fail; fix A validated with the module's tests and static checks. |
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 490 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,311 words
- Registry title
- Function-query vector similarity value sources (Float/Byte/Float16VectorSimilarityFunction) leave the similarity function out of equals but hash it, so EUCLIDEAN and COSINE sources over the same vectors compare equal with different hash codes
- Area
- queries / function queries: valuesource.VectorSimilarityFunction equality (base of Float/Byte/Float16VectorSimilarityFunction, and so of the FunctionQuery, FunctionRangeQuery and FunctionScoreQuery that wrap them)
- Priority
- Low, rank 859: Since 9.7.0, vector similarity value sources with different similarity functions compare equal but have different hash codes. Lucene's own consumers are hash-based or never cache these queries, so in practice no search result changes and only application code that relies on equals alone is affected, which keeps it Low.
- Impact class
- minor
- Impact note
- Only code that builds two of these lucene-queries value sources (or the FunctionQuery, FunctionRangeQuery or FunctionScoreQuery that wrap them) over the same two vector sources with different similarity functions, and then compares them with equals alone (List.contains or indexOf, equality-based de-duplication or caching, tests that assert inequality), treats a COSINE source as the same as a EUCLIDEAN one; inside Lucene no search result changes in practice, because these weights are not cacheable (FunctionQuery, FunctionRangeQuery and ValueSource.asDoubleValuesSource return isCacheable false) and in-tree de-duplication (the BooleanQuery and DisjunctionMaxQuery Multiset/HashSet) is hash-based while the hash codes differ; no index or scale is needed, -ea and -da behave the same, and it has been in every release since 9.7.0.
- Summary
- You build two function-query value sources over the same vector field and the same query vector, one with EUCLIDEAN and one with COSINE (new FloatVectorSimilarityFunction(EUCLIDEAN, field, target) and the same with COSINE), and expect them to be unequal because they compute different scores; the class itself treats the function as part of its identity, since hashCode() and description() include it. Instead equals() compares only the two vector sources and returns true, while hashCode() returns different values, which breaks the java.lang.Object rule that equal objects have equal hash codes. The Byte and Float16 subclasses share this equals, and FunctionQuery and the other wrappers pass the false equality on.
- Introduced
- 14c18d86245 (apache/lucene#12253 'GITHUB-12252: Add function queries for computing similarity scores between knn vectors', author eliaporciani, co-author and merger alessandrobenedetti; author date 2023-06-14T14:49Z, the same instant as the PR merge; branch_9x backport 433aa49b2ec the same day): it added the classes with this equals [read: git blame of equals and hashCode on main and at 9.7.0; exec: 9.6.0 lacks the classes, 9.7.0 fails]. Inside the squash-merged PR, commit 7219987d52 ('Addressing review', 2023-06-13) rewrote equals with the getClass() and null checks that uschindler had asked for and dropped the similarityFunction term that the previous PR commit 901febe400 still compared; hashCode kept the field (bc504291cc made it Objects.hash(similarityFunction, vector1, vector2)) [read: PR commits via the GitHub API]. The only later commit to the file, 05af6514226 (#15329, author date 2025-10-12, doubleVal), does not touch equals or hashCode. (2023-06-14)
- First affected version
- 9.7.0 [exec: the repro fails on the 9.7.0 release jars; 9.6.0 has none of the classes (compile error)]; there is no release in which this equals was correct
- Affected versions
- every release from 9.7.0 through 10.6.0 (released 2026-10-29, 4ae946e5a9b) and main e1faeba08bde (12.0.0-SNAPSHOT); never fixed (upstream main head was e1faeba08bd at the 2026-11-02 search, and the last commit to the file, 05af6514226 of 2025-10-12, does not touch equals or hashCode; re-checked 2026-11-02). [exec] the one-test repro fails 1 of 1 on main with -ea and -da (seeds DEADBEEF, 5EED0497, 5EED1101 and 5EED1102), on the 10.6.0 release jars with -ea and -da, and on 10.5.2, 10.0.0, 9.12.3 and 9.7.0; the independent reproduction's own Byte-subclass test fails on main (-ea and -da), 10.6.0, 10.5.2, 9.12.3 and 9.7.0; 9.6.0 does not compile it (classes absent). [read] the equals block is byte-identical, and hashCode is Objects.hash(similarityFunction, vector1, vector2), at every releases/lucene tag from 9.7.0 to 10.6.0. The Float16VectorSimilarityFunction subclass (#16383, CHANGES 11.0.0) inherits the same equals on main and branch_11x (both unreleased). Not new in 10.6.
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; a fix PR is open upstream.
- Found by
- semi-formal
- Upstream and prior reports
- none found (2026-11-02, re-checked the same day): four scripted prior-report search passes (the finder's two and two follow-up passes, each with about 16 GitHub issue/PR, 3 commit, 2 downstream, 20 mailing-list (dev@, java-user@, issues@, commits@) and 4 JIRA-archive queries, plus web queries); manual GitHub searches in apache/lucene (issues and PRs incl. the migrated JIRA archive), apache/solr, elastic/elasticsearch and opensearch-project/OpenSearch; the changed files of the open apache/lucene PRs matching VectorSimilarityFunction, valuesource or FunctionQuery (none touches queries/function/valuesource); every apache/lucene issue and PR updated since 2026-10-31 (none related); author dates of every commit to the four valuesource files on main, branch_10x and branch_10_6; the comments of #12253, #12252, #16383, #15328 and #15329; Solr JIRA searches. Related, not reports: #12253 (the introducing PR; its review asked for getClass() and null checks in equals and did not mention the function), #15328/#15329 (same class, doubleVal), #16383 (added the Float16 subclass, review silent on equals), and the same defect class fixed in other classes (LUCENE-2092, LUCENE-10026, LUCENE-10552, #8861 = LUCENE-7810).
- Lucene commit tested
- main
e1faeba08bde(2026-11-02) through gradle in a scratch worktree with assertions on and off; the 10.6.0 release jars (assertions on and off) and the 9.7.0 release jars through the standalone JUnit runner. The hunt and the follow-up also ran the same EUCLIDEAN/COSINE pair on 10.5.2, 10.0.0, 9.12.3 and 9.7.0 (all fail) and confirmed that 9.6.0 has no such classes. The fix was validated in the scratch worktree: the test,TestKnnVectorSimilarityFunctions(assertions on and off), the wholelucene/queriessuite (assertions on) and:lucene:queries:check -x test.
- Finding record
lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/VectorSimilarityFunction.java:29-31 (fields), 79-85 (equals compares vector1 and vector2 only), 87-90 (hashCode = Objects.hash(similarityFunction, vector1, vector2)), 92-100 (description() starts with similarityFunction.name()); FloatVectorSimilarityFunction.java:46 (func calls similarityFunction.compare)lucene/queries/src/java/org/apache/lucene/queries/function/ValueSource.java:51-55 (abstract equals and hashCode), 150 and 230 (isCacheable false); FunctionQuery.java:82 (isCacheable false), 180-187 (equals and hashCode delegate to the value source); FunctionRangeQuery.java:177 (isCacheable false)lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java:132-140 (clause sets are Multiset/HashSet); DisjunctionMaxQuery.java:47 (Multiset of disjuncts)
- The finding in 30 seconds
lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/VectorSimilarityFunction.java:29-31 (at e1faeba08bde) (fields), :33-41 (constructor), :79-85 (equals), :87-90 (hashCode), :92-100 (description)lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/FloatVectorSimilarityFunction.java:46, (at e1faeba08bde) ByteVectorSimilarityFunction.java:47, Float16VectorSimilarityFunction.java:46 (func returns similarityFunction.compare)lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/ConstKnnFloatValueSource.java:58-63, (at e1faeba08bde) FloatKnnVectorFieldSource.java:85-90 (operand equals)lucene/queries/src/java/org/apache/lucene/queries/function/FunctionQuery.java:180-187, (at e1faeba08bde) FunctionRangeQuery.java:115-125, FunctionScoreQuery.java:144-149, ValueSource.java:51-55, :249-253lucene/CHANGES.txt:1381-1382 (at e1faeba08bde) (GITHUB#15328 under 10.4.0: doubleVal, FunctionScoreQuery use)
- Who is affected
lucene/queries/src/java/org/apache/lucene/queries/function/FunctionQuery.java:82-83, (at e1faeba08bde) FunctionRangeQuery.java:177-178, ValueSource.java:150-151, :230-231 (isCacheable false)lucene/queries/src/java/org/apache/lucene/queries/function/FunctionScoreQuery.java:117-120 (at e1faeba08bde) (returns the inner weight when scores are not needed), :280-282 (FunctionScoreWeight.isCacheable = inner && valueSource)lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java:132-140, (at e1faeba08bde) :714-716 (deduplicateClauses: HashMap, HashSet), DisjunctionMaxQuery.java:47 (hash-based clause sets)apache/solr main f489a30a31b solr/core/src/java/org/apache/solr/search/VectorSimilaritySourceParser.java:84-88, :134-143 (read through the GitHub API, 2026-11-02)
- Why it looks unintended
lucene/queries/src/java/org/apache/lucene/queries/function/ValueSource.java:51-55; (at e1faeba08bde) lucene/core/src/java/org/apache/lucene/search/Query.java:95-107lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/LinearFloatFunction.java:85-91 (at e1faeba08bde)apache/lucene#8787 (LUCENE-7736) comments by jpountz (JIRA comment 16211070) and romseygeek (16211308), read with gh issue view, 2026-11-02apache/lucene#12253: PR commits 901febe400 and 7219987d52 (contents of VectorSimilarityFunction.java at each, GitHub API), review comment 1228085889 (uschindler, 2023-06-13T12:54:19Z)
- Suggested fix
apache/lucene#8787 (LUCENE-7736, jpountz's comment)
- History
a Lucene clone releases/lucene/9.7.0, 9.12.3, 10.6.0 and e1faeba08bde: md5 of the equals block (identical), Objects.hash(similarityFunction, vector1, vector2) presentlucene/CHANGES.txt:3555 (at e1faeba08bde) (GITHUB#12252 under 9.7.0), :210 (GITHUB#16383 under 11.0.0)a Lucene clone origin/branch_11x da3ea39fa59 (2026-11-01): git ls-tree lists Float16VectorSimilarityFunction.java; VectorSimilarityFunction.java blob 860f0c05617 (same as e1faeba08bde); git merge-base --is-ancestor bfe5c847d3c origin/branch_11x exits 0; build-options.properties version.base=11.0.0; origin/branch_10_6 has no Float16 subclass
- TestVectorSimilarityFunctionEquals.java (lucene/queries, public API):
repro/LFV-1079/TestVectorSimilarityFunctionEquals.java(35 lines)
A light summary card (the PNG), made from a probe program run on main e1faeba08bde with assertions on and off and with the fix as a control.
assets/LFV-1079/bug.png(490 KB)
![Summary card. Panel 1, what the user does: a = new FloatVectorSimilarityFunction(EUCLIDEAN, field, target), b = the same with COSINE, over the field v and the constant query vector [1, 2]; for one document with v = [2, 1], a scores 1 / (1 + 2) = 0.333 and b scores (1 + 0.8) / 2 = 0.9; expected a.equals(b) false, actual true on main with assertions on and off. Panel 2, why: the similarity function decides the score, the hash code and the description but is missing from equals, so a.equals(b) is true while the hash codes differ; ArrayList.contains(b) is true, HashSet.contains(b) is false, and FunctionQuery passes the equality on. Fix strip: one more condition in equals, similarityFunction == ((VectorSimilarityFunction) o).similarityFunction; the repro passes and a source with the same function and operands stays equal.](../assets/LFV-1079/bug.png)