From c3fa3cbf1f5cb44adc3870685bd580c1fbc6faac Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Mon, 9 Sep 2024 14:52:36 +0100 Subject: [PATCH 1/5] Switch to using interfaces for search instead of query objects. --- .../Memory/VectorSearch_SearchTypes.cs | 241 ++++++++++++++++++ .../Concepts/Memory/VectorSearch_Simple.cs | 143 +++++++++++ ...ISearchVectorStoreRecordCollectionTests.cs | 38 ++- ...zureAISearchVectorStoreRecordCollection.cs | 90 ++++--- ...mosDBMongoDBVectorStoreRecordCollection.cs | 6 + ...osmosDBNoSQLVectorStoreRecordCollection.cs | 6 + .../PineconeVectorStoreRecordCollection.cs | 6 + .../QdrantVectorStoreRecordCollection.cs | 94 +++---- ...RedisHashSetVectorStoreRecordCollection.cs | 64 +++-- .../RedisJsonVectorStoreRecordCollection.cs | 64 +++-- ...RedisVectorStoreCollectionSearchMapping.cs | 18 +- .../QdrantVectorStoreRecordCollectionTests.cs | 7 +- ...HashSetVectorStoreRecordCollectionTests.cs | 4 +- ...disJsonVectorStoreRecordCollectionTests.cs | 4 +- ...VectorStoreCollectionSearchMappingTests.cs | 19 +- ...ISearchVectorStoreRecordCollectionTests.cs | 40 +-- .../QdrantVectorStoreRecordCollectionTests.cs | 17 +- ...HashSetVectorStoreRecordCollectionTests.cs | 12 +- ...disJsonVectorStoreRecordCollectionTests.cs | 12 +- .../Memory/Redis/RedisVectorStoreFixture.cs | 2 + .../VectorSearch/IVectorizableTextSearch.cs | 26 ++ ...{IVectorSearch.cs => IVectorizedSearch.cs} | 15 +- .../VectorSearch/VectorSearchExtensions.cs | 53 ---- .../VectorSearchQueries/VectorSearchQuery.cs | 53 ---- .../VectorSearchQueryType.cs | 22 -- .../VectorizableTextSearchQuery.cs | 46 ---- .../VectorizedSearchQuery.cs | 44 ---- .../IVectorStoreRecordCollection.cs | 2 +- .../VolatileVectorStoreRecordCollection.cs | 86 +++---- ...olatileVectorStoreRecordCollectionTests.cs | 26 +- 30 files changed, 740 insertions(+), 520 deletions(-) create mode 100644 dotnet/samples/Concepts/Memory/VectorSearch_SearchTypes.cs create mode 100644 dotnet/samples/Concepts/Memory/VectorSearch_Simple.cs create mode 100644 dotnet/src/SemanticKernel.Abstractions/Data/VectorSearch/IVectorizableTextSearch.cs rename dotnet/src/SemanticKernel.Abstractions/Data/VectorSearch/{IVectorSearch.cs => IVectorizedSearch.cs} (56%) delete mode 100644 dotnet/src/SemanticKernel.Abstractions/Data/VectorSearch/VectorSearchExtensions.cs delete mode 100644 dotnet/src/SemanticKernel.Abstractions/Data/VectorSearchQueries/VectorSearchQuery.cs delete mode 100644 dotnet/src/SemanticKernel.Abstractions/Data/VectorSearchQueries/VectorSearchQueryType.cs delete mode 100644 dotnet/src/SemanticKernel.Abstractions/Data/VectorSearchQueries/VectorizableTextSearchQuery.cs delete mode 100644 dotnet/src/SemanticKernel.Abstractions/Data/VectorSearchQueries/VectorizedSearchQuery.cs diff --git a/dotnet/samples/Concepts/Memory/VectorSearch_SearchTypes.cs b/dotnet/samples/Concepts/Memory/VectorSearch_SearchTypes.cs new file mode 100644 index 000000000000..669695fcda60 --- /dev/null +++ b/dotnet/samples/Concepts/Memory/VectorSearch_SearchTypes.cs @@ -0,0 +1,241 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure; +using Azure.Search.Documents.Indexes; +using Azure.Search.Documents.Indexes.Models; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.AzureAISearch; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.Data; +using Microsoft.SemanticKernel.Embeddings; + +namespace Memory; + +public class VectorSearch_SearchTypes(ITestOutputHelper output) : BaseTest(output) +{ + private static readonly IConfigurationRoot s_configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + + private static readonly string? s_azureAISearchServiceUrl = s_configuration.GetRequiredSection("AzureAISearch").GetValue("ServiceUrl"); + private static readonly string? s_apiKey = s_configuration.GetRequiredSection("AzureAISearch").GetValue("ApiKey"); + private const string IndexName = "skconceptssearchtypes"; + + [Fact] + public async Task DIExampleAsync() + { + // Create a kernel with the required services. + var kernelBuilder = Kernel.CreateBuilder() + .AddAzureOpenAITextEmbeddingGeneration( + TestConfiguration.AzureOpenAIEmbeddings.DeploymentName, + TestConfiguration.AzureOpenAIEmbeddings.Endpoint, + TestConfiguration.AzureOpenAIEmbeddings.ApiKey) + .AddAzureAISearchVectorStore(new Uri(s_azureAISearchServiceUrl!), new AzureKeyCredential(s_apiKey!)); + + kernelBuilder.Services.AddSingleton(); + kernelBuilder.Services.AddSingleton(this.Output); + kernelBuilder.Services.AddSingleton>((sp) => + { + var vectorStore = sp.GetRequiredService(); + return vectorStore.GetCollection(IndexName); + }); + kernelBuilder.Services.AddSingleton>((sp) => + { + var vectorStore = sp.GetRequiredService(); + return (IVectorizableTextSearch)sp.GetRequiredService>(); + }); + + var kernel = kernelBuilder.Build(); + + var recordProcessor = kernel.GetRequiredService(); + + await CreateIndexAsync(); + await recordProcessor.ExecuteProcessAsync(); + } + + [Fact] + public async Task ExampleAsync() + { + // Create an embedding generation service. + var textEmbeddingGenerationService = new AzureOpenAITextEmbeddingGenerationService( + TestConfiguration.AzureOpenAIEmbeddings.DeploymentName, + TestConfiguration.AzureOpenAIEmbeddings.Endpoint, + TestConfiguration.AzureOpenAIEmbeddings.ApiKey); + + // Construct the vector store. + var vectorStore = new AzureAISearchVectorStore(new SearchIndexClient(new Uri(s_azureAISearchServiceUrl!), new AzureKeyCredential(s_apiKey!))); + var collection = vectorStore.GetCollection(IndexName); + var recordProcessor = new RecordProcessor( + this.Output, + collection, + (IVectorizableTextSearch)collection, + textEmbeddingGenerationService); + + await CreateIndexAsync(); + await recordProcessor.ExecuteProcessAsync(); + } + + private sealed class RecordProcessor( + ITestOutputHelper Console, + IVectorStoreRecordCollection collection, + IVectorizableTextSearch vectorizableTextSearch, + ITextEmbeddingGenerationService textEmbeddingGenerationService) + { + public async Task ExecuteProcessAsync() + { + // Create glossary entries and generate embeddings for them. + var glossaryEntries = CreateGlossaryEntries().ToList(); + var tasks = glossaryEntries.Select(entry => Task.Run(async () => + { + entry.DefinitionEmbedding = await textEmbeddingGenerationService.GenerateEmbeddingAsync(entry.Definition); + })); + await Task.WhenAll(tasks); + + // Upsert the glossary entries into the collection and return their keys. + var upsertedKeysTasks = glossaryEntries.Select(x => collection.UpsertAsync(x)); + var upsertedKeys = await Task.WhenAll(upsertedKeysTasks); + await Task.Delay(1000); + + // Search the collection using a vector search. + var searchString = "What is an Application Programming Interface"; + var searchVector = await textEmbeddingGenerationService.GenerateEmbeddingAsync(searchString); + var searchResult = await collection.VectorizedSearchAsync(searchVector, new() { Limit = 1 }).ToListAsync(); + + Console.WriteLine("Vector Search 1"); + Console.WriteLine("Search string: " + searchString); + Console.WriteLine("Result: " + searchResult.First().Record.Definition); + Console.WriteLine(); + + // Search the collection using a vector search. + searchString = "What is Retrieval Augmented Generation"; + searchVector = await textEmbeddingGenerationService.GenerateEmbeddingAsync(searchString); + searchResult = await collection.VectorizedSearchAsync(searchVector, new() { Limit = 1 }).ToListAsync(); + + Console.WriteLine("Vector Search 2"); + Console.WriteLine("Search string: " + searchString); + Console.WriteLine("Result: " + searchResult.First().Record.Definition); + Console.WriteLine(); + + // Search the collection using a vector search with pre-filtering. + searchString = "What is Retrieval Augmented Generation"; + searchVector = await textEmbeddingGenerationService.GenerateEmbeddingAsync(searchString); + var filter = new VectorSearchFilter().EqualTo(nameof(Glossary.Category), "External Definitions"); + searchResult = await collection.VectorizedSearchAsync(searchVector, new() { Limit = 3, Filter = filter }).ToListAsync(); + + Console.WriteLine("Vector Search with pre-filtering"); + Console.WriteLine("Search string: " + searchString); + Console.WriteLine("Number of results: " + searchResult.Count); + Console.WriteLine("Result 1 Score: " + searchResult[0].Score); + Console.WriteLine("Result 1: " + searchResult[0].Record.Definition); + Console.WriteLine("Result 2 Score: " + searchResult[1].Score); + Console.WriteLine("Result 2: " + searchResult[1].Record.Definition); + Console.WriteLine(); + + // Search the collection using a vectorizable search with pre-filtering. + searchResult = await vectorizableTextSearch.VectorizableTextSearchAsync(searchString, new() { Limit = 3, Filter = filter }).ToListAsync(); + + Console.WriteLine("Vectorizable text search with pre-filtering"); + Console.WriteLine("Search string: " + searchString); + Console.WriteLine("Number of results: " + searchResult.Count); + Console.WriteLine("Result 1 Score: " + searchResult[0].Score); + Console.WriteLine("Result 1: " + searchResult[0].Record.Definition); + Console.WriteLine("Result 2 Score: " + searchResult[1].Score); + Console.WriteLine("Result 2: " + searchResult[1].Record.Definition); + + await collection.DeleteCollectionAsync(); + } + } + + /// + /// Sample model class that represents a glossary entry. + /// + /// + /// Note that each property is decorated with an attribute that specifies how the property should be treated by the vector store. + /// This allows us to create a collection in the vector store and upsert and retrieve instances of this class without any further configuration. + /// + private sealed class Glossary + { + [VectorStoreRecordKey] + public string Key { get; set; } + + [VectorStoreRecordData(IsFilterable = true)] + public string Category { get; set; } + + [VectorStoreRecordData] + public string Term { get; set; } + + [VectorStoreRecordData] + public string Definition { get; set; } + + [VectorStoreRecordVector(1536)] + public ReadOnlyMemory DefinitionEmbedding { get; set; } + } + + /// + /// Create some sample glossary entries. + /// + /// A list of sample glossary entries. + private static IEnumerable CreateGlossaryEntries() + { + yield return new Glossary + { + Key = "1", + Category = "External Definitions", + Term = "API", + Definition = "Application Programming Interface. A set of rules and specifications that allow software components to communicate and exchange data." + }; + + yield return new Glossary + { + Key = "2", + Category = "Core Definitions", + Term = "Connectors", + Definition = "Connectors allow you to integrate with various services provide AI capabilities, including LLM, AudioToText, TextToAudio, Embedding generation, etc." + }; + + yield return new Glossary + { + Key = "3", + Category = "External Definitions", + Term = "RAG", + Definition = "Retrieval Augmented Generation - a term that refers to the process of retrieving additional data to provide as context to an LLM to use when generating a response (completion) to a user’s question (prompt)." + }; + } + + private static async Task CreateIndexAsync() + { + var indexName = VectorSearch_SearchTypes.IndexName; + var searchIndexClient = new SearchIndexClient(new Uri(s_azureAISearchServiceUrl!), new AzureKeyCredential(s_apiKey!)); + + // Build the list of fields from the model, and then replace the DescriptionEmbedding field with a vector field, to work around + // issue where the field is not recognized as an array on parsing on the server side when apply the VectorSearchFieldAttribute. + var searchFields = new List(); + searchFields.Add(new SimpleField("Key", SearchFieldDataType.String) { IsFilterable = true, IsKey = true }); + searchFields.Add(new SimpleField("Category", SearchFieldDataType.String) { IsFilterable = true }); + searchFields.Add(new SimpleField("Term", SearchFieldDataType.String) { IsFilterable = true }); + searchFields.Add(new SimpleField("Definition", SearchFieldDataType.String) { IsFilterable = true }); + searchFields.Add(new VectorSearchField("DefinitionEmbedding", 1536, "my-vector-profile")); + + // Create an index definition with a vectorizer to use when doing vector searches using text. + var definition = new SearchIndex(indexName, searchFields); + definition.VectorSearch = new VectorSearch(); + definition.VectorSearch.Vectorizers.Add(new AzureOpenAIVectorizer("text-embedding-vectorizer") + { + Parameters = new AzureOpenAIVectorizerParameters + { + ResourceUri = new Uri(TestConfiguration.AzureOpenAIEmbeddings.Endpoint), + DeploymentName = TestConfiguration.AzureOpenAIEmbeddings.DeploymentName, + ApiKey = TestConfiguration.AzureOpenAIEmbeddings.ApiKey, + ModelName = "text-embedding-ada-002" + } + }); + definition.VectorSearch.Algorithms.Add(new HnswAlgorithmConfiguration("my-hnsw-vector-config-1") { Parameters = new HnswParameters { Metric = VectorSearchAlgorithmMetric.Cosine } }); + definition.VectorSearch.Profiles.Add(new VectorSearchProfile("my-vector-profile", "my-hnsw-vector-config-1") { VectorizerName = "text-embedding-vectorizer" }); + + await searchIndexClient.CreateOrUpdateIndexAsync(definition); + } +} diff --git a/dotnet/samples/Concepts/Memory/VectorSearch_Simple.cs b/dotnet/samples/Concepts/Memory/VectorSearch_Simple.cs new file mode 100644 index 000000000000..de3a6e488800 --- /dev/null +++ b/dotnet/samples/Concepts/Memory/VectorSearch_Simple.cs @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Memory.VectorStoreFixtures; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.Connectors.Qdrant; +using Microsoft.SemanticKernel.Data; +using Microsoft.SemanticKernel.Embeddings; +using Qdrant.Client; + +namespace Memory; + +/// +/// A simple example showing how to ingest data into a vector store and then use vector search to find related records to a given string. +/// +/// The example shows the following steps: +/// 1. Create an embedding generator. +/// 2. Create a Qdrant Vector Store. +/// 3. Ingest some data into the vector store. +/// 4. Search the vector store with various text and filtering options. +/// +/// You need a local instance of Docker running, since the associated fixture will try and start a Qdrant container in the local docker instance to run against. +/// +public class VectorSearch_Simple(ITestOutputHelper output, VectorStoreQdrantContainerFixture qdrantFixture) : BaseTest(output), IClassFixture +{ + [Fact] + public async Task ExampleAsync() + { + // Create an embedding generation service. + var textEmbeddingGenerationService = new AzureOpenAITextEmbeddingGenerationService( + TestConfiguration.AzureOpenAIEmbeddings.DeploymentName, + TestConfiguration.AzureOpenAIEmbeddings.Endpoint, + TestConfiguration.AzureOpenAIEmbeddings.ApiKey); + + // Initiate the docker container and construct the vector store. + await qdrantFixture.ManualInitializeAsync(); + var vectorStore = new QdrantVectorStore(new QdrantClient("localhost")); + + // Get and create collection if it doesn't exist. + var collection = vectorStore.GetCollection("skglossary"); + await collection.CreateCollectionIfNotExistsAsync(); + + // Create glossary entries and generate embeddings for them. + var glossaryEntries = CreateGlossaryEntries().ToList(); + var tasks = glossaryEntries.Select(entry => Task.Run(async () => + { + entry.DefinitionEmbedding = await textEmbeddingGenerationService.GenerateEmbeddingAsync(entry.Definition); + })); + await Task.WhenAll(tasks); + + // Upsert the glossary entries into the collection and return their keys. + var upsertedKeysTasks = glossaryEntries.Select(x => collection.UpsertAsync(x)); + var upsertedKeys = await Task.WhenAll(upsertedKeysTasks); + + var vectorSearch = collection as IVectorizedSearch; + + // Search the collection using a vector search. + var searchString = "What is an Application Programming Interface"; + var searchVector = await textEmbeddingGenerationService.GenerateEmbeddingAsync(searchString); + var searchResult = await vectorSearch!.VectorizedSearchAsync(searchVector, new() { Limit = 1 }).ToListAsync(); + + Console.WriteLine("Search string: " + searchString); + Console.WriteLine("Result: " + searchResult.First().Record.Definition); + Console.WriteLine(); + + // Search the collection using a vector search. + searchString = "What is Retrieval Augmented Generation"; + searchVector = await textEmbeddingGenerationService.GenerateEmbeddingAsync(searchString); + searchResult = await vectorSearch!.VectorizedSearchAsync(searchVector, new() { Limit = 1 }).ToListAsync(); + + Console.WriteLine("Search string: " + searchString); + Console.WriteLine("Result: " + searchResult.First().Record.Definition); + Console.WriteLine(); + + // Search the collection using a vector search with pre-filtering. + searchString = "What is Retrieval Augmented Generation"; + searchVector = await textEmbeddingGenerationService.GenerateEmbeddingAsync(searchString); + var filter = new VectorSearchFilter().EqualTo(nameof(Glossary.Category), "External Definitions"); + searchResult = await vectorSearch!.VectorizedSearchAsync(searchVector, new() { Limit = 3, Filter = filter }).ToListAsync(); + + Console.WriteLine("Search string: " + searchString); + Console.WriteLine("Number of results: " + searchResult.Count); + Console.WriteLine("Result 1 Score: " + searchResult[0].Score); + Console.WriteLine("Result 1: " + searchResult[0].Record.Definition); + Console.WriteLine("Result 2 Score: " + searchResult[1].Score); + Console.WriteLine("Result 2: " + searchResult[1].Record.Definition); + } + + /// + /// Sample model class that represents a glossary entry. + /// + /// + /// Note that each property is decorated with an attribute that specifies how the property should be treated by the vector store. + /// This allows us to create a collection in the vector store and upsert and retrieve instances of this class without any further configuration. + /// + private sealed class Glossary + { + [VectorStoreRecordKey] + public ulong Key { get; set; } + + [VectorStoreRecordData(IsFilterable = true)] + public string Category { get; set; } + + [VectorStoreRecordData] + public string Term { get; set; } + + [VectorStoreRecordData] + public string Definition { get; set; } + + [VectorStoreRecordVector(1536)] + public ReadOnlyMemory DefinitionEmbedding { get; set; } + } + + /// + /// Create some sample glossary entries. + /// + /// A list of sample glossary entries. + private static IEnumerable CreateGlossaryEntries() + { + yield return new Glossary + { + Key = 1, + Category = "External Definitions", + Term = "API", + Definition = "Application Programming Interface. A set of rules and specifications that allow software components to communicate and exchange data." + }; + + yield return new Glossary + { + Key = 2, + Category = "Core Definitions", + Term = "Connectors", + Definition = "Connectors allow you to integrate with various services provide AI capabilities, including LLM, AudioToText, TextToAudio, Embedding generation, etc." + }; + + yield return new Glossary + { + Key = 3, + Category = "External Definitions", + Term = "RAG", + Definition = "Retrieval Augmented Generation - a term that refers to the process of retrieving additional data to provide as context to an LLM to use when generating a response (completion) to a user’s question (prompt)." + }; + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureAISearch.UnitTests/AzureAISearchVectorStoreRecordCollectionTests.cs b/dotnet/src/Connectors/Connectors.AzureAISearch.UnitTests/AzureAISearchVectorStoreRecordCollectionTests.cs index d9e31a183c29..434db3ed7382 100644 --- a/dotnet/src/Connectors/Connectors.AzureAISearch.UnitTests/AzureAISearchVectorStoreRecordCollectionTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureAISearch.UnitTests/AzureAISearchVectorStoreRecordCollectionTests.cs @@ -567,16 +567,15 @@ public async Task CanSearchWithVectorAndFilterAsync() var filter = new VectorSearchFilter().EqualTo(nameof(MultiPropsModel.Data1), "Data1FilterValue"); // Act. - var searchResults = await sut.SearchAsync( - VectorSearchQuery.CreateQuery( - new ReadOnlyMemory(new float[4]), - new() - { - Limit = 5, - Offset = 3, - Filter = filter, - VectorFieldName = nameof(MultiPropsModel.Vector1) - }), + var searchResults = await sut.VectorizedSearchAsync( + new ReadOnlyMemory(new float[4]), + new() + { + Limit = 5, + Offset = 3, + Filter = filter, + VectorFieldName = nameof(MultiPropsModel.Vector1) + }, this._testCancellationToken).ToListAsync(); // Assert. @@ -608,16 +607,15 @@ public async Task CanSearchWithTextAndFilterAsync() var filter = new VectorSearchFilter().EqualTo(nameof(MultiPropsModel.Data1), "Data1FilterValue"); // Act. - var searchResults = await sut.SearchAsync( - VectorSearchQuery.CreateQuery( - "search string", - new() - { - Limit = 5, - Offset = 3, - Filter = filter, - VectorFieldName = nameof(MultiPropsModel.Vector1) - }), + var searchResults = await sut.VectorizableTextSearchAsync( + "search string", + new() + { + Limit = 5, + Offset = 3, + Filter = filter, + VectorFieldName = nameof(MultiPropsModel.Vector1) + }, this._testCancellationToken).ToListAsync(); // Assert. diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchVectorStoreRecordCollection.cs b/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchVectorStoreRecordCollection.cs index e27f0b4363fe..852367fc12dd 100644 --- a/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchVectorStoreRecordCollection.cs +++ b/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchVectorStoreRecordCollection.cs @@ -22,7 +22,7 @@ namespace Microsoft.SemanticKernel.Connectors.AzureAISearch; /// /// The data model to use for adding, updating and retrieving data from storage. #pragma warning disable CA1711 // Identifiers should not have incorrect suffix -public sealed class AzureAISearchVectorStoreRecordCollection : IVectorStoreRecordCollection, IVectorSearch +public sealed class AzureAISearchVectorStoreRecordCollection : IVectorStoreRecordCollection, IVectorizableTextSearch #pragma warning restore CA1711 // Identifiers should not have incorrect suffix where TRecord : class { @@ -317,72 +317,84 @@ public async IAsyncEnumerable UpsertBatchAsync(IEnumerable reco } /// - public IAsyncEnumerable> SearchAsync(VectorSearchQuery vectorQuery, CancellationToken cancellationToken = default) + public IAsyncEnumerable> VectorizedSearchAsync(TVector vector, Data.VectorSearchOptions? options = null, CancellationToken cancellationToken = default) { - Verify.NotNull(vectorQuery); + Verify.NotNull(vector); if (this._firstVectorPropertyName is null) { throw new InvalidOperationException("The collection does not have any vector fields, so vector search is not possible."); } - string? queryText = null; - string? filterString = null; - var searchFields = new List(); + if (vector is not ReadOnlyMemory floatVector) + { + throw new NotSupportedException($"The provided vector type {vector.GetType().Name} is not supported by the Azure AI Search connector."); + } + + // Resolve options. + var internalOptions = options ?? Data.VectorSearchOptions.Default; + string? vectorFieldName = this.ResolveVectorFieldName(internalOptions.VectorFieldName); + + // Configure search settings. var vectorQueries = new List(); - int limit = 3; - int offset = 0; - bool includeVectors = false; + vectorQueries.Add(new VectorizedQuery(floatVector) { KNearestNeighborsCount = internalOptions.Limit, Fields = { vectorFieldName } }); + var filterString = AzureAISearchVectorStoreCollectionSearchMapping.BuildFilterString(internalOptions.Filter, this._storagePropertyNames); - if (vectorQuery is VectorizedSearchQuery> floatVectorQuery) + // Build search options. + var searchOptions = new SearchOptions { - // Resolve options. - var internalOptions = floatVectorQuery.SearchOptions ?? Data.VectorSearchOptions.Default; - string? vectorFieldName = this.ResolveVectorFieldName(internalOptions.VectorFieldName); - - // Configure search settings. - filterString = AzureAISearchVectorStoreCollectionSearchMapping.BuildFilterString(internalOptions.Filter, this._storagePropertyNames); - vectorQueries.Add(new VectorizedQuery(floatVectorQuery.Vector) { KNearestNeighborsCount = internalOptions.Limit, Fields = { vectorFieldName } }); - limit = internalOptions.Limit; - offset = internalOptions.Offset; - includeVectors = internalOptions.IncludeVectors; - } - else if (vectorQuery is VectorizableTextSearchQuery vectorizableTextQuery) + VectorSearch = new(), + Size = internalOptions.Limit, + Skip = internalOptions.Offset, + Filter = filterString, + }; + searchOptions.VectorSearch.Queries.AddRange(vectorQueries); + + // Filter out vector fields if requested. + if (!internalOptions.IncludeVectors) { - // Resolve options. - var internalOptions = vectorizableTextQuery.SearchOptions ?? Data.VectorSearchOptions.Default; - string? vectorFieldName = this.ResolveVectorFieldName(internalOptions.VectorFieldName); - - // Configure search settings. - filterString = AzureAISearchVectorStoreCollectionSearchMapping.BuildFilterString(internalOptions.Filter, this._storagePropertyNames); - vectorQueries.Add(new VectorizableTextQuery(vectorizableTextQuery.QueryText) { KNearestNeighborsCount = internalOptions.Limit, Fields = { vectorFieldName } }); - limit = internalOptions.Limit; - offset = internalOptions.Offset; - includeVectors = internalOptions.IncludeVectors; + searchOptions.Select.AddRange(this._nonVectorStoragePropertyNames); } - else + + return this.SearchAndMapToDataModelAsync(null, searchOptions, internalOptions.IncludeVectors, cancellationToken); + } + + /// + public IAsyncEnumerable> VectorizableTextSearchAsync(string searchText, Data.VectorSearchOptions? options = null, CancellationToken cancellationToken = default) + { + Verify.NotNull(searchText); + + if (this._firstVectorPropertyName is null) { - throw new NotSupportedException($"A {nameof(VectorSearchQuery)} of type {vectorQuery.QueryType} is not supported by the Azure AI Search connector."); + throw new InvalidOperationException("The collection does not have any vector fields, so vector search is not possible."); } + // Resolve options. + var internalOptions = options ?? Data.VectorSearchOptions.Default; + string? vectorFieldName = this.ResolveVectorFieldName(internalOptions.VectorFieldName); + + // Configure search settings. + var vectorQueries = new List(); + vectorQueries.Add(new VectorizableTextQuery(searchText) { KNearestNeighborsCount = internalOptions.Limit, Fields = { vectorFieldName } }); + var filterString = AzureAISearchVectorStoreCollectionSearchMapping.BuildFilterString(internalOptions.Filter, this._storagePropertyNames); + // Build search options. var searchOptions = new SearchOptions { VectorSearch = new(), - Size = limit, - Skip = offset, + Size = internalOptions.Limit, + Skip = internalOptions.Offset, Filter = filterString, }; - searchOptions.SearchFields.AddRange(searchFields); searchOptions.VectorSearch.Queries.AddRange(vectorQueries); // Filter out vector fields if requested. - if (!includeVectors) + if (!internalOptions.IncludeVectors) { searchOptions.Select.AddRange(this._nonVectorStoragePropertyNames); } - return this.SearchAndMapToDataModelAsync(queryText, searchOptions, includeVectors, cancellationToken); + return this.SearchAndMapToDataModelAsync(null, searchOptions, internalOptions.IncludeVectors, cancellationToken); } /// diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreRecordCollection.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreRecordCollection.cs index 2d409ef61c54..f08026edcd7e 100644 --- a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreRecordCollection.cs +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreRecordCollection.cs @@ -237,6 +237,12 @@ public async IAsyncEnumerable UpsertBatchAsync( } } + /// + public IAsyncEnumerable> VectorizedSearchAsync(TVector vector, VectorSearchOptions? options = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + #region private private async Task CreateIndexAsync(string collectionName, CancellationToken cancellationToken) diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreRecordCollection.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreRecordCollection.cs index cf443dfcf4ac..f896e421eabc 100644 --- a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreRecordCollection.cs +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreRecordCollection.cs @@ -360,6 +360,12 @@ async IAsyncEnumerable IVectorStoreRecordCollect } } + /// + public IAsyncEnumerable> VectorizedSearchAsync(TVector vector, VectorSearchOptions? options = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + #endregion #region private diff --git a/dotnet/src/Connectors/Connectors.Memory.Pinecone/PineconeVectorStoreRecordCollection.cs b/dotnet/src/Connectors/Connectors.Memory.Pinecone/PineconeVectorStoreRecordCollection.cs index 323681f629be..606d1e7ea0aa 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Pinecone/PineconeVectorStoreRecordCollection.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Pinecone/PineconeVectorStoreRecordCollection.cs @@ -227,6 +227,12 @@ await this.RunOperationAsync( } } + /// + public IAsyncEnumerable> VectorizedSearchAsync(TVector vector, VectorSearchOptions? options = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + private async Task RunOperationAsync(string operationName, Func> operation) { try diff --git a/dotnet/src/Connectors/Connectors.Memory.Qdrant/QdrantVectorStoreRecordCollection.cs b/dotnet/src/Connectors/Connectors.Memory.Qdrant/QdrantVectorStoreRecordCollection.cs index 1c162160c62b..4d2fbdfa75a0 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Qdrant/QdrantVectorStoreRecordCollection.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Qdrant/QdrantVectorStoreRecordCollection.cs @@ -18,7 +18,7 @@ namespace Microsoft.SemanticKernel.Connectors.Qdrant; /// /// The data model to use for adding, updating and retrieving data from storage. #pragma warning disable CA1711 // Identifiers should not have incorrect suffix -public sealed class QdrantVectorStoreRecordCollection : IVectorStoreRecordCollection, IVectorStoreRecordCollection, IVectorSearch +public sealed class QdrantVectorStoreRecordCollection : IVectorStoreRecordCollection, IVectorStoreRecordCollection #pragma warning restore CA1711 // Identifiers should not have incorrect suffix where TRecord : class { @@ -440,65 +440,65 @@ private async IAsyncEnumerable GetBatchByPointIdAsync( } /// - public async IAsyncEnumerable> SearchAsync(VectorSearchQuery vectorQuery, [EnumeratorCancellation] CancellationToken cancellationToken = default) + public async IAsyncEnumerable> VectorizedSearchAsync(TVector vector, VectorSearchOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { + Verify.NotNull(vector); + if (this._firstVectorPropertyName is null) { throw new InvalidOperationException("The collection does not have any vector fields, so vector search is not possible."); } - if (vectorQuery is VectorizedSearchQuery> floatVectorQuery) + if (vector is not ReadOnlyMemory floatVector) { - var internalOptions = floatVectorQuery.SearchOptions ?? Data.VectorSearchOptions.Default; - - // Build filter object. - var filter = QdrantVectorStoreCollectionSearchMapping.BuildFilter(internalOptions.Filter, this._storagePropertyNames); - - // Specify the vector name if named vectors are used. - string? vectorName = null; - if (this._options.HasNamedVectors) - { - vectorName = this.ResolveVectorFieldName(internalOptions.VectorFieldName); - } + throw new NotSupportedException($"The provided vector type {vector.GetType().Name} is not supported by the Qdrant connector."); + } - // Specify whether to include vectors in the search results. - var vectorsSelector = new WithVectorsSelector(); - vectorsSelector.Enable = internalOptions.IncludeVectors; + var internalOptions = options ?? Data.VectorSearchOptions.Default; - var query = new Query - { - Nearest = new VectorInput(floatVectorQuery.Vector.ToArray()), - }; + // Build filter object. + var filter = QdrantVectorStoreCollectionSearchMapping.BuildFilter(internalOptions.Filter, this._storagePropertyNames); - // Execute Search. - var points = await this.RunOperationAsync( - "Query", - () => this._qdrantClient.QueryAsync( - this.CollectionName, - query: query, - usingVector: vectorName, - filter: filter, - limit: (ulong)internalOptions.Limit, - offset: (ulong)internalOptions.Offset, - vectorsSelector: vectorsSelector, - cancellationToken: cancellationToken)).ConfigureAwait(false); + // Specify the vector name if named vectors are used. + string? vectorName = null; + if (this._options.HasNamedVectors) + { + vectorName = this.ResolveVectorFieldName(internalOptions.VectorFieldName); + } - // Map to data model and return results. - foreach (var point in points) - { - yield return QdrantVectorStoreCollectionSearchMapping.MapScoredPointToVectorSearchResult( - point, - this._mapper, - internalOptions.IncludeVectors, - DatabaseName, - this._collectionName, - "Query"); - } + // Specify whether to include vectors in the search results. + var vectorsSelector = new WithVectorsSelector(); + vectorsSelector.Enable = internalOptions.IncludeVectors; - yield break; + var query = new Query + { + Nearest = new VectorInput(floatVector.ToArray()), + }; + + // Execute Search. + var points = await this.RunOperationAsync( + "Query", + () => this._qdrantClient.QueryAsync( + this.CollectionName, + query: query, + usingVector: vectorName, + filter: filter, + limit: (ulong)internalOptions.Limit, + offset: (ulong)internalOptions.Offset, + vectorsSelector: vectorsSelector, + cancellationToken: cancellationToken)).ConfigureAwait(false); + + // Map to data model and return results. + foreach (var point in points) + { + yield return QdrantVectorStoreCollectionSearchMapping.MapScoredPointToVectorSearchResult( + point, + this._mapper, + internalOptions.IncludeVectors, + DatabaseName, + this._collectionName, + "Query"); } - - throw new NotSupportedException($"A {nameof(VectorSearchQuery)} of type {vectorQuery.QueryType} is not supported by the Qdrant connector."); } /// diff --git a/dotnet/src/Connectors/Connectors.Memory.Redis/RedisHashSetVectorStoreRecordCollection.cs b/dotnet/src/Connectors/Connectors.Memory.Redis/RedisHashSetVectorStoreRecordCollection.cs index 8188ec2bb826..cae2781a9df9 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Redis/RedisHashSetVectorStoreRecordCollection.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Redis/RedisHashSetVectorStoreRecordCollection.cs @@ -19,7 +19,7 @@ namespace Microsoft.SemanticKernel.Connectors.Redis; /// /// The data model to use for adding, updating and retrieving data from storage. #pragma warning disable CA1711 // Identifiers should not have incorrect suffix -public sealed class RedisHashSetVectorStoreRecordCollection : IVectorStoreRecordCollection, IVectorSearch +public sealed class RedisHashSetVectorStoreRecordCollection : IVectorStoreRecordCollection #pragma warning restore CA1711 // Identifiers should not have incorrect suffix where TRecord : class { @@ -333,50 +333,48 @@ public async IAsyncEnumerable UpsertBatchAsync(IEnumerable reco } /// - public async IAsyncEnumerable> SearchAsync(VectorSearchQuery vectorQuery, [EnumeratorCancellation] CancellationToken cancellationToken = default) + public async IAsyncEnumerable> VectorizedSearchAsync(TVector vector, VectorSearchOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - Verify.NotNull(vectorQuery); + Verify.NotNull(vector); if (this._firstVectorPropertyName is null) { throw new InvalidOperationException("The collection does not have any vector fields, so vector search is not possible."); } - if (vectorQuery is VectorizedSearchQuery> floatVectorQuery) + if (vector is not ReadOnlyMemory floatVector) { - var internalOptions = floatVectorQuery.SearchOptions ?? Data.VectorSearchOptions.Default; + throw new NotSupportedException($"The provided vector type {vector.GetType().Name} is not supported by the Redis HashSet connector."); + } - // Build query & search. - var selectFields = internalOptions.IncludeVectors ? null : this._dataStoragePropertyNames; - var query = RedisVectorStoreCollectionSearchMapping.BuildQuery(floatVectorQuery, this._storagePropertyNames, this._firstVectorPropertyName, selectFields); - var results = await this.RunOperationAsync( - "FT.SEARCH", - () => this._database - .FT() - .SearchAsync(this._collectionName, query)).ConfigureAwait(false); + var internalOptions = options ?? Data.VectorSearchOptions.Default; - // Loop through result and convert to the caller's data model. - foreach (var result in results.Documents) - { - var retrievedHashEntries = this._dataAndVectorStoragePropertyNames.Select(propertyName => new HashEntry(propertyName, result[propertyName])).ToArray(); - - // Convert to the caller's data model. - var dataModel = VectorStoreErrorHandler.RunModelConversion( - DatabaseName, - this._collectionName, - "FT.SEARCH", - () => - { - return this._mapper.MapFromStorageToDataModel((this.RemoveKeyPrefixIfNeeded(result.Id), retrievedHashEntries), new() { IncludeVectors = internalOptions.IncludeVectors }); - }); - - yield return new VectorSearchResult(dataModel, result.Score); - } + // Build query & search. + var selectFields = internalOptions.IncludeVectors ? null : this._dataStoragePropertyNames; + var query = RedisVectorStoreCollectionSearchMapping.BuildQuery(floatVector, internalOptions, this._storagePropertyNames, this._firstVectorPropertyName, selectFields); + var results = await this.RunOperationAsync( + "FT.SEARCH", + () => this._database + .FT() + .SearchAsync(this._collectionName, query)).ConfigureAwait(false); - yield break; - } + // Loop through result and convert to the caller's data model. + foreach (var result in results.Documents) + { + var retrievedHashEntries = this._dataAndVectorStoragePropertyNames.Select(propertyName => new HashEntry(propertyName, result[propertyName])).ToArray(); - throw new NotSupportedException($"A {nameof(VectorSearchQuery)} of type {vectorQuery.QueryType} is not supported by the Redis HashSet connector."); + // Convert to the caller's data model. + var dataModel = VectorStoreErrorHandler.RunModelConversion( + DatabaseName, + this._collectionName, + "FT.SEARCH", + () => + { + return this._mapper.MapFromStorageToDataModel((this.RemoveKeyPrefixIfNeeded(result.Id), retrievedHashEntries), new() { IncludeVectors = internalOptions.IncludeVectors }); + }); + + yield return new VectorSearchResult(dataModel, result.Score); + } } /// diff --git a/dotnet/src/Connectors/Connectors.Memory.Redis/RedisJsonVectorStoreRecordCollection.cs b/dotnet/src/Connectors/Connectors.Memory.Redis/RedisJsonVectorStoreRecordCollection.cs index 01ef1a008e87..a33cc20286d8 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Redis/RedisJsonVectorStoreRecordCollection.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Redis/RedisJsonVectorStoreRecordCollection.cs @@ -22,7 +22,7 @@ namespace Microsoft.SemanticKernel.Connectors.Redis; /// /// The data model to use for adding, updating and retrieving data from storage. #pragma warning disable CA1711 // Identifiers should not have incorrect suffix -public sealed class RedisJsonVectorStoreRecordCollection : IVectorStoreRecordCollection, IVectorSearch +public sealed class RedisJsonVectorStoreRecordCollection : IVectorStoreRecordCollection #pragma warning restore CA1711 // Identifiers should not have incorrect suffix where TRecord : class { @@ -369,50 +369,48 @@ await this.RunOperationAsync( } /// - public async IAsyncEnumerable> SearchAsync(VectorSearchQuery vectorQuery, [EnumeratorCancellation] CancellationToken cancellationToken = default) + public async IAsyncEnumerable> VectorizedSearchAsync(TVector vector, VectorSearchOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - Verify.NotNull(vectorQuery); + Verify.NotNull(vector); if (this._firstVectorPropertyName is null) { throw new InvalidOperationException("The collection does not have any vector fields, so vector search is not possible."); } - if (vectorQuery is VectorizedSearchQuery> floatVectorQuery) + if (vector is not ReadOnlyMemory floatVector) { - var internalOptions = floatVectorQuery.SearchOptions ?? Data.VectorSearchOptions.Default; + throw new NotSupportedException($"The provided vector type {vector.GetType().Name} is not supported by the Redis JSON connector."); + } - // Build query & search. - var query = RedisVectorStoreCollectionSearchMapping.BuildQuery(floatVectorQuery, this._storagePropertyNames, this._firstVectorPropertyName, null); - var results = await this.RunOperationAsync( - "FT.SEARCH", - () => this._database - .FT() - .SearchAsync(this._collectionName, query)).ConfigureAwait(false); + var internalOptions = options ?? Data.VectorSearchOptions.Default; - // Loop through result and convert to the caller's data model. - foreach (var result in results.Documents) - { - var redisResultString = result["json"].ToString(); - var mappedRecord = VectorStoreErrorHandler.RunModelConversion( - DatabaseName, - this._collectionName, - "FT.SEARCH", - () => - { - var node = JsonSerializer.Deserialize(redisResultString, this._jsonSerializerOptions)!; - return this._mapper.MapFromStorageToDataModel( - (this.RemoveKeyPrefixIfNeeded(result.Id), node), - new() { IncludeVectors = internalOptions.IncludeVectors }); - }); - - yield return new VectorSearchResult(mappedRecord, result.Score); - } + // Build query & search. + var query = RedisVectorStoreCollectionSearchMapping.BuildQuery(floatVector, internalOptions, this._storagePropertyNames, this._firstVectorPropertyName, null); + var results = await this.RunOperationAsync( + "FT.SEARCH", + () => this._database + .FT() + .SearchAsync(this._collectionName, query)).ConfigureAwait(false); - yield break; - } + // Loop through result and convert to the caller's data model. + foreach (var result in results.Documents) + { + var redisResultString = result["json"].ToString(); + var mappedRecord = VectorStoreErrorHandler.RunModelConversion( + DatabaseName, + this._collectionName, + "FT.SEARCH", + () => + { + var node = JsonSerializer.Deserialize(redisResultString, this._jsonSerializerOptions)!; + return this._mapper.MapFromStorageToDataModel( + (this.RemoveKeyPrefixIfNeeded(result.Id), node), + new() { IncludeVectors = internalOptions.IncludeVectors }); + }); - throw new NotSupportedException($"A {nameof(VectorSearchQuery)} of type {vectorQuery.QueryType} is not supported by the Redis JSON connector."); + yield return new VectorSearchResult(mappedRecord, result.Score); + } } /// diff --git a/dotnet/src/Connectors/Connectors.Memory.Redis/RedisVectorStoreCollectionSearchMapping.cs b/dotnet/src/Connectors/Connectors.Memory.Redis/RedisVectorStoreCollectionSearchMapping.cs index a4516ba4a77b..7e8e8110994e 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Redis/RedisVectorStoreCollectionSearchMapping.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Redis/RedisVectorStoreCollectionSearchMapping.cs @@ -15,26 +15,26 @@ namespace Microsoft.SemanticKernel.Connectors.Redis; internal static class RedisVectorStoreCollectionSearchMapping { /// - /// Build a Redis object from the given . + /// Build a Redis object from the given vector and options. /// - /// The to build the Redis query from. + /// The vector to search the database with. + /// The options to configure the behavior of the search. /// A mapping of data model property names to the names under which they are stored. /// The name of the first vector property in the data model. /// The set of fields to limit the results to. Null for all. /// The . - public static Query BuildQuery(VectorizedSearchQuery> floatVectorQuery, Dictionary storagePropertyNames, string firstVectorPropertyName, string[]? selectFields) + public static Query BuildQuery(ReadOnlyMemory floatVector, VectorSearchOptions options, Dictionary storagePropertyNames, string firstVectorPropertyName, string[]? selectFields) { // Resolve options. - var internalOptions = floatVectorQuery.SearchOptions ?? Data.VectorSearchOptions.Default; - var vectorPropertyName = ResolveVectorFieldName(internalOptions.VectorFieldName, storagePropertyNames, firstVectorPropertyName); + var vectorPropertyName = ResolveVectorFieldName(options.VectorFieldName, storagePropertyNames, firstVectorPropertyName); // Build search query. - var filter = RedisVectorStoreCollectionSearchMapping.BuildFilter(internalOptions.Filter, storagePropertyNames); - var vectorBytes = MemoryMarshal.AsBytes(floatVectorQuery.Vector.Span).ToArray(); - var query = new Query($"{filter}=>[KNN {internalOptions.Limit} @{vectorPropertyName} $embedding AS vector_score]") + var filter = RedisVectorStoreCollectionSearchMapping.BuildFilter(options.Filter, storagePropertyNames); + var vectorBytes = MemoryMarshal.AsBytes(floatVector.Span).ToArray(); + var query = new Query($"{filter}=>[KNN {options.Limit} @{vectorPropertyName} $embedding AS vector_score]") .AddParam("embedding", vectorBytes) .SetSortBy("vector_score") - .Limit(internalOptions.Offset, internalOptions.Limit) + .Limit(options.Offset, options.Limit) .SetWithScores(true) .Dialect(2); diff --git a/dotnet/src/Connectors/Connectors.Qdrant.UnitTests/QdrantVectorStoreRecordCollectionTests.cs b/dotnet/src/Connectors/Connectors.Qdrant.UnitTests/QdrantVectorStoreRecordCollectionTests.cs index f42946b7ec9f..54c84e8f6fe6 100644 --- a/dotnet/src/Connectors/Connectors.Qdrant.UnitTests/QdrantVectorStoreRecordCollectionTests.cs +++ b/dotnet/src/Connectors/Connectors.Qdrant.UnitTests/QdrantVectorStoreRecordCollectionTests.cs @@ -553,7 +553,7 @@ public void CanCreateCollectionWithMismatchedDefinitionAndType() public async Task CanSearchWithVectorAndFilterAsync(bool useDefinition, bool hasNamedVectors, TKey testRecordKey) where TKey : notnull { - var sut = this.CreateRecordCollection(useDefinition, hasNamedVectors) as IVectorSearch>; + var sut = this.CreateRecordCollection(useDefinition, hasNamedVectors); // Arrange. var scoredPoint = CreateScoredPoint(hasNamedVectors, testRecordKey); @@ -561,8 +561,9 @@ public async Task CanSearchWithVectorAndFilterAsync(bool useDefinition, bo var filter = new VectorSearchFilter().EqualTo(nameof(SinglePropsModel.Data), "data 1"); // Act. - var actual = await sut!.SearchAsync( - VectorSearchQuery.CreateQuery(new ReadOnlyMemory(new[] { 1f, 2f, 3f, 4f }), new() { IncludeVectors = true, Filter = filter, Limit = 5, Offset = 2 }), + var actual = await sut.VectorizedSearchAsync( + new ReadOnlyMemory(new[] { 1f, 2f, 3f, 4f }), + new() { IncludeVectors = true, Filter = filter, Limit = 5, Offset = 2 }, this._testCancellationToken).ToListAsync(); // Assert. diff --git a/dotnet/src/Connectors/Connectors.Redis.UnitTests/RedisHashSetVectorStoreRecordCollectionTests.cs b/dotnet/src/Connectors/Connectors.Redis.UnitTests/RedisHashSetVectorStoreRecordCollectionTests.cs index 3c76c53fd354..8f3d01349a2a 100644 --- a/dotnet/src/Connectors/Connectors.Redis.UnitTests/RedisHashSetVectorStoreRecordCollectionTests.cs +++ b/dotnet/src/Connectors/Connectors.Redis.UnitTests/RedisHashSetVectorStoreRecordCollectionTests.cs @@ -443,7 +443,7 @@ public async Task CanSearchWithVectorAndFilterAsync(bool useDefinition, bool inc var filter = new VectorSearchFilter().EqualTo(nameof(SinglePropsModel.Data), "data 1"); // Act. - var actual = await sut!.SearchAsync(VectorSearchQuery.CreateQuery( + var actual = await sut.VectorizedSearchAsync( new ReadOnlyMemory(new[] { 1f, 2f, 3f, 4f }), new() { @@ -451,7 +451,7 @@ public async Task CanSearchWithVectorAndFilterAsync(bool useDefinition, bool inc Filter = filter, Limit = 5, Offset = 2 - })).ToListAsync(); + }).ToListAsync(); // Assert. var expectedArgsPart1 = new object[] diff --git a/dotnet/src/Connectors/Connectors.Redis.UnitTests/RedisJsonVectorStoreRecordCollectionTests.cs b/dotnet/src/Connectors/Connectors.Redis.UnitTests/RedisJsonVectorStoreRecordCollectionTests.cs index 808e78f73801..9a6995a5da9a 100644 --- a/dotnet/src/Connectors/Connectors.Redis.UnitTests/RedisJsonVectorStoreRecordCollectionTests.cs +++ b/dotnet/src/Connectors/Connectors.Redis.UnitTests/RedisJsonVectorStoreRecordCollectionTests.cs @@ -458,7 +458,7 @@ public async Task CanSearchWithVectorAndFilterAsync(bool useDefinition) var filter = new VectorSearchFilter().EqualTo(nameof(MultiPropsModel.Data1), "data 1"); // Act. - var actual = await sut!.SearchAsync(VectorSearchQuery.CreateQuery( + var actual = await sut.VectorizedSearchAsync( new ReadOnlyMemory(new[] { 1f, 2f, 3f, 4f }), new() { @@ -466,7 +466,7 @@ public async Task CanSearchWithVectorAndFilterAsync(bool useDefinition) Filter = filter, Limit = 5, Offset = 2 - })).ToListAsync(); + }).ToListAsync(); // Assert. var expectedArgs = new object[] diff --git a/dotnet/src/Connectors/Connectors.Redis.UnitTests/RedisVectorStoreCollectionSearchMappingTests.cs b/dotnet/src/Connectors/Connectors.Redis.UnitTests/RedisVectorStoreCollectionSearchMappingTests.cs index c5588ac2eb3a..8cf18240ba39 100644 --- a/dotnet/src/Connectors/Connectors.Redis.UnitTests/RedisVectorStoreCollectionSearchMappingTests.cs +++ b/dotnet/src/Connectors/Connectors.Redis.UnitTests/RedisVectorStoreCollectionSearchMappingTests.cs @@ -16,8 +16,7 @@ public class RedisVectorStoreCollectionSearchMappingTests public void BuildQueryBuildsRedisQueryWithDefaults() { // Arrange. - var floatVectorQuery = VectorSearchQuery.CreateQuery( - new ReadOnlyMemory(new float[] { 1.0f, 2.0f, 3.0f })); + var floatVector = new ReadOnlyMemory(new float[] { 1.0f, 2.0f, 3.0f }); var storagePropertyNames = new Dictionary() { { "Vector", "storage_Vector" }, @@ -25,7 +24,7 @@ public void BuildQueryBuildsRedisQueryWithDefaults() var firstVectorPropertyName = "storage_Vector"; // Act. - var query = RedisVectorStoreCollectionSearchMapping.BuildQuery(floatVectorQuery, storagePropertyNames, firstVectorPropertyName, null); + var query = RedisVectorStoreCollectionSearchMapping.BuildQuery(floatVector, VectorSearchOptions.Default, storagePropertyNames, firstVectorPropertyName, null); // Assert. Assert.NotNull(query); @@ -39,9 +38,8 @@ public void BuildQueryBuildsRedisQueryWithDefaults() public void BuildQueryBuildsRedisQueryWithCustomVectorName() { // Arrange. - var floatVectorQuery = VectorSearchQuery.CreateQuery( - new ReadOnlyMemory(new float[] { 1.0f, 2.0f, 3.0f }), - new VectorSearchOptions { Limit = 5, Offset = 3, VectorFieldName = "Vector" }); + var floatVector = new ReadOnlyMemory(new float[] { 1.0f, 2.0f, 3.0f }); + var vectorSearchOptions = new VectorSearchOptions { Limit = 5, Offset = 3, VectorFieldName = "Vector" }; var storagePropertyNames = new Dictionary() { { "Vector", "storage_Vector" }, @@ -50,7 +48,7 @@ public void BuildQueryBuildsRedisQueryWithCustomVectorName() var selectFields = new string[] { "storage_Field1", "storage_Field2" }; // Act. - var query = RedisVectorStoreCollectionSearchMapping.BuildQuery(floatVectorQuery, storagePropertyNames, firstVectorPropertyName, selectFields); + var query = RedisVectorStoreCollectionSearchMapping.BuildQuery(floatVector, vectorSearchOptions, storagePropertyNames, firstVectorPropertyName, selectFields); // Assert. Assert.NotNull(query); @@ -61,9 +59,8 @@ public void BuildQueryBuildsRedisQueryWithCustomVectorName() public void BuildQueryFailsForInvalidVectorName() { // Arrange. - var floatVectorQuery = VectorSearchQuery.CreateQuery( - new ReadOnlyMemory(new float[] { 1.0f, 2.0f, 3.0f }), - new VectorSearchOptions { VectorFieldName = "UnknownVector" }); + var floatVector = new ReadOnlyMemory(new float[] { 1.0f, 2.0f, 3.0f }); + var vectorSearchOptions = new VectorSearchOptions { VectorFieldName = "UnknownVector" }; var storagePropertyNames = new Dictionary() { { "Vector", "storage_Vector" }, @@ -73,7 +70,7 @@ public void BuildQueryFailsForInvalidVectorName() // Act & Assert. Assert.Throws(() => { - var query = RedisVectorStoreCollectionSearchMapping.BuildQuery(floatVectorQuery, storagePropertyNames, firstVectorPropertyName, null); + var query = RedisVectorStoreCollectionSearchMapping.BuildQuery(floatVector, vectorSearchOptions, storagePropertyNames, firstVectorPropertyName, null); }); } diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/AzureAISearch/AzureAISearchVectorStoreRecordCollectionTests.cs b/dotnet/src/IntegrationTests/Connectors/Memory/AzureAISearch/AzureAISearchVectorStoreRecordCollectionTests.cs index a67cf33b9dba..c240221d926c 100644 --- a/dotnet/src/IntegrationTests/Connectors/Memory/AzureAISearch/AzureAISearchVectorStoreRecordCollectionTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/Memory/AzureAISearch/AzureAISearchVectorStoreRecordCollectionTests.cs @@ -61,7 +61,13 @@ public async Task ItCanCreateACollectionUpsertGetAndSearchAsync(bool useRecordDe var upsertResult = await sut.UpsertAsync(hotel); var getResult = await sut.GetAsync("Upsert-1"); var embedding = new ReadOnlyMemory(AzureAISearchVectorStoreFixture.CreateTestEmbedding()); - var searchResult = await sut.SearchAsync(VectorSearchQuery.CreateQuery(embedding, new VectorSearchOptions { IncludeVectors = true, Filter = new VectorSearchFilter().EqualTo("HotelName", "MyHotel Upsert-1") })).ToListAsync(); + var searchResult = await sut.VectorizedSearchAsync( + embedding, + new VectorSearchOptions + { + IncludeVectors = true, + Filter = new VectorSearchFilter().EqualTo("HotelName", "MyHotel Upsert-1") + }).ToListAsync(); // Assert var collectionExistResult = await sut.CollectionExistsAsync(); @@ -331,15 +337,14 @@ public async Task ItCanSearchWithVectorAndFiltersAsync(string option, bool inclu // Act. var filter = option == "equality" ? new VectorSearchFilter().EqualTo("HotelName", "Hotel 3") : new VectorSearchFilter().AnyTagEqualTo("Tags", "bar"); - var searchResults = sut.SearchAsync( - VectorSearchQuery.CreateQuery( - new ReadOnlyMemory(AzureAISearchVectorStoreFixture.CreateTestEmbedding()), - new() - { - IncludeVectors = includeVectors, - VectorFieldName = "DescriptionEmbedding", - Filter = filter, - })); + var searchResults = sut.VectorizedSearchAsync( + new ReadOnlyMemory(AzureAISearchVectorStoreFixture.CreateTestEmbedding()), + new() + { + IncludeVectors = includeVectors, + VectorFieldName = "DescriptionEmbedding", + Filter = filter, + }); // Assert. Assert.NotNull(searchResults); @@ -372,14 +377,13 @@ public async Task ItCanSearchWithVectorizableTextAndFiltersAsync() // Act. var filter = new VectorSearchFilter().EqualTo("HotelName", "Hotel 3"); - var searchResults = sut.SearchAsync( - VectorSearchQuery.CreateQuery( - "A hotel with great views.", - new() - { - VectorFieldName = "DescriptionEmbedding", - Filter = filter, - })); + var searchResults = sut.VectorizableTextSearchAsync( + "A hotel with great views.", + new() + { + VectorFieldName = "DescriptionEmbedding", + Filter = filter, + }); // Assert. Assert.NotNull(searchResults); diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/Qdrant/QdrantVectorStoreRecordCollectionTests.cs b/dotnet/src/IntegrationTests/Connectors/Memory/Qdrant/QdrantVectorStoreRecordCollectionTests.cs index a24e4180b27a..cb2ff3d053f2 100644 --- a/dotnet/src/IntegrationTests/Connectors/Memory/Qdrant/QdrantVectorStoreRecordCollectionTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/Memory/Qdrant/QdrantVectorStoreRecordCollectionTests.cs @@ -61,7 +61,9 @@ public async Task ItCanCreateACollectionUpsertGetAndSearchAsync(bool hasNamedVec await sut.CreateCollectionAsync(); var upsertResult = await sut.UpsertAsync(record); var getResult = await sut.GetAsync(30, new GetRecordOptions { IncludeVectors = true }); - var searchResult = await sut.SearchAsync(VectorSearchQuery.CreateQuery(new ReadOnlyMemory(new[] { 30f, 31f, 32f, 33f }), new VectorSearchOptions { Filter = new VectorSearchFilter().EqualTo("HotelCode", 30) })).ToListAsync(); + var searchResult = await sut.VectorizedSearchAsync( + new ReadOnlyMemory(new[] { 30f, 31f, 32f, 33f }), + new VectorSearchOptions { Filter = new VectorSearchFilter().EqualTo("HotelCode", 30) }).ToListAsync(); // Assert var collectionExistResult = await sut.CollectionExistsAsync(); @@ -383,13 +385,12 @@ public async Task ItCanSearchWithFilterAsync(bool useRecordDefinition, string co // Act. var filter = filterType == "equality" ? new VectorSearchFilter().EqualTo("HotelName", "My Hotel 11") : new VectorSearchFilter().AnyTagEqualTo("Tags", "t1"); - var searchResults = sut.SearchAsync( - VectorSearchQuery.CreateQuery( - new ReadOnlyMemory([30f, 31f, 32f, 33f]), - new() - { - Filter = filter - })); + var searchResults = sut.VectorizedSearchAsync( + new ReadOnlyMemory([30f, 31f, 32f, 33f]), + new() + { + Filter = filter + }); // Assert. Assert.NotNull(searchResults); diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/Redis/RedisHashSetVectorStoreRecordCollectionTests.cs b/dotnet/src/IntegrationTests/Connectors/Memory/Redis/RedisHashSetVectorStoreRecordCollectionTests.cs index 7d55ba796298..0807db46ac73 100644 --- a/dotnet/src/IntegrationTests/Connectors/Memory/Redis/RedisHashSetVectorStoreRecordCollectionTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/Memory/Redis/RedisHashSetVectorStoreRecordCollectionTests.cs @@ -64,11 +64,9 @@ public async Task ItCanCreateACollectionUpsertGetAndSearchAsync(bool useRecordDe var upsertResult = await sut.UpsertAsync(record); var getResult = await sut.GetAsync("HUpsert-1", new GetRecordOptions { IncludeVectors = true }); var searchResult = await sut - .SearchAsync( - VectorSearchQuery.CreateQuery( - new ReadOnlyMemory(new[] { 30f, 31f, 32f, 33f }), - new VectorSearchOptions { Filter = new VectorSearchFilter().EqualTo("HotelCode", 1), IncludeVectors = true })) - .ToListAsync(); + .VectorizedSearchAsync( + new ReadOnlyMemory(new[] { 30f, 31f, 32f, 33f }), + new VectorSearchOptions { Filter = new VectorSearchFilter().EqualTo("HotelCode", 1), IncludeVectors = true }).ToListAsync(); // Assert var collectionExistResult = await sut.CollectionExistsAsync(); @@ -315,13 +313,13 @@ public async Task ItCanSearchWithVectorAndFilterAsync(string filterType, bool in var filter = filterType == "equality" ? new VectorSearchFilter().EqualTo("HotelCode", 1) : new VectorSearchFilter().EqualTo("HotelName", "My Hotel 1"); // Act - var actual = await sut.SearchAsync(VectorSearchQuery.CreateQuery( + var actual = await sut.VectorizedSearchAsync( vector, new VectorSearchOptions { IncludeVectors = includeVectors, Filter = filter - })).ToListAsync(); + }).ToListAsync(); // Assert Assert.Single(actual); diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/Redis/RedisJsonVectorStoreRecordCollectionTests.cs b/dotnet/src/IntegrationTests/Connectors/Memory/Redis/RedisJsonVectorStoreRecordCollectionTests.cs index 07ae9b55df2f..eb1f7d8ff017 100644 --- a/dotnet/src/IntegrationTests/Connectors/Memory/Redis/RedisJsonVectorStoreRecordCollectionTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/Memory/Redis/RedisJsonVectorStoreRecordCollectionTests.cs @@ -63,7 +63,9 @@ public async Task ItCanCreateACollectionUpsertGetAndSearchAsync(bool useRecordDe await sut.CreateCollectionAsync(); var upsertResult = await sut.UpsertAsync(record); var getResult = await sut.GetAsync("Upsert-10", new GetRecordOptions { IncludeVectors = true }); - var searchResult = await sut.SearchAsync(VectorSearchQuery.CreateQuery(new ReadOnlyMemory(new[] { 30f, 31f, 32f, 33f }), new VectorSearchOptions { Filter = new VectorSearchFilter().EqualTo("HotelCode", 10) })).ToListAsync(); + var searchResult = await sut.VectorizedSearchAsync( + new ReadOnlyMemory(new[] { 30f, 31f, 32f, 33f }), + new VectorSearchOptions { Filter = new VectorSearchFilter().EqualTo("HotelCode", 10) }).ToListAsync(); // Assert var collectionExistResult = await sut.CollectionExistsAsync(); @@ -341,11 +343,9 @@ public async Task ItCanSearchWithVectorAndFilterAsync(string filterType) var filter = filterType == "equality" ? new VectorSearchFilter().EqualTo("HotelCode", 1) : new VectorSearchFilter().AnyTagEqualTo("Tags", "pool"); // Act - var actual = await sut.SearchAsync( - VectorSearchQuery.CreateQuery( - vector, - new VectorSearchOptions { IncludeVectors = true, Filter = filter })) - .ToListAsync(); + var actual = await sut.VectorizedSearchAsync( + vector, + new VectorSearchOptions { IncludeVectors = true, Filter = filter }).ToListAsync(); // Assert Assert.Single(actual); diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/Redis/RedisVectorStoreFixture.cs b/dotnet/src/IntegrationTests/Connectors/Memory/Redis/RedisVectorStoreFixture.cs index a452eeb01c9d..c1f6e4bad5b1 100644 --- a/dotnet/src/IntegrationTests/Connectors/Memory/Redis/RedisVectorStoreFixture.cs +++ b/dotnet/src/IntegrationTests/Connectors/Memory/Redis/RedisVectorStoreFixture.cs @@ -230,6 +230,8 @@ await client.Containers.StartContainerAsync( container.ID, new ContainerStartParameters()); + await Task.Delay(1000); + return container.ID; } diff --git a/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearch/IVectorizableTextSearch.cs b/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearch/IVectorizableTextSearch.cs new file mode 100644 index 000000000000..7dd58196e185 --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearch/IVectorizableTextSearch.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading; + +namespace Microsoft.SemanticKernel.Data; + +/// +/// Contains a method for doing a vector search using text that will be vectorized downstream. +/// +/// The record data model to use for retrieving data from the store. +public interface IVectorizableTextSearch + where TRecord : class +{ + /// + /// Search the vector store for records that match the given text and filter. The text string will be vectorized downstream and used for the vector search. + /// + /// The text to search the store with. + /// The options that control the behavior of the search. + /// The to monitor for cancellation requests. The default is . + /// The records found by the vector search, including their result scores. + IAsyncEnumerable> VectorizableTextSearchAsync( + string searchText, + VectorSearchOptions? options = default, + CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearch/IVectorSearch.cs b/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearch/IVectorizedSearch.cs similarity index 56% rename from dotnet/src/SemanticKernel.Abstractions/Data/VectorSearch/IVectorSearch.cs rename to dotnet/src/SemanticKernel.Abstractions/Data/VectorSearch/IVectorizedSearch.cs index 4fec423d3f04..bc5c2d4107ef 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearch/IVectorSearch.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearch/IVectorizedSearch.cs @@ -1,24 +1,27 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Threading; namespace Microsoft.SemanticKernel.Data; /// -/// Interface for searching a vector store. +/// Contains a method for doing a vector search on a vector store. /// /// The record data model to use for retrieving data from the store. -[Experimental("SKEXP0001")] -public interface IVectorSearch +public interface IVectorizedSearch where TRecord : class { /// /// Search the vector store for records that match the given embedding and filter. /// - /// The vector to search with. + /// The type of the vector. + /// The vector to search the store with. + /// The options that control the behavior of the search. /// The to monitor for cancellation requests. The default is . /// The records found by the vector search, including their result scores. - IAsyncEnumerable> SearchAsync(VectorSearchQuery vectorQuery, CancellationToken cancellationToken = default); + IAsyncEnumerable> VectorizedSearchAsync( + TVector vector, + VectorSearchOptions? options = default, + CancellationToken cancellationToken = default); } diff --git a/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearch/VectorSearchExtensions.cs b/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearch/VectorSearchExtensions.cs deleted file mode 100644 index 207de181311c..000000000000 --- a/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearch/VectorSearchExtensions.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Threading; - -namespace Microsoft.SemanticKernel.Data; - -/// -/// Contains extension methods for the interface. -/// -[Experimental("SKEXP0001")] -public static class VectorSearchExtensions -{ - /// - /// Search the vector store for records that match the given embedding and filter. - /// - /// The type of record stored in the database and returned by the search. - /// The type of the vector. - /// The search interface that this method extends. - /// The vector to search the store with. - /// The options that control the behavior of the search. - /// The to monitor for cancellation requests. The default is . - /// The records found by the vector search, including their result scores. - public static IAsyncEnumerable> SearchAsync( - this IVectorSearch search, - TVector vector, - VectorSearchOptions? options = default, - CancellationToken cancellationToken = default) - where TRecord : class - { - return search.SearchAsync(new VectorizedSearchQuery(vector, options), cancellationToken); - } - - /// - /// Search the vector store for records that match the given text and filter. The text string will be vectorized downstream and used for the vector search. - /// - /// The type of record stored in the database and returned by the search. - /// The search interface that this method extends. - /// The text to search the store with. - /// The options that control the behavior of the search. - /// The to monitor for cancellation requests. The default is . - /// The records found by the vector search, including their result scores. - public static IAsyncEnumerable> SearchAsync( - this IVectorSearch search, - string searchText, - VectorSearchOptions? options = default, - CancellationToken cancellationToken = default) - where TRecord : class - { - return search.SearchAsync(new VectorizableTextSearchQuery(searchText, options), cancellationToken); - } -} diff --git a/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearchQueries/VectorSearchQuery.cs b/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearchQueries/VectorSearchQuery.cs deleted file mode 100644 index 13a6f191d8e1..000000000000 --- a/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearchQueries/VectorSearchQuery.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.SemanticKernel.Data; - -/// -/// Contains query information to use when searching a vector store. -/// -[Experimental("SKEXP0001")] -public abstract class VectorSearchQuery -{ - /// - /// Initializes a new instance of the class. - /// - /// A string that idenifies the type of query. - /// The options that control the behavior of the search. - internal VectorSearchQuery(string queryType, object? searchOptions) - { - this.QueryType = queryType; - this.SearchOptions = searchOptions; - } - - /// - /// Gets a string that identifies the type of query. - /// - /// - /// For a list of types see . - /// - public string QueryType { get; } - - /// - /// Gets the options that control the behavior of the search. - /// - public object? SearchOptions { get; } - - /// - /// Creates a new query to search a vector store using a vector. - /// - /// The type of the vector. - /// The vector to search the store with. - /// The options that control the behavior of the search. - /// The query object. - public static VectorizedSearchQuery CreateQuery(TVector vector, VectorSearchOptions? options = default) => new(vector, options); - - /// - /// Creates a new query to search a vector store using a text string that will be vectorized downstream. - /// - /// The text to search the store with. - /// The options that control the behavior of the search. - /// The query object. - public static VectorizableTextSearchQuery CreateQuery(string searchText, VectorSearchOptions? options = default) => new(searchText, options); -} diff --git a/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearchQueries/VectorSearchQueryType.cs b/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearchQueries/VectorSearchQueryType.cs deleted file mode 100644 index 2988e051f658..000000000000 --- a/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearchQueries/VectorSearchQueryType.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.SemanticKernel.Data; - -/// -/// Contains the list of vector search query types supported by Semantic Kernel Vector Search. -/// -[Experimental("SKEXP0001")] -public static class VectorSearchQueryType -{ - /// - /// A type of query that searches a vector store using a vector. - /// - public const string VectorizedSearchQuery = nameof(VectorizedSearchQuery); - - /// - /// A type of query that searches a vector store using a text string that will be vectorized downstream. - /// - public const string VectorizableTextSearchQuery = nameof(VectorizableTextSearchQuery); -} diff --git a/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearchQueries/VectorizableTextSearchQuery.cs b/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearchQueries/VectorizableTextSearchQuery.cs deleted file mode 100644 index 4f4a3be2d33f..000000000000 --- a/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearchQueries/VectorizableTextSearchQuery.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.SemanticKernel.Data; - -/// -/// Contains query information to use when searching a vector store using a -/// text string, where the text string will be turned into a vector either downstream -/// in the client pipeline or on the server, if the service supports this functionality. -/// -[Experimental("SKEXP0001")] -public sealed class VectorizableTextSearchQuery : VectorSearchQuery -{ - /// - /// Initializes a new instance of the class. - /// - /// The text to search the vector store with.> - /// Options that control the behavior of the search. - /// Thrown when is not provided. - internal VectorizableTextSearchQuery(string queryText, VectorSearchOptions? searchOptions) - : base(VectorSearchQueryType.VectorizableTextSearchQuery, searchOptions) - { - Verify.NotNullOrWhiteSpace(queryText); - - if (searchOptions != null) - { - Verify.True(searchOptions.Limit > 0, "VectorSearchOptions.Limit must be greater than 0.", nameof(searchOptions)); - Verify.True(searchOptions.Offset >= 0, "VectorSearchOptions.Offset must be 0 or greater.", nameof(searchOptions)); - } - - this.QueryText = queryText; - this.SearchOptions = searchOptions; - } - - /// - /// Gets the text to use when searching the vector store. - /// - public string QueryText { get; } - - /// - /// Gets the options that control the behavior of the search. - /// - public new VectorSearchOptions? SearchOptions { get; } -} diff --git a/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearchQueries/VectorizedSearchQuery.cs b/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearchQueries/VectorizedSearchQuery.cs deleted file mode 100644 index 1bea57c4a179..000000000000 --- a/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearchQueries/VectorizedSearchQuery.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.SemanticKernel.Data; - -/// -/// Contains query information to use when searching a vector store using a vector. -/// -[Experimental("SKEXP0001")] -public sealed class VectorizedSearchQuery : VectorSearchQuery -{ - /// - /// Initializes a new instance of the class. - /// - /// The vector to search the vector store with. - /// Options that control the behavior of the search. - /// Thrown when is not provided. - internal VectorizedSearchQuery(TVector vector, VectorSearchOptions? searchOptions) - : base(VectorSearchQueryType.VectorizedSearchQuery, searchOptions) - { - Verify.NotNull(vector); - - if (searchOptions != null) - { - Verify.True(searchOptions.Limit > 0, "VectorSearchOptions.Limit must be greater than 0.", nameof(searchOptions)); - Verify.True(searchOptions.Offset >= 0, "VectorSearchOptions.Offset must be 0 or greater.", nameof(searchOptions)); - } - - this.Vector = vector; - this.SearchOptions = searchOptions; - } - - /// - /// Gets the vector to use when searching the vector store. - /// - public TVector Vector { get; } - - /// - /// Gets the options that control the behavior of the search. - /// - public new VectorSearchOptions? SearchOptions { get; } -} diff --git a/dotnet/src/SemanticKernel.Abstractions/Data/VectorStorage/IVectorStoreRecordCollection.cs b/dotnet/src/SemanticKernel.Abstractions/Data/VectorStorage/IVectorStoreRecordCollection.cs index 5071412014a8..7db8e5879325 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Data/VectorStorage/IVectorStoreRecordCollection.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Data/VectorStorage/IVectorStoreRecordCollection.cs @@ -14,7 +14,7 @@ namespace Microsoft.SemanticKernel.Data; /// The record data model to use for adding, updating and retrieving data from the store. [Experimental("SKEXP0001")] #pragma warning disable CA1711 // Identifiers should not have incorrect suffix -public interface IVectorStoreRecordCollection +public interface IVectorStoreRecordCollection : IVectorizedSearch #pragma warning restore CA1711 // Identifiers should not have incorrect suffix where TKey : notnull where TRecord : class diff --git a/dotnet/src/SemanticKernel.Core/Data/VolatileVectorStoreRecordCollection.cs b/dotnet/src/SemanticKernel.Core/Data/VolatileVectorStoreRecordCollection.cs index ecd3107cab1f..7b4a9a1b1c48 100644 --- a/dotnet/src/SemanticKernel.Core/Data/VolatileVectorStoreRecordCollection.cs +++ b/dotnet/src/SemanticKernel.Core/Data/VolatileVectorStoreRecordCollection.cs @@ -19,7 +19,7 @@ namespace Microsoft.SemanticKernel.Data; /// The data model to use for adding, updating and retrieving data from storage. [Experimental("SKEXP0001")] #pragma warning disable CA1711 // Identifiers should not have incorrect suffix -public sealed class VolatileVectorStoreRecordCollection : IVectorStoreRecordCollection, IVectorSearch +public sealed class VolatileVectorStoreRecordCollection : IVectorStoreRecordCollection #pragma warning restore CA1711 // Identifiers should not have incorrect suffix where TKey : notnull where TRecord : class @@ -215,64 +215,64 @@ public async IAsyncEnumerable UpsertBatchAsync(IEnumerable record /// #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - Need to satisfy the interface which returns IAsyncEnumerable - public async IAsyncEnumerable> SearchAsync(VectorSearchQuery vectorQuery, [EnumeratorCancellation] CancellationToken cancellationToken = default) + public async IAsyncEnumerable> VectorizedSearchAsync(TVector vector, VectorSearchOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) #pragma warning restore CS1998 { + Verify.NotNull(vector); + if (this._firstVectorPropertyInfo is null) { throw new InvalidOperationException("The collection does not have any vector fields, so vector search is not possible."); } - if (vectorQuery is VectorizedSearchQuery> floatVectorQuery) + if (vector is not ReadOnlyMemory floatVector) { - // Resolve options and get requested vector property or first as default. - var internalOptions = floatVectorQuery.SearchOptions ?? Data.VectorSearchOptions.Default; - PropertyInfo? vectorPropertyInfo; - if (internalOptions.VectorFieldName is not null) - { - if (!this._vectorPropertiesInfo.TryGetValue(internalOptions.VectorFieldName, out vectorPropertyInfo)) - { - throw new InvalidOperationException($"The collection does not have a vector field named '{internalOptions.VectorFieldName}', so vector search is not possible."); - } - } - else + throw new NotSupportedException($"The provided vector type {vector.GetType().Name} is not supported by the Qdrant connector."); + } + + // Resolve options and get requested vector property or first as default. + var internalOptions = options ?? Data.VectorSearchOptions.Default; + PropertyInfo? vectorPropertyInfo; + if (internalOptions.VectorFieldName is not null) + { + if (!this._vectorPropertiesInfo.TryGetValue(internalOptions.VectorFieldName, out vectorPropertyInfo)) { - vectorPropertyInfo = this._firstVectorPropertyInfo; + throw new InvalidOperationException($"The collection does not have a vector field named '{internalOptions.VectorFieldName}', so vector search is not possible."); } + } + else + { + vectorPropertyInfo = this._firstVectorPropertyInfo; + } - var vectorProperty = this._vectorProperties[vectorPropertyInfo.Name]; + var vectorProperty = this._vectorProperties[vectorPropertyInfo.Name]; - // Filter records using the provided filter before doing the vector comparison. - var filteredRecords = VolatileVectorStoreCollectionSearchMapping.FilterRecords(internalOptions.Filter, this.GetCollectionDictionary().Values); + // Filter records using the provided filter before doing the vector comparison. + var filteredRecords = VolatileVectorStoreCollectionSearchMapping.FilterRecords(internalOptions.Filter, this.GetCollectionDictionary().Values); - // Compare each vector in the filtered results with the provided vector. - var results = filteredRecords.Select((record) => - { - var vector = (ReadOnlyMemory?)vectorPropertyInfo.GetValue(record); - if (vector is not null) - { - var score = VolatileVectorStoreCollectionSearchMapping.CompareVectors(floatVectorQuery.Vector.Span, vector.Value.Span, vectorProperty.DistanceFunction); - var convertedscore = VolatileVectorStoreCollectionSearchMapping.ConvertScore(score, vectorProperty.DistanceFunction); - return (record, convertedscore); - } - - return null; - }); - - // Get the non-null results, sort them appropriately for the selected distance function and return the requested page. - var nonNullResults = results.Where(x => x.HasValue).Select(x => x!.Value); - var sortedScoredResults = VolatileVectorStoreCollectionSearchMapping.ShouldSortDescending(vectorProperty.DistanceFunction) ? - nonNullResults.OrderByDescending(x => x.score) : - nonNullResults.OrderBy(x => x.score); - - foreach (var scoredResult in sortedScoredResults.Skip(internalOptions.Offset).Take(internalOptions.Limit)) + // Compare each vector in the filtered results with the provided vector. + var results = filteredRecords.Select((record) => + { + var dbVector = (ReadOnlyMemory?)vectorPropertyInfo.GetValue(record); + if (dbVector is not null) { - yield return new VectorSearchResult((TRecord)scoredResult.record, scoredResult.score); + var score = VolatileVectorStoreCollectionSearchMapping.CompareVectors(floatVector.Span, dbVector.Value.Span, vectorProperty.DistanceFunction); + var convertedscore = VolatileVectorStoreCollectionSearchMapping.ConvertScore(score, vectorProperty.DistanceFunction); + return (record, convertedscore); } - } - else + + return null; + }); + + // Get the non-null results, sort them appropriately for the selected distance function and return the requested page. + var nonNullResults = results.Where(x => x.HasValue).Select(x => x!.Value); + var sortedScoredResults = VolatileVectorStoreCollectionSearchMapping.ShouldSortDescending(vectorProperty.DistanceFunction) ? + nonNullResults.OrderByDescending(x => x.score) : + nonNullResults.OrderBy(x => x.score); + + foreach (var scoredResult in sortedScoredResults.Skip(internalOptions.Offset).Take(internalOptions.Limit)) { - throw new NotSupportedException($"A {nameof(VectorSearchQuery)} of type {vectorQuery.QueryType} is not supported by the Volatile connector."); + yield return new VectorSearchResult((TRecord)scoredResult.record, scoredResult.score); } } diff --git a/dotnet/src/SemanticKernel.UnitTests/Data/VolatileVectorStoreRecordCollectionTests.cs b/dotnet/src/SemanticKernel.UnitTests/Data/VolatileVectorStoreRecordCollectionTests.cs index 38d31f0e7ab2..ee64e71354b3 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Data/VolatileVectorStoreRecordCollectionTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Data/VolatileVectorStoreRecordCollectionTests.cs @@ -290,10 +290,9 @@ public async Task CanSearchWithVectorAsync(bool useDefinition, TKey testKe var sut = this.CreateRecordCollection(useDefinition); // Act - var actual = await sut.SearchAsync( - VectorSearchQuery.CreateQuery( - new ReadOnlyMemory(new float[] { 1, 1, 1, 1 }), - new VectorSearchOptions { IncludeVectors = true }), + var actual = await sut.VectorizedSearchAsync( + new ReadOnlyMemory(new float[] { 1, 1, 1, 1 }), + new VectorSearchOptions { IncludeVectors = true }, this._testCancellationToken).ToListAsync(); // Assert @@ -333,10 +332,9 @@ public async Task CanSearchWithVectorAndFilterAsync(bool useDefinition, TK // Act var filter = filterType == "Equality" ? new VectorSearchFilter().EqualTo("Data", $"data {testKey2}") : new VectorSearchFilter().AnyTagEqualTo("Tags", $"tag {testKey2}"); - var actual = await sut.SearchAsync( - VectorSearchQuery.CreateQuery( - new ReadOnlyMemory(new float[] { 1, 1, 1, 1 }), - new VectorSearchOptions { IncludeVectors = true, Filter = filter }), + var actual = await sut.VectorizedSearchAsync( + new ReadOnlyMemory(new float[] { 1, 1, 1, 1 }), + new VectorSearchOptions { IncludeVectors = true, Filter = filter }, this._testCancellationToken).ToListAsync(); // Assert @@ -384,8 +382,9 @@ public async Task CanSearchWithDifferentDistanceFunctionsAsync(string distanceFu }); // Act - var actual = await sut.SearchAsync( - VectorSearchQuery.CreateQuery(new ReadOnlyMemory(new float[] { 1, 1, 1, 1 }), new VectorSearchOptions { IncludeVectors = true }), + var actual = await sut.VectorizedSearchAsync( + new ReadOnlyMemory(new float[] { 1, 1, 1, 1 }), + new VectorSearchOptions { IncludeVectors = true }, this._testCancellationToken).ToListAsync(); // Assert @@ -423,10 +422,9 @@ public async Task CanSearchManyRecordsAsync(bool useDefinition) var sut = this.CreateRecordCollection(useDefinition); // Act - var actual = await sut.SearchAsync( - VectorSearchQuery.CreateQuery( - new ReadOnlyMemory(new float[] { 1, 1, 1, 1 }), - new VectorSearchOptions { IncludeVectors = true, Limit = 10, Offset = 10 }), + var actual = await sut.VectorizedSearchAsync( + new ReadOnlyMemory(new float[] { 1, 1, 1, 1 }), + new VectorSearchOptions { IncludeVectors = true, Limit = 10, Offset = 10 }, this._testCancellationToken).ToListAsync(); // Assert From 1d0dfcfd889df1ec01a02ee573b8ac1f0bdbbbb0 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Thu, 12 Sep 2024 10:11:13 +0100 Subject: [PATCH 2/5] Remove example that needs reworking before release. --- .../Memory/VectorSearch_SearchTypes.cs | 241 ------------------ 1 file changed, 241 deletions(-) delete mode 100644 dotnet/samples/Concepts/Memory/VectorSearch_SearchTypes.cs diff --git a/dotnet/samples/Concepts/Memory/VectorSearch_SearchTypes.cs b/dotnet/samples/Concepts/Memory/VectorSearch_SearchTypes.cs deleted file mode 100644 index 669695fcda60..000000000000 --- a/dotnet/samples/Concepts/Memory/VectorSearch_SearchTypes.cs +++ /dev/null @@ -1,241 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Azure; -using Azure.Search.Documents.Indexes; -using Azure.Search.Documents.Indexes.Models; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.AzureAISearch; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Microsoft.SemanticKernel.Data; -using Microsoft.SemanticKernel.Embeddings; - -namespace Memory; - -public class VectorSearch_SearchTypes(ITestOutputHelper output) : BaseTest(output) -{ - private static readonly IConfigurationRoot s_configuration = new ConfigurationBuilder() - .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) - .AddEnvironmentVariables() - .AddUserSecrets() - .Build(); - - private static readonly string? s_azureAISearchServiceUrl = s_configuration.GetRequiredSection("AzureAISearch").GetValue("ServiceUrl"); - private static readonly string? s_apiKey = s_configuration.GetRequiredSection("AzureAISearch").GetValue("ApiKey"); - private const string IndexName = "skconceptssearchtypes"; - - [Fact] - public async Task DIExampleAsync() - { - // Create a kernel with the required services. - var kernelBuilder = Kernel.CreateBuilder() - .AddAzureOpenAITextEmbeddingGeneration( - TestConfiguration.AzureOpenAIEmbeddings.DeploymentName, - TestConfiguration.AzureOpenAIEmbeddings.Endpoint, - TestConfiguration.AzureOpenAIEmbeddings.ApiKey) - .AddAzureAISearchVectorStore(new Uri(s_azureAISearchServiceUrl!), new AzureKeyCredential(s_apiKey!)); - - kernelBuilder.Services.AddSingleton(); - kernelBuilder.Services.AddSingleton(this.Output); - kernelBuilder.Services.AddSingleton>((sp) => - { - var vectorStore = sp.GetRequiredService(); - return vectorStore.GetCollection(IndexName); - }); - kernelBuilder.Services.AddSingleton>((sp) => - { - var vectorStore = sp.GetRequiredService(); - return (IVectorizableTextSearch)sp.GetRequiredService>(); - }); - - var kernel = kernelBuilder.Build(); - - var recordProcessor = kernel.GetRequiredService(); - - await CreateIndexAsync(); - await recordProcessor.ExecuteProcessAsync(); - } - - [Fact] - public async Task ExampleAsync() - { - // Create an embedding generation service. - var textEmbeddingGenerationService = new AzureOpenAITextEmbeddingGenerationService( - TestConfiguration.AzureOpenAIEmbeddings.DeploymentName, - TestConfiguration.AzureOpenAIEmbeddings.Endpoint, - TestConfiguration.AzureOpenAIEmbeddings.ApiKey); - - // Construct the vector store. - var vectorStore = new AzureAISearchVectorStore(new SearchIndexClient(new Uri(s_azureAISearchServiceUrl!), new AzureKeyCredential(s_apiKey!))); - var collection = vectorStore.GetCollection(IndexName); - var recordProcessor = new RecordProcessor( - this.Output, - collection, - (IVectorizableTextSearch)collection, - textEmbeddingGenerationService); - - await CreateIndexAsync(); - await recordProcessor.ExecuteProcessAsync(); - } - - private sealed class RecordProcessor( - ITestOutputHelper Console, - IVectorStoreRecordCollection collection, - IVectorizableTextSearch vectorizableTextSearch, - ITextEmbeddingGenerationService textEmbeddingGenerationService) - { - public async Task ExecuteProcessAsync() - { - // Create glossary entries and generate embeddings for them. - var glossaryEntries = CreateGlossaryEntries().ToList(); - var tasks = glossaryEntries.Select(entry => Task.Run(async () => - { - entry.DefinitionEmbedding = await textEmbeddingGenerationService.GenerateEmbeddingAsync(entry.Definition); - })); - await Task.WhenAll(tasks); - - // Upsert the glossary entries into the collection and return their keys. - var upsertedKeysTasks = glossaryEntries.Select(x => collection.UpsertAsync(x)); - var upsertedKeys = await Task.WhenAll(upsertedKeysTasks); - await Task.Delay(1000); - - // Search the collection using a vector search. - var searchString = "What is an Application Programming Interface"; - var searchVector = await textEmbeddingGenerationService.GenerateEmbeddingAsync(searchString); - var searchResult = await collection.VectorizedSearchAsync(searchVector, new() { Limit = 1 }).ToListAsync(); - - Console.WriteLine("Vector Search 1"); - Console.WriteLine("Search string: " + searchString); - Console.WriteLine("Result: " + searchResult.First().Record.Definition); - Console.WriteLine(); - - // Search the collection using a vector search. - searchString = "What is Retrieval Augmented Generation"; - searchVector = await textEmbeddingGenerationService.GenerateEmbeddingAsync(searchString); - searchResult = await collection.VectorizedSearchAsync(searchVector, new() { Limit = 1 }).ToListAsync(); - - Console.WriteLine("Vector Search 2"); - Console.WriteLine("Search string: " + searchString); - Console.WriteLine("Result: " + searchResult.First().Record.Definition); - Console.WriteLine(); - - // Search the collection using a vector search with pre-filtering. - searchString = "What is Retrieval Augmented Generation"; - searchVector = await textEmbeddingGenerationService.GenerateEmbeddingAsync(searchString); - var filter = new VectorSearchFilter().EqualTo(nameof(Glossary.Category), "External Definitions"); - searchResult = await collection.VectorizedSearchAsync(searchVector, new() { Limit = 3, Filter = filter }).ToListAsync(); - - Console.WriteLine("Vector Search with pre-filtering"); - Console.WriteLine("Search string: " + searchString); - Console.WriteLine("Number of results: " + searchResult.Count); - Console.WriteLine("Result 1 Score: " + searchResult[0].Score); - Console.WriteLine("Result 1: " + searchResult[0].Record.Definition); - Console.WriteLine("Result 2 Score: " + searchResult[1].Score); - Console.WriteLine("Result 2: " + searchResult[1].Record.Definition); - Console.WriteLine(); - - // Search the collection using a vectorizable search with pre-filtering. - searchResult = await vectorizableTextSearch.VectorizableTextSearchAsync(searchString, new() { Limit = 3, Filter = filter }).ToListAsync(); - - Console.WriteLine("Vectorizable text search with pre-filtering"); - Console.WriteLine("Search string: " + searchString); - Console.WriteLine("Number of results: " + searchResult.Count); - Console.WriteLine("Result 1 Score: " + searchResult[0].Score); - Console.WriteLine("Result 1: " + searchResult[0].Record.Definition); - Console.WriteLine("Result 2 Score: " + searchResult[1].Score); - Console.WriteLine("Result 2: " + searchResult[1].Record.Definition); - - await collection.DeleteCollectionAsync(); - } - } - - /// - /// Sample model class that represents a glossary entry. - /// - /// - /// Note that each property is decorated with an attribute that specifies how the property should be treated by the vector store. - /// This allows us to create a collection in the vector store and upsert and retrieve instances of this class without any further configuration. - /// - private sealed class Glossary - { - [VectorStoreRecordKey] - public string Key { get; set; } - - [VectorStoreRecordData(IsFilterable = true)] - public string Category { get; set; } - - [VectorStoreRecordData] - public string Term { get; set; } - - [VectorStoreRecordData] - public string Definition { get; set; } - - [VectorStoreRecordVector(1536)] - public ReadOnlyMemory DefinitionEmbedding { get; set; } - } - - /// - /// Create some sample glossary entries. - /// - /// A list of sample glossary entries. - private static IEnumerable CreateGlossaryEntries() - { - yield return new Glossary - { - Key = "1", - Category = "External Definitions", - Term = "API", - Definition = "Application Programming Interface. A set of rules and specifications that allow software components to communicate and exchange data." - }; - - yield return new Glossary - { - Key = "2", - Category = "Core Definitions", - Term = "Connectors", - Definition = "Connectors allow you to integrate with various services provide AI capabilities, including LLM, AudioToText, TextToAudio, Embedding generation, etc." - }; - - yield return new Glossary - { - Key = "3", - Category = "External Definitions", - Term = "RAG", - Definition = "Retrieval Augmented Generation - a term that refers to the process of retrieving additional data to provide as context to an LLM to use when generating a response (completion) to a user’s question (prompt)." - }; - } - - private static async Task CreateIndexAsync() - { - var indexName = VectorSearch_SearchTypes.IndexName; - var searchIndexClient = new SearchIndexClient(new Uri(s_azureAISearchServiceUrl!), new AzureKeyCredential(s_apiKey!)); - - // Build the list of fields from the model, and then replace the DescriptionEmbedding field with a vector field, to work around - // issue where the field is not recognized as an array on parsing on the server side when apply the VectorSearchFieldAttribute. - var searchFields = new List(); - searchFields.Add(new SimpleField("Key", SearchFieldDataType.String) { IsFilterable = true, IsKey = true }); - searchFields.Add(new SimpleField("Category", SearchFieldDataType.String) { IsFilterable = true }); - searchFields.Add(new SimpleField("Term", SearchFieldDataType.String) { IsFilterable = true }); - searchFields.Add(new SimpleField("Definition", SearchFieldDataType.String) { IsFilterable = true }); - searchFields.Add(new VectorSearchField("DefinitionEmbedding", 1536, "my-vector-profile")); - - // Create an index definition with a vectorizer to use when doing vector searches using text. - var definition = new SearchIndex(indexName, searchFields); - definition.VectorSearch = new VectorSearch(); - definition.VectorSearch.Vectorizers.Add(new AzureOpenAIVectorizer("text-embedding-vectorizer") - { - Parameters = new AzureOpenAIVectorizerParameters - { - ResourceUri = new Uri(TestConfiguration.AzureOpenAIEmbeddings.Endpoint), - DeploymentName = TestConfiguration.AzureOpenAIEmbeddings.DeploymentName, - ApiKey = TestConfiguration.AzureOpenAIEmbeddings.ApiKey, - ModelName = "text-embedding-ada-002" - } - }); - definition.VectorSearch.Algorithms.Add(new HnswAlgorithmConfiguration("my-hnsw-vector-config-1") { Parameters = new HnswParameters { Metric = VectorSearchAlgorithmMetric.Cosine } }); - definition.VectorSearch.Profiles.Add(new VectorSearchProfile("my-vector-profile", "my-hnsw-vector-config-1") { VectorizerName = "text-embedding-vectorizer" }); - - await searchIndexClient.CreateOrUpdateIndexAsync(definition); - } -} From f652952b5788ca4279d24a430514c8414104aa66 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Thu, 12 Sep 2024 10:42:38 +0100 Subject: [PATCH 3/5] Fix sample after azure open ai migration. --- dotnet/samples/Concepts/Memory/VectorSearch_Simple.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/samples/Concepts/Memory/VectorSearch_Simple.cs b/dotnet/samples/Concepts/Memory/VectorSearch_Simple.cs index de3a6e488800..84e0a6de91b5 100644 --- a/dotnet/samples/Concepts/Memory/VectorSearch_Simple.cs +++ b/dotnet/samples/Concepts/Memory/VectorSearch_Simple.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using Memory.VectorStoreFixtures; -using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Connectors.Qdrant; using Microsoft.SemanticKernel.Data; using Microsoft.SemanticKernel.Embeddings; From 0b0879567c0d9f31f7ce643fd1fee5db171877b5 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Thu, 12 Sep 2024 10:50:03 +0100 Subject: [PATCH 4/5] Update adr with design decision. --- docs/decisions/00NN-vector-search-design.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/decisions/00NN-vector-search-design.md b/docs/decisions/00NN-vector-search-design.md index 85fdea04b673..c60ce2073018 100644 --- a/docs/decisions/00NN-vector-search-design.md +++ b/docs/decisions/00NN-vector-search-design.md @@ -371,3 +371,7 @@ class AzureAISearchVectorStoreRecordCollection: IVectorStoreRecordColle ``` ## Decision Outcome + +Chosen option: 4 + +The consensus is that option 4 is easier to understand for users, where only functionality that works for all vector stores are exposed by default. From 4310e11e24b0a8cd84203a1113f4615dbf4cd1e2 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Fri, 13 Sep 2024 10:31:14 +0100 Subject: [PATCH 5/5] Addressing pr comments. --- .../samples/Concepts/Memory/VectorSearch_Simple.cs | 12 +++--------- .../Data/VectorSearch/IVectorizedSearch.cs | 2 +- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/dotnet/samples/Concepts/Memory/VectorSearch_Simple.cs b/dotnet/samples/Concepts/Memory/VectorSearch_Simple.cs index 84e0a6de91b5..56abbbabf940 100644 --- a/dotnet/samples/Concepts/Memory/VectorSearch_Simple.cs +++ b/dotnet/samples/Concepts/Memory/VectorSearch_Simple.cs @@ -1,11 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. -using Memory.VectorStoreFixtures; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; -using Microsoft.SemanticKernel.Connectors.Qdrant; using Microsoft.SemanticKernel.Data; using Microsoft.SemanticKernel.Embeddings; -using Qdrant.Client; namespace Memory; @@ -14,13 +11,11 @@ namespace Memory; /// /// The example shows the following steps: /// 1. Create an embedding generator. -/// 2. Create a Qdrant Vector Store. +/// 2. Create a Volatile Vector Store. /// 3. Ingest some data into the vector store. /// 4. Search the vector store with various text and filtering options. -/// -/// You need a local instance of Docker running, since the associated fixture will try and start a Qdrant container in the local docker instance to run against. /// -public class VectorSearch_Simple(ITestOutputHelper output, VectorStoreQdrantContainerFixture qdrantFixture) : BaseTest(output), IClassFixture +public class VectorSearch_Simple(ITestOutputHelper output) : BaseTest(output) { [Fact] public async Task ExampleAsync() @@ -32,8 +27,7 @@ public async Task ExampleAsync() TestConfiguration.AzureOpenAIEmbeddings.ApiKey); // Initiate the docker container and construct the vector store. - await qdrantFixture.ManualInitializeAsync(); - var vectorStore = new QdrantVectorStore(new QdrantClient("localhost")); + var vectorStore = new VolatileVectorStore(); // Get and create collection if it doesn't exist. var collection = vectorStore.GetCollection("skglossary"); diff --git a/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearch/IVectorizedSearch.cs b/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearch/IVectorizedSearch.cs index bc5c2d4107ef..15b3090054f1 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearch/IVectorizedSearch.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearch/IVectorizedSearch.cs @@ -6,7 +6,7 @@ namespace Microsoft.SemanticKernel.Data; /// -/// Contains a method for doing a vector search on a vector store. +/// Contains a method for doing a vector search using a vector. /// /// The record data model to use for retrieving data from the store. public interface IVectorizedSearch