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 61b2d64c9d [python] Support Ray options in vector result conversions
(#9998)
61b2d64c9d is described below
commit 61b2d64c9df312d2dc02cfe79a1baf213375109a
Author: chaoyang <[email protected]>
AuthorDate: Sun Sep 20 16:11:15 2026 +0800
[python] Support Ray options in vector result conversions (#9998)
---
docs/docs/pypaimon/multimodal-search.md | 6 ++
paimon-python/pypaimon/multimodal/query.py | 24 +++++-
.../tests/ray_vector_result_conversion_test.py | 87 ++++++++++++++++++++++
3 files changed, 113 insertions(+), 4 deletions(-)
diff --git a/docs/docs/pypaimon/multimodal-search.md
b/docs/docs/pypaimon/multimodal-search.md
index 2fc052bd7f..8c98e2e331 100644
--- a/docs/docs/pypaimon/multimodal-search.md
+++ b/docs/docs/pypaimon/multimodal-search.md
@@ -147,6 +147,12 @@ neighbors = (
the number of in-flight Ray tasks, with a default of 4. `ray_remote_args`
supplies
Ray task options, including resources and retry settings; `num_returns` is
managed by PyPaimon. These two arguments require `execution="ray"`.
+
+For single and batch vector queries, `to_pandas()` and `to_list()` accept the
+same `execution`, `concurrency`, and `ray_remote_args` arguments as
`to_arrow()`.
+For example, use `query.to_pandas(execution="ray", concurrency=4)` to return a
+DataFrame, or one DataFrame per query for a batch. Result conversion runs on
the
+driver after the search and lookup complete.
Single-vector Ray queries require finite query values and fail if stored
vectors produce NaN scores.
diff --git a/paimon-python/pypaimon/multimodal/query.py
b/paimon-python/pypaimon/multimodal/query.py
index c3aa7b1bd2..af2a983f9a 100644
--- a/paimon-python/pypaimon/multimodal/query.py
+++ b/paimon-python/pypaimon/multimodal/query.py
@@ -554,6 +554,18 @@ class VectorQuery(_PreFilterQuery):
concurrency=concurrency, ray_remote_args=ray_remote_args)
return query._read_global_index_result(result)
+ def to_pandas(self, *, execution="local", concurrency=None,
ray_remote_args=None):
+ """Execute with the same options as to_arrow and return a DataFrame."""
+ return self.to_arrow(
+ execution=execution, concurrency=concurrency,
+ ray_remote_args=ray_remote_args).to_pandas()
+
+ def to_list(self, *, execution="local", concurrency=None,
ray_remote_args=None) -> List[dict]:
+ """Execute with the same options as to_arrow and return rows."""
+ return self.to_arrow(
+ execution=execution, concurrency=concurrency,
+ ray_remote_args=ray_remote_args).to_pylist()
+
def _execute_vector(self, query):
return self._vector_search_builder(query).execute_local()
@@ -716,11 +728,15 @@ class BatchVectorQuery(_PreFilterQuery):
output.append(table.take(pa.array(selected, type=pa.int64())))
return output
- def to_pandas(self):
- return [table.to_pandas() for table in self.to_arrow()]
+ def to_pandas(self, *, execution="local", concurrency=None,
ray_remote_args=None):
+ """Return one DataFrame per query, using the same options as
to_arrow."""
+ return [table.to_pandas() for table in self.to_arrow(
+ execution=execution, concurrency=concurrency,
ray_remote_args=ray_remote_args)]
- def to_list(self) -> List[List[dict]]:
- return [table.to_pylist() for table in self.to_arrow()]
+ def to_list(self, *, execution="local", concurrency=None,
ray_remote_args=None) -> List[List[dict]]:
+ """Return rows per query, using the same options as to_arrow."""
+ return [table.to_pylist() for table in self.to_arrow(
+ execution=execution, concurrency=concurrency,
ray_remote_args=ray_remote_args)]
def _execute_batch_vector(self, query):
return self._batch_vector_search_builder(query).execute_batch_local()
diff --git a/paimon-python/pypaimon/tests/ray_vector_result_conversion_test.py
b/paimon-python/pypaimon/tests/ray_vector_result_conversion_test.py
new file mode 100644
index 0000000000..5c6925c7b5
--- /dev/null
+++ b/paimon-python/pypaimon/tests/ray_vector_result_conversion_test.py
@@ -0,0 +1,87 @@
+# 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.
+
+from unittest.mock import patch
+
+import pytest
+
+pytest.importorskip("ray")
+
+from pypaimon.ray import batch_vector_search, vector_search
+from pypaimon.tests import ray_vector_search_test as fixtures
+from pypaimon.tests.ray_vector_search_test import VECTORS, add_rows,
build_index
+
+ray_cluster = fixtures.ray_cluster
+table = fixtures.table
+
+
+def make_query(table, batch):
+ queries = [[1., 1.], [-1., 2.], [1., 1.]]
+ return (table.search_vectors(queries) if batch else
table.search(queries[0])).select(["id"]).limit(2)
+
+
[email protected]("batch", [False, True])
[email protected]("method", ["to_pandas", "to_list"])
[email protected]("indexed", [False, True])
+def test_ray_result_conversion(table, ray_cluster, batch, method, indexed):
+ add_rows(table, VECTORS)
+ if indexed:
+ build_index(table)
+ add_rows(table, [[1., 1.]], 6)
+ query = make_query(table, batch)
+ expected = query.to_list()
+ module = batch_vector_search if batch else vector_search
+ name = "_execute_batch_vector_search" if batch else
"_execute_vector_search"
+ with patch.object(module, name, wraps=getattr(module, name)) as execute:
+ actual = getattr(query, method)(
+ execution="ray", concurrency=2, ray_remote_args={"num_cpus": 1})
+ execute.assert_called_once()
+ assert execute.call_args[1] == {"concurrency": 2, "ray_remote_args":
{"num_cpus": 1}}
+ if method == "to_pandas":
+ actual = [frame.to_dict("records") for frame in actual] if batch else
actual.to_dict("records")
+ assert actual == expected
+
+
[email protected]("batch", [False, True])
[email protected]("method", ["to_pandas", "to_list"])
+def test_conversion_keeps_empty_results_and_default_local_execution(table,
batch, method):
+ query = make_query(table, batch)
+ with patch.object(vector_search, "_execute_vector_search",
side_effect=AssertionError("Ray dispatch")), \
+ patch.object(batch_vector_search, "_execute_batch_vector_search",
+ side_effect=AssertionError("Ray dispatch")):
+ actual = getattr(query, method)()
+ values = actual if batch else [actual]
+ assert len(values) == (3 if batch else 1)
+ assert all(len(value) == 0 for value in values)
+ if method == "to_pandas":
+ assert all(list(frame.columns) == ["id"] for frame in values)
+
+
[email protected]("batch", [False, True])
[email protected]("method", ["to_pandas", "to_list"])
+def test_conversion_validates_options_and_propagates_failure(table, batch,
method):
+ query = make_query(table, batch)
+ with pytest.raises(ValueError, match="execution must"):
+ getattr(query, method)(execution="invalid")
+ with pytest.raises(ValueError, match="require execution"):
+ getattr(query, method)(concurrency=2)
+ module = batch_vector_search if batch else vector_search
+ name = "_execute_batch_vector_search" if batch else
"_execute_vector_search"
+ failure = RuntimeError("worker failed")
+ with patch.object(module, name, side_effect=failure),
pytest.raises(RuntimeError) as exc:
+ getattr(query, method)(execution="ray")
+ assert exc.value is failure