diff --git a/.github/workflows/mevd.yml b/.github/workflows/mevd.yml
new file mode 100644
index 0000000..b88d4b5
--- /dev/null
+++ b/.github/workflows/mevd.yml
@@ -0,0 +1,102 @@
+name: MEVD
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - 'MEVD/**'
+ - 'Shared/**'
+ - 'eng/**'
+ - 'Directory.Build.props'
+ - 'Directory.Packages.props'
+ - 'global.json'
+ - '.github/workflows/mevd.yml'
+ pull_request:
+ branches: [main]
+ paths:
+ - 'MEVD/**'
+ - 'Shared/**'
+ - 'eng/**'
+ - 'Directory.Build.props'
+ - 'Directory.Packages.props'
+ - 'global.json'
+ - '.github/workflows/mevd.yml'
+
+env:
+ DOTNET_NOLOGO: true
+ DOTNET_CLI_TELEMETRY_OPTOUT: true
+
+jobs:
+ build:
+ name: Build
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ global-json-file: global.json
+
+ - name: Build
+ run: dotnet build MEVD/MEVD.slnf
+
+ unit-tests:
+ name: 'Unit: ${{ matrix.provider }} (${{ matrix.os }})'
+ needs: build
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, windows-latest]
+ provider:
+ - InMemory
+ - AzureAISearch
+ - CosmosMongoDB
+ - PgVector
+ - Pinecone
+ - Qdrant
+ - Redis
+ - SqliteVec
+ - Weaviate
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ global-json-file: global.json
+
+ - name: Test
+ run: dotnet test MEVD/test/${{ matrix.provider }}.UnitTests
+
+ # Conformance tests run on Ubuntu only: most providers use Testcontainers with Linux Docker
+ # images, which aren't available on Windows GitHub-hosted runners.
+ conformance-tests:
+ name: 'Conformance: ${{ matrix.provider }}'
+ needs: build
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ provider:
+ # AzureAISearch and CosmosMongoDB require cloud instances and are not run in CI.
+ # - AzureAISearch
+ # - CosmosMongoDB
+ - InMemory
+ - PgVector
+ - Pinecone
+ - Qdrant
+ - Redis
+ - SqliteVec
+ - Weaviate
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ global-json-file: global.json
+
+ - name: Test
+ run: dotnet test MEVD/test/${{ matrix.provider }}.ConformanceTests
diff --git a/CommunityToolkit.AI.slnx b/CommunityToolkit.AI.slnx
new file mode 100644
index 0000000..79732c7
--- /dev/null
+++ b/CommunityToolkit.AI.slnx
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Directory.Build.props b/Directory.Build.props
index 3c67d1d..c9a0319 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -9,4 +9,6 @@
true
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 396aed1..7199d1e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,4 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MEVD/MEVD.slnf b/MEVD/MEVD.slnf
new file mode 100644
index 0000000..517fa78
--- /dev/null
+++ b/MEVD/MEVD.slnf
@@ -0,0 +1,36 @@
+{
+ "solution": {
+ "path": "../CommunityToolkit.AI.slnx",
+ "projects":
+ [
+ "MEVD/src/AzureAISearch/AzureAISearch.csproj",
+ "MEVD/src/CosmosMongoDB/CosmosMongoDB.csproj",
+ "MEVD/src/InMemory/InMemory.csproj",
+ "MEVD/src/Pinecone/Pinecone.csproj",
+ "MEVD/src/PgVector/PgVector.csproj",
+ "MEVD/src/Qdrant/Qdrant.csproj",
+ "MEVD/src/Redis/Redis.csproj",
+ "MEVD/src/SqliteVec/SqliteVec.csproj",
+ "MEVD/src/Weaviate/Weaviate.csproj",
+
+ "MEVD/test/AzureAISearch.UnitTests/AzureAISearch.UnitTests.csproj",
+ "MEVD/test/AzureAISearch.ConformanceTests/AzureAISearch.ConformanceTests.csproj",
+ "MEVD/test/CosmosMongoDB.UnitTests/CosmosMongoDB.UnitTests.csproj",
+ "MEVD/test/CosmosMongoDB.ConformanceTests/CosmosMongoDB.ConformanceTests.csproj",
+ "MEVD/test/InMemory.UnitTests/InMemory.UnitTests.csproj",
+ "MEVD/test/InMemory.ConformanceTests/InMemory.ConformanceTests.csproj",
+ "MEVD/test/PgVector.UnitTests/PgVector.UnitTests.csproj",
+ "MEVD/test/PgVector.ConformanceTests/PgVector.ConformanceTests.csproj",
+ "MEVD/test/Pinecone.UnitTests/Pinecone.UnitTests.csproj",
+ "MEVD/test/Pinecone.ConformanceTests/Pinecone.ConformanceTests.csproj",
+ "MEVD/test/Qdrant.UnitTests/Qdrant.UnitTests.csproj",
+ "MEVD/test/Qdrant.ConformanceTests/Qdrant.ConformanceTests.csproj",
+ "MEVD/test/Redis.UnitTests/Redis.UnitTests.csproj",
+ "MEVD/test/Redis.ConformanceTests/Redis.ConformanceTests.csproj",
+ "MEVD/test/SqliteVec.UnitTests/SqliteVec.UnitTests.csproj",
+ "MEVD/test/SqliteVec.ConformanceTests/SqliteVec.ConformanceTests.csproj",
+ "MEVD/test/Weaviate.UnitTests/Weaviate.UnitTests.csproj",
+ "MEVD/test/Weaviate.ConformanceTests/Weaviate.ConformanceTests.csproj"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/MEVD/src/AzureAISearch/AssemblyInfo.cs b/MEVD/src/AzureAISearch/AssemblyInfo.cs
new file mode 100644
index 0000000..d6395e8
--- /dev/null
+++ b/MEVD/src/AzureAISearch/AssemblyInfo.cs
@@ -0,0 +1,2 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
diff --git a/MEVD/src/AzureAISearch/AzureAISearch.csproj b/MEVD/src/AzureAISearch/AzureAISearch.csproj
new file mode 100644
index 0000000..9b238b5
--- /dev/null
+++ b/MEVD/src/AzureAISearch/AzureAISearch.csproj
@@ -0,0 +1,27 @@
+
+
+ 1.0.0-preview.1
+ CommunityToolkit.VectorData.AzureAISearch
+ $(AssemblyName)
+ net10.0;net8.0;netstandard2.0;net462
+
+ Azure AI Search provider for Microsoft.Extensions.VectorData
+ Azure AI Search provider for Microsoft.Extensions.VectorData by Semantic Kernel
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MEVD/src/AzureAISearch/AzureAISearchCollection.cs b/MEVD/src/AzureAISearch/AzureAISearchCollection.cs
new file mode 100644
index 0000000..8e42c09
--- /dev/null
+++ b/MEVD/src/AzureAISearch/AzureAISearchCollection.cs
@@ -0,0 +1,813 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+using System.Runtime.CompilerServices;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Azure;
+using Azure.Search.Documents;
+using Azure.Search.Documents.Indexes;
+using Azure.Search.Documents.Indexes.Models;
+using Azure.Search.Documents.Models;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.VectorData;
+using Microsoft.Extensions.VectorData.ProviderServices;
+using MEAI = Microsoft.Extensions.AI;
+using Microsoft.Shared.Diagnostics;
+
+namespace CommunityToolkit.VectorData.AzureAISearch;
+
+///
+/// Service for storing and retrieving vector records, that uses Azure AI Search as the underlying storage.
+///
+/// The data type of the record key. Must be .
+/// The data model to use for adding, updating and retrieving data from storage.
+#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
+public class AzureAISearchCollection : VectorStoreCollection, IKeywordHybridSearchable
+ where TKey : notnull
+ where TRecord : class
+#pragma warning restore CA1711 // Identifiers should not have incorrect suffix
+{
+ /// Metadata about vector store record collection.
+ private readonly VectorStoreCollectionMetadata _collectionMetadata;
+
+ /// The default options for vector search.
+ private static readonly VectorSearchOptions s_defaultVectorSearchOptions = new();
+
+ /// The default options for hybrid vector search.
+ private static readonly HybridSearchOptions s_defaultKeywordVectorizedHybridSearchOptions = new();
+
+ /// Azure AI Search client that can be used to manage the list of indices in an Azure AI Search Service.
+ private readonly SearchIndexClient _searchIndexClient;
+
+ /// Azure AI Search client that can be used to manage data in an Azure AI Search Service index.
+ private readonly SearchClient _searchClient;
+
+ /// A mapper to use for converting between the data model and the Azure AI Search record.
+ private readonly IAzureAISearchMapper _mappper;
+
+ /// The model for this collection.
+ private readonly CollectionModel _model;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Azure AI Search client that can be used to manage the list of indices in an Azure AI Search Service.
+ /// The name of the collection that this will access.
+ /// Optional configuration options for this class.
+ /// Thrown when is null.
+ /// Thrown when options are misconfigured.
+ [RequiresUnreferencedCode("The Azure AI Search provider is currently incompatible with trimming.")]
+ [RequiresDynamicCode("The Azure AI Search provider is currently incompatible with NativeAOT.")]
+ public AzureAISearchCollection(SearchIndexClient searchIndexClient, string name, AzureAISearchCollectionOptions? options = default)
+ : this(
+ searchIndexClient,
+ name,
+ static options => typeof(TRecord) == typeof(Dictionary)
+ ? throw new NotSupportedException(VectorDataStrings.NonDynamicCollectionWithDictionaryNotSupported(typeof(AzureAISearchDynamicCollection)))
+ : new AzureAISearchModelBuilder()
+ .Build(typeof(TRecord), typeof(TKey), options.Definition, options.EmbeddingGenerator, options.JsonSerializerOptions ?? JsonSerializerOptions.Default),
+ options)
+ {
+ }
+
+ internal AzureAISearchCollection(SearchIndexClient searchIndexClient, string name, Func modelFactory, AzureAISearchCollectionOptions? options)
+ {
+ // Verify.
+ Throw.IfNull(searchIndexClient);
+ Throw.IfNullOrWhitespace(name);
+
+ if (typeof(TKey) != typeof(string) && typeof(TKey) != typeof(Guid) && typeof(TKey) != typeof(object))
+ {
+ throw new NotSupportedException("Only string and Guid keys are supported.");
+ }
+
+ options ??= AzureAISearchCollectionOptions.Default;
+
+ // Assign.
+ Name = name;
+ _model = modelFactory(options);
+ _searchIndexClient = searchIndexClient;
+ _searchClient = _searchIndexClient.GetSearchClient(name);
+
+ _mappper = typeof(TRecord) == typeof(Dictionary) ?
+ (IAzureAISearchMapper)(object)new AzureAISearchDynamicMapper(_model, options.JsonSerializerOptions) :
+ new AzureAISearchMapper(_model, options.JsonSerializerOptions);
+
+ _collectionMetadata = new()
+ {
+ VectorStoreSystemName = AzureAISearchConstants.VectorStoreSystemName,
+ VectorStoreName = searchIndexClient.ServiceName,
+ CollectionName = name
+ };
+ }
+
+ ///
+ public override string Name { get; }
+
+ ///
+ public override async Task CollectionExistsAsync(CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ await _searchIndexClient.GetIndexAsync(Name, cancellationToken).ConfigureAwait(false);
+ return true;
+ }
+ catch (RequestFailedException ex) when (ex.Status == 404)
+ {
+ return false;
+ }
+ catch (RequestFailedException ex)
+ {
+ throw new VectorStoreException("Call to vector store failed.", ex)
+ {
+ VectorStoreSystemName = AzureAISearchConstants.VectorStoreSystemName,
+ VectorStoreName = _collectionMetadata.VectorStoreName,
+ CollectionName = Name,
+ OperationName = "GetIndex"
+ };
+ }
+ }
+
+ ///
+ public override async Task EnsureCollectionExistsAsync(CancellationToken cancellationToken = default)
+ {
+ const string OperationName = "CreateIndex";
+
+ // Don't even try to create if the collection already exists.
+ if (await CollectionExistsAsync(cancellationToken).ConfigureAwait(false))
+ {
+ return;
+ }
+
+ var vectorSearchConfig = new VectorSearch();
+ var searchFields = new List();
+
+ // Loop through all properties and create the search fields.
+ foreach (var property in _model.Properties)
+ {
+ switch (property)
+ {
+ case KeyPropertyModel p:
+ searchFields.Add(AzureAISearchCollectionCreateMapping.MapKeyField(p));
+ break;
+
+ case DataPropertyModel p:
+ searchFields.Add(AzureAISearchCollectionCreateMapping.MapDataField(p));
+ break;
+
+ case VectorPropertyModel p:
+ (VectorSearchField vectorSearchField, VectorSearchAlgorithmConfiguration algorithmConfiguration, VectorSearchProfile vectorSearchProfile) = AzureAISearchCollectionCreateMapping.MapVectorField(p);
+
+ // Add the search field, plus its profile and algorithm configuration to the search config.
+ searchFields.Add(vectorSearchField);
+ vectorSearchConfig.Algorithms.Add(algorithmConfiguration);
+ vectorSearchConfig.Profiles.Add(vectorSearchProfile);
+ break;
+
+ default:
+ throw new UnreachableException();
+ }
+ }
+
+ // Create the index definition.
+ var searchIndex = new SearchIndex(Name, searchFields);
+ searchIndex.VectorSearch = vectorSearchConfig;
+
+ try
+ {
+ await _searchIndexClient.CreateIndexAsync(searchIndex, cancellationToken).ConfigureAwait(false);
+ }
+ catch (RequestFailedException ex) when (ex.ErrorCode == "ResourceNameAlreadyInUse")
+ {
+ // Index already exists, ignore.
+ }
+ catch (RequestFailedException ex)
+ {
+ throw new VectorStoreException("Call to vector store failed.", ex)
+ {
+ VectorStoreSystemName = AzureAISearchConstants.VectorStoreSystemName,
+ VectorStoreName = _collectionMetadata.VectorStoreName,
+ CollectionName = Name,
+ OperationName = OperationName
+ };
+ }
+ catch (AggregateException ex) when (ex.InnerException is RequestFailedException innerEx)
+ {
+ throw new VectorStoreException("Call to vector store failed.", ex)
+ {
+ VectorStoreSystemName = AzureAISearchConstants.VectorStoreSystemName,
+ VectorStoreName = _collectionMetadata.VectorStoreName,
+ CollectionName = Name,
+ OperationName = OperationName
+ };
+ }
+ }
+
+ ///
+ public override Task EnsureCollectionDeletedAsync(CancellationToken cancellationToken = default)
+ {
+ return RunOperationAsync(
+ "DeleteIndex",
+ async () =>
+ {
+ try
+ {
+ return await _searchIndexClient.DeleteIndexAsync(Name, cancellationToken).ConfigureAwait(false);
+ }
+ catch (RequestFailedException ex) when (ex.Status == 404)
+ {
+ return null!;
+ }
+ });
+ }
+
+ ///
+ public override Task GetAsync(TKey key, RecordRetrievalOptions? options = default, CancellationToken cancellationToken = default)
+ {
+ // Create Options.
+ var innerOptions = ConvertGetDocumentOptions(options);
+ var includeVectors = options?.IncludeVectors ?? false;
+ if (includeVectors && _model.EmbeddingGenerationRequired)
+ {
+ throw new NotSupportedException(VectorDataStrings.IncludeVectorsNotSupportedWithEmbeddingGeneration);
+ }
+
+ // Get record.
+ return GetDocumentAndMapToDataModelAsync(key, includeVectors, innerOptions, cancellationToken);
+ }
+
+ ///
+ public override async IAsyncEnumerable GetAsync(IEnumerable keys, RecordRetrievalOptions? options = default, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ Throw.IfNull(keys);
+
+ // Create Options
+ var innerOptions = ConvertGetDocumentOptions(options);
+ var includeVectors = options?.IncludeVectors ?? false;
+ if (includeVectors && _model.EmbeddingGenerationRequired)
+ {
+ throw new NotSupportedException(VectorDataStrings.IncludeVectorsNotSupportedWithEmbeddingGeneration);
+ }
+
+ foreach (var key in keys)
+ {
+ var record = await GetDocumentAndMapToDataModelAsync(key, includeVectors, innerOptions, cancellationToken).ConfigureAwait(false);
+
+ if (record is not null)
+ {
+ yield return record;
+ }
+ }
+ }
+
+ ///
+ public override Task DeleteAsync(TKey key, CancellationToken cancellationToken = default)
+ {
+ var stringKey = GetStringKey(key);
+
+ // Remove record.
+ return RunOperationAsync(
+ "DeleteDocuments",
+ () => _searchClient.DeleteDocumentsAsync(_model.KeyProperty.StorageName, [stringKey], new IndexDocumentsOptions(), cancellationToken));
+ }
+
+ ///
+ public override Task DeleteAsync(IEnumerable keys, CancellationToken cancellationToken = default)
+ {
+ Throw.IfNull(keys);
+ if (!keys.Any())
+ {
+ return Task.CompletedTask;
+ }
+
+ var stringKeys = keys is IEnumerable k ? k : keys.Select(GetStringKey);
+
+ // Remove records.
+ return RunOperationAsync(
+ "DeleteDocuments",
+ () => _searchClient.DeleteDocumentsAsync(_model.KeyProperty.StorageName, stringKeys, new IndexDocumentsOptions(), cancellationToken));
+ }
+
+ ///
+ public override async Task UpsertAsync(TRecord record, CancellationToken cancellationToken = default)
+ {
+ Throw.IfNull(record);
+
+ // Create options.
+ var innerOptions = new IndexDocumentsOptions { ThrowOnAnyError = true };
+
+ // Upsert record.
+ await MapToStorageModelAndUploadDocumentAsync([record], innerOptions, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ public override async Task UpsertAsync(IEnumerable records, CancellationToken cancellationToken = default)
+ {
+ Throw.IfNull(records);
+ if (!records.Any())
+ {
+ return;
+ }
+
+ // Create Options
+ var innerOptions = new IndexDocumentsOptions { ThrowOnAnyError = true };
+
+ // Upsert records
+ await MapToStorageModelAndUploadDocumentAsync(records, innerOptions, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ public override IAsyncEnumerable GetAsync(Expression> filter, int top,
+ FilteredRecordRetrievalOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ Throw.IfNull(filter);
+ Throw.IfLessThan(top, 1);
+
+ options ??= new();
+
+ var includeVectors = options.IncludeVectors;
+ if (includeVectors && _model.EmbeddingGenerationRequired)
+ {
+ throw new NotSupportedException(VectorDataStrings.IncludeVectorsNotSupportedWithEmbeddingGeneration);
+ }
+
+ SearchOptions searchOptions = new()
+ {
+ VectorSearch = new(),
+ Size = top,
+ Skip = options.Skip,
+ Filter = new AzureAISearchFilterTranslator().Translate(filter, _model)
+ };
+
+ // Filter out vector fields if requested.
+ if (!options.IncludeVectors)
+ {
+ searchOptions.Select.Add(_model.KeyProperty.StorageName);
+
+ foreach (var dataProperty in _model.DataProperties)
+ {
+ searchOptions.Select.Add(dataProperty.StorageName);
+ }
+ }
+
+ if (options.OrderBy is not null)
+ {
+ foreach (var pair in options.OrderBy(new()).Values)
+ {
+ PropertyModel property = _model.GetDataOrKeyProperty(pair.PropertySelector);
+ string name = property.StorageName;
+ // From https://learn.microsoft.com/dotnet/api/azure.search.documents.searchoptions.orderby:
+ // "Each expression can be followed by asc to indicate ascending, or desc to indicate descending".
+ // "The default is ascending order."
+ if (!pair.Ascending)
+ {
+ name += " desc";
+ }
+
+ searchOptions.OrderBy.Add(name);
+ }
+ }
+
+ return SearchAndMapToDataModelAsync(null, searchOptions, options.IncludeVectors, cancellationToken)
+ .Select(result => result.Record);
+ }
+
+ #region Search
+
+ ///
+ public override async IAsyncEnumerable> SearchAsync(
+ TInput searchValue,
+ int top,
+ VectorSearchOptions? options = null,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ Throw.IfNull(searchValue);
+ Throw.IfLessThan(top, 1);
+
+ options ??= s_defaultVectorSearchOptions;
+ var vectorProperty = _model.GetVectorPropertyOrSingle(options);
+ var floatVector = await GetSearchVectorAsync(searchValue, vectorProperty, cancellationToken).ConfigureAwait(false);
+
+ var searchOptions = BuildSearchOptions(
+ _model,
+ options,
+ top,
+ floatVector is null
+ ? new VectorizableTextQuery((string)(object)searchValue) { KNearestNeighborsCount = top + options.Skip, Fields = { vectorProperty.StorageName } }
+ : new VectorizedQuery(floatVector.Value) { KNearestNeighborsCount = top + options.Skip, Fields = { vectorProperty.StorageName } });
+
+ await foreach (var record in SearchAndMapToDataModelAsync(null, searchOptions, options.IncludeVectors, cancellationToken).ConfigureAwait(false))
+ {
+ // Azure AI Search threshold filtering is in preview:
+ // https://learn.microsoft.com/azure/search/vector-search-how-to-query#set-thresholds-to-exclude-low-scoring-results-preview
+ // See https://github.com/microsoft/semantic-kernel/issues/13500.
+ // For now, perform post-filtering on the client-side.
+ if (options.ScoreThreshold.HasValue && record.Score < options.ScoreThreshold.Value)
+ {
+ continue;
+ }
+
+ yield return record;
+ }
+ }
+
+ ///
+ public async IAsyncEnumerable> HybridSearchAsync(
+ TInput searchValue,
+ ICollection keywords,
+ int top,
+ HybridSearchOptions? options = null,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ where TInput : notnull
+ {
+ Throw.IfNull(keywords);
+ Throw.IfLessThan(top, 1);
+
+ // Resolve options.
+ options ??= s_defaultKeywordVectorizedHybridSearchOptions;
+ var vectorProperty = _model.GetVectorPropertyOrSingle(new() { VectorProperty = options.VectorProperty });
+ var floatVector = await GetSearchVectorAsync(searchValue, vectorProperty, cancellationToken).ConfigureAwait(false);
+
+ var textDataProperty = _model.GetFullTextDataPropertyOrSingle(options.AdditionalProperty);
+
+ // Build search options.
+ var searchOptions = BuildSearchOptions(
+ _model,
+ new()
+ {
+ Filter = options.Filter,
+ VectorProperty = options.VectorProperty,
+ Skip = options.Skip,
+ },
+ top,
+ floatVector is null
+ ? new VectorizableTextQuery((string)(object)searchValue) { KNearestNeighborsCount = top + options.Skip, Fields = { vectorProperty.StorageName } }
+ : new VectorizedQuery(floatVector.Value) { KNearestNeighborsCount = top + options.Skip, Fields = { vectorProperty.StorageName } });
+
+ searchOptions.SearchFields.Add(textDataProperty.StorageName);
+ var keywordsCombined = string.Join(" ", keywords);
+
+ await foreach (var record in SearchAndMapToDataModelAsync(keywordsCombined, searchOptions, options.IncludeVectors, cancellationToken).ConfigureAwait(false))
+ {
+ // Azure AI Search returns scores where higher values indicate more relevant results.
+ if (options.ScoreThreshold.HasValue && record.Score < options.ScoreThreshold.Value)
+ {
+ continue;
+ }
+
+ yield return record;
+ }
+ }
+
+ private static async ValueTask?> GetSearchVectorAsync(TInput searchValue, VectorPropertyModel vectorProperty, CancellationToken cancellationToken)
+ where TInput : notnull
+ => searchValue switch
+ {
+ ReadOnlyMemory r => r,
+ float[] f => new ReadOnlyMemory(f),
+ Embedding e => e.Vector,
+ _ when vectorProperty.EmbeddingGenerationDispatcher is not null
+ => ((Embedding)await vectorProperty.GenerateEmbeddingAsync(searchValue, cancellationToken).ConfigureAwait(false)).Vector,
+
+ // A string was passed without an embedding generator being configured; send the string to Azure AI Search for backend embedding generation.
+ string when vectorProperty.EmbeddingGenerator is null => (ReadOnlyMemory?)null,
+
+ _ => vectorProperty.EmbeddingGenerator is null
+ ? throw new NotSupportedException(VectorDataStrings.InvalidSearchInputAndNoEmbeddingGeneratorWasConfigured(searchValue.GetType(), AzureAISearchModelBuilder.SupportedVectorTypes))
+ : throw new InvalidOperationException(VectorDataStrings.IncompatibleEmbeddingGeneratorWasConfiguredForInputType(typeof(TInput), vectorProperty.EmbeddingGenerator.GetType()))
+ };
+
+ #endregion Search
+
+ ///
+ public override object? GetService(Type serviceType, object? serviceKey = null)
+ {
+ Throw.IfNull(serviceType);
+
+ return
+ serviceKey is not null ? null :
+ serviceType == typeof(VectorStoreCollectionMetadata) ? _collectionMetadata :
+ serviceType == typeof(SearchIndexClient) ? _searchIndexClient :
+ serviceType == typeof(SearchClient) ? _searchClient :
+ serviceType.IsInstanceOfType(this) ? this :
+ null;
+ }
+
+ ///
+ /// Get the document with the given key and map it to the data model using the configured mapper type.
+ ///
+ /// The key of the record to get.
+ /// A value indicating whether to include vectors in the result or not.
+ /// The Azure AI Search sdk options for getting a document.
+ /// The to monitor for cancellation requests. The default is .
+ /// The retrieved document, mapped to the consumer data model.
+ private async Task GetDocumentAndMapToDataModelAsync(
+ TKey key,
+ bool includeVectors,
+ GetDocumentOptions innerOptions,
+ CancellationToken cancellationToken)
+ {
+ const string OperationName = "GetDocument";
+
+ var stringKey = GetStringKey(key);
+
+ var jsonObject = await RunOperationAsync(
+ OperationName,
+ () => GetDocumentWithNotFoundHandlingAsync(_searchClient, stringKey, innerOptions, cancellationToken)).ConfigureAwait(false);
+
+ if (jsonObject is null)
+ {
+ return default;
+ }
+
+ return (TRecord)(object)_mappper!.MapFromStorageToDataModel(jsonObject, includeVectors);
+ }
+
+ ///
+ /// Search for the documents matching the given options and map them to the data model using the configured mapper type.
+ ///
+ /// Text to use if doing a hybrid search. Null for non-hybrid search.
+ /// The options controlling the behavior of the search operation.
+ /// A value indicating whether to include vectors in the result or not.
+ /// The to monitor for cancellation requests. The default is .
+ /// The mapped search results.
+ private async IAsyncEnumerable> SearchAndMapToDataModelAsync(
+ string? searchText,
+ SearchOptions searchOptions,
+ bool includeVectors,
+ [EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ const string OperationName = "Search";
+
+ var jsonObjectResults = await RunOperationAsync(
+ OperationName,
+ () => _searchClient.SearchAsync(searchText, searchOptions, cancellationToken)).ConfigureAwait(false);
+
+ await foreach (var result in MapSearchResultsAsync(jsonObjectResults.Value.GetResultsAsync(), OperationName, includeVectors).ConfigureAwait(false))
+ {
+ yield return result;
+ }
+ }
+
+ ///
+ /// Map the data model to the storage model and upload the document.
+ ///
+ /// The records to upload.
+ /// The Azure AI Search sdk options for uploading a document.
+ /// The to monitor for cancellation requests. The default is .
+ /// The document upload result.
+ private async Task> MapToStorageModelAndUploadDocumentAsync(
+ IEnumerable records,
+ IndexDocumentsOptions innerOptions,
+ CancellationToken cancellationToken)
+ {
+ const string OperationName = "UploadDocuments";
+
+ (records, var generatedEmbeddings) = await ProcessEmbeddingsAsync(_model, records, cancellationToken).ConfigureAwait(false);
+
+ // Handle auto-generated keys (client-side for Azure AI Search, which doesn't support server-side auto-generation)
+ var keyProperty = _model.KeyProperty;
+ var jsonObjects = new List();
+ var recordIndex = 0;
+ foreach (var record in records)
+ {
+ if (keyProperty.IsAutoGenerated && keyProperty.GetValue(record) == Guid.Empty)
+ {
+ keyProperty.SetValue(record, Guid.NewGuid());
+ }
+
+ jsonObjects.Add(_mappper!.MapFromDataToStorageModel(record, recordIndex++, generatedEmbeddings));
+ }
+
+ return await RunOperationAsync(
+ OperationName,
+ () => _searchClient.UploadDocumentsAsync(jsonObjects, innerOptions, cancellationToken)).ConfigureAwait(false);
+ }
+
+ ///
+ /// Map the search results from to objects using the configured mapper type.
+ ///
+ /// The search results to map.
+ /// The name of the current operation for telemetry purposes.
+ /// A value indicating whether to include vectors in the resultset or not.
+ /// The mapped results.
+ private async IAsyncEnumerable> MapSearchResultsAsync(IAsyncEnumerable> results, string operationName, bool includeVectors)
+ {
+ await foreach (var result in results.ConfigureAwait(false))
+ {
+ var document = (TRecord)(object)_mappper!.MapFromStorageToDataModel(result.Document, includeVectors);
+ yield return new VectorSearchResult(document, result.Score);
+ }
+ }
+
+ ///
+ /// Map the search results from to objects.
+ ///
+ /// The search results to map.
+ /// The mapped results.
+ private async IAsyncEnumerable> MapSearchResultsAsync(IAsyncEnumerable> results)
+ {
+ await foreach (var result in results.ConfigureAwait(false))
+ {
+ yield return new VectorSearchResult(result.Document, result.Score);
+ }
+ }
+
+ ///
+ /// Convert the public options model to the Azure AI Search options model.
+ ///
+ /// The public options model.
+ /// The Azure AI Search options model.
+ private GetDocumentOptions ConvertGetDocumentOptions(RecordRetrievalOptions? options)
+ {
+ var innerOptions = new GetDocumentOptions();
+ if (options?.IncludeVectors is not true)
+ {
+ innerOptions.SelectedFields.Add(_model.KeyProperty.StorageName);
+
+ foreach (var dataProperty in _model.DataProperties)
+ {
+ innerOptions.SelectedFields.Add(dataProperty.StorageName);
+ }
+ }
+
+ return innerOptions;
+ }
+
+ ///
+ /// Build the search options for a vector search, where the type of vector search can be provided as input.
+ /// E.g. VectorizedQuery or VectorizableTextQuery.
+ ///
+ private static SearchOptions BuildSearchOptions(CollectionModel model, VectorSearchOptions options, int top, VectorQuery? vectorQuery)
+ {
+ if (model.VectorProperties.Count == 0)
+ {
+ throw new InvalidOperationException("The collection does not have any vector fields, so vector search is not possible.");
+ }
+
+ if (options.IncludeVectors && model.EmbeddingGenerationRequired)
+ {
+ throw new NotSupportedException(VectorDataStrings.IncludeVectorsNotSupportedWithEmbeddingGeneration);
+ }
+
+ // Build filter object.
+ var filter = options.Filter is not null
+ ? new AzureAISearchFilterTranslator().Translate(options.Filter, model)
+ : null;
+
+ // Build search options.
+ var searchOptions = new SearchOptions
+ {
+ VectorSearch = new(),
+ Size = top,
+ Skip = options.Skip,
+ };
+
+ if (filter is not null)
+ {
+ searchOptions.Filter = filter;
+ }
+
+ searchOptions.VectorSearch.Queries.Add(vectorQuery);
+
+ // Filter out vector fields if requested.
+ if (!options.IncludeVectors)
+ {
+ searchOptions.Select.Add(model.KeyProperty.StorageName);
+
+ foreach (var dataProperty in model.DataProperties)
+ {
+ searchOptions.Select.Add(dataProperty.StorageName);
+ }
+ }
+
+ return searchOptions;
+ }
+
+ private static async ValueTask<(IEnumerable records, IReadOnlyList?[]?)> ProcessEmbeddingsAsync(
+ CollectionModel model,
+ IEnumerable records,
+ CancellationToken cancellationToken)
+ {
+ IReadOnlyList? recordsList = null;
+
+ // If an embedding generator is defined, invoke it once per property for all records.
+ IReadOnlyList?[]? generatedEmbeddings = null;
+
+ var vectorPropertyCount = model.VectorProperties.Count;
+ for (var i = 0; i < vectorPropertyCount; i++)
+ {
+ var vectorProperty = model.VectorProperties[i];
+
+ if (AzureAISearchModelBuilder.IsVectorPropertyTypeValidCore(vectorProperty.Type, out _))
+ {
+ continue;
+ }
+
+ // We have a vector property whose type isn't natively supported - we need to generate embeddings.
+ Debug.Assert(vectorProperty.EmbeddingGenerator is not null);
+
+ // We have a property with embedding generation; materialize the records' enumerable if needed, to
+ // prevent multiple enumeration.
+ if (recordsList is null)
+ {
+ recordsList = records is IReadOnlyList r ? r : records.ToList();
+
+ if (recordsList.Count == 0)
+ {
+ return (records, null);
+ }
+
+ records = recordsList;
+ }
+
+ // TODO: Ideally we'd group together vector properties using the same generator (and with the same input and output properties),
+ // and generate embeddings for them in a single batch. That's some more complexity though.
+ generatedEmbeddings ??= new IReadOnlyList?[vectorPropertyCount];
+ generatedEmbeddings[i] = await vectorProperty.GenerateEmbeddingsAsync(records.Select(r => vectorProperty.GetValueAsObject(r)), cancellationToken).ConfigureAwait(false);
+ }
+
+ return (records, generatedEmbeddings);
+ }
+
+ ///
+ /// Get a document with the given key, and return null if it is not found.
+ ///
+ /// The type to deserialize the document to.
+ /// The search client to use when fetching the document.
+ /// The key of the record to get.
+ /// The Azure AI Search sdk options for getting a document.
+ /// The to monitor for cancellation requests. The default is .
+ /// The retrieved document, mapped to the consumer data model, or null if not found.
+ private async Task GetDocumentWithNotFoundHandlingAsync(
+ SearchClient searchClient,
+ string key,
+ GetDocumentOptions innerOptions,
+ CancellationToken cancellationToken)
+ {
+ const string OperationName = "GetDocument";
+
+ try
+ {
+ return await searchClient.GetDocumentAsync(key, innerOptions, cancellationToken).ConfigureAwait(false);
+ }
+ catch (RequestFailedException ex) when (ex.Status == 404)
+ {
+ return default;
+ }
+ catch (AggregateException ex) when (ex.InnerException is RequestFailedException innerEx)
+ {
+ throw new VectorStoreException("Call to vector store failed.", ex)
+ {
+ VectorStoreSystemName = AzureAISearchConstants.VectorStoreSystemName,
+ VectorStoreName = _collectionMetadata.VectorStoreName,
+ CollectionName = Name,
+ OperationName = OperationName
+ };
+ }
+ catch (RequestFailedException ex)
+ {
+ throw new VectorStoreException("Call to vector store failed.", ex)
+ {
+ VectorStoreSystemName = AzureAISearchConstants.VectorStoreSystemName,
+ VectorStoreName = _collectionMetadata.VectorStoreName,
+ CollectionName = Name,
+ OperationName = OperationName
+ };
+ }
+ }
+
+ ///
+ /// Run the given operation and wrap any with ."/>
+ ///
+ /// The response type of the operation.
+ /// The type of database operation being run.
+ /// The operation to run.
+ /// The result of the operation.
+ private Task RunOperationAsync(string operationName, Func> operation) =>
+ VectorStoreErrorHandler.RunOperationAsync(
+ _collectionMetadata,
+ operationName,
+ operation);
+
+ private static string GetStringKey(TKey key)
+ {
+ Throw.IfNull(key);
+
+ var stringKey = key switch
+ {
+ string s => s,
+ Guid g => g.ToString(),
+
+ _ => throw new UnreachableException("string key should have been validated during model building")
+ };
+
+ Throw.IfNullOrWhitespace(stringKey);
+
+ return stringKey;
+ }
+}
diff --git a/MEVD/src/AzureAISearch/AzureAISearchCollectionCreateMapping.cs b/MEVD/src/AzureAISearch/AzureAISearchCollectionCreateMapping.cs
new file mode 100644
index 0000000..575b8f5
--- /dev/null
+++ b/MEVD/src/AzureAISearch/AzureAISearchCollectionCreateMapping.cs
@@ -0,0 +1,159 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Azure.Search.Documents.Indexes.Models;
+using Microsoft.Extensions.VectorData;
+using Microsoft.Extensions.VectorData.ProviderServices;
+
+namespace CommunityToolkit.VectorData.AzureAISearch;
+
+///
+/// Contains mapping helpers to use when creating a Azure AI Search vector collection.
+///
+internal static class AzureAISearchCollectionCreateMapping
+{
+ ///
+ /// Map from a to an Azure AI Search .
+ ///
+ /// The key property definition.
+ /// The for the provided property definition.
+ public static SearchableField MapKeyField(KeyPropertyModel keyProperty)
+ {
+ return new SearchableField(keyProperty.StorageName) { IsKey = true, IsFilterable = true };
+ }
+
+ ///
+ /// Map from a to an Azure AI Search .
+ ///
+ /// The data property definition.
+ /// The for the provided property definition.
+ /// Throws when the definition is missing required information.
+ public static SimpleField MapDataField(DataPropertyModel dataProperty)
+ {
+ if (dataProperty.IsFullTextIndexed)
+ {
+ if (dataProperty.Type != typeof(string))
+ {
+ throw new InvalidOperationException($"Property {nameof(dataProperty.IsFullTextIndexed)} on {nameof(VectorStoreDataProperty)} '{dataProperty.ModelName}' is set to true, but the property type is not a string. The Azure AI Search VectorStore supports {nameof(dataProperty.IsFullTextIndexed)} on string properties only.");
+ }
+
+ return new SearchableField(dataProperty.StorageName)
+ {
+ IsFilterable = dataProperty.IsIndexed,
+ // Sometimes the users ask to also OrderBy given filterable property, so we make it sortable.
+ IsSortable = dataProperty.IsIndexed
+ };
+ }
+
+ var fieldType = AzureAISearchCollectionCreateMapping.GetSDKFieldDataType(dataProperty.Type);
+ return new SimpleField(dataProperty.StorageName, fieldType)
+ {
+ IsFilterable = dataProperty.IsIndexed,
+ // Sometimes the users ask to also OrderBy given filterable property, so we make it sortable.
+ IsSortable = dataProperty.IsIndexed && !fieldType.IsCollection
+ };
+ }
+
+ ///
+ /// Map form a to an Azure AI Search and generate the required index configuration.
+ ///
+ /// The vector property definition.
+ /// The and required index configuration.
+ /// Throws when the definition is missing required information, or unsupported options are configured.
+ public static (VectorSearchField vectorSearchField, VectorSearchAlgorithmConfiguration algorithmConfiguration, VectorSearchProfile vectorSearchProfile) MapVectorField(VectorPropertyModel vectorProperty)
+ {
+ // Build a name for the profile and algorithm configuration based on the property name
+ // since we'll just create a separate one for each vector property.
+ var vectorSearchProfileName = $"{vectorProperty.StorageName}Profile";
+ var algorithmConfigName = $"{vectorProperty.StorageName}AlgoConfig";
+
+ // Read the vector index settings from the property definition and create the right index configuration.
+ var indexKind = AzureAISearchCollectionCreateMapping.GetSKIndexKind(vectorProperty);
+ var algorithmMetric = AzureAISearchCollectionCreateMapping.GetSDKDistanceAlgorithm(vectorProperty);
+
+ VectorSearchAlgorithmConfiguration algorithmConfiguration = indexKind switch
+ {
+ IndexKind.Hnsw => new HnswAlgorithmConfiguration(algorithmConfigName) { Parameters = new HnswParameters { Metric = algorithmMetric } },
+ IndexKind.Flat => new ExhaustiveKnnAlgorithmConfiguration(algorithmConfigName) { Parameters = new ExhaustiveKnnParameters { Metric = algorithmMetric } },
+
+ _ => throw new NotSupportedException($"Index kind '{indexKind}' on {nameof(VectorStoreVectorProperty)} '{vectorProperty.ModelName}' is not supported by the Azure AI Search VectorStore.")
+ };
+
+ var vectorSearchProfile = new VectorSearchProfile(vectorSearchProfileName, algorithmConfigName);
+
+ return (new VectorSearchField(vectorProperty.StorageName, vectorProperty.Dimensions, vectorSearchProfileName), algorithmConfiguration, vectorSearchProfile);
+ }
+
+ ///
+ /// Get the configured from the given .
+ /// If none is configured the default is .
+ ///
+ /// The vector property definition.
+ /// The configured or default .
+ public static string GetSKIndexKind(VectorPropertyModel vectorProperty)
+ => vectorProperty.IndexKind ?? IndexKind.Hnsw;
+
+ ///
+ /// Get the configured from the given .
+ /// If none is configured, the default is .
+ ///
+ /// The vector property definition.
+ /// The chosen .
+ /// Thrown if a distance function is chosen that isn't supported by Azure AI Search.
+ public static VectorSearchAlgorithmMetric GetSDKDistanceAlgorithm(VectorPropertyModel vectorProperty)
+ => vectorProperty.DistanceFunction switch
+ {
+ DistanceFunction.CosineSimilarity or null => VectorSearchAlgorithmMetric.Cosine,
+ DistanceFunction.DotProductSimilarity => VectorSearchAlgorithmMetric.DotProduct,
+ DistanceFunction.EuclideanDistance => VectorSearchAlgorithmMetric.Euclidean,
+
+ _ => throw new NotSupportedException($"Distance function '{vectorProperty.DistanceFunction}' for {nameof(VectorStoreVectorProperty)} '{vectorProperty.ModelName}' is not supported by the Azure AI Search VectorStore.")
+ };
+
+ ///
+ /// Maps the given property type to the corresponding .
+ ///
+ /// The property type to map.
+ /// The that corresponds to the given property type."
+ /// Thrown if the given type is not supported.
+ public static SearchFieldDataType GetSDKFieldDataType(Type propertyType)
+ => (Nullable.GetUnderlyingType(propertyType) ?? propertyType) switch
+ {
+ Type t when t == typeof(string) => SearchFieldDataType.String,
+ Type t when t == typeof(bool) => SearchFieldDataType.Boolean,
+ Type t when t == typeof(int) => SearchFieldDataType.Int32,
+ Type t when t == typeof(long) => SearchFieldDataType.Int64,
+ // We don't map float to SearchFieldDataType.Single, because Azure AI Search doesn't support it.
+ // Half is also listed by the SDK, but currently not supported.
+ Type t when t == typeof(float) => SearchFieldDataType.Double,
+ Type t when t == typeof(double) => SearchFieldDataType.Double,
+ Type t when t == typeof(DateTime) => SearchFieldDataType.DateTimeOffset,
+ Type t when t == typeof(DateTimeOffset) => SearchFieldDataType.DateTimeOffset,
+#if NET
+ Type t when t == typeof(DateOnly) => SearchFieldDataType.DateTimeOffset,
+#endif
+
+ Type t when t == typeof(string[]) => SearchFieldDataType.Collection(SearchFieldDataType.String),
+ Type t when t == typeof(List) => SearchFieldDataType.Collection(SearchFieldDataType.String),
+ Type t when t == typeof(bool[]) => SearchFieldDataType.Collection(SearchFieldDataType.Boolean),
+ Type t when t == typeof(List) => SearchFieldDataType.Collection(SearchFieldDataType.Boolean),
+ Type t when t == typeof(int[]) => SearchFieldDataType.Collection(SearchFieldDataType.Int32),
+ Type t when t == typeof(List) => SearchFieldDataType.Collection(SearchFieldDataType.Int32),
+ Type t when t == typeof(long[]) => SearchFieldDataType.Collection(SearchFieldDataType.Int64),
+ Type t when t == typeof(List) => SearchFieldDataType.Collection(SearchFieldDataType.Int64),
+ Type t when t == typeof(float[]) => SearchFieldDataType.Collection(SearchFieldDataType.Double),
+ Type t when t == typeof(List) => SearchFieldDataType.Collection(SearchFieldDataType.Double),
+ Type t when t == typeof(double[]) => SearchFieldDataType.Collection(SearchFieldDataType.Double),
+ Type t when t == typeof(List) => SearchFieldDataType.Collection(SearchFieldDataType.Double),
+ Type t when t == typeof(DateTime[]) => SearchFieldDataType.Collection(SearchFieldDataType.DateTimeOffset),
+ Type t when t == typeof(List) => SearchFieldDataType.Collection(SearchFieldDataType.DateTimeOffset),
+ Type t when t == typeof(DateTimeOffset[]) => SearchFieldDataType.Collection(SearchFieldDataType.DateTimeOffset),
+ Type t when t == typeof(List) => SearchFieldDataType.Collection(SearchFieldDataType.DateTimeOffset),
+#if NET
+ Type t when t == typeof(DateOnly[]) => SearchFieldDataType.Collection(SearchFieldDataType.DateTimeOffset),
+ Type t when t == typeof(List) => SearchFieldDataType.Collection(SearchFieldDataType.DateTimeOffset),
+#endif
+
+ _ => throw new NotSupportedException($"Data type '{propertyType}' for {nameof(VectorStoreDataProperty)} is not supported by the Azure AI Search VectorStore.")
+ };
+}
diff --git a/MEVD/src/AzureAISearch/AzureAISearchCollectionOptions.cs b/MEVD/src/AzureAISearch/AzureAISearchCollectionOptions.cs
new file mode 100644
index 0000000..fb15950
--- /dev/null
+++ b/MEVD/src/AzureAISearch/AzureAISearchCollectionOptions.cs
@@ -0,0 +1,35 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text.Json;
+using Azure.Search.Documents.Indexes;
+using Microsoft.Extensions.VectorData;
+
+namespace CommunityToolkit.VectorData.AzureAISearch;
+
+///
+/// Options when creating a .
+///
+public sealed class AzureAISearchCollectionOptions : VectorStoreCollectionOptions
+{
+ internal static readonly AzureAISearchCollectionOptions Default = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public AzureAISearchCollectionOptions()
+ {
+ }
+
+ internal AzureAISearchCollectionOptions(AzureAISearchCollectionOptions? source) : base(source)
+ {
+ JsonSerializerOptions = source?.JsonSerializerOptions;
+ }
+
+ ///
+ /// Gets or sets the JSON serializer options to use when converting between the data model and the Azure AI Search record.
+ /// Note that when using the default mapper and you are constructing your own , you will need
+ /// to provide the same set of both here and when constructing the .
+ ///
+ public JsonSerializerOptions? JsonSerializerOptions { get; set; }
+}
diff --git a/MEVD/src/AzureAISearch/AzureAISearchConstants.cs b/MEVD/src/AzureAISearch/AzureAISearchConstants.cs
new file mode 100644
index 0000000..40a5c0d
--- /dev/null
+++ b/MEVD/src/AzureAISearch/AzureAISearchConstants.cs
@@ -0,0 +1,9 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace CommunityToolkit.VectorData.AzureAISearch;
+
+internal static class AzureAISearchConstants
+{
+ internal const string VectorStoreSystemName = "azure.aisearch";
+}
diff --git a/MEVD/src/AzureAISearch/AzureAISearchDynamicCollection.cs b/MEVD/src/AzureAISearch/AzureAISearchDynamicCollection.cs
new file mode 100644
index 0000000..63d9174
--- /dev/null
+++ b/MEVD/src/AzureAISearch/AzureAISearchDynamicCollection.cs
@@ -0,0 +1,34 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+using Azure.Search.Documents.Indexes;
+
+namespace CommunityToolkit.VectorData.AzureAISearch;
+
+///
+/// Represents a collection of vector store records in a AzureAISearch database, mapped to a dynamic Dictionary<string, object?>.
+///
+#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
+public sealed class AzureAISearchDynamicCollection : AzureAISearchCollection