This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new c4308ca177 [vector] Expose IVF search tuning options (#9367)
c4308ca177 is described below
commit c4308ca17768bf72c1fa65ad487abd0820eb3639
Author: shyjsarah <[email protected]>
AuthorDate: Mon Aug 24 11:45:00 2026 +0800
[vector] Expose IVF search tuning options (#9367)
---
docs/docs/multimodal-table/global-index/vector.mdx | 9 ++++
.../index/NativeVectorGlobalIndexReader.java | 62 ++++++++++++++++------
.../vector/index/NativeVectorGlobalIndexTest.java | 50 +++++++++++++++++
3 files changed, 106 insertions(+), 15 deletions(-)
diff --git a/docs/docs/multimodal-table/global-index/vector.mdx
b/docs/docs/multimodal-table/global-index/vector.mdx
index 217390ac63..8bcd42f7cb 100644
--- a/docs/docs/multimodal-table/global-index/vector.mdx
+++ b/docs/docs/multimodal-table/global-index/vector.mdx
@@ -284,13 +284,22 @@ Search-time options are passed with each vector search
request:
| Option | Default | Description |
|---|---|---|
| `ivf.nprobe` | Automatic | Explicit number of IVF clusters to probe. When
omitted, paimon-vindex derives the width from the index, `top_k`, and filter
selectivity. |
+| `ivf.max_initial_filter_expansion_factor` | Disabled | Positive integer
limiting filter-driven expansion of the initial automatic IVF probe width. A
factor of `1` disables initial filter expansion. Progressive retries may still
probe more clusters when fewer than `top_k` filtered results are found. |
| `ivf.refine_factor` | Disabled | Retrieves `top_k * refine_factor` IVF
candidates and reranks them with the original vectors stored in the Paimon
table. It is most useful for compressed indexes such as `ivf-pq`, `ivf-sq`, and
`ivf-rq` when recall is more important than latency. |
+| `ivf_pq.batch_table_reuse` | `auto` | IVF-PQ batch search distance-table
reuse mode: `auto`, `on`, or `off`. Other index types and scalar searches
ignore it. |
+| `ivf_pq.batch_table_reuse.max_bytes` | 512 MiB | Positive long integer
limiting the memory used by IVF-PQ batch distance-table reuse. Search falls
back to direct table construction when the reusable tables exceed the budget. |
| `diskann.l_search` | Automatic | paimon-vindex DiskANN graph candidate
width. The automatic value uses calibration when available, otherwise `max(100,
2 * top_k)`. |
Use the same distance metric at build time and query time. Search options can
be passed per query,
so you can use a larger `ivf.nprobe` or `diskann.l_search` for higher recall
queries and a smaller
value for latency-sensitive queries. Do not set both in one query.
+`ivf.max_initial_filter_expansion_factor` applies only to automatic IVF search
and cannot be combined
+with `ivf.nprobe` or `diskann.l_search`. Lower factors reduce initial
filtered-search work but may
+reduce Recall@K compared with uncapped automatic search. Progressive expansion
occurs only when
+fewer than `top_k` valid results are returned; if the capped initial search
already fills `top_k`,
+probing stops.
+
`ivf.refine_factor` can also be configured with `refine_factor`,
`rerank_factor`, and hyphenated
spellings such as `ivf.refine-factor`. Setting `ivf.refine_factor=1` still
performs the raw-vector
rerank for the indexed candidates; leaving it unset skips the rerank stage.
diff --git
a/paimon-vector/src/main/java/org/apache/paimon/vector/index/NativeVectorGlobalIndexReader.java
b/paimon-vector/src/main/java/org/apache/paimon/vector/index/NativeVectorGlobalIndexReader.java
index 4056212e92..7b9c2d7872 100644
---
a/paimon-vector/src/main/java/org/apache/paimon/vector/index/NativeVectorGlobalIndexReader.java
+++
b/paimon-vector/src/main/java/org/apache/paimon/vector/index/NativeVectorGlobalIndexReader.java
@@ -65,6 +65,11 @@ public class NativeVectorGlobalIndexReader implements
GlobalIndexReader {
private static final String NPROBE_PARAMETER = "ivf.nprobe";
private static final String L_SEARCH_PARAMETER = "diskann.l_search";
+ private static final String MAX_INITIAL_FILTER_EXPANSION_FACTOR_PARAMETER =
+ "ivf.max_initial_filter_expansion_factor";
+ private static final String IVF_PQ_BATCH_TABLE_REUSE_PARAMETER =
"ivf_pq.batch_table_reuse";
+ private static final String IVF_PQ_BATCH_TABLE_REUSE_MAX_BYTES_PARAMETER =
+ "ivf_pq.batch_table_reuse.max_bytes";
private static final int VECTOR_INDEX_MIN_SEEK_FOR_VECTOR_READS = 16 *
1024;
private static final int VECTOR_INDEX_PARALLELISM_FOR_VECTOR_READS = 32;
@@ -151,6 +156,8 @@ public class NativeVectorGlobalIndexReader implements
GlobalIndexReader {
if (scope == null) {
return emptyResults(n);
}
+ VectorSearchParams searchParams =
+ batchSearchParams(batchVectorSearch.options(),
scope.effectiveK);
// Flatten query vectors into one contiguous array for a single native
call.
float[] queries = new float[n * dim];
@@ -160,15 +167,8 @@ public class NativeVectorGlobalIndexReader implements
GlobalIndexReader {
VectorSearchBatchResult batchResult =
scope.filterBytes != null
- ? vectorReader.searchBatch(
- queries,
- n,
- searchParams(batchVectorSearch.options(),
scope.effectiveK),
- scope.filterBytes)
- : vectorReader.searchBatch(
- queries,
- n,
- searchParams(batchVectorSearch.options(),
scope.effectiveK));
+ ? vectorReader.searchBatch(queries, n, searchParams,
scope.filterBytes)
+ : vectorReader.searchBatch(queries, n, searchParams);
// result i corresponds to vectors[i], matching input order.
List<Optional<ScoredGlobalIndexResult>> results = new ArrayList<>(n);
@@ -287,17 +287,36 @@ public class NativeVectorGlobalIndexReader implements
GlobalIndexReader {
static VectorSearchParams searchParams(Map<String, String> parameters, int
topK) {
Integer nprobe = intParameter(parameters, NPROBE_PARAMETER);
Integer lSearch = intParameter(parameters, L_SEARCH_PARAMETER);
+ Integer maxInitialFilterExpansionFactor =
+ intParameter(parameters,
MAX_INITIAL_FILTER_EXPANSION_FACTOR_PARAMETER);
if (nprobe != null && lSearch != null) {
throw new IllegalArgumentException(
"Cannot set both '" + NPROBE_PARAMETER + "' and '" +
L_SEARCH_PARAMETER + "'.");
}
+ VectorSearchParams searchParams;
if (nprobe != null) {
- return VectorSearchParams.ivf(topK, nprobe);
- }
- if (lSearch != null) {
- return VectorSearchParams.diskAnn(topK, lSearch);
- }
- return VectorSearchParams.automatic(topK);
+ searchParams = VectorSearchParams.ivf(topK, nprobe);
+ } else if (lSearch != null) {
+ searchParams = VectorSearchParams.diskAnn(topK, lSearch);
+ } else {
+ searchParams = VectorSearchParams.automatic(topK);
+ }
+ return maxInitialFilterExpansionFactor == null
+ ? searchParams
+ :
searchParams.withMaxInitialFilterExpansionFactor(maxInitialFilterExpansionFactor);
+ }
+
+ static VectorSearchParams batchSearchParams(Map<String, String>
parameters, int topK) {
+ VectorSearchParams searchParams = searchParams(parameters, topK);
+ String reuseMode = parameters.get(IVF_PQ_BATCH_TABLE_REUSE_PARAMETER);
+ if (reuseMode != null) {
+ searchParams = searchParams.withIvfPqBatchTableReuse(reuseMode);
+ }
+ Long reuseMaxBytes =
+ longParameter(parameters,
IVF_PQ_BATCH_TABLE_REUSE_MAX_BYTES_PARAMETER);
+ return reuseMaxBytes == null
+ ? searchParams
+ : searchParams.withIvfPqBatchTableReuseMaxBytes(reuseMaxBytes);
}
private static Integer intParameter(Map<String, String> parameters, String
key) {
@@ -313,6 +332,19 @@ public class NativeVectorGlobalIndexReader implements
GlobalIndexReader {
}
}
+ private static Long longParameter(Map<String, String> parameters, String
key) {
+ String value = parameters.get(key);
+ if (value == null) {
+ return null;
+ }
+ try {
+ return Long.parseLong(value);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(
+ "Invalid value for '" + key + "': " + value + ". Must be a
long integer.", e);
+ }
+ }
+
private void validateSearchVector(Object vector) {
if (!(vector instanceof float[])) {
throw new IllegalArgumentException(
diff --git
a/paimon-vector/src/test/java/org/apache/paimon/vector/index/NativeVectorGlobalIndexTest.java
b/paimon-vector/src/test/java/org/apache/paimon/vector/index/NativeVectorGlobalIndexTest.java
index eb7750915a..d4aeb7f316 100644
---
a/paimon-vector/src/test/java/org/apache/paimon/vector/index/NativeVectorGlobalIndexTest.java
+++
b/paimon-vector/src/test/java/org/apache/paimon/vector/index/NativeVectorGlobalIndexTest.java
@@ -27,6 +27,7 @@ import org.apache.paimon.globalindex.ResultEntry;
import org.apache.paimon.globalindex.ScoredGlobalIndexResult;
import org.apache.paimon.globalindex.io.GlobalIndexFileReader;
import org.apache.paimon.globalindex.io.GlobalIndexFileWriter;
+import org.apache.paimon.index.vector.IvfPqBatchTableReuseMode;
import org.apache.paimon.index.vector.VectorSearchParams;
import org.apache.paimon.options.Options;
import org.apache.paimon.predicate.BatchVectorSearch;
@@ -255,6 +256,55 @@ public class NativeVectorGlobalIndexTest {
assertThat(diskAnnParams.topK()).isEqualTo(10);
}
+ @Test
+ public void testIvfInitialFilterExpansionFactorValidationIsPropagated() {
+ assertThatThrownBy(
+ () ->
+ NativeVectorGlobalIndexReader.searchParams(
+ Collections.singletonMap(
+
"ivf.max_initial_filter_expansion_factor", "0"),
+ 10))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("greater than 0");
+
+ Map<String, String> parameters = new HashMap<>();
+ parameters.put("ivf.nprobe", "16");
+ parameters.put("ivf.max_initial_filter_expansion_factor", "4");
+ assertThatThrownBy(() ->
NativeVectorGlobalIndexReader.searchParams(parameters, 10))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("requires automatic IVF search");
+ }
+
+ @Test
+ public void testIvfPqBatchTableReuseIsPropagatedToBatchSearchParams() {
+ VectorSearchParams params =
+ NativeVectorGlobalIndexReader.batchSearchParams(
+ Collections.singletonMap("ivf_pq.batch_table_reuse",
"on"), 10);
+
+
assertThat(params.ivfPqBatchTableReuse()).isEqualTo(IvfPqBatchTableReuseMode.ON);
+ }
+
+ @Test
+ public void testIvfPqBatchTableReuseMaxBytesIsPropagated() {
+ VectorSearchParams params =
+ NativeVectorGlobalIndexReader.batchSearchParams(
+
Collections.singletonMap("ivf_pq.batch_table_reuse.max_bytes", "134217728"),
+ 10);
+
+ assertThat(params.ivfPqBatchTableReuseMaxBytes()).isEqualTo(128L *
1024 * 1024);
+ }
+
+ @Test
+ public void testIvfPqBatchTableReuseMaxBytesSupportsLongValues() {
+ VectorSearchParams params =
+ NativeVectorGlobalIndexReader.batchSearchParams(
+ Collections.singletonMap(
+ "ivf_pq.batch_table_reuse.max_bytes",
"5368709120"),
+ 10);
+
+ assertThat(params.ivfPqBatchTableReuseMaxBytes()).isEqualTo(5L * 1024
* 1024 * 1024);
+ }
+
@Test
public void testVectorSearchParameterRangeValidationDelegatedToNative() {
assertThat(