@@ -64,10 +64,15 @@ public RandomVectorScorer getRandomVectorScorer(
6464 if (vectorValues instanceof QuantizedByteVectorValues qv ) {
6565 checkDimensions (target .length , qv .dimension ());
6666 OptimizedScalarQuantizer quantizer = qv .getQuantizer ();
67- byte [] targetQuantized =
68- new byte
69- [OptimizedScalarQuantizer .discretize (
70- target .length , qv .getScalarEncoding ().getDimensionsPerByte ())];
67+ Lucene104ScalarQuantizedVectorsFormat .ScalarEncoding scalarEncoding = qv .getScalarEncoding ();
68+ byte [] scratch = new byte [scalarEncoding .getDiscreteDimensions (qv .dimension ())];
69+ final byte [] targetQuantized ;
70+ if (scalarEncoding .isAsymmetric () == false ) {
71+ targetQuantized = scratch ;
72+ } else {
73+ // This is asymmetric quantization, we will pack the vector
74+ targetQuantized = new byte [scalarEncoding .getQueryPackedLength (scratch .length )];
75+ }
7176 // We make a copy as the quantization process mutates the input
7277 float [] copy = ArrayUtil .copyOfSubArray (target , 0 , target .length );
7378 if (similarityFunction == COSINE ) {
@@ -76,7 +81,12 @@ public RandomVectorScorer getRandomVectorScorer(
7681 target = copy ;
7782 var targetCorrectiveTerms =
7883 quantizer .scalarQuantize (
79- target , targetQuantized , qv .getScalarEncoding ().getBits (), qv .getCentroid ());
84+ target , scratch , scalarEncoding .getQueryBits (), qv .getCentroid ());
85+ // for single bit query nibble, we need to transpose the nibbles for fast scoring comparisons
86+ if (scalarEncoding
87+ == Lucene104ScalarQuantizedVectorsFormat .ScalarEncoding .SINGLE_BIT_QUERY_NIBBLE ) {
88+ OptimizedScalarQuantizer .transposeHalfByte (scratch , targetQuantized );
89+ }
8090 return new RandomVectorScorer .AbstractRandomVectorScorer (qv ) {
8191 @ Override
8292 public float score (int node ) throws IOException {
@@ -96,13 +106,68 @@ public RandomVectorScorer getRandomVectorScorer(
96106 return nonQuantizedDelegate .getRandomVectorScorer (similarityFunction , vectorValues , target );
97107 }
98108
109+ RandomVectorScorerSupplier getRandomVectorScorerSupplier (
110+ VectorSimilarityFunction similarityFunction ,
111+ QuantizedByteVectorValues scoringVectors ,
112+ QuantizedByteVectorValues targetVectors ) {
113+ return new AsymmetricQuantizedRandomVectorScorerSupplier (
114+ scoringVectors , targetVectors , similarityFunction );
115+ }
116+
99117 @ Override
100118 public String toString () {
101119 return "Lucene104ScalarQuantizedVectorScorer(nonQuantizedDelegate="
102120 + nonQuantizedDelegate
103121 + ")" ;
104122 }
105123
124+ static class AsymmetricQuantizedRandomVectorScorerSupplier implements RandomVectorScorerSupplier {
125+ private final QuantizedByteVectorValues queryVectors ;
126+ private final QuantizedByteVectorValues targetVectors ;
127+ private final VectorSimilarityFunction similarityFunction ;
128+
129+ AsymmetricQuantizedRandomVectorScorerSupplier (
130+ QuantizedByteVectorValues queryVectors ,
131+ QuantizedByteVectorValues targetVectors ,
132+ VectorSimilarityFunction similarityFunction ) {
133+ assert targetVectors .getScalarEncoding ().isAsymmetric ();
134+ this .queryVectors = queryVectors ;
135+ this .targetVectors = targetVectors ;
136+ this .similarityFunction = similarityFunction ;
137+ }
138+
139+ @ Override
140+ public UpdateableRandomVectorScorer scorer () throws IOException {
141+ final QuantizedByteVectorValues targetVectors = this .targetVectors .copy ();
142+ final QuantizedByteVectorValues queryVectors = this .queryVectors .copy ();
143+ return new UpdateableRandomVectorScorer .AbstractUpdateableRandomVectorScorer (targetVectors ) {
144+ private OptimizedScalarQuantizer .QuantizationResult queryCorrections = null ;
145+ private byte [] vector = null ;
146+
147+ @ Override
148+ public void setScoringOrdinal (int node ) throws IOException {
149+ vector = queryVectors .vectorValue (node );
150+ queryCorrections = queryVectors .getCorrectiveTerms (node );
151+ }
152+
153+ @ Override
154+ public float score (int node ) throws IOException {
155+ if (vector == null || queryCorrections == null ) {
156+ throw new IllegalStateException ("setScoringOrdinal was not called" );
157+ }
158+
159+ return quantizedScore (vector , queryCorrections , targetVectors , node , similarityFunction );
160+ }
161+ };
162+ }
163+
164+ @ Override
165+ public RandomVectorScorerSupplier copy () throws IOException {
166+ return new AsymmetricQuantizedRandomVectorScorerSupplier (
167+ queryVectors .copy (), targetVectors .copy (), similarityFunction );
168+ }
169+ }
170+
106171 private static final class ScalarQuantizedVectorScorerSupplier
107172 implements RandomVectorScorerSupplier {
108173 private final QuantizedByteVectorValues targetValues ;
@@ -111,6 +176,7 @@ private static final class ScalarQuantizedVectorScorerSupplier
111176
112177 public ScalarQuantizedVectorScorerSupplier (
113178 QuantizedByteVectorValues values , VectorSimilarityFunction similarity ) throws IOException {
179+ assert values .getScalarEncoding ().isAsymmetric () == false ;
114180 this .targetValues = values .copy ();
115181 this .values = values ;
116182 this .similarity = similarity ;
@@ -131,14 +197,17 @@ public float score(int node) throws IOException {
131197 public void setScoringOrdinal (int node ) throws IOException {
132198 var rawTargetVector = targetValues .vectorValue (node );
133199 switch (values .getScalarEncoding ()) {
134- case UNSIGNED_BYTE -> targetVector = rawTargetVector ;
135- case SEVEN_BIT -> targetVector = rawTargetVector ;
200+ case UNSIGNED_BYTE , SEVEN_BIT -> targetVector = rawTargetVector ;
136201 case PACKED_NIBBLE -> {
137202 if (targetVector == null ) {
138203 targetVector = new byte [OptimizedScalarQuantizer .discretize (values .dimension (), 2 )];
139204 }
140205 OffHeapScalarQuantizedVectorValues .unpackNibbles (rawTargetVector , targetVector );
141206 }
207+ case SINGLE_BIT_QUERY_NIBBLE -> {
208+ throw new IllegalStateException (
209+ "SINGLE_BIT_QUERY_NIBBLE encoding is not supported for symmetric quantization" );
210+ }
142211 }
143212 targetCorrectiveTerms = targetValues .getCorrectiveTerms (node );
144213 }
@@ -177,16 +246,19 @@ private static float quantizedScore(
177246 case UNSIGNED_BYTE -> VectorUtil .uint8DotProduct (quantizedQuery , quantizedDoc );
178247 case SEVEN_BIT -> VectorUtil .dotProduct (quantizedQuery , quantizedDoc );
179248 case PACKED_NIBBLE -> VectorUtil .int4DotProductSinglePacked (quantizedQuery , quantizedDoc );
249+ case SINGLE_BIT_QUERY_NIBBLE ->
250+ VectorUtil .int4BitDotProduct (quantizedQuery , quantizedDoc );
180251 };
181252 OptimizedScalarQuantizer .QuantizationResult indexCorrections =
182253 targetVectors .getCorrectiveTerms (targetOrd );
254+ float queryScale = SCALE_LUT [scalarEncoding .getQueryBits () - 1 ];
183255 float scale = SCALE_LUT [scalarEncoding .getBits () - 1 ];
184256 float x1 = indexCorrections .quantizedComponentSum ();
185257 float ax = indexCorrections .lowerInterval ();
186258 // Here we must scale according to the bits
187259 float lx = (indexCorrections .upperInterval () - ax ) * scale ;
188260 float ay = queryCorrections .lowerInterval ();
189- float ly = (queryCorrections .upperInterval () - ay ) * scale ;
261+ float ly = (queryCorrections .upperInterval () - ay ) * queryScale ;
190262 float y1 = queryCorrections .quantizedComponentSum ();
191263 float score =
192264 ax * ay * targetVectors .dimension () + ay * lx * x1 + ax * ly * y1 + lx * ly * qcDist ;
0 commit comments