Skip to content

Commit 7ceee5d

Browse files
authored
Put sensible bounds on quantized scores (#15411)
Loss from quantization can yield some unexpected values from the corrected dot product, sometimes producing values that are out-of-bounds. This is more likely when the inputs are "extreme" in the sense that they are very far from the segment-level centroid. Bound euclidean distance to a non-negative value -- negative values do not make any sense. Clamp dot product/cosine score to [-1,1] as the normalized dot product should always return values in this range. This works well enough for 4+ bit quantization but may not work as well for 1-bit quantization since the loss is so great. Fix the testSingleVectorCase to l2 normalize all vectors for DOT_PRODUCT similarity. This is a partial fix for #15408
1 parent efa5204 commit 7ceee5d

File tree

2 files changed

+21
-6
lines changed

2 files changed

+21
-6
lines changed

lucene/core/src/java/org/apache/lucene/codecs/lucene104/Lucene104ScalarQuantizedVectorScorer.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,9 @@ private static float quantizedScore(
263263
queryCorrections.additionalCorrection()
264264
+ indexCorrections.additionalCorrection()
265265
- 2 * score;
266-
return Math.max(1 / (1f + score), 0);
266+
// Ensure that 'score' (the squared euclidean distance) is non-negative. The computed value
267+
// may be negative as a result of quantization loss.
268+
return 1 / (1f + Math.max(score, 0f));
267269
} else {
268270
// For cosine and max inner product, we need to apply the additional correction, which is
269271
// assumed to be the non-centered dot-product between the vector and the centroid
@@ -274,7 +276,10 @@ private static float quantizedScore(
274276
if (similarityFunction == MAXIMUM_INNER_PRODUCT) {
275277
return VectorUtil.scaleMaxInnerProductScore(score);
276278
}
277-
return Math.max((1f + score) / 2f, 0);
279+
// Ensure that 'score' (a normalized dot product) is in [-1,1]. The computed value may be out
280+
// of bounds as a result of quantization loss.
281+
score = Math.clamp(score, -1, 1);
282+
return (1f + score) / 2f;
278283
}
279284
}
280285
}

lucene/core/src/test/org/apache/lucene/codecs/lucene104/TestLucene104HnswScalarQuantizedVectorsFormat.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@
4747
import org.apache.lucene.store.Directory;
4848
import org.apache.lucene.tests.index.BaseKnnVectorsFormatTestCase;
4949
import org.apache.lucene.tests.util.TestUtil;
50+
import org.apache.lucene.util.ArrayUtil;
5051
import org.apache.lucene.util.SameThreadExecutorService;
52+
import org.apache.lucene.util.VectorUtil;
5153
import org.junit.Before;
5254

5355
public class TestLucene104HnswScalarQuantizedVectorsFormat extends BaseKnnVectorsFormatTestCase {
@@ -109,7 +111,11 @@ public void testSingleVectorCase() throws Exception {
109111
try (Directory dir = newDirectory();
110112
IndexWriter w = new IndexWriter(dir, newIndexWriterConfig())) {
111113
Document doc = new Document();
112-
doc.add(new KnnFloatVectorField("f", vector, similarityFunction));
114+
float[] docVector =
115+
similarityFunction == VectorSimilarityFunction.DOT_PRODUCT
116+
? VectorUtil.l2normalize(ArrayUtil.copyArray(vector))
117+
: vector;
118+
doc.add(new KnnFloatVectorField("f", docVector, similarityFunction));
113119
w.addDocument(doc);
114120
w.commit();
115121
try (IndexReader reader = DirectoryReader.open(w)) {
@@ -118,10 +124,14 @@ public void testSingleVectorCase() throws Exception {
118124
KnnVectorValues.DocIndexIterator docIndexIterator = vectorValues.iterator();
119125
assert (vectorValues.size() == 1);
120126
while (docIndexIterator.nextDoc() != NO_MORE_DOCS) {
121-
assertArrayEquals(vector, vectorValues.vectorValue(docIndexIterator.index()), 0.00001f);
127+
assertArrayEquals(
128+
docVector, vectorValues.vectorValue(docIndexIterator.index()), 0.00001f);
122129
}
123-
float[] randomVector = randomVector(vector.length);
124-
float trueScore = similarityFunction.compare(vector, randomVector);
130+
float[] randomVector =
131+
similarityFunction == VectorSimilarityFunction.DOT_PRODUCT
132+
? randomNormalizedVector(vector.length)
133+
: randomVector(vector.length);
134+
float trueScore = similarityFunction.compare(docVector, randomVector);
125135
TopDocs td =
126136
r.searchNearestVectors(
127137
"f",

0 commit comments

Comments
 (0)