From eb2f45b5887055f615d434c596c093aff6c88ec0 Mon Sep 17 00:00:00 2001 From: Wasim Date: Fri, 30 Oct 2026 10:46:23 +0530 Subject: [PATCH] Python: fix(azure_ai_search): use storage_name aliases in hybrid_search (#14684) --- .../connectors/azure_ai_search.py | 29 +++++-- .../connectors/memory/test_azure_ai_search.py | 78 +++++++++++++++++++ 2 files changed, 99 insertions(+), 8 deletions(-) diff --git a/python/semantic_kernel/connectors/azure_ai_search.py b/python/semantic_kernel/connectors/azure_ai_search.py index f01bef97a96f..212bbdf8faaf 100644 --- a/python/semantic_kernel/connectors/azure_ai_search.py +++ b/python/semantic_kernel/connectors/azure_ai_search.py @@ -595,15 +595,28 @@ async def _inner_search( if values is None: raise VectorStoreOperationException("No vector and/or keywords provided for search.") vector_field = self.definition.try_get_vector_field(options.vector_property_name) - search_args["search_fields"] = ( - [options.additional_property_name] - if options.additional_property_name is not None - else [ - field.name + vector_field_name = vector_field.storage_name or vector_field.name if vector_field else None + if options.additional_property_name is not None: + target_field = next( + ( + field + for field in self.definition.fields + if field.name == options.additional_property_name + or field.storage_name == options.additional_property_name + ), + None, + ) + search_args["search_fields"] = [ + target_field.storage_name or target_field.name + if target_field + else options.additional_property_name + ] + else: + search_args["search_fields"] = [ + field.storage_name or field.name for field in self.definition.fields if field.field_type == FieldTypes.DATA and field.is_full_text_indexed ] - ) if not search_args["search_fields"]: raise VectorStoreOperationException("No searchable fields found for hybrid search.") search_args["search_text"] = values @@ -613,14 +626,14 @@ async def _inner_search( search_args["vector_queries"] = [ VectorizedQuery( vector=vector, # type: ignore - fields=vector_field.name if vector_field else None, + fields=vector_field_name, ) ] else: search_args["vector_queries"] = [ VectorizableTextQuery( text=values, - fields=vector_field.name if vector_field else None, + fields=vector_field_name, ) ] try: diff --git a/python/tests/unit/connectors/memory/test_azure_ai_search.py b/python/tests/unit/connectors/memory/test_azure_ai_search.py index d5a8ba2111e0..c87df9f1e292 100644 --- a/python/tests/unit/connectors/memory/test_azure_ai_search.py +++ b/python/tests/unit/connectors/memory/test_azure_ai_search.py @@ -18,6 +18,10 @@ _get_search_index_client, _resolve_credential, ) +from semantic_kernel.data.vector import ( + VectorStoreCollectionDefinition, + VectorStoreField, +) from semantic_kernel.exceptions import ( ServiceInitializationError, VectorStoreInitializationException, @@ -440,6 +444,80 @@ async def test_search_keyword_hybrid_search(collection, mock_search, include_vec assert call[1]["vector_queries"][0].fields == "vector" +@fixture +def definition_with_storage_names() -> VectorStoreCollectionDefinition: + return VectorStoreCollectionDefinition( + fields=[ + VectorStoreField("key", name="id", storage_name="doc_id", type="str"), + VectorStoreField("data", name="content", storage_name="body", type="str", is_full_text_indexed=True), + VectorStoreField( + "vector", + name="embedding", + storage_name="vec", + dimensions=3, + index_kind="hnsw", + distance_function="cosine_distance", + type="float", + ), + ] + ) + + +@fixture +def collection_with_storage_names(azure_ai_search_unit_test_env, definition_with_storage_names): + return AzureAISearchCollection(record_type=dict, definition=definition_with_storage_names) + + +@mark.parametrize( + "additional_prop, expected_search_fields", + [ + ("content", ["body"]), + ("body", ["body"]), + (None, ["body"]), + ], + ids=["property_name", "storage_name", "default_all_indexed"], +) +async def test_search_keyword_hybrid_search_storage_names( + collection_with_storage_names, mock_search, additional_prop, expected_search_fields +): + results = await collection_with_storage_names.hybrid_search( + values="test query", + vector=[0.1, 0.2, 0.3], + additional_property_name=additional_prop, + ) + assert results is not None + call_args = mock_search.call_args[1] + assert call_args["search_fields"] == expected_search_fields + assert call_args["vector_queries"][0].fields == "vec" + assert call_args["vector_queries"][0].vector == [0.1, 0.2, 0.3] + + +async def test_search_keyword_hybrid_search_text_query_storage_names(collection_with_storage_names, mock_search): + with patch.object(collection_with_storage_names, "_generate_vector_from_values", return_value=None): + results = await collection_with_storage_names.hybrid_search( + values="test query", + ) + assert results is not None + call_args = mock_search.call_args[1] + assert call_args["search_fields"] == ["body"] + assert call_args["vector_queries"][0].fields == "vec" + assert call_args["vector_queries"][0].text == "test query" + + +async def test_vector_and_hybrid_search_fields_agree_with_storage_names(collection_with_storage_names, mock_search): + await collection_with_storage_names.search(vector=[0.1, 0.2, 0.3]) + vector_call_kwargs = mock_search.call_args[1] + + mock_search.reset_mock() + + await collection_with_storage_names.hybrid_search(values="test query", vector=[0.1, 0.2, 0.3]) + hybrid_call_kwargs = mock_search.call_args[1] + + assert vector_call_kwargs["vector_queries"][0].fields == "vec" + assert hybrid_call_kwargs["vector_queries"][0].fields == "vec" + assert vector_call_kwargs["vector_queries"][0].fields == hybrid_call_kwargs["vector_queries"][0].fields + + @mark.parametrize("filter, result", filter_lambda_list("ai_search")) def test_lambda_filter(collection, filter, result): if isinstance(result, type) and issubclass(result, Exception):