blob: f984df8fbc22739abd94880ad136dcf4df794c47 [file]
# 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.search_execution import RaySearchTaskExecutor
from pypaimon.tests.search_execution_test_helpers import task_parts
from pypaimon.execution.python.read.search import distributed_vector_search as search_module
from pypaimon.read.table_read import TableRead
from pypaimon.tests import ray_vector_search_test as fixtures
from pypaimon.tests.ray_vector_search_test import VECTORS, add_rows, build_index, ids
from pypaimon.utils.range import Range
from pypaimon.utils.roaring_bitmap import RoaringBitmap64
ray_cluster = fixtures.ray_cluster
table = fixtures.table
@pytest.mark.parametrize("metric", ["l2", "cosine", "inner_product"])
@pytest.mark.parametrize("concurrency", [1, 2])
def test_refinement_reads_vectors_only_on_workers(table, ray_cluster, metric, concurrency):
add_rows(table, VECTORS[:3])
add_rows(table, VECTORS[3:], 3)
build_index(table, metric)
query = table.search([1., 1.], options={"refine_factor": "2"}).select(["id"]).limit(3)
expected = ids(query.to_arrow())
original_read = TableRead._new_arrow_batch_reader
original_map = RaySearchTaskExecutor.map
dispatched = []
def no_driver_vectors(read, *args, **kwargs):
assert "embedding" not in [field.name for field in read.read_type]
return original_read(read, *args, **kwargs)
def record(executor, tasks, ordered=False):
worker, context, items = task_parts(tasks)
if worker is search_module._search_raw_split:
dispatched.append(len(items))
return original_map(executor, tasks, ordered=ordered)
with patch.object(TableRead, "_new_arrow_batch_reader", no_driver_vectors), \
patch.object(RaySearchTaskExecutor, "map", record):
assert ids(query.to_arrow(execution="ray", concurrency=concurrency)) == expected
assert dispatched == [2]
def test_candidate_intersection_excludes_nan_and_streams_real_splits(table, ray_cluster):
add_rows(table, [[0., 1.], [float("nan"), 1.]])
add_rows(table, [[float("nan"), 1.], [1., 1.]], 2)
query = table.search([1., 1.], options={"metric": "l2"}).limit(1)._for_execution()
reader = query._vector_search_builder(query).new_vector_search_read()
distributed = search_module.DistributedVectorSearchRead(reader, RaySearchTaskExecutor(2, {}))
candidates = RoaringBitmap64()
candidates.add(0)
candidates.add(3)
result = distributed._read_raw_search(
[Range(0, 3)], None, [1., 1.], score_candidates=candidates,
snapshot=query._table._read_snapshot)
assert list(result.results()) == [3]
assert result.score_getter()(3) == 1.0
candidates.add(1)
with pytest.raises(ValueError, match="cannot rank NaN"):
distributed._read_raw_search(
[Range(0, 3)], None, [1., 1.], score_candidates=candidates,
snapshot=query._table._read_snapshot)
def test_empty_refinement_does_not_plan_or_dispatch(table):
query = table.search([1., 1.]).limit(1)._for_execution()
reader = query._vector_search_builder(query).new_vector_search_read()
distributed = search_module.DistributedVectorSearchRead(reader, RaySearchTaskExecutor(2, {}))
with patch.object(distributed, "_plan_raw_read", side_effect=AssertionError("empty read")), \
patch.object(RaySearchTaskExecutor, "map", side_effect=AssertionError("empty task")):
result = distributed._read_raw_search(
[Range(0, 100)], None, [1., 1.], score_candidates=RoaringBitmap64())
assert result.results().is_empty()
@pytest.mark.parametrize("selector", ["snapshot", "tag"])
def test_refinement_keeps_historical_filters_and_deletions(table, ray_cluster, selector):
add_rows(table, VECTORS)
build_index(table)
table.update("id = 2", {"category": "no"})
table.delete("id = 0")
snapshot = table.raw_table.snapshot_manager().get_latest_snapshot()
table.raw_table.create_tag("saved", snapshot_id=snapshot.id)
table.delete("id = 4")
if selector == "tag":
table.raw_table.file_io.delete(table.raw_table.snapshot_manager().get_snapshot_path(snapshot.id))
options = {"tag_name": "saved"}
else:
options = {"snapshot_id": snapshot.id}
query = table.search([1., 1.], pre_filter="category = 'yes'",
options={"refine_factor": "2"}, **options).select(["id"]).limit(3)
assert ids(query.to_arrow()) == [4]
assert ids(query.to_arrow(execution="ray")) == [4]
def test_commit_between_candidate_selection_and_refinement_keeps_snapshot(table, ray_cluster):
add_rows(table, VECTORS)
build_index(table)
table.raw_table = table.raw_table.copy({"global-index.column-update-action": "DROP_PARTITION_INDEX"})
query = table.search([1., 1.], options={"refine_factor": "2"}).select(["id"]).limit(1)
expected = ids(query.to_arrow())
original = RaySearchTaskExecutor.map
changed = []
def commit_before_refine(executor, tasks, ordered=False):
worker, context, items = task_parts(tasks)
if worker is search_module._search_raw_split and not changed:
table.update("id = 0", {"embedding": [100., 1.]})
changed.append(True)
return original(executor, tasks, ordered=ordered)
with patch.object(RaySearchTaskExecutor, "map", commit_before_refine):
assert ids(query.to_arrow(execution="ray")) == expected
assert changed == [True]
assert ids(query.to_arrow()) != expected
assert ids(query.to_arrow(execution="ray")) == ids(query.to_arrow())