Repository: datastax/cassandra
Pull request #1705: CNDB-13625: Implement rerankless vector search through ANN_OPTIONS
Author: login: michaeljmarshall
Created: 2025-04-22T22:17:41Z | Merged: 2025-04-23T16:54:12Z

### What is the issue
Fixes: https://github.com/riptano/cndb/issues/13625

### What does this PR fix and why was it fixed
When `rerank_k` is non-positive, we will skip reranking where possible.

A minor note: this change is not strictly backwards compatible because of the original implementation. However, it will fail quickly if the coordinator accepts the value but the writer does not, and since this is only a query-time parameter, I propose that we move forward and accept the net-new value that was previously disallowed.

This PR will have conflicts with https://github.com/riptano/cndb/pull/13826. I plan on merging that PR and then resolving conflicts here.



Diff:
diff --git a/src/java/org/apache/cassandra/db/filter/ANNOptions.java b/src/java/org/apache/cassandra/db/filter/ANNOptions.java
index 874052011dd8..bdda239e2769 100644
--- a/src/java/org/apache/cassandra/db/filter/ANNOptions.java
+++ b/src/java/org/apache/cassandra/db/filter/ANNOptions.java
@@ -80,8 +80,8 @@ public void validate(QueryState state, String keyspace, int limit)
 
         if (rerankK != null)
         {
-            if (rerankK < limit)
-                throw new InvalidRequestException(String.format("Invalid rerank_k value %d lesser than limit %d", rerankK, limit));
+            if (rerankK > 0 && rerankK < limit)
+                throw new InvalidRequestException(String.format("Invalid rerank_k value %d greater than 0 and less than limit %d", rerankK, limit));
 
             Guardrails.annRerankKMaxValue.guard(rerankK, "ANN options", false, state);
         }
diff --git a/src/java/org/apache/cassandra/index/sai/disk/v2/V2VectorIndexSearcher.java b/src/java/org/apache/cassandra/index/sai/disk/v2/V2VectorIndexSearcher.java
index 315c1cbc9013..361749a93ad5 100644
--- a/src/java/org/apache/cassandra/index/sai/disk/v2/V2VectorIndexSearcher.java
+++ b/src/java/org/apache/cassandra/index/sai/disk/v2/V2VectorIndexSearcher.java
@@ -272,6 +272,8 @@ private CloseableIterator<RowIdWithScore> searchInternal(AbstractBounds<Partitio
 
     private CloseableIterator<RowIdWithScore> orderByBruteForce(VectorFloat<?> queryVector, SegmentRowIdOrdinalPairs segmentOrdinalPairs, int limit, int rerankK) throws IOException
     {
+        // We allow for negative rerankK, but for our cost calculations, it only makes sense to use 0 here.
+        rerankK = Math.max(0, rerankK);
         // If we use compressed vectors, we still have to order rerankK results using full resolution similarity
         // scores, so only use the compressed vectors when there are enough vectors to make it worthwhile.
         double twoPassCost = segmentOrdinalPairs.size() * CostCoefficients.ANN_SIMILARITY_COST
@@ -291,20 +293,28 @@ private CloseableIterator<RowIdWithScore> orderByBruteForce(CompressedVectors cv
                                                                 VectorFloat<?> queryVector,
                                                                 SegmentRowIdOrdinalPairs segmentOrdinalPairs,
                                                                 int limit,
-                                                                int rerankK) throws IOException
+                                                                int rerankK)
     {
         // Use the jvector NodeQueue to avoid unnecessary object allocations since this part of the code operates on
         // many rows.
         var approximateScores = new NodeQueue(new BoundedLongHeap(segmentOrdinalPairs.size()), NodeQueue.Order.MAX_HEAP);
         var similarityFunction = indexContext.getIndexWriterConfig().getSimilarityFunction();
         var scoreFunction = cv.precomputedScoreFunctionFor(queryVector, similarityFunction);
+        columnQueryMetrics.onBruteForceNodesVisited(segmentOrdinalPairs.size());
+
+        if (rerankK <= 0)
+        {
+            // Rerankless search, so we go straight to the NodeQueueRowIdIterator.
+            var iter = segmentOrdinalPairs.mapToSegmentRowIdScoreIterator(scoreFunction);
+            approximateScores.pushMany(iter, segmentOrdinalPairs.size());
+            return new NodeQueueRowIdIterator(approximateScores);
+        }
 
         // Store the index of the (rowId, ordinal) pair from the segmentOrdinalPairs in the NodeQueue so that we can
         // retrieve both values with O(1) lookup when we need to resolve the full resolution score in the
         // BruteForceRowIdIterator.
         var iter = segmentOrdinalPairs.mapToIndexScoreIterator(scoreFunction);
         approximateScores.pushMany(iter, segmentOrdinalPairs.size());
-        columnQueryMetrics.onBruteForceNodesVisited(segmentOrdinalPairs.size());
         var reranker = new CloseableReranker(similarityFunction, queryVector, graph.getView());
         return new BruteForceRowIdIterator(approximateScores, segmentOrdinalPairs, reranker, limit, rerankK, columnQueryMetrics);
     }
diff --git a/src/java/org/apache/cassandra/index/sai/disk/vector/CassandraDiskAnn.java b/src/java/org/apache/cassandra/index/sai/disk/vector/CassandraDiskAnn.java
index 01d9e2b05352..7aa2cda5d174 100644
--- a/src/java/org/apache/cassandra/index/sai/disk/vector/CassandraDiskAnn.java
+++ b/src/java/org/apache/cassandra/index/sai/disk/vector/CassandraDiskAnn.java
@@ -198,7 +198,9 @@ public int size()
     /**
      * @param queryVector the query vector
      * @param limit the number of results to look for in the index (>= limit)
-     * @param rerankK the number of results to look for in the index (>= limit)
+     * @param rerankK the number of quantized results to look for in the index (>= limit or <= 0). If rerankK is
+     *                non-positive, then we will use limit as the value and will skip reranking. Rerankless search
+     *                only applies when the graph has compressed vectors.
      * @param threshold the minimum similarity score to accept
      * @param usePruning whether to use pruning to speed up the search
      * @param acceptBits a Bits indicating which row IDs are acceptable, or null if no constraints
@@ -217,6 +219,9 @@ public CloseableIterator<RowIdWithScore> search(VectorFloat<?> queryVector,
                                                     IntConsumer nodesVisitedConsumer)
     {
         VectorValidation.validateIndexable(queryVector, similarityFunction);
+        boolean isRerankless = rerankK <= 0;
+        if (isRerankless)
+            rerankK = limit;
 
         var graphAccessManager = searchers.get();
         var searcher = graphAccessManager.get();
@@ -232,11 +237,12 @@ public CloseableIterator<RowIdWithScore> search(VectorFloat<?> queryVector,
             if (features.contains(FeatureId.FUSED_ADC))
             {
                 var asf = view.approximateScoreFunctionFor(queryVector, similarityFunction);
-                var rr = view.rerankerFor(queryVector, similarityFunction);
+                var rr = isRerankless ? null : view.rerankerFor(queryVector, similarityFunction);
                 ssp = new SearchScoreProvider(asf, rr);
             }
             else if (compressedVectors == null)
             {
+                // no compression, so we ignore isRerankless (except for setting rerankK to limit)
                 ssp = new SearchScoreProvider(view.rerankerFor(queryVector, similarityFunction));
             }
             else
@@ -247,7 +253,7 @@ else if (compressedVectors == null)
                          ? VectorSimilarityFunction.COSINE
                          : similarityFunction;
                 var asf = compressedVectors.precomputedScoreFunctionFor(queryVector, sf);
-                var rr = view.rerankerFor(queryVector, similarityFunction);
+                var rr = isRerankless ? null : view.rerankerFor(queryVector, similarityFunction);
                 ssp = new SearchScoreProvider(asf, rr);
             }
             long start = System.nanoTime();
@@ -255,8 +261,8 @@ else if (compressedVectors == null)
             long elapsed = System.nanoTime() - start;
             if (V3OnDiskFormat.ENABLE_RERANK_FLOOR)
                 context.updateAnnRerankFloor(result.getWorstApproximateScoreInTopK());
-            Tracing.trace("DiskANN search for {}/{} (usePruning: {}) visited {} nodes, reranked {} to return {} results from {}",
-                          limit, rerankK, usePruning, result.getVisitedCount(), result.getRerankedCount(), result.getNodes().length, source);
+            Tracing.trace("DiskANN search for {}/{} rerankless={}, usePruning={} visited {} nodes, reranked {} to return {} results from {}",
+                          limit, rerankK, isRerankless, usePruning, result.getVisitedCount(), result.getRerankedCount(), result.getNodes().length, source);
             columnQueryMetrics.onSearchResult(result, elapsed, false);
             context.addAnnGraphSearchLatency(elapsed);
             if (threshold > 0)
diff --git a/src/java/org/apache/cassandra/index/sai/disk/vector/CassandraOnHeapGraph.java b/src/java/org/apache/cassandra/index/sai/disk/vector/CassandraOnHeapGraph.java
index a0476c0a1815..5c2dbc2190c6 100644
--- a/src/java/org/apache/cassandra/index/sai/disk/vector/CassandraOnHeapGraph.java
+++ b/src/java/org/apache/cassandra/index/sai/disk/vector/CassandraOnHeapGraph.java
@@ -328,6 +328,10 @@ public CloseableIterator<SearchResult.NodeScore> search(QueryContext context, Ve
         // search() errors out when an empty graph is passed to it
         if (vectorValues.size() == 0)
             return CloseableIterator.emptyIterator();
+        // This configuration indicates rerankless search, but that is only applicable to disk search, so we set
+        // rerankK to limit and otherwise ignore the setting.
+        if (rerankK <= 0)
+            rerankK = limit;
 
         Bits bits = hasDeletions ? BitsUtil.bitsIgnoringDeleted(toAccept, postingsByOrdinal) : toAccept;
         var graphAccessManager = searchers.get();
diff --git a/test/unit/org/apache/cassandra/db/filter/ANNOptionsTest.java b/test/unit/org/apache/cassandra/db/filter/ANNOptionsTest.java
index 72f29eb4abd1..e752f5cc8733 100644
--- a/test/unit/org/apache/cassandra/db/filter/ANNOptionsTest.java
+++ b/test/unit/org/apache/cassandra/db/filter/ANNOptionsTest.java
@@ -91,6 +91,8 @@ public void testParseAndValidate()
         execute("SELECT * FROM %s WHERE k=0 ORDER BY v ANN OF [1, 1] WITH ann_options = {}");
 
         // correct queries with specific ANN options - rerank_k
+        execute("SELECT * FROM %s ORDER BY v ANN OF [1, 1] LIMIT 100 WITH ann_options = {'rerank_k': -1}");
+        execute("SELECT * FROM %s ORDER BY v ANN OF [1, 1] LIMIT 100 WITH ann_options = {'rerank_k': 0}");
         execute("SELECT * FROM %s ORDER BY v ANN OF [1, 1] LIMIT 10 WITH ann_options = {'rerank_k': 10}");
         execute("SELECT * FROM %s ORDER BY v ANN OF [1, 1] LIMIT 10 WITH ann_options = {'rerank_k': 11}");
         execute("SELECT * FROM %s ORDER BY v ANN OF [1, 1] LIMIT 10 WITH ann_options = {'rerank_k': 1000}");
@@ -106,14 +108,6 @@ public void testParseAndValidate()
         execute("SELECT * FROM %s ORDER BY v ANN OF [1, 1] LIMIT 10 WITH ann_options = {'rerank_k': 10, 'use_pruning': true}");
         execute("SELECT * FROM %s ORDER BY v ANN OF [1, 1] LIMIT 10 WITH ann_options = {'use_pruning': false, 'rerank_k': 20}");
 
-        // Queries with invalid ann options that will eventually be valid when we support disabling reranking
-        assertInvalidThrowMessage("Invalid rerank_k value -1 lesser than limit 100",
-                                  InvalidRequestException.class,
-                                  "SELECT * FROM %s ORDER BY v ANN OF [1, 1] LIMIT 100 WITH ann_options = {'rerank_k': -1}");
-        assertInvalidThrowMessage("Invalid rerank_k value 0 lesser than limit 100",
-                                  InvalidRequestException.class,
-                                  "SELECT * FROM %s ORDER BY v ANN OF [1, 1] LIMIT 100 WITH ann_options = {'rerank_k': 0}");
-
         // Queries that exceed the failure threshold for the guardrail. Specifies a protocol version to trigger
         // validation in the coordinator.
         assertInvalidThrowMessage(Optional.of(ProtocolVersion.V5),
@@ -154,7 +148,7 @@ public void testParseAndValidate()
                                   baseQuery + " WITH ann_options = {'rerank_k': 'a'}");
 
         // ANN options with rerank lesser than limit
-        assertInvalidThrowMessage("Invalid rerank_k value 10 lesser than limit 100",
+        assertInvalidThrowMessage("Invalid rerank_k value 10 greater than 0 and less than limit 100",
                                   InvalidRequestException.class,
                                   baseQuery + "LIMIT 100 WITH ann_options = {'rerank_k': 10}");
 
@@ -226,13 +220,15 @@ public void testTransport()
         testTransport("SELECT * FROM %s ORDER BY v ANN OF [1, 1]", ANNOptions.NONE);
         testTransport("SELECT * FROM %s ORDER BY v ANN OF [1, 1] WITH ann_options = {}", ANNOptions.NONE);
 
-        // TODO re-enable this test when we support negative rerank_k values
         // some random negative values, all should be accepted and not be mapped to NONE
-//        String negativeQuery = "SELECT * FROM %%s ORDER BY v ANN OF [1, 1] LIMIT 10 WITH ann_options = {'rerank_k': %d}";
-//        QuickTheory.qt()
-//                   .withExamples(100)
-//                   .forAll(integers().allPositive())
-//                   .checkAssert(i -> testTransport(String.format(negativeQuery, -i), ANNOptions.create(-i)));
+        String negativeQuery = "SELECT * FROM %%s ORDER BY v ANN OF [1, 1] LIMIT 10 WITH ann_options = {'rerank_k': %d}";
+        QuickTheory.qt()
+                   .withExamples(100)
+                   .forAll(integers().allPositive())
+                   .checkAssert(i -> testTransport(String.format(negativeQuery, -i), ANNOptions.create(-i, null)));
+
+        // rerankK = 0 must also work
+        testTransport("SELECT * FROM %s ORDER BY v ANN OF [1, 1] LIMIT 10 WITH ann_options = {'rerank_k': 0}", ANNOptions.create(0, null));
 
         // test use_pruning values
         testTransport("SELECT * FROM %s ORDER BY v ANN OF [1, 1] WITH ann_options = {'use_pruning': true}", 
diff --git a/test/unit/org/apache/cassandra/index/sai/cql/VectorHybridSearchTest.java b/test/unit/org/apache/cassandra/index/sai/cql/VectorHybridSearchTest.java
index b5952d8ce348..4f14e84c6a9f 100644
--- a/test/unit/org/apache/cassandra/index/sai/cql/VectorHybridSearchTest.java
+++ b/test/unit/org/apache/cassandra/index/sai/cql/VectorHybridSearchTest.java
@@ -18,10 +18,14 @@
 
 package org.apache.cassandra.index.sai.cql;
 
+import java.util.stream.IntStream;
+
 import org.junit.Test;
 
 import org.apache.cassandra.index.sai.plan.QueryController;
 
+import static org.apache.cassandra.index.sai.disk.vector.CassandraOnHeapGraph.MIN_PQ_ROWS;
+
 public class VectorHybridSearchTest extends VectorTester.VersionedWithChecksums
 {
     @Test
@@ -214,4 +218,36 @@ public void testHybridQueryWithMissingVectorValuesForMaxSegmentRow() throws Thro
             assertRows(execute("SELECT i FROM %s WHERE c >= 1 ORDER BY v ANN OF [1,1] LIMIT 1"), row(1));
         });
     }
+
+    @Test
+    public void testReranklessHybridSearch()
+    {
+        // Want to test the search then order path
+        QueryController.QUERY_OPT_LEVEL = 0;
+
+        createTable("CREATE TABLE %s (pk int, val int, vec vector<float, 128>, PRIMARY KEY(pk))");
+        createIndex("CREATE CUSTOM INDEX ON %s(vec) USING 'StorageAttachedIndex'");
+        createIndex("CREATE CUSTOM INDEX ON %s(val) USING 'StorageAttachedIndex'");
+
+        // Insert many rows in parallel
+        IntStream.range(0, MIN_PQ_ROWS * 2).parallel().forEach(i -> {
+            execute("INSERT INTO %s (pk, val, vec) VALUES (?, ?, ?)", i, i, randomVectorBoxed(128));
+        });
+
+        flush();
+
+        // Search the graph with rerankless search. We restrict val to 1/4th of the dataset
+        setMaxBruteForceRows(0);
+        var result = execute("SELECT pk FROM %s WHERE val < ? ORDER BY vec ANN OF ? LIMIT 10 with ann_options = { 'rerank_k': 0 }", MIN_PQ_ROWS / 2, randomVectorBoxed(128));
+        // Just testing that we can run the query, so only assert that we got results
+        assertRowCount(result, 10);
+
+        // Now search with brute force (skipping the graph). We restrict to 1/10th of the dataset to trigger brute force.
+        setMaxBruteForceRows(MIN_PQ_ROWS * 2);
+        result = execute("SELECT pk FROM %s WHERE val < ? ORDER BY vec ANN OF ? LIMIT 10 with ann_options = { 'rerank_k': 0 }", MIN_PQ_ROWS / 5, randomVectorBoxed(128));
+        assertRowCount(result, 10);
+        // Also ensure that a negative rerank_k value works
+        result = execute("SELECT pk FROM %s WHERE val < ? ORDER BY vec ANN OF ? LIMIT 10 with ann_options = { 'rerank_k': -1 }", MIN_PQ_ROWS / 5, randomVectorBoxed(128));
+        assertRowCount(result, 10);
+    }
 }
diff --git a/test/unit/org/apache/cassandra/index/sai/cql/VectorSiftSmallTest.java b/test/unit/org/apache/cassandra/index/sai/cql/VectorSiftSmallTest.java
index 910f51eb95fa..8fc9b37c17e8 100644
--- a/test/unit/org/apache/cassandra/index/sai/cql/VectorSiftSmallTest.java
+++ b/test/unit/org/apache/cassandra/index/sai/cql/VectorSiftSmallTest.java
@@ -88,10 +88,18 @@ private void ensureIncreasingRerankKIncreasesRecall(List<float[]> queryVectors,
         double previousRecall = 0;
         int limit = 10;
         int strictlyIncreasedCount = 0;
-        // Testing shows that we acheive 100% recall at about rerank_k = 45, so no need to go higher
+
+        // First test with rerank_k = 0, which should have the worst recall
+        double zeroRerankRecall = testRecall(limit, queryVectors, groundTruth, 0, null);
+
+        // Testing shows that we achieve 100% recall at about rerank_k = 45, so no need to go higher
         for (int rerankK = limit; rerankK <= 50; rerankK += 5)
         {
             var recall = testRecall(limit, queryVectors, groundTruth, rerankK, null);
+            // All recalls should be better than rerank_k = 0
+            assertTrue("Recall for rerank_k = " + rerankK + " should be at least as good as with rerank_k = 0",
+                      recall >= zeroRerankRecall);
+
             // Recall varies, so we can only assert that it does not get worse on a per-run basis. However, it should
             // get better strictly at least some of the time
             assertTrue("Recall for rerank_k = " + rerankK + " is " + recall, recall >= previousRecall);


Reviews:
user: login: eolivelli
state: APPROVED
author_association: NONE
submitted_at: 2025-04-23T04:40:51Z
user: login: adelapena
body: Looks good to me, I have only left a few nits.
state: APPROVED
author_association: NONE
submitted_at: 2025-04-23T15:07:48Z
user: login: michaeljmarshall
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-23T16:42:33Z
user: login: michaeljmarshall
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-23T16:42:56Z

Review comments:
diff_hunk: @@ -214,4 +218,36 @@ public void testHybridQueryWithMissingVectorValuesForMaxSegmentRow() throws Thro
             assertRows(execute("SELECT i FROM %s WHERE c >= 1 ORDER BY v ANN OF [1,1] LIMIT 1"), row(1));
         });
     }
+
+    @Test
+    public void testReranklessHybidSearch()
path: test/unit/org/apache/cassandra/index/sai/cql/VectorHybridSearchTest.java
user: login: adelapena
body: Nit: typo
```suggestion
    public void testReranklessHybridSearch()
```
created_at: 2025-04-23T14:42:14Z
author_association: NONE
diff_hunk: @@ -247,16 +253,16 @@ else if (compressedVectors == null)
                          ? VectorSimilarityFunction.COSINE
                          : similarityFunction;
                 var asf = compressedVectors.precomputedScoreFunctionFor(queryVector, sf);
-                var rr = view.rerankerFor(queryVector, similarityFunction);
+                var rr = !isRerankless ? view.rerankerFor(queryVector, similarityFunction) : null;
                 ssp = new SearchScoreProvider(asf, rr);
             }
             long start = System.nanoTime();
             var result = searcher.search(ssp, limit, rerankK, threshold, context.getAnnRerankFloor(), ordinalsMap.ignoringDeleted(acceptBits));
             long elapsed = System.nanoTime() - start;
             if (V3OnDiskFormat.ENABLE_RERANK_FLOOR)
                 context.updateAnnRerankFloor(result.getWorstApproximateScoreInTopK());
-            Tracing.trace("DiskANN search for {}/{} (usePruning: {}) visited {} nodes, reranked {} to return {} results from {}",
-                          limit, rerankK, usePruning, result.getVisitedCount(), result.getRerankedCount(), result.getNodes().length, source);
+            Tracing.trace("DiskANN search for {}/{} rerankless={}, usePruning: {} visited {} nodes, reranked {} to return {} results from {}",
path: src/java/org/apache/cassandra/index/sai/disk/vector/CassandraDiskAnn.java
user: login: adelapena
body: Nit: I'd use either
```suggestion
            Tracing.trace("DiskANN search for {}/{} rerankless: {}, usePruning: {} visited {} nodes, reranked {} to return {} results from {}",
```
or
```suggestion
            Tracing.trace("DiskANN search for {}/{} rerankless={}, usePruning={} visited {} nodes, reranked {} to return {} results from {}",
```
created_at: 2025-04-23T15:00:44Z
author_association: NONE
diff_hunk: @@ -232,11 +237,12 @@ public CloseableIterator<RowIdWithScore> search(VectorFloat<?> queryVector,
             if (features.contains(FeatureId.FUSED_ADC))
             {
                 var asf = view.approximateScoreFunctionFor(queryVector, similarityFunction);
-                var rr = view.rerankerFor(queryVector, similarityFunction);
+                var rr = !isRerankless ? view.rerankerFor(queryVector, similarityFunction) : null;
path: src/java/org/apache/cassandra/index/sai/disk/vector/CassandraDiskAnn.java
user: login: adelapena
body: Nit: we can do this without the negation
```suggestion
                var rr = isRerankless ? null : view.rerankerFor(queryVector, similarityFunction);
```
created_at: 2025-04-23T15:03:04Z
author_association: NONE
diff_hunk: @@ -247,16 +253,16 @@ else if (compressedVectors == null)
                          ? VectorSimilarityFunction.COSINE
                          : similarityFunction;
                 var asf = compressedVectors.precomputedScoreFunctionFor(queryVector, sf);
-                var rr = view.rerankerFor(queryVector, similarityFunction);
+                var rr = !isRerankless ? view.rerankerFor(queryVector, similarityFunction) : null;
path: src/java/org/apache/cassandra/index/sai/disk/vector/CassandraDiskAnn.java
user: login: adelapena
body: ```suggestion
                var rr = isRerankless ? null : view.rerankerFor(queryVector, similarityFunction);
```
created_at: 2025-04-23T15:03:29Z
author_association: NONE
diff_hunk: @@ -198,7 +198,9 @@ public int size()
     /**
      * @param queryVector the query vector
      * @param limit the number of results to look for in the index (>= limit)
-     * @param rerankK the number of results to look for in the index (>= limit)
+     * @param rerankK the number of quantized results to look for in the index (>= limit or < 0). If rerankK is
path: src/java/org/apache/cassandra/index/sai/disk/vector/CassandraDiskAnn.java
user: login: adelapena
body: ```suggestion
     * @param rerankK the number of quantized results to look for in the index (>= limit or <= 0). If rerankK is
```
created_at: 2025-04-23T15:05:02Z
author_association: NONE
diff_hunk: @@ -232,11 +237,12 @@ public CloseableIterator<RowIdWithScore> search(VectorFloat<?> queryVector,
             if (features.contains(FeatureId.FUSED_ADC))
             {
                 var asf = view.approximateScoreFunctionFor(queryVector, similarityFunction);
-                var rr = view.rerankerFor(queryVector, similarityFunction);
+                var rr = !isRerankless ? view.rerankerFor(queryVector, similarityFunction) : null;
path: src/java/org/apache/cassandra/index/sai/disk/vector/CassandraDiskAnn.java
user: login: michaeljmarshall
body: Yeah, I thought about this and I'll change it, but I really like having the null at the end of the line and having the variable name as `isRerankless`
created_at: 2025-04-23T16:42:33Z
author_association: NONE
diff_hunk: @@ -198,7 +198,9 @@ public int size()
     /**
      * @param queryVector the query vector
      * @param limit the number of results to look for in the index (>= limit)
-     * @param rerankK the number of results to look for in the index (>= limit)
+     * @param rerankK the number of quantized results to look for in the index (>= limit or < 0). If rerankK is
path: src/java/org/apache/cassandra/index/sai/disk/vector/CassandraDiskAnn.java
user: login: michaeljmarshall
body: great catch, thanks for the close review
created_at: 2025-04-23T16:42:56Z
author_association: NONE

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-22T22:17:58Z
body: ### Checklist before you submit for review
- [ ] Make sure there is a PR in the CNDB project updating the Converged Cassandra version
- [ ] Use `NoSpamLogger` for log lines that may appear frequently in the logs
- [ ] Verify test results on Butler
- [ ] Test coverage for new/modified code is > 80%
- [ ] Proper code formatting
- [ ] Proper title for each commit staring with the project-issue number, like CNDB-1234
- [ ] Each commit has a meaningful description
- [ ] Each commit is not very long and contains related changes
- [ ] Renames, moves and reformatting are in distinct commits
- [ ] All new files should contain the DataStax copyright header instead of the Apache License one

author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-04-23T17:27:49Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=cassandra-stargazer&pullRequest=1705) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=cassandra-stargazer&pullRequest=1705&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=cassandra-stargazer&pullRequest=1705&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=cassandra-stargazer&pullRequest=1705&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [90.9% Coverage on New Code](https://sonarcloud.io/component_measures?id=cassandra-stargazer&pullRequest=1705&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=cassandra-stargazer&pullRequest=1705&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=cassandra-stargazer&pullRequest=1705)


author_association: NONE
user: login: cassci-bot
created_at: 2025-04-23T17:32:35Z
body: <!-- ds-cassandra-build-status -->
### :x: Build ds-cassandra-pr-gate/PR-1705 rejected by Butler

---
2 new test failure(s) in 6 builds
See build details [here](http://butler-stargazer.aws.dsinternal.org/#/ci/upstream/compare/ds-cassandra-pr-gate/cndb-13625/to/ds-cassandra-build-nightly/main)

---

#### Found 2 new test failures

|Test|Explanation|Branch history|Upstream history|
|---|---|---|---|
|<span title="sstable_generation_loading_test.TestSSTableGenerationAndLoadingLegacyIndex.test_sstableloader_with_failing_2i">...gLegacyIndex.test_sstableloader_with_failing_2i</span>|regression|<span title="failed">:red_circle:</span><span title="failed">:red_circle:</span><span title="failed">:red_circle:</span><span title="failed">:red_circle:</span><span title="failed">:red_circle:</span><span title="failed">:red_circle:</span>|<span title="passed">:large_blue_circle:</span><span title="passed">:large_blue_circle:</span><span title="passed">:large_blue_circle:</span><span title="passed">:large_blue_circle:</span><span title="passed">:large_blue_circle:</span><span title="passed">:large_blue_circle:</span><span title="passed">:large_blue_circle:</span>|
|<span title="org.apache.cassandra.utils.binlog.BinLogTest.testTruncationReleasesLogSpace">o.a.c.u.b.BinLogTest.testTruncationReleasesLogS...</span>|regression|<span title="failed">:red_circle:</span><span title="failed">:red_circle:</span><span title="passed">:large_blue_circle:</span><span title="failed">:red_circle:</span><span title="failed">:red_circle:</span><span title="failed">:red_circle:</span>|<span title="passed">:large_blue_circle:</span><span title="passed">:large_blue_circle:</span><span title="passed">:large_blue_circle:</span><span title="passed">:large_blue_circle:</span><span title="passed">:large_blue_circle:</span><span title="passed">:large_blue_circle:</span><span title="passed">:large_blue_circle:</span>|


---

#### Found 10 known test failures

author_association: NONE