Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
84 changes: 69 additions & 15 deletions python/semantic_kernel/connectors/azure_ai_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,19 +149,43 @@ class AzureAISearchSettings(KernelBaseSettings):


def _get_search_client(
search_index_client: SearchIndexClient, collection_name: str | None, **kwargs: Any
endpoint: str,
collection_name: str | None,
credential: "AzureKeyCredential | AsyncTokenCredential | TokenCredential",
**kwargs: Any,
) -> SearchClient:
"""Create a search client for a collection."""
if not collection_name:
raise VectorStoreInitializationException("Collection name is required to create a search client.")
try:
return SearchClient(search_index_client._endpoint, collection_name, search_index_client._credential, **kwargs)
return SearchClient(endpoint, collection_name, credential, **kwargs)
except ValueError as exc:
raise VectorStoreInitializationException(
f"Failed to create Azure Cognitive Search client for collection {collection_name}."
) from exc


def _resolve_credential(
azure_ai_search_settings: AzureAISearchSettings,
azure_credential: AzureKeyCredential | None = None,
token_credential: "AsyncTokenCredential | TokenCredential | None" = None,
) -> "AzureKeyCredential | AsyncTokenCredential | TokenCredential":
"""Resolve the credential to use for Azure AI Search.

Args:
azure_ai_search_settings: Azure AI Search settings.
azure_credential: Optional Azure credentials (default: {None}).
token_credential: Optional Token credential (default: {None}).
"""
if azure_credential:
return azure_credential
if token_credential:
return token_credential
if azure_ai_search_settings.api_key:
return AzureKeyCredential(azure_ai_search_settings.api_key.get_secret_value())
raise ServiceInitializationError("Error: missing Azure AI Search client credentials.")


def _get_search_index_client(
azure_ai_search_settings: AzureAISearchSettings,
azure_credential: AzureKeyCredential | None = None,
Expand All @@ -174,16 +198,7 @@ def _get_search_index_client(
azure_credential: Optional Azure credentials (default: {None}).
token_credential: Optional Token credential (default: {None}).
"""
# Credentials
credential: "AzureKeyCredential | AsyncTokenCredential | TokenCredential | None" = None
if azure_credential:
credential = azure_credential
elif token_credential:
credential = token_credential
elif azure_ai_search_settings.api_key:
credential = AzureKeyCredential(azure_ai_search_settings.api_key.get_secret_value())
else:
raise ServiceInitializationError("Error: missing Azure AI Search client credentials.")
credential = _resolve_credential(azure_ai_search_settings, azure_credential, token_credential)

return SearchIndexClient(
endpoint=str(azure_ai_search_settings.endpoint),
Expand Down Expand Up @@ -286,6 +301,8 @@ class AzureAISearchCollection(

search_client: SearchClient
search_index_client: SearchIndexClient
search_endpoint: str = ""
search_credential: Any = None
supported_key_types: ClassVar[set[str] | None] = {"str"}
supported_vector_types: ClassVar[set[str] | None] = {"float", "int"}
supported_search_types: ClassVar[set[SearchType]] = {SearchType.VECTOR, SearchType.KEYWORD_HYBRID}
Expand All @@ -299,6 +316,7 @@ def __init__(
search_index_client: SearchIndexClient | None = None,
search_client: SearchClient | None = None,
embedding_generator: "EmbeddingGeneratorBase | None" = None,
search_credential: "AzureKeyCredential | AsyncTokenCredential | TokenCredential | None" = None,
**kwargs: Any,
) -> None:
"""Initializes a new instance of the AzureAISearchCollection class.
Expand All @@ -319,6 +337,9 @@ def __init__(
used for creating and deleting indexes.
search_client: The search client for interacting with Azure AI Search,
used for record operations.
search_credential: The credential used to authenticate with Azure AI Search.
If not provided, it will be resolved from azure_credentials, token_credentials,
or api_key in kwargs/environment.
embedding_generator: The embedding generator, optional.
**kwargs: Additional keyword arguments, including:
The same keyword arguments used for AzureAISearchVectorStore:
Expand Down Expand Up @@ -360,14 +381,24 @@ def __init__(
)
except ValidationError as exc:
raise VectorStoreInitializationException("Failed to create Azure Cognitive Search settings.") from exc
endpoint = str(azure_ai_search_settings.endpoint)
credential = search_credential or _resolve_credential(
azure_ai_search_settings,
azure_credential=kwargs.get("azure_credentials"),
token_credential=kwargs.get("token_credentials"),
)
super().__init__(
record_type=record_type,
definition=definition,
collection_name=azure_ai_search_settings.index_name,
search_client=_get_search_client(
search_index_client=search_index_client, collection_name=azure_ai_search_settings.index_name
endpoint=endpoint,
collection_name=azure_ai_search_settings.index_name,
credential=credential,
),
search_index_client=search_index_client,
search_endpoint=endpoint,
search_credential=credential,
managed_search_index_client=False,
embedding_generator=embedding_generator,
)
Expand All @@ -383,6 +414,12 @@ def __init__(
)
except ValidationError as exc:
raise VectorStoreInitializationException("Failed to create Azure Cognitive Search settings.") from exc
endpoint = str(azure_ai_search_settings.endpoint)
credential = search_credential or _resolve_credential(
azure_ai_search_settings,
azure_credential=kwargs.get("azure_credentials"),
token_credential=kwargs.get("token_credentials"),
)
search_index_client = _get_search_index_client(
azure_ai_search_settings=azure_ai_search_settings,
azure_credential=kwargs.get("azure_credentials"),
Expand All @@ -393,10 +430,13 @@ def __init__(
definition=definition,
collection_name=azure_ai_search_settings.index_name,
search_client=_get_search_client(
search_index_client=search_index_client,
collection_name=azure_ai_search_settings.index_name, # type: ignore
endpoint=endpoint,
collection_name=azure_ai_search_settings.index_name,
credential=credential,
),
search_index_client=search_index_client,
search_endpoint=endpoint,
search_credential=credential,
embedding_generator=embedding_generator,
)

Expand Down Expand Up @@ -711,6 +751,8 @@ class AzureAISearchStore(VectorStore):
"""Azure AI Search store implementation."""

search_index_client: SearchIndexClient
search_endpoint: str = ""
search_credential: Any = None

def __init__(
self,
Expand All @@ -725,6 +767,8 @@ def __init__(
) -> None:
"""Initializes a new instance of the AzureAISearchStore class."""
managed_client: bool = False
endpoint: str = ""
credential: "AzureKeyCredential | AsyncTokenCredential | TokenCredential | None" = None
if not search_index_client:
try:
Comment thread
SergeyMenshykh marked this conversation as resolved.
Resolved
azure_ai_search_settings = AzureAISearchSettings(
Expand All @@ -735,6 +779,12 @@ def __init__(
)
except ValidationError as exc:
raise VectorStoreInitializationException("Failed to create Azure AI Search settings.") from exc
endpoint = str(azure_ai_search_settings.endpoint)
credential = _resolve_credential(
azure_ai_search_settings,
azure_credential=azure_credentials,
token_credential=token_credentials,
)
search_index_client = _get_search_index_client(
azure_ai_search_settings=azure_ai_search_settings,
azure_credential=azure_credentials,
Expand All @@ -744,6 +794,8 @@ def __init__(

Comment thread
SergeyMenshykh marked this conversation as resolved.
Resolved
super().__init__(
search_index_client=search_index_client,
search_endpoint=endpoint,
search_credential=credential,
managed_client=managed_client,
embedding_generator=embedding_generator,
)
Expand Down Expand Up @@ -777,6 +829,8 @@ def get_collection(
search_index_client=self.search_index_client,
search_client=search_client,
embedding_generator=embedding_generator or self.embedding_generator,
search_credential=self.search_credential,
search_endpoint=self.search_endpoint,
**kwargs,
)

Expand Down
26 changes: 21 additions & 5 deletions python/tests/unit/connectors/memory/test_azure_ai_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
AzureAISearchStore,
_definition_to_azure_ai_search_index,
_get_search_index_client,
_resolve_credential,
)
from semantic_kernel.exceptions import (
ServiceInitializationError,
Expand Down Expand Up @@ -171,8 +172,6 @@ def test_init_with_search_index_client(azure_ai_search_unit_test_env, definition
@mark.parametrize("exclude_list", [["AZURE_AI_SEARCH_INDEX_NAME"]], indirect=True)
def test_init_with_search_index_client_fail(azure_ai_search_unit_test_env, definition):
search_index_client = MagicMock(spec=SearchIndexClient)
search_index_client._endpoint = "test-endpoint"
search_index_client._credential = "test-credential"
with raises(VectorStoreInitializationException):
AzureAISearchCollection(
record_type=dict,
Expand Down Expand Up @@ -301,7 +300,8 @@ def test_get_collection(vector_store, definition):
assert collection.collection_name == "test"
assert collection.search_index_client == vector_store.search_index_client
assert collection.search_client is not None
assert collection.search_client._endpoint == vector_store.search_index_client._endpoint
assert collection.search_endpoint == vector_store.search_endpoint
assert collection.search_credential == vector_store.search_credential


@mark.parametrize("exclude_list", [["AZURE_AI_SEARCH_API_KEY"]], indirect=True)
Expand All @@ -313,20 +313,36 @@ def test_get_search_index_client(azure_ai_search_unit_test_env):
azure_credential = MagicMock(spec=AzureKeyCredential)
client = _get_search_index_client(settings, azure_credential=azure_credential)
assert client is not None
assert client._credential == azure_credential

token_credential = MagicMock(spec=TokenCredential)
client2 = _get_search_index_client(
settings,
token_credential=token_credential,
)
assert client2 is not None
assert client2._credential == token_credential

with raises(ServiceInitializationError):
_get_search_index_client(settings)


@mark.parametrize("exclude_list", [["AZURE_AI_SEARCH_API_KEY"]], indirect=True)
def test_resolve_credential(azure_ai_search_unit_test_env):
from azure.core.credentials import AzureKeyCredential, TokenCredential

settings = AzureAISearchSettings(**azure_ai_search_unit_test_env, env_file_path="test.env")

azure_credential = MagicMock(spec=AzureKeyCredential)
resolved = _resolve_credential(settings, azure_credential=azure_credential)
assert resolved == azure_credential

token_credential = MagicMock(spec=TokenCredential)
resolved = _resolve_credential(settings, token_credential=token_credential)
assert resolved == token_credential

with raises(ServiceInitializationError):
_resolve_credential(settings)


@mark.parametrize("include_vectors", [True, False])
async def test_search_vectorized_search(collection, mock_search, include_vectors):
results = await collection.search(vector=[0.1, 0.2, 0.3], include_vectors=include_vectors)
Expand Down
Loading