JunRuiLee commented on code in PR #104:
URL: 
https://github.com/apache/paimon-vector-index/pull/104#discussion_r4013089462


##########
core/src/ivfrq_io.rs:
##########
@@ -609,6 +613,217 @@ impl<R: SeekRead> IVFRQIndexReader<R> {
         let filter = decode_roaring_filter(roaring_filter_bytes)?;
         self.search_with_filter(query, k, nprobe, Some(&filter))
     }
+
+    /// Returns every eligible row in the probed lists whose IVF-RQ estimated
+    /// distance is in the requested band. Only squared L2 is supported.
+    ///
+    /// Membership uses the one-bit estimate or, for multi-bit codes, the full
+    /// estimate. It does not use top-K's coarse lower-bound or FastScan 
pruning:
+    /// those bounds do not certify membership in a band of estimated 
distances.
+    /// Estimates are not clamped to zero. Even probing all lists does not make
+    /// membership exact with respect to the original vectors.
+    ///
+    /// Results are uncapped and unordered. Non-finite centroids, consumed
+    /// factors or computed distances return `InvalidData`. Filtered-out rows
+    /// are not evaluated. Range statistics live in the result; the last top-K
+    /// statistics are left unchanged.
+    pub fn range_search(
+        &mut self,
+        query: &[f32],
+        params: VectorRangeSearchParams,
+    ) -> io::Result<RangeSearchResult> {
+        self.range_search_with_filter(query, params, None)
+    }
+
+    pub fn range_search_with_filter(
+        &mut self,
+        query: &[f32],
+        params: VectorRangeSearchParams,
+        filter: Option<&dyn RowIdFilter>,
+    ) -> io::Result<RangeSearchResult> {
+        self.range_search_batch_with_filter(query, 1, params, filter)
+    }
+
+    /// Range search restricted to a serialized Roaring allow-list.
+    pub fn range_search_with_roaring_filter(
+        &mut self,
+        query: &[f32],
+        params: VectorRangeSearchParams,
+        roaring_filter_bytes: &[u8],
+    ) -> io::Result<RangeSearchResult> {
+        let filter = decode_roaring_filter(roaring_filter_bytes)?;
+        self.range_search_with_filter(query, params, Some(&filter))
+    }
+
+    /// Batched range search with the same estimates and membership as single
+    /// queries. Each unique non-empty probed list is read once per call.
+    pub fn range_search_batch(
+        &mut self,
+        queries: &[f32],
+        nq: usize,
+        params: VectorRangeSearchParams,
+    ) -> io::Result<RangeSearchResult> {
+        self.range_search_batch_with_filter(queries, nq, params, None)
+    }
+
+    /// Batched range search restricted to a serialized Roaring allow-list.
+    pub fn range_search_batch_with_roaring_filter(
+        &mut self,
+        queries: &[f32],
+        nq: usize,
+        params: VectorRangeSearchParams,
+        roaring_filter_bytes: &[u8],
+    ) -> io::Result<RangeSearchResult> {
+        let filter = decode_roaring_filter(roaring_filter_bytes)?;
+        self.range_search_batch_with_filter(queries, nq, params, Some(&filter))
+    }
+
+    pub fn range_search_batch_with_filter(
+        &mut self,
+        queries: &[f32],
+        nq: usize,
+        params: VectorRangeSearchParams,
+        filter: Option<&dyn RowIdFilter>,
+    ) -> io::Result<RangeSearchResult> {
+        validate_queries(queries, nq, self.d)?;
+        if params.band().metric() != self.metric {
+            return Err(invalid_input(format!(
+                "band metric {:?} does not match index metric {:?}",
+                params.band().metric(),
+                self.metric
+            )));
+        }
+        let nprobe = params.validate(self.nlist)?;
+        let mut builder = RangeResultBuilder::new(nq);
+        if params.band().is_empty() {
+            return Ok(builder.build());
+        }
+        self.ensure_loaded()?;
+        if self
+            .quantizer_centroids
+            .iter()
+            .any(|value| !value.is_finite())
+        {
+            return Err(invalid_data("non-finite IVF-RQ centroid"));
+        }
+        let probe_lists = queries
+            .par_chunks_exact(self.d)
+            .map(|query| {
+                let mut distances = self
+                    .quantizer_centroids
+                    .chunks_exact(self.d)
+                    .enumerate()
+                    .map(|(list_id, centroid)| {
+                        let distance = fvec_l2sqr(query, centroid);
+                        if !distance.is_finite() {
+                            return Err(invalid_data(format!(
+                                "non-finite IVF-RQ query-centroid distance for 
list {list_id}"
+                            )));
+                        }
+                        Ok((list_id, distance))
+                    })
+                    .collect::<io::Result<Vec<_>>>()?;
+                let compare = |left: &(usize, f32), right: &(usize, f32)| {
+                    left.1.total_cmp(&right.1).then(left.0.cmp(&right.0))
+                };
+                if nprobe < distances.len() {
+                    distances.select_nth_unstable_by(nprobe - 1, compare);
+                    distances.truncate(nprobe);
+                    distances.shrink_to_fit();
+                }
+                distances.sort_unstable_by(compare);
+                Ok(distances)
+            })
+            .collect::<io::Result<Vec<_>>>()?;

Review Comment:
   Thanks for calling this out. Addressed in 
7a9547e18ccba82ee6b2ae5d9e4aeb01f75dbab5, now pushed to this PR.
   
   The range path now calls `kmeans::find_topk_checked`, which keeps at most 
`min(nlist, 2 * nprobe)` centroid candidates. It periodically partitions that 
buffer back to `nprobe` using the existing distance-then-list-ID ordering, 
rather than collecting every centroid distance. Final sorting is in-place 
(`sort_unstable_by`), so wide probes do not allocate another sorting buffer. It 
still evaluates and validates every direct L2 distance before discarding a 
candidate, and passes the selected distances into `RQQueryTerms`. Query-level 
Rayon parallelism and the SIMD distance kernel are preserved; existing top-K 
behavior is unchanged. The buffer bound applies only to centroid selection, not 
to the number of returned range rows.
   
   A local allocation probe with 65,536 lists and `nprobe=16` measured the 
selector's largest allocation dropping from **1,048,576 bytes to 512 bytes** 
(not a claim about total reader memory). Peak-live-allocation checks also pass 
for wide and full probes, including 1,024 lists with `nprobe=512` and 
`nprobe=1024`.
   
   I did not directly substitute `find_topk_batch_with_centroid_norms` for two 
reasons:
   
   1. **Error semantics:** that helper can discard a non-finite distance when 
selecting top-K. For example, two queries `[0]`, centroids `[0]` and `[1e20]`, 
and `nprobe=1` return the finite selected distance without an error. This PR 
explicitly requires `InvalidData` for overflow even in an unselected centroid. 
The checked selector preserves that contract without adding another full 
distance pass.
   2. **Performance is workload-dependent:** in a local ARM64 probe-only 
benchmark (`nq=100`, `nlist=65536`, `nprobe=16`, 12 Rayon threads; nine-run 
medians with the three implementations interleaved in one process), the old/new 
paths took **15.7/14.3 ms at d=128** and **155.2/153.8 ms at d=768**. The 
existing batch helper took about **44.5/174.6 ms**, respectively. It *was* 
faster with one Rayon thread, so this is not a claim that direct scanning 
always wins; the bounded change fixes candidate memory without unconditionally 
switching the execution strategy.
   
   Validation: the full core test suite passes, including **548 library tests 
and 42 range-search tests**, with existing ignores unchanged. Added coverage 
checks bounded candidate capacity, repeated compaction, ties, probe widths, 
large offsets, and multi-list single/batch/filter equivalence; the 
unselected-overflow regression now also covers 65 lists. Formatting and 
workspace/all-target Clippy with `-D warnings` pass.
   



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

Reply via email to