Copilot commented on code in PR #18091:
URL: https://github.com/apache/pinot/pull/18091#discussion_r3036192666


##########
pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/creator/VectorIndexConfigValidator.java:
##########
@@ -121,6 +125,10 @@ private static void 
validateBackendSpecificProperties(VectorIndexConfig config,
         validateNoForeignProperties(properties, HNSW_PROPERTIES, "IVF_FLAT", 
"HNSW");
         validateIvfFlatProperties(properties);
         break;
+      case IVF_PQ:
+        validateNoForeignProperties(properties, HNSW_PROPERTIES, "IVF_PQ", 
"HNSW");
+        validateIvfPqProperties(config, properties);
+        break;

Review Comment:
   `validateBackendSpecificProperties()` only rejects foreign keys for one 
other backend (e.g., HNSW rejects IVF_FLAT keys, but not IVF_PQ keys). As a 
result, backend-specific properties can still be mixed across 
HNSW/IVF_FLAT/IVF_PQ (e.g., `pqM` on IVF_FLAT, or `minRowsForIndex` on IVF_PQ) 
without validation errors, contradicting the stated validation rules. Consider 
validating against *both* other backend key sets for each case (or using 
allow-lists per backend and rejecting known keys from the other backends).



##########
pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/creator/impl/vector/pq/benchmark/VectorIndexBenchmark.java:
##########
@@ -0,0 +1,289 @@
+/**
+ * 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.pinot.segment.local.segment.creator.impl.vector.pq.benchmark;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import org.apache.commons.io.FileUtils;
+import 
org.apache.pinot.segment.local.segment.creator.impl.vector.pq.IvfPqVectorIndexCreator;
+import 
org.apache.pinot.segment.local.segment.creator.impl.vector.pq.VectorDistanceUtil;
+import 
org.apache.pinot.segment.local.segment.index.readers.vector.IvfPqVectorIndexReader;
+import org.apache.pinot.segment.spi.index.creator.VectorIndexConfig;
+import 
org.apache.pinot.spi.utils.CommonConstants.Broker.Request.QueryOptionKey;
+import org.roaringbitmap.buffer.ImmutableRoaringBitmap;
+import org.testng.annotations.Test;
+
+
+/**
+ * Reproducible benchmark suite for vector index backends.
+ *
+ * <p>Compares: exact scan, IVF_PQ (various configs).
+ * HNSW benchmark requires Lucene integration which is tested separately.
+ *
+ * <p>This benchmark is designed to run as a unit test but produces a formatted
+ * results table on stdout. It is NOT run as part of normal CI — invoke 
explicitly with:
+ * {@code mvn -pl pinot-segment-local -Dtest=VectorIndexBenchmark test}
+ */
+public class VectorIndexBenchmark {
+  private static final int DIMENSION = 64;
+  private static final int NUM_VECTORS = 10_000;
+  private static final int NUM_QUERIES = 100;
+  private static final int[] TOP_K_VALUES = {10, 100};
+
+  @Test(enabled = false, description = "Manual benchmark — run with 
-Dtest=VectorIndexBenchmark")
+  public void runBenchmark()
+      throws IOException {
+    System.out.println("=== Vector Index Benchmark ===");
+    System.out.println("Vectors: " + NUM_VECTORS + ", Dimension: " + DIMENSION 
+ ", Queries: " + NUM_QUERIES);
+    System.out.println();
+
+    // Generate L2-oriented dataset (uniform random)
+    Random random = new Random(42);
+    float[][] l2Vectors = generateUniformVectors(NUM_VECTORS, DIMENSION, 
random);
+    float[][] l2Queries = generateUniformVectors(NUM_QUERIES, DIMENSION, new 
Random(999));
+
+    // Generate normalized cosine dataset
+    float[][] cosineVectors = generateNormalizedVectors(NUM_VECTORS, 
DIMENSION, random);
+    float[][] cosineQueries = generateNormalizedVectors(NUM_QUERIES, 
DIMENSION, new Random(999));
+
+    System.out.println("## L2/Euclidean Dataset");
+    benchmarkDataset("L2", l2Vectors, l2Queries, "EUCLIDEAN",
+        VectorIndexConfig.VectorDistanceFunction.EUCLIDEAN);
+
+    System.out.println();
+    System.out.println("## Cosine Dataset (normalized vectors)");
+    benchmarkDataset("Cosine", cosineVectors, cosineQueries, "COSINE",
+        VectorIndexConfig.VectorDistanceFunction.COSINE);
+  }
+
+  private void benchmarkDataset(String label, float[][] vectors, float[][] 
queries, String distFuncStr,
+      VectorIndexConfig.VectorDistanceFunction distFunc)
+      throws IOException {
+    int distFuncCode = distFunc == 
VectorIndexConfig.VectorDistanceFunction.EUCLIDEAN ? 1 : 0;
+
+    // Precompute exact results for recall computation
+    Map<Integer, int[][]> exactResults = new HashMap<>();
+    for (int topK : TOP_K_VALUES) {
+      int[][] exact = new int[queries.length][];
+      for (int q = 0; q < queries.length; q++) {
+        exact[q] = findExactTopK(queries[q], vectors, topK, distFuncCode);
+      }
+      exactResults.put(topK, exact);
+    }
+
+    // Exact scan benchmark
+    benchmarkExactScan(label, vectors, queries, exactResults, distFuncCode);
+
+    // IVF_PQ configs to benchmark
+    int[][] configs = {
+        // {nlist, pqM, pqNbits, nprobe}
+        {64, 8, 8, 4},
+        {64, 8, 8, 8},
+        {64, 8, 8, 16},
+        {128, 16, 8, 8},
+        {128, 16, 8, 16},
+        {256, 16, 8, 16},
+        {256, 16, 8, 32},
+    };
+
+    printHeader();
+    for (int[] cfg : configs) {
+      benchmarkIvfPq(label, vectors, queries, exactResults, distFuncStr, 
distFunc, cfg[0], cfg[1], cfg[2], cfg[3]);
+    }
+
+    // Rerank on/off comparison
+    System.out.println();
+    System.out.println("### Rerank Effect (nlist=128, pqM=16, pqNbits=8, 
nprobe=16)");
+    printHeader();
+    benchmarkIvfPqRerank(label, vectors, queries, exactResults, distFuncStr, 
distFunc, 128, 16, 8, 16, true);
+    benchmarkIvfPqRerank(label, vectors, queries, exactResults, distFuncStr, 
distFunc, 128, 16, 8, 16, false);
+  }
+
+  private void benchmarkExactScan(String label, float[][] vectors, float[][] 
queries,
+      Map<Integer, int[][]> exactResults, int distFuncCode) {
+    System.out.println("### Exact Scan");
+    System.out.printf("| %-35s | %10s | %10s | %10s | %10s | %10s | %10s |%n",
+        "Config", "BuildMs", "SizeMB", "Recall@10", "Recall@100", "p50us", 
"p95us");
+    System.out.println("|" + "-".repeat(37) + "|" + ("-".repeat(12) + 
"|").repeat(6));
+
+    long[] latencies = new long[queries.length];
+    for (int q = 0; q < queries.length; q++) {
+      long start = System.nanoTime();
+      findExactTopK(queries[q], vectors, 100, distFuncCode);
+      latencies[q] = (System.nanoTime() - start) / 1000; // microseconds
+    }
+    Arrays.sort(latencies);
+    System.out.printf("| %-35s | %10d | %10.2f | %10.4f | %10.4f | %10d | %10d 
|%n",
+        "ExactScan", 0, 0.0, 1.0, 1.0,
+        latencies[(int) (queries.length * 0.5)], latencies[(int) 
(queries.length * 0.95)]);
+    System.out.println();
+  }
+
+  private void printHeader() {
+    System.out.printf("| %-35s | %10s | %10s | %10s | %10s | %10s | %10s |%n",
+        "Config", "BuildMs", "SizeMB", "Recall@10", "Recall@100", "p50us", 
"p95us");
+    System.out.println("|" + "-".repeat(37) + "|" + ("-".repeat(12) + 
"|").repeat(6));
+  }
+
+  private void benchmarkIvfPq(String label, float[][] vectors, float[][] 
queries,
+      Map<Integer, int[][]> exactResults, String distFuncStr, 
VectorIndexConfig.VectorDistanceFunction distFunc,
+      int nlist, int pqM, int pqNbits, int nprobe)
+      throws IOException {
+    benchmarkIvfPqRerank(label, vectors, queries, exactResults, distFuncStr, 
distFunc,
+        nlist, pqM, pqNbits, nprobe, true);
+  }
+
+  private void benchmarkIvfPqRerank(String label, float[][] vectors, float[][] 
queries,
+      Map<Integer, int[][]> exactResults, String distFuncStr, 
VectorIndexConfig.VectorDistanceFunction distFunc,
+      int nlist, int pqM, int pqNbits, int nprobe, boolean rerank)
+      throws IOException {
+    File tempDir = new File(FileUtils.getTempDirectory(), "bench_" + 
System.nanoTime());
+    tempDir.mkdirs();
+
+    try {
+      VectorIndexConfig config = createConfig(distFuncStr, distFunc, nlist, 
pqM, pqNbits);
+
+      // Build
+      long buildStart = System.nanoTime();
+      try (IvfPqVectorIndexCreator creator = new 
IvfPqVectorIndexCreator("emb", tempDir, config)) {
+        for (float[] v : vectors) {
+          creator.add(v);
+        }
+        creator.seal();
+      }
+      long buildMs = (System.nanoTime() - buildStart) / 1_000_000;
+
+      File indexFile = new File(tempDir, "emb" + 
IvfPqVectorIndexCreator.FILE_EXTENSION);
+      double sizeMB = indexFile.length() / (1024.0 * 1024.0);
+
+      // Search
+      Map<String, String> opts = new HashMap<>();
+      opts.put(QueryOptionKey.VECTOR_NPROBE, String.valueOf(nprobe));
+      opts.put(QueryOptionKey.VECTOR_EXACT_RERANK, String.valueOf(rerank));
+
+      long[] latencies = new long[queries.length];
+      double recall10 = 0;
+      double recall100 = 0;
+
+      try (IvfPqVectorIndexReader reader = new IvfPqVectorIndexReader("emb", 
tempDir, vectors.length, nprobe)) {
+        for (int q = 0; q < queries.length; q++) {
+          long start = System.nanoTime();
+          ImmutableRoaringBitmap result10 = reader.getDocIds(queries[q], 10, 
opts);
+          latencies[q] = (System.nanoTime() - start) / 1000;
+          recall10 += computeRecall(result10, exactResults.get(10)[q]);
+
+          ImmutableRoaringBitmap result100 = reader.getDocIds(queries[q], 100, 
opts);
+          recall100 += computeRecall(result100, exactResults.get(100)[q]);

Review Comment:
   This benchmark passes query options via `reader.getDocIds(query, topK, 
opts)` (nprobe + rerank), but `VectorIndexReader#getDocIds(..., searchParams)` 
currently defaults to ignoring `searchParams`, and `IvfPqVectorIndexReader` 
does not override it. As written, `vectorNprobe` / `vectorExactRerank` in 
`opts` have no effect, so the reported benchmark numbers are misleading. 
Consider either (a) overriding `getDocIds(..., searchParams)` in IVF_PQ to 
honor `vectorNprobe` (without mutating shared state), and removing 
`vectorExactRerank` from the reader-level benchmark (since rerank happens at 
operator layer), or (b) explicitly calling `setNprobe()` and implementing 
rerank logic in the benchmark itself.



##########
pinot-core/src/main/java/org/apache/pinot/core/operator/filter/VectorSimilarityFilterOperator.java:
##########
@@ -136,12 +137,27 @@ public List<Operator> getChildOperators() {
 
   @Override
   public String toExplainString() {
-    return EXPLAIN_NAME + "(indexLookUp:vector_index"
-        + ", operator:" + _predicate.getType()
-        + ", vector identifier:" + _predicate.getLhs().getIdentifier()
-        + ", vector literal:" + Arrays.toString(_predicate.getValue())
-        + ", topK to search:" + _predicate.getTopK()
-        + ')';
+    Map<String, String> debugInfo = _vectorIndexReader.getIndexDebugInfo();
+    StringBuilder sb = new StringBuilder();
+    sb.append(EXPLAIN_NAME);
+    sb.append("(indexLookUp:vector_index");
+    sb.append(", operator:").append(_predicate.getType());
+    sb.append(", vector 
identifier:").append(_predicate.getLhs().getIdentifier());
+    sb.append(", vector 
literal:").append(Arrays.toString(_predicate.getValue()));
+    sb.append(", topK to search:").append(_predicate.getTopK());
+    if (!debugInfo.isEmpty()) {
+      sb.append(", backend:").append(debugInfo.getOrDefault("backend", 
"UNKNOWN"));
+      String nprobe = debugInfo.get("nprobe");
+      if (nprobe != null) {
+        sb.append(", nprobe:").append(nprobe);
+      }
+      String rerank = debugInfo.get("exactRerank");
+      if (rerank != null) {
+        sb.append(", exactRerank:").append(rerank);
+      }
+    }
+    sb.append(')');
+    return sb.toString();

Review Comment:
   `toExplainString()` only appends backend/nprobe details when 
`getIndexDebugInfo()` is non-empty. Currently, IVF_FLAT readers don’t implement 
`getIndexDebugInfo()`, so EXPLAIN omits backend details for IVF_FLAT queries 
even though the PR description suggests backend should be shown. Consider 
either adding `getIndexDebugInfo()` to `IvfFlatVectorIndexReader` (e.g., 
backend=IVF_FLAT, nprobe=...) or enhancing this method to fall back to 
`instanceof NprobeAware` to still populate backend info.



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/store/VectorIndexUtils.java:
##########
@@ -56,12 +58,52 @@ static void cleanupVectorIndex(File segDir, String column) {
     // Remove the IVF_FLAT index file
     File ivfFlatIndexFile = new File(segDir, column + 
Indexes.VECTOR_IVF_FLAT_INDEX_FILE_EXTENSION);
     FileUtils.deleteQuietly(ivfFlatIndexFile);
+
+    // Remove IVF_PQ index file
+    File ivfPqIndexFile = new File(segDir, column + 
Indexes.VECTOR_IVFPQ_INDEX_FILE_EXTENSION);
+    FileUtils.deleteQuietly(ivfPqIndexFile);
   }
 
   static boolean hasVectorIndex(File segDir, String column) {
     return new File(segDir, column + 
Indexes.VECTOR_V912_HNSW_INDEX_FILE_EXTENSION).exists()
         || new File(segDir, column + 
Indexes.VECTOR_V912_INDEX_FILE_EXTENSION).exists()
-        || new File(segDir, column + 
Indexes.VECTOR_IVF_FLAT_INDEX_FILE_EXTENSION).exists();
+        || new File(segDir, column + 
Indexes.VECTOR_IVF_FLAT_INDEX_FILE_EXTENSION).exists()
+        || new File(segDir, column + 
Indexes.VECTOR_IVFPQ_INDEX_FILE_EXTENSION).exists();

Review Comment:
   `cleanupVectorIndex()` and `hasVectorIndex()` only check/delete files in the 
segment root directory. For V3 segments, vector index files can live under the 
V3 subdirectory (as handled by the new `detectVectorIndexBackend()`), so these 
methods can (a) fail to detect existing vector indexes and (b) leave stale 
vector index files behind when removing/rebuilding indexes. Consider mirroring 
the V3-aware logic here (check/delete in both `segDir` and 
`SegmentDirectoryPaths.findSegmentDirectory(segDir)` when different).



##########
pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/creator/VectorIndexConfigValidator.java:
##########
@@ -49,6 +49,10 @@ public final class VectorIndexConfigValidator {
   static final Set<String> IVF_FLAT_PROPERTIES = 
Collections.unmodifiableSet(new HashSet<>(
       Arrays.asList("nlist", "trainSampleSize", "trainingSeed", 
"minRowsForIndex")));
 
+  // IVF_PQ-specific property keys
+  static final Set<String> IVF_PQ_PROPERTIES = Collections.unmodifiableSet(new 
HashSet<>(
+      Arrays.asList("nlist", "pqM", "pqNbits", "nprobe", "trainSampleSize", 
"trainingSeed")));
+

Review Comment:
   `IVF_PQ_PROPERTIES` is declared but never referenced in this class (only 
`HNSW_PROPERTIES` and `IVF_FLAT_PROPERTIES` are used in 
`validateNoForeignProperties`). Either wire this set into the foreign-property 
validation logic or remove it to avoid dead code and configuration drift.
   ```suggestion
   
   ```



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/vector/VectorIndexType.java:
##########
@@ -200,6 +216,11 @@ public MutableIndex createMutableIndex(MutableIndexContext 
context, VectorIndexC
         .isSingleValueField()) {
       return null;
     }
+    // IVF_PQ does not support mutable/realtime segments. Return null so the
+    // consuming segment falls back to exact scan rather than silently using 
HNSW.
+    if ("IVF_PQ".equals(config.getVectorIndexType())) {
+      return null;
+    }

Review Comment:
   `createMutableIndex()` returns early when `config.getVectorIndexType()` is 
exactly `"IVF_PQ"`, which bypasses the `switch` and its WARN log for 
unsupported mutable IVF_PQ. This makes the IVF_PQ realtime fallback silent and 
also leaves a now-unreachable `case IVF_PQ` block below. Consider removing the 
early return and letting the `IVF_PQ` switch case handle the (logged) fallback 
consistently.
   ```suggestion
   
   ```



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/vector/IvfPqVectorIndexReader.java:
##########
@@ -0,0 +1,292 @@
+/**
+ * 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.pinot.segment.local.segment.index.readers.vector;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteOrder;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.PriorityQueue;
+import 
org.apache.pinot.segment.local.segment.creator.impl.vector.pq.IvfPqIndexFormat;
+import 
org.apache.pinot.segment.local.segment.creator.impl.vector.pq.IvfPqVectorIndexCreator;
+import org.apache.pinot.segment.local.segment.creator.impl.vector.pq.KMeans;
+import 
org.apache.pinot.segment.local.segment.creator.impl.vector.pq.ProductQuantizer;
+import 
org.apache.pinot.segment.local.segment.creator.impl.vector.pq.VectorDistanceUtil;
+import org.apache.pinot.segment.spi.index.reader.NprobeAware;
+import org.apache.pinot.segment.spi.index.reader.VectorIndexReader;
+import org.apache.pinot.segment.spi.store.SegmentDirectoryPaths;
+import org.roaringbitmap.buffer.MutableRoaringBitmap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * Reader for IVF_PQ vector index. Loads the compact index (no original 
vectors)
+ * and performs approximate nearest-neighbor search using IVF + Product 
Quantization.
+ *
+ * <p>Exact rerank is handled by the operator layer via the forward index 
reader,
+ * not by this reader. This keeps the index file compact (only PQ codes + 
docIds).</p>
+ *
+ * <p>Search flow:
+ * <ol>
+ *   <li>Find the nprobe closest coarse centroids to the query vector</li>
+ *   <li>Score all candidates in probed lists using approximate PQ distances 
(ADC)</li>
+ *   <li>Return top-K approximate candidates as doc IDs</li>
+ * </ol>
+ */
+public class IvfPqVectorIndexReader implements VectorIndexReader, NprobeAware {
+  private static final Logger LOGGER = 
LoggerFactory.getLogger(IvfPqVectorIndexReader.class);
+  private static final int DEFAULT_NPROBE = 8;
+
+  private final String _column;
+  private final int _dimension;
+  private final int _nlist;
+  private final int _pqM;
+  private final int _pqNbits;
+  private final int _numVectors;
+  private final int _distanceFunctionCode;
+
+  private final float[][] _coarseCentroids;
+  private final ProductQuantizer _pq;
+
+  // Inverted lists: docIds and PQ codes per list
+  private final int[][] _listDocIds;
+  private final byte[][][] _listPqCodes;
+
+  private volatile int _nprobe;
+  private final Closeable _closeHandle;
+
+  public IvfPqVectorIndexReader(String column, File segmentDir, int numDocs, 
int nprobe) {
+    _column = column;
+    _nprobe = nprobe > 0 ? nprobe : DEFAULT_NPROBE;
+
+    File indexFile = findIndexFile(segmentDir);
+    if (indexFile == null) {
+      throw new IllegalStateException("Failed to find IVF_PQ index file for 
column: " + column);
+    }
+
+    try {
+      RandomAccessFile raf = new RandomAccessFile(indexFile, "r");
+      MappedByteBuffer buffer = 
raf.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, raf.length());
+      buffer.order(ByteOrder.LITTLE_ENDIAN);
+      _closeHandle = raf;
+
+      // Read header
+      int magic = buffer.getInt();
+      if (magic != IvfPqIndexFormat.MAGIC) {
+        throw new IOException("Invalid IVF_PQ index magic: " + 
Integer.toHexString(magic));
+      }
+      int version = buffer.getInt();
+      if (version != IvfPqIndexFormat.VERSION) {
+        throw new IOException("Unsupported IVF_PQ index version: " + version);
+      }
+      _dimension = buffer.getInt();
+      _nlist = buffer.getInt();
+      _pqM = buffer.getInt();
+      _pqNbits = buffer.getInt();
+      _distanceFunctionCode = buffer.getInt();
+      _numVectors = buffer.getInt();
+
+      int ksub = 1 << _pqNbits;
+      int dsub = _pqM > 0 ? _dimension / _pqM : 1;
+
+      if (_numVectors == 0 || _nlist == 0) {
+        _coarseCentroids = new float[0][];
+        int safeDim = Math.max(_dimension, 1);
+        int safeM = Math.max(_pqM, 1);
+        if (safeDim % safeM != 0) {
+          safeM = 1;
+        }
+        _pq = new ProductQuantizer(safeDim, safeM, Math.max(_pqNbits, 1));
+        _listDocIds = new int[0][];
+        _listPqCodes = new byte[0][][];
+        return;
+      }
+
+      // Read coarse centroids
+      _coarseCentroids = new float[_nlist][_dimension];
+      for (int c = 0; c < _nlist; c++) {
+        for (int d = 0; d < _dimension; d++) {
+          _coarseCentroids[c][d] = buffer.getFloat();
+        }
+      }
+
+      // Read PQ codebooks
+      float[][][] codebooks = new float[_pqM][ksub][dsub];
+      for (int sub = 0; sub < _pqM; sub++) {
+        for (int code = 0; code < ksub; code++) {
+          for (int d = 0; d < dsub; d++) {
+            codebooks[sub][code][d] = buffer.getFloat();
+          }
+        }
+      }
+      _pq = new ProductQuantizer(_dimension, _pqM, _pqNbits);
+      _pq.setCodebooks(codebooks);
+
+      // Read inverted lists (docIds + PQ codes only — no original vectors)
+      _listDocIds = new int[_nlist][];
+      _listPqCodes = new byte[_nlist][][];
+      for (int list = 0; list < _nlist; list++) {
+        int listSize = buffer.getInt();
+        _listDocIds[list] = new int[listSize];
+        for (int i = 0; i < listSize; i++) {
+          _listDocIds[list][i] = buffer.getInt();
+        }
+        _listPqCodes[list] = new byte[listSize][_pqM];
+        for (int i = 0; i < listSize; i++) {
+          buffer.get(_listPqCodes[list][i]);
+        }
+      }
+
+      LOGGER.info("Loaded IVF_PQ index for column: {} with {} lists, {} 
vectors, dimension={}, pqM={}, pqNbits={}",
+          column, _nlist, _numVectors, _dimension, _pqM, _pqNbits);
+    } catch (IOException e) {
+      throw new RuntimeException("Failed to load IVF_PQ index for column: " + 
column, e);
+    }
+  }
+
+  @Override
+  public MutableRoaringBitmap getDocIds(float[] queryVector, int topK) {
+    if (_numVectors == 0 || _nlist == 0) {
+      return new MutableRoaringBitmap();
+    }
+
+    // For COSINE and INNER_PRODUCT, normalize query to match the normalized 
index vectors.
+    // L2 on unit-normalized vectors preserves both cosine and inner-product 
ranking.
+    float[] searchVector = (_distanceFunctionCode == 
IvfPqIndexFormat.DIST_COSINE
+        || _distanceFunctionCode == IvfPqIndexFormat.DIST_INNER_PRODUCT)

Review Comment:
   The INNER_PRODUCT path normalizes vectors and then uses L2-based centroid 
probing + PQ ADC distances (see comment "L2 on unit-normalized vectors 
preserves ... inner-product ranking"). This changes the semantics from maximum 
inner product to cosine-on-normalized-vectors and does **not** match the 
existing IVF_FLAT behavior (which uses `-dotProduct` for 
INNER_PRODUCT/DOT_PRODUCT) or Lucene's `MAXIMUM_INNER_PRODUCT`. Either 
disable/forbid INNER_PRODUCT for IVF_PQ (fail validation) or implement a 
distance computation that preserves true inner-product semantics for this 
backend.
   ```suggestion
       if (_distanceFunctionCode == IvfPqIndexFormat.DIST_INNER_PRODUCT) {
         throw new UnsupportedOperationException(
             "INNER_PRODUCT is not supported for IVF_PQ because the current 
implementation uses "
                 + "normalized vectors with L2-based centroid probing and PQ 
ADC scoring, which does not preserve "
                 + "true maximum inner-product semantics");
       }
   
       if (_numVectors == 0 || _nlist == 0) {
         return new MutableRoaringBitmap();
       }
   
       // For COSINE, normalize query to match the normalized index vectors.
       float[] searchVector = _distanceFunctionCode == 
IvfPqIndexFormat.DIST_COSINE
   ```



##########
pinot-core/src/main/java/org/apache/pinot/core/operator/filter/VectorSimilarityFilterOperator.java:
##########
@@ -160,6 +176,14 @@ protected void explainAttributes(ExplainAttributeBuilder 
attributeBuilder) {
     if (_searchParams.isExactRerank()) {
       attributeBuilder.putString("exactRerank", "true");
     }
+    Map<String, String> debugInfo = _vectorIndexReader.getIndexDebugInfo();
+    if (!debugInfo.isEmpty()) {
+      attributeBuilder.putString("backend", debugInfo.getOrDefault("backend", 
"UNKNOWN"));
+      String nprobe = debugInfo.get("nprobe");
+      if (nprobe != null) {
+        attributeBuilder.putString("nprobe", nprobe);
+      }
+    }

Review Comment:
   PR description indicates operator-level exact rerank should use the 
segment’s configured distance function, and EXPLAIN output should surface 
effective rerank/nprobe settings. However, `applyExactRerank()` in this class 
still hardcodes L2 distance (see existing TODO in that method), so rerank can 
produce incorrect ordering for COSINE / INNER_PRODUCT segments (including 
IVF_PQ). Consider plumbing the configured distance function into rerank and 
reflecting effective settings in explain output (use `_searchParams` for 
rerank/nprobe instead of relying on reader debugInfo keys like `exactRerank`).



-- 
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