blob: fda7e15fbdf797c21b1c5874c21f23598e53a6ea [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.
"""Coordinate search execution and transport without coupling the backends.
The selected backend consumes ordinary snapshot-and-split plans, including
transported plans. An execution error never triggers another search.
"""
from contextlib import contextmanager
from copy import copy
from functools import partial
from threading import local
_search_execution = local()
@contextmanager
def _python_search_execution():
"""Retain Python selection through hooks delegating to public methods."""
previous = getattr(_search_execution, 'python', False)
_search_execution.python = True
try:
yield
finally:
_search_execution.python = previous
def _python_search_request(request):
if getattr(_search_execution, 'python', False):
request = copy(request)
request._table = request._table.copy_without_time_travel({
'read.native.enabled': 'false', 'scan.native-plan.enabled': 'false'})
return request
def _python_vector_scan(execution):
from pypaimon.execution.python.read.search.factories import new_vector_scan
return new_vector_scan(_python_search_request(execution))
def _python_vector_read(builder, batch):
from pypaimon.execution.python.read.search.factories import new_batch_vector_read, new_vector_read
builder = _python_search_request(builder)
return new_batch_vector_read(builder) if batch else new_vector_read(builder)
def _python_full_text_scan(execution):
from pypaimon.execution.python.read.search.factories import new_full_text_scan
return new_full_text_scan(_python_search_request(execution))
def _python_full_text_read(execution):
from pypaimon.execution.python.read.search.factories import new_full_text_read
return new_full_text_read(_python_search_request(execution))
class _SearchScan:
def __init__(self, native, python_factory):
self._native_scan = native
self._python_factory = python_factory
self._python_executor = None
def scan(self):
if self._native_scan.available():
return self._native_scan.scan()
if self._python_executor is None:
self._python_executor = self._python_factory()
return self._python_executor.scan()
class _SearchRead:
def __init__(self, native, python_factory):
self._native_reader = native
self._python_factory = python_factory
self._python_executor = None
def _python_reader(self):
if self._python_executor is None:
self._python_executor = self._python_factory()
return self._python_executor
def read_plan(self, plan, executor=None):
return (self._native_reader.read_plan(plan) if executor is None else
self._native_reader.read_plan(plan, executor=executor))
def read_batch_plan(self, plan, executor=None):
return (self._native_reader.read_batch_plan(plan) if executor is None else
self._native_reader.read_batch_plan(plan, executor=executor))
class _SplitSearchRead(_SearchRead):
def read(self, splits):
return self._native_reader.read(splits)
class _BatchSearchRead(_SearchRead):
def read_batch(self, splits):
return self._native_reader.read_batch(splits)
def python_search_reader(reader):
"""Select Python explicitly for backend parity and transported-plan checks."""
return reader._python_reader() if isinstance(reader, _SearchRead) else reader
def _native_requested(builder):
if getattr(_search_execution, 'python', False):
return False
table = getattr(builder, '_table', None)
enabled = getattr(getattr(table, 'options', None), 'native_read_enabled', None)
return callable(enabled) and enabled()
def create_vector_scan(builder, execution, batch=False):
python_factory = partial(_python_vector_scan, execution)
if not _native_requested(builder):
return python_factory()
from pypaimon.execution.native.read.native_search_plan import native_vector_scan
native = native_vector_scan(builder, batch)
return python_factory() if native is None else _SearchScan(native, python_factory)
def create_vector_read(builder, batch=False):
python_factory = partial(_python_vector_read, builder, batch)
if not _native_requested(builder):
return python_factory()
from pypaimon.execution.native.read.native_search_plan import native_vector_read
native = native_vector_read(builder, batch)
if native is None:
return python_factory()
reader_type = _BatchSearchRead if batch else _SplitSearchRead
return reader_type(native, python_factory)
def create_full_text_scan(builder, execution):
python_factory = partial(_python_full_text_scan, execution)
if not _native_requested(builder):
return python_factory()
from pypaimon.execution.native.read.native_search_plan import native_full_text_scan
native = native_full_text_scan(builder)
return python_factory() if native is None else _SearchScan(native, python_factory)
def create_full_text_read(builder, execution):
python_factory = partial(_python_full_text_read, execution)
if not _native_requested(builder):
return python_factory()
from pypaimon.execution.native.read.native_search_plan import native_full_text_read
native = native_full_text_read(builder)
return python_factory() if native is None else _SplitSearchRead(native, python_factory)
def execute_vector_search(builder, queries, batch=False):
if _native_requested(builder):
from pypaimon.execution.native.read.native_vector_search import try_native_vector_search
result = try_native_vector_search(builder, queries, batch)
if result is not None:
return result
reader = (builder.new_batch_vector_search_read() if batch
else builder.new_vector_search_read())
plan = builder.new_vector_search_scan().scan()
return reader.read_batch_plan(plan) if batch else reader.read_plan(plan)
def execute_full_text_search(builder):
if _native_requested(builder):
from pypaimon.execution.native.read.native_full_text_search import try_native_full_text_search
result = try_native_full_text_search(builder)
if result is not None:
return result
return builder.new_full_text_read().read_plan(builder.new_full_text_scan().scan())
def _standard_hybrid_builder(builder):
from pypaimon.table.source.hybrid_search_builder import HybridSearchBuilderImpl
# Instance and subclass hooks own their execution protocol. Native cannot
# replace them, and copying their owner would change observable side effects.
return type(builder) is HybridSearchBuilderImpl and not any(name in vars(builder) for name in (
'route_builders', 'to_route_result', 'rank', '_new_vector_search_builder', '_new_full_text_search_builder'))
def execute_hybrid_search(builder, executor=None):
standard = _standard_hybrid_builder(builder)
if standard and _native_requested(builder):
from pypaimon.execution.native.read.native_hybrid_search import try_native_hybrid_search
result = (try_native_hybrid_search(builder) if executor is None else
try_native_hybrid_search(builder, executor=executor))
if result is not None:
return result
from pypaimon.execution.python.read.search.hybrid_search import execute
return execute(builder, executor)
def hybrid_route_builders(builder):
from pypaimon.catalog.table_query_auth import reject_search_under_query_auth
# Authorization precedes runtime discovery and snapshot resolution, as in
# Java's routeBuilders(). Selecting a backend must not hide a rejection.
reject_search_under_query_auth(builder._table)
if _native_requested(builder):
from pypaimon.execution.native.read.native_hybrid_search import try_native_hybrid_routes
routes = try_native_hybrid_routes(builder)
if routes is not None:
return routes
from pypaimon.execution.python.read.search.hybrid_search import route_builders
return route_builders(builder)
def rank_hybrid_results(builder, route_results):
builder._validate_search()
if _native_requested(builder):
from pypaimon.execution.native.runtime import native_runtime_available
if native_runtime_available():
from pypaimon.execution.native.read.native_hybrid_search import rank_hybrid_results as rank
return rank(builder, route_results)
from pypaimon.execution.python.read.search.hybrid_search import rank
return rank(builder, route_results)