Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions python/semantic_kernel/connectors/oracle.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
import re
import sys
import uuid
from collections.abc import AsyncIterable, Mapping, Sequence
from collections.abc import AsyncIterable, Callable, Mapping, Sequence
from inspect import getsource
from typing import Any, ClassVar, Final, Generic, TypeVar

# Third-party Libraries
Expand All @@ -26,6 +27,7 @@
GetFilteredRecordOptions,
IndexKind,
KernelSearchResults,
LambdaVisitor,
SearchType,
VectorSearch,
VectorSearchOptions,
Expand All @@ -45,7 +47,7 @@

# Semantic Kernel Utilities & Config
from semantic_kernel.kernel_pydantic import KernelBaseSettings
from semantic_kernel.kernel_types import OneOrMany
from semantic_kernel.kernel_types import OneOrMany, OptionalOneOrMany
from semantic_kernel.utils.feature_stage_decorator import release_candidate

oracledb.defaults.fetch_lobs = False
Expand Down Expand Up @@ -1010,6 +1012,29 @@ async def _inner_search_vector(
bind_values.extend(filter_binds)
return query, bind_values, columns

@override
def _build_filter(self, search_filter: OptionalOneOrMany[Callable | str] | None) -> OptionalOneOrMany[Any]:
"""Build one (sql_expression, bind_values_dict) tuple from one or more filters.

Multiple filters are combined with AND. All filters share one bind counter, so the generated
bind variable names stay unique across the combined WHERE clause.
"""
if not search_filter:
return None
filters = search_filter if isinstance(search_filter, list) else [search_filter]
bind_counter = BindCounter()
visitor = LambdaVisitor(lambda node: self._lambda_parser(node, bind_counter))
for filter_ in filters:
visitor.visit(ast.parse(filter_ if isinstance(filter_, str) else getsource(filter_).strip())) # type: ignore[arg-type]
if not visitor.output_filters:
raise VectorStoreOperationException("No filter strings found.")
if len(visitor.output_filters) == 1:
return visitor.output_filters[0]
combined_binds: dict[str, Any] = {}
for _, binds in visitor.output_filters:
combined_binds.update(binds)
return (" AND ".join(f"({sql})" for sql, _ in visitor.output_filters), combined_binds)

@override
def _lambda_parser(self, node: ast.AST, bind_counter: BindCounter | None = None) -> Any:
"""Parse a lambda AST node and return a tuple: (sql_expression, bind_values_dict).
Expand Down
40 changes: 40 additions & 0 deletions python/tests/unit/connectors/memory/test_oracle.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from semantic_kernel.data.vector import (
DistanceFunction,
IndexKind,
VectorSearchOptions,
VectorStoreCollectionDefinition,
VectorStoreField,
vectorstoremodel,
Expand Down Expand Up @@ -419,3 +420,42 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
assert len(results) == 1
assert results[0].record.id == 1
assert results[0].record.vector == [0.1, 0.2, 0.3]


@vectorstoremodel
@dataclass
class FilterModel:
id: Annotated[int, VectorStoreField("key")]
color: Annotated[str, VectorStoreField("data", is_indexed=True)]
size: Annotated[int, VectorStoreField("data", is_indexed=True)]
vector: Annotated[
list[float] | None,
VectorStoreField("vector", type="float", dimensions=3, distance_function=DistanceFunction.COSINE_DISTANCE),
] = None


@pytest.mark.asyncio
async def test_search_multiple_filters_are_combined_with_and(oracle_store):
collection = oracle_store.get_collection(
model=FilterModel, record_type=FilterModel, collection_name="MY_COLLECTION"
)
options = VectorSearchOptions(
vector_property_name="vector",
filter=["lambda x: x.color == 'red'", "lambda x: x.size > 5 or x.size < 2"],
)
sql, binds, _ = await collection._inner_search_vector(options, None, [0.1, 0.2, 0.3])

assert 'WHERE ("color" = :bind_val1) AND (("size" > :bind_val2 OR "size" < :bind_val3))' in sql
assert binds[1:] == ["red", 5, 2]


@pytest.mark.asyncio
async def test_search_single_filter_is_unchanged(oracle_store):
collection = oracle_store.get_collection(
model=FilterModel, record_type=FilterModel, collection_name="MY_COLLECTION"
)
options = VectorSearchOptions(vector_property_name="vector", filter="lambda x: x.color == 'red'")
sql, binds, _ = await collection._inner_search_vector(options, None, [0.1, 0.2, 0.3])

assert 'WHERE "color" = :bind_val1' in sql
assert binds[1:] == ["red"]
Loading