kaivalnp commented on code in PR #16710:
URL: https://github.com/apache/lucene/pull/16710#discussion_r4166943124


##########
lucene/sandbox/src/java/org/apache/lucene/sandbox/codecs/dedup/DedupScalarQuantizedVectorsReader.java:
##########
@@ -462,9 +487,19 @@ public CloseableRandomVectorScorerSupplier 
getRandomVectorScorerSupplierForMerge
 
     // Asymmetric encodings compare query-encoded vectors against the stored 
doc-encoded vectors.
     // Write a query-encoded record per distinct vector of the group into a 
temporary file; both
-    // sides then resolve through the shared fieldOrdToGroupOrd translation.
-    DedupVectorValues.FloatImpl rawValues = getRawFloatVectorValues(entry);
-    FloatVectorValues groupView = rawValues.getGroupView();
+    // sides then resolve through the shared fieldOrdToGroupOrd translation. 
The group's raw vectors
+    // are read directly from the group view (indexed by group ordinal, not 
field ordinal) as
+    // float[] (FLOAT16 groups are inflated from short[]).
+    FloatVectorValues floatVectorValues;
+    if (fieldInfo.getVectorEncoding() == VectorEncoding.FLOAT16) {
+      DedupVectorValues dedupValues = (DedupVectorValues) 
getFloat16VectorValues(fieldInfo.name);
+      floatVectorValues =
+          new Float16AsFloatVectorValues((Float16VectorValues) 
dedupValues.getGroupView());

Review Comment:
   This path reads `FieldEntry` again, also building unused quantized views. I 
wonder if we could use `getRaw*VectorValues().getGroupView()` like before, 
avoiding runtime casts too?



##########
lucene/sandbox/src/java/org/apache/lucene/sandbox/codecs/dedup/DedupScalarQuantizedVectorsReader.java:
##########
@@ -447,11 +472,11 @@ public ScalarQuantizer getQuantizationState(String 
fieldName) {
   public CloseableRandomVectorScorerSupplier 
getRandomVectorScorerSupplierForMerge(
       FieldInfo fieldInfo, SegmentWriteState segmentWriteState) throws 
IOException {
     FieldEntry entry = fields.get(fieldInfo.name);
-    if (entry == null || entry.fieldInfo().encoding() != FLOAT32) {
-      // BYTE and FLOAT16 fields are stored raw only
+    if (entry == null || entry.fieldInfo().encoding().isFloatingPoint() == 
false) {
+      // Not a quantized field (missing, or e.g. BYTE stored raw only)
       return null;
     }
-    FieldValues quantizedValues = getQuantizedVectorValues(entry);
+    FieldValues quantizedValues = (FieldValues) 
getQuantizedVectorValues(fieldInfo.name);

Review Comment:
   We can change the function signature to return `FieldValues` and avoid 
casting.



##########
lucene/sandbox/src/java/org/apache/lucene/sandbox/codecs/dedup/DedupScalarQuantizedVectorsReader.java:
##########
@@ -427,14 +428,38 @@ private Float16VectorValues 
getFloat16VectorValues(FieldEntry entry) throws IOEx
         entry.fieldInfo().fieldOrdToGroupOrdBitsPerValue());
   }
 
+  private FieldValues getFloat16QuantizedVectorValues(FieldEntry entry) throws 
IOException {

Review Comment:
   I realized that 
[`QuantizedByteVectorValues`](https://github.com/apache/lucene/blob/3970208e0d4d5e997a10baac816bd7d1d7693350/lucene/core/src/java/org/apache/lucene/util/quantization/QuantizedByteVectorValues.java#L27)
 makes no assumptions about the vector encoding used to create the quantized 
bytes, and allows scoring against either `float[]` or `short[]`.
   
   I wonder if we can remove this function and 
`DedupScalarQuantizedVectorValues.loadQuantizedFloat16` entirely? (replacing 
with `loadQuantized` everywhere)
   
   `loadQuantized` uses a float32 specific initializer to build the `fieldView` 
[here](https://github.com/Pranshu-S/lucene/blob/8e198015173a8178813929db1701d43706f66ce2/lucene/sandbox/src/java/org/apache/lucene/sandbox/codecs/dedup/DedupScalarQuantizedVectorValues.java#L103)
 -- which could be replaced with 
[`OffHeapScalarQuantizedVectorValues.load`](https://github.com/Pranshu-S/lucene/blob/8e198015173a8178813929db1701d43706f66ce2/lucene/core/src/java/org/apache/lucene/codecs/lucene104/OffHeapScalarQuantizedVectorValues.java#L195)
 along the same lines (need to make it `public`).
   
   We could also replace the type of `FieldValues.fieldView` from 
`KnnVectorValues` -> a more concrete `OffHeapScalarQuantizedVectorValues` after 
this?



##########
lucene/sandbox/src/java/org/apache/lucene/sandbox/codecs/dedup/DedupUtil.java:
##########
@@ -212,6 +212,17 @@ static long hashBytes(byte[] bytes) {
     return murmurhash3_x64_128(bytes, 0, bytes.length, GOOD_FAST_HASH_SEED)[0];
   }
 
+  /**
+   * Inflates a float16 vector ({@code short[]}) into the reused {@code 
float[]} buffer for the
+   * fp32-based scalar quantizer.
+   */
+  static float[] inflateFloat16(short[] float16Vector, float[] dest) {

Review Comment:
   Can we add an assert for `float16Vector.length == dest.length`?



##########
lucene/sandbox/src/test/org/apache/lucene/sandbox/codecs/dedup/TestDedupScalarQuantizedVectorsFormat.java:
##########
@@ -399,6 +525,148 @@ public void testAllEncodingsMergeAndSearch() throws 
Exception {
     }
   }
 
+  /**
+   * Every encoding survives a merge of FLOAT16 fields with graph building 
forced (exercising the
+   * merge-time scorer suppliers, including the temporary query-vectors file 
of asymmetric encodings
+   * and the FLOAT16 {@code short[]} -> {@code float[]} inflation on the raw 
side) and searches
+   * sanely afterwards.
+   */
+  public void testFloat16AllEncodingsMergeAndSearch() throws Exception {
+    int dimension = 32;
+    int numDistinct = 20;
+    float[][] distinct = new float[numDistinct][];
+    for (int i = 0; i < numDistinct; i++) {
+      distinct[i] = randomVector(dimension);
+    }
+    for (ScalarEncoding encoding : ScalarEncoding.values()) {
+      for (VectorSimilarityFunction function : 
VectorSimilarityFunction.values()) {
+        IndexWriterConfig config =
+            newIndexWriterConfig()
+                .setCodec(
+                    TestUtil.alwaysKnnVectorsFormat(
+                        new DedupHnswScalarQuantizedVectorsFormat(
+                            encoding,
+                            DEFAULT_MAX_CONN,
+                            DEFAULT_BEAM_WIDTH,
+                            1,
+                            null,
+                            0))); // always build graphs
+        try (Directory dir = newDirectory();
+            IndexWriter w = new IndexWriter(dir, config)) {
+          for (int segment = 0; segment < 2; segment++) { // duplicates across 
segments
+            for (int i = 0; i < numDistinct; i++) {
+              Document doc = new Document();
+              float[] vector =
+                  function == DOT_PRODUCT
+                      ? VectorUtil.l2normalize(distinct[i].clone())
+                      : distinct[i];
+              doc.add(new KnnFloat16VectorField("f", toFloat16(vector), 
function));
+              w.addDocument(doc);
+            }
+            w.commit();
+          }
+          w.forceMerge(1); // exercises the merge-time graph construction 
scorer
+
+          try (DirectoryReader reader = DirectoryReader.open(w)) {
+            LeafReader leafReader = getOnlyLeafReader(reader);
+            Float16VectorValues values = 
leafReader.getFloat16VectorValues("f");
+            assertEquals(2 * numDistinct, values.size());
+            assertEquals(numDistinct, groupNumVectors(values)); // 
de-duplicated across segments
+            assertGraphBuilt(leafReader, "f"); // threshold=0 forced the 
merge-time graph build
+
+            float[] queryFloat =
+                function == DOT_PRODUCT ? 
VectorUtil.l2normalize(distinct[0].clone()) : distinct[0];
+            short[] query = toFloat16(queryFloat);
+            TopDocs topDocs =
+                leafReader.searchNearestVectors(
+                    "f",
+                    query,
+                    2,
+                    AcceptDocs.fromLiveDocs(null, leafReader.maxDoc()),
+                    Integer.MAX_VALUE);
+            String context = "encoding=" + encoding + ", function=" + function;
+            assertEquals(context, 2, topDocs.scoreDocs.length);
+            // the two duplicates of the query vector share the (top) score
+            assertEquals(context, topDocs.scoreDocs[0].score, 
topDocs.scoreDocs[1].score, 0f);
+          }
+        }
+      }
+    }
+  }
+
+  public void testFloat16SharedGroupAsymmetricMerge() throws Exception {
+    int dimension = 32;
+    int smallCount = 2;
+    int bigCount = 20;
+    float[][] small = new float[smallCount][];
+    float[][] big = new float[bigCount][];
+    for (int i = 0; i < smallCount; i++) {
+      small[i] = randomVector(dimension);
+    }
+    for (int i = 0; i < bigCount; i++) {
+      big[i] = randomVector(dimension);
+    }
+    for (ScalarEncoding encoding : ScalarEncoding.values()) {
+      if (encoding.isAsymmetric() == false) {
+        continue; // the EOF only reproduces on the asymmetric merge path
+      }
+      IndexWriterConfig config =
+          newIndexWriterConfig()
+              .setCodec(
+                  TestUtil.alwaysKnnVectorsFormat(
+                      new DedupHnswScalarQuantizedVectorsFormat(
+                          encoding,
+                          DEFAULT_MAX_CONN,
+                          DEFAULT_BEAM_WIDTH,
+                          1,
+                          null,
+                          0))); // always build graphs
+      try (Directory dir = newDirectory();
+          IndexWriter w = new IndexWriter(dir, config)) {
+        for (int segment = 0; segment < 2; segment++) { // duplicates across 
segments
+          for (int i = 0; i < smallCount; i++) {
+            Document d = new Document();
+            d.add(new KnnFloat16VectorField("small", toFloat16(small[i]), 
EUCLIDEAN));
+            w.addDocument(d);
+          }
+          for (int i = 0; i < bigCount; i++) {
+            Document d = new Document();
+            d.add(new KnnFloat16VectorField("big", toFloat16(big[i]), 
EUCLIDEAN));
+            w.addDocument(d);
+          }
+          w.commit();
+        }
+        // the "small" field has only smallCount*2 doc-entries but shares a 
group of
+        // smallCount+bigCount distinct vectors: merging it previously overran 
its map.

Review Comment:
   Context leak in comment?



##########
lucene/sandbox/src/java/org/apache/lucene/sandbox/codecs/dedup/DedupScalarQuantizedVectorsReader.java:
##########
@@ -462,9 +488,19 @@ public CloseableRandomVectorScorerSupplier 
getRandomVectorScorerSupplierForMerge
 
     // Asymmetric encodings compare query-encoded vectors against the stored 
doc-encoded vectors.
     // Write a query-encoded record per distinct vector of the group into a 
temporary file; both
-    // sides then resolve through the shared fieldOrdToGroupOrd translation.
-    DedupVectorValues.FloatImpl rawValues = getRawFloatVectorValues(entry);
-    FloatVectorValues groupView = rawValues.getGroupView();
+    // sides then resolve through the shared fieldOrdToGroupOrd translation. 
The group's raw vectors
+    // are read directly from the group view (indexed by group ordinal, not 
field ordinal) as
+    // float[] (FLOAT16 groups are inflated from short[]).
+    FloatVectorValues floatVectorValues;

Review Comment:
   Sorry about the confusion, but I mistakenly thought that 
`Float16AsFloatVectorValues` was accessible here.
   
   Your earlier version to use something like `FloatVectorSupplier 
groupViewAsFloats` looks more suitable than duplicating that class just for 
ordinal-based vector lookup.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to