blob: 1e094e543f62447114aef9a94f99a784280097de [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.
"""Builder to build full-text search."""
from abc import ABC, abstractmethod
from typing import Optional
from pypaimon.common.predicate_builder import PredicateBuilder
from pypaimon.globalindex.global_index_result import GlobalIndexResult
from pypaimon.table.source.full_text_read import FullTextRead, DataEvolutionFullTextRead
from pypaimon.table.source.full_text_scan import FullTextScan, DataEvolutionFullTextScan
from pypaimon.common.options.core_options import CoreOptions
from pypaimon.common.options.options import Options
from pypaimon.index.pk.primary_key_index_definition import PrimaryKeyIndexFamily
from pypaimon.index.pk.primary_key_index_definitions import PrimaryKeyIndexDefinitions
class FullTextSearchBuilder(ABC):
"""Builder to build full-text search."""
@abstractmethod
def with_limit(self, limit: int) -> 'FullTextSearchBuilder':
"""The top k results to return."""
pass
@abstractmethod
def with_query(self, field_name: str, query: str) -> 'FullTextSearchBuilder':
"""The full-text query string to search against the given field."""
pass
@abstractmethod
def with_partition_filter(self, partition_filter) -> 'FullTextSearchBuilder':
"""Partition predicate used to prune index manifest entries."""
pass
@abstractmethod
def new_full_text_scan(self) -> FullTextScan:
"""Create full-text scan to scan index files."""
pass
@abstractmethod
def new_full_text_read(self) -> FullTextRead:
"""Create full-text read to read index files."""
pass
def execute_local(self) -> GlobalIndexResult:
"""Execute full-text index search in local."""
return self.new_full_text_read().read_plan(self.new_full_text_scan().scan())
class FullTextSearchBuilderImpl(FullTextSearchBuilder):
"""Implementation for FullTextSearchBuilder."""
def __init__(self, table: 'FileStoreTable'):
self._table = table
self._limit: int = 0
self._field_name: Optional[str] = None
self._query: Optional[str] = None
self._partition_filter = None
def with_limit(self, limit: int) -> 'FullTextSearchBuilder':
self._limit = limit
return self
def with_query(self, field_name: str, query: str) -> 'FullTextSearchBuilder':
self._field_name = field_name
self._query = query
return self
def with_partition_filter(self, partition_filter) -> 'FullTextSearchBuilder':
if partition_filter is None:
self._partition_filter = None
return self
partition_keys = list(self._table.partition_keys or [])
if not partition_keys:
raise ValueError(
"with_partition_filter called on a non-partitioned table")
from pypaimon.read.push_down_utils import _get_all_fields
referenced = _get_all_fields(partition_filter)
extras = referenced - set(partition_keys)
if extras:
raise ValueError(
"Partition filter must reference only partition keys "
"(%s); got non-partition field(s): %s"
% (partition_keys, sorted(extras)))
rebuilt = self._rebuild_leaf_indices_by_name(
partition_filter,
{name: idx for idx, name in enumerate(partition_keys)},
)
if self._partition_filter is None:
self._partition_filter = rebuilt
else:
self._partition_filter = PredicateBuilder.and_predicates(
[self._partition_filter, rebuilt])
return self
@classmethod
def _rebuild_leaf_indices_by_name(cls, predicate, name_to_idx):
if predicate.method in ('and', 'or'):
return predicate.new_literals(
[cls._rebuild_leaf_indices_by_name(c, name_to_idx)
for c in (predicate.literals or [])])
return predicate.new_index(name_to_idx[predicate.field])
def new_full_text_scan(self) -> FullTextScan:
definition = self._primary_key_full_text_definition()
if definition is not None:
from pypaimon.table.source.primary_key_full_text_scan import PrimaryKeyFullTextScan
return PrimaryKeyFullTextScan(
self._table, definition, partition_filter=self._partition_filter)
return DataEvolutionFullTextScan(
self._table,
self._text_columns(),
partition_filter=self._partition_filter,
)
def new_full_text_read(self) -> FullTextRead:
if self._limit <= 0:
raise ValueError("Limit must be positive, set via with_limit()")
definition = self._primary_key_full_text_definition()
if definition is not None:
from pypaimon.common.options.core_options import GlobalIndexSearchMode
mode = CoreOptions(
Options(dict(self._table.table_schema.options))
).full_text_index_search_mode()
if mode != GlobalIndexSearchMode.FAST:
raise NotImplementedError(
"Primary-key full-text search only supports the FAST "
"full-text index search mode; FULL and DETAIL require "
"merge-aware logical-row fallback.")
from pypaimon.table.source.primary_key_full_text_read import PrimaryKeyFullTextRead
return PrimaryKeyFullTextRead(
self._table, self._limit, self._text_columns(), self._query,
definition=definition, partition_filter=self._partition_filter)
return DataEvolutionFullTextRead(
self._table,
self._limit,
self._text_columns(),
self._query,
partition_filter=self._partition_filter,
)
def _text_columns(self):
if self._query is None:
raise ValueError("Query must be set via with_query()")
if self._field_name is None:
raise ValueError("Field name must be set via with_query()")
field_dict = {f.name: f for f in self._table.fields}
if self._field_name not in field_dict:
raise ValueError(
f"Text column '{self._field_name}' not found in table schema")
return [field_dict[self._field_name]]
def _primary_key_full_text_definition(self):
text_column = self._text_columns()[0]
core = CoreOptions(Options(dict(self._table.table_schema.options)))
if (core.data_evolution_enabled()
or not core.primary_key_full_text_index_columns()):
return None
for definition in PrimaryKeyIndexDefinitions.create(
self._table.table_schema).definitions:
if (definition.family == PrimaryKeyIndexFamily.FULL_TEXT
and definition.field_id == text_column.id):
return definition
return None