Skip to content

Commit 688ea6d

Browse files
committed
First version
1 parent 1ec4e0b commit 688ea6d

File tree

10 files changed

+2530
-1
lines changed

10 files changed

+2530
-1
lines changed

server/src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,7 @@
457457
org.elasticsearch.index.codec.vectors.es816.ES816HnswBinaryQuantizedVectorsFormat,
458458
org.elasticsearch.index.codec.vectors.es818.ES818BinaryQuantizedVectorsFormat,
459459
org.elasticsearch.index.codec.vectors.es818.ES818HnswBinaryQuantizedVectorsFormat,
460+
org.elasticsearch.index.codec.vectors.es910.ES910BinaryQuantizedVectorsFormat,
460461
org.elasticsearch.index.codec.vectors.IVFVectorsFormat;
461462

462463
provides org.apache.lucene.codecs.Codec
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* @notice
3+
* Licensed to the Apache Software Foundation (ASF) under one or more
4+
* contributor license agreements. See the NOTICE file distributed with
5+
* this work for additional information regarding copyright ownership.
6+
* The ASF licenses this file to You under the Apache License, Version 2.0
7+
* (the "License"); you may not use this file except in compliance with
8+
* the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*
18+
* Modifications copyright (C) 2024 Elasticsearch B.V.
19+
*/
20+
package org.elasticsearch.index.codec.vectors.es910;
21+
22+
import org.apache.lucene.index.ByteVectorValues;
23+
import org.apache.lucene.search.VectorScorer;
24+
import org.apache.lucene.util.VectorUtil;
25+
import org.elasticsearch.index.codec.vectors.OptimizedScalarQuantizer;
26+
27+
import java.io.IOException;
28+
29+
/**
30+
* Copied from Lucene, replace with Lucene's implementation sometime after Lucene 10
31+
*/
32+
abstract class BinarizedByteVectorValues extends ByteVectorValues {
33+
34+
/**
35+
* Retrieve the corrective terms for the given vector ordinal. For the dot-product family of
36+
* distances, the corrective terms are, in order
37+
*
38+
* <ul>
39+
* <li>the lower optimized interval
40+
* <li>the upper optimized interval
41+
* <li>the dot-product of the non-centered vector with the centroid
42+
* <li>the sum of quantized components
43+
* </ul>
44+
*
45+
* For euclidean:
46+
*
47+
* <ul>
48+
* <li>the lower optimized interval
49+
* <li>the upper optimized interval
50+
* <li>the l2norm of the centered vector
51+
* <li>the sum of quantized components
52+
* </ul>
53+
*
54+
* @param vectorOrd the vector ordinal
55+
* @return the corrective terms
56+
* @throws IOException if an I/O error occurs
57+
*/
58+
public abstract OptimizedScalarQuantizer.QuantizationResult getCorrectiveTerms(int vectorOrd) throws IOException;
59+
60+
/**
61+
* @return the quantizer used to quantize the vectors
62+
*/
63+
public abstract OptimizedScalarQuantizer getQuantizer();
64+
65+
public abstract float[] getCentroid() throws IOException;
66+
67+
/**
68+
* Return a {@link VectorScorer} for the given query vector.
69+
*
70+
* @param query the query vector
71+
* @return a {@link VectorScorer} instance or null
72+
*/
73+
public abstract VectorScorer scorer(float[] query) throws IOException;
74+
75+
@Override
76+
public abstract BinarizedByteVectorValues copy() throws IOException;
77+
78+
float getCentroidDP() throws IOException {
79+
// this only gets executed on-merge
80+
float[] centroid = getCentroid();
81+
return VectorUtil.dotProduct(centroid, centroid);
82+
}
83+
}
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
/*
2+
* @notice
3+
* Licensed to the Apache Software Foundation (ASF) under one or more
4+
* contributor license agreements. See the NOTICE file distributed with
5+
* this work for additional information regarding copyright ownership.
6+
* The ASF licenses this file to You under the Apache License, Version 2.0
7+
* (the "License"); you may not use this file except in compliance with
8+
* the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*
18+
* Modifications copyright (C) 2024 Elasticsearch B.V.
19+
*/
20+
package org.elasticsearch.index.codec.vectors.es910;
21+
22+
import org.apache.lucene.codecs.hnsw.FlatVectorsScorer;
23+
import org.apache.lucene.index.KnnVectorValues;
24+
import org.apache.lucene.index.VectorSimilarityFunction;
25+
import org.apache.lucene.util.ArrayUtil;
26+
import org.apache.lucene.util.VectorUtil;
27+
import org.apache.lucene.util.hnsw.RandomVectorScorer;
28+
import org.apache.lucene.util.hnsw.RandomVectorScorerSupplier;
29+
import org.apache.lucene.util.hnsw.UpdateableRandomVectorScorer;
30+
import org.elasticsearch.index.codec.vectors.OptimizedScalarQuantizer;
31+
32+
import java.io.IOException;
33+
34+
import static org.apache.lucene.index.VectorSimilarityFunction.COSINE;
35+
import static org.apache.lucene.index.VectorSimilarityFunction.EUCLIDEAN;
36+
import static org.apache.lucene.index.VectorSimilarityFunction.MAXIMUM_INNER_PRODUCT;
37+
38+
/** Vector scorer over binarized vector values */
39+
public class ES910BinaryFlatVectorsScorer implements FlatVectorsScorer {
40+
private final FlatVectorsScorer nonQuantizedDelegate;
41+
private final byte queryBits;
42+
43+
public ES910BinaryFlatVectorsScorer(FlatVectorsScorer nonQuantizedDelegate, byte queryBits) {
44+
this.nonQuantizedDelegate = nonQuantizedDelegate;
45+
this.queryBits = queryBits;
46+
}
47+
48+
@Override
49+
public RandomVectorScorerSupplier getRandomVectorScorerSupplier(
50+
VectorSimilarityFunction similarityFunction,
51+
KnnVectorValues vectorValues
52+
) throws IOException {
53+
if (vectorValues instanceof BinarizedByteVectorValues) {
54+
throw new UnsupportedOperationException(
55+
"getRandomVectorScorerSupplier(VectorSimilarityFunction,RandomAccessVectorValues) not implemented for binarized format"
56+
);
57+
}
58+
return nonQuantizedDelegate.getRandomVectorScorerSupplier(similarityFunction, vectorValues);
59+
}
60+
61+
@Override
62+
public RandomVectorScorer getRandomVectorScorer(
63+
VectorSimilarityFunction similarityFunction,
64+
KnnVectorValues vectorValues,
65+
float[] target
66+
) throws IOException {
67+
if (vectorValues instanceof BinarizedByteVectorValues binarizedVectors) {
68+
OptimizedScalarQuantizer quantizer = binarizedVectors.getQuantizer();
69+
float[] centroid = binarizedVectors.getCentroid();
70+
// We make a copy as the quantization process mutates the input
71+
float[] copy = ArrayUtil.copyOfSubArray(target, 0, target.length);
72+
if (similarityFunction == COSINE) {
73+
VectorUtil.l2normalize(copy);
74+
}
75+
byte[] quantized = new byte[copy.length];
76+
OptimizedScalarQuantizer.QuantizationResult queryCorrections = quantizer.scalarQuantize(copy, quantized, queryBits, centroid);
77+
return new RandomVectorScorer.AbstractRandomVectorScorer(vectorValues) {
78+
@Override
79+
public float score(int i) throws IOException {
80+
return quantizedScore(
81+
binarizedVectors.dimension(),
82+
similarityFunction,
83+
binarizedVectors.getCentroidDP(),
84+
quantized,
85+
queryCorrections,
86+
binarizedVectors.vectorValue(i),
87+
binarizedVectors.getCorrectiveTerms(i),
88+
getBitsScale()
89+
);
90+
}
91+
};
92+
}
93+
return nonQuantizedDelegate.getRandomVectorScorer(similarityFunction, vectorValues, target);
94+
}
95+
96+
private float getBitsScale() {
97+
return 1f / ((1 << queryBits) - 1);
98+
}
99+
100+
@Override
101+
public RandomVectorScorer getRandomVectorScorer(
102+
VectorSimilarityFunction similarityFunction,
103+
KnnVectorValues vectorValues,
104+
byte[] target
105+
) throws IOException {
106+
return nonQuantizedDelegate.getRandomVectorScorer(similarityFunction, vectorValues, target);
107+
}
108+
109+
RandomVectorScorerSupplier getRandomVectorScorerSupplier(
110+
VectorSimilarityFunction similarityFunction,
111+
ES910BinaryQuantizedVectorsWriter.OffHeapBinarizedQueryVectorValues scoringVectors,
112+
BinarizedByteVectorValues targetVectors
113+
) {
114+
return new BinarizedRandomVectorScorerSupplier(scoringVectors, targetVectors, similarityFunction, queryBits);
115+
}
116+
117+
@Override
118+
public String toString() {
119+
return "ES910BinaryFlatVectorsScorer(nonQuantizedDelegate=" + nonQuantizedDelegate + ")";
120+
}
121+
122+
/** Vector scorer supplier over binarized vector values */
123+
static class BinarizedRandomVectorScorerSupplier implements RandomVectorScorerSupplier {
124+
private final ES910BinaryQuantizedVectorsWriter.OffHeapBinarizedQueryVectorValues queryVectors;
125+
private final BinarizedByteVectorValues targetVectors;
126+
private final VectorSimilarityFunction similarityFunction;
127+
private final byte queryBits;
128+
129+
BinarizedRandomVectorScorerSupplier(
130+
ES910BinaryQuantizedVectorsWriter.OffHeapBinarizedQueryVectorValues queryVectors,
131+
BinarizedByteVectorValues targetVectors,
132+
VectorSimilarityFunction similarityFunction,
133+
byte queryBits
134+
) {
135+
this.queryVectors = queryVectors;
136+
this.targetVectors = targetVectors;
137+
this.similarityFunction = similarityFunction;
138+
this.queryBits = queryBits;
139+
}
140+
141+
@Override
142+
public BinarizedRandomVectorScorer scorer() throws IOException {
143+
return new BinarizedRandomVectorScorer(queryVectors.copy(), targetVectors.copy(), similarityFunction, queryBits);
144+
}
145+
146+
@Override
147+
public RandomVectorScorerSupplier copy() throws IOException {
148+
return new BinarizedRandomVectorScorerSupplier(queryVectors, targetVectors, similarityFunction, queryBits);
149+
}
150+
}
151+
152+
/** Vector scorer over binarized vector values */
153+
public static class BinarizedRandomVectorScorer extends UpdateableRandomVectorScorer.AbstractUpdateableRandomVectorScorer {
154+
private final ES910BinaryQuantizedVectorsWriter.OffHeapBinarizedQueryVectorValues queryVectors;
155+
private final BinarizedByteVectorValues targetVectors;
156+
private final VectorSimilarityFunction similarityFunction;
157+
private final byte[] quantizedQuery;
158+
private OptimizedScalarQuantizer.QuantizationResult queryCorrections = null;
159+
private int currentOrdinal = -1;
160+
private final float bitScale;
161+
162+
BinarizedRandomVectorScorer(
163+
ES910BinaryQuantizedVectorsWriter.OffHeapBinarizedQueryVectorValues queryVectors,
164+
BinarizedByteVectorValues targetVectors,
165+
VectorSimilarityFunction similarityFunction,
166+
byte queryBits
167+
) {
168+
super(targetVectors);
169+
this.queryVectors = queryVectors;
170+
this.quantizedQuery = new byte[queryVectors.quantizedDimension()];
171+
this.targetVectors = targetVectors;
172+
this.similarityFunction = similarityFunction;
173+
bitScale = 1.0F / (float) ((1 << queryBits) - 1);
174+
175+
}
176+
177+
@Override
178+
public float score(int targetOrd) throws IOException {
179+
if (queryCorrections == null) {
180+
throw new IllegalStateException("score() called before setScoringOrdinal()");
181+
}
182+
return quantizedScore(
183+
targetVectors.dimension(),
184+
similarityFunction,
185+
targetVectors.getCentroidDP(),
186+
quantizedQuery,
187+
queryCorrections,
188+
targetVectors.vectorValue(targetOrd),
189+
targetVectors.getCorrectiveTerms(targetOrd),
190+
bitScale
191+
);
192+
}
193+
194+
@Override
195+
public void setScoringOrdinal(int i) throws IOException {
196+
if (i == currentOrdinal) {
197+
return;
198+
}
199+
System.arraycopy(queryVectors.vectorValue(i), 0, quantizedQuery, 0, quantizedQuery.length);
200+
queryCorrections = queryVectors.getCorrectiveTerms(i);
201+
currentOrdinal = i;
202+
}
203+
}
204+
205+
private static float quantizedScore(
206+
int dims,
207+
VectorSimilarityFunction similarityFunction,
208+
float centroidDp,
209+
byte[] q,
210+
OptimizedScalarQuantizer.QuantizationResult queryCorrections,
211+
byte[] d,
212+
OptimizedScalarQuantizer.QuantizationResult indexCorrections,
213+
float bitsScale
214+
) {
215+
float qcDist = VectorUtil.dotProduct(q, d);
216+
float x1 = indexCorrections.quantizedComponentSum();
217+
float ax = indexCorrections.lowerInterval();
218+
// Here we assume `lx` is simply bit vectors, so the scaling isn't necessary
219+
float lx = indexCorrections.upperInterval() - ax;
220+
float ay = queryCorrections.lowerInterval();
221+
float ly = (queryCorrections.upperInterval() - ay) * bitsScale;
222+
float y1 = queryCorrections.quantizedComponentSum();
223+
float score = ax * ay * dims + ay * lx * x1 + ax * ly * y1 + lx * ly * qcDist;
224+
// For euclidean, we need to invert the score and apply the additional correction, which is
225+
// assumed to be the squared l2norm of the centroid centered vectors.
226+
if (similarityFunction == EUCLIDEAN) {
227+
score = queryCorrections.additionalCorrection() + indexCorrections.additionalCorrection() - 2 * score;
228+
return Math.max(1 / (1f + score), 0);
229+
} else {
230+
// For cosine and max inner product, we need to apply the additional correction, which is
231+
// assumed to be the non-centered dot-product between the vector and the centroid
232+
score += queryCorrections.additionalCorrection() + indexCorrections.additionalCorrection() - centroidDp;
233+
if (similarityFunction == MAXIMUM_INNER_PRODUCT) {
234+
return VectorUtil.scaleMaxInnerProductScore(score);
235+
}
236+
return Math.max((1f + score) / 2f, 0);
237+
}
238+
}
239+
}

0 commit comments

Comments
 (0)