diff --git a/CommunityToolkit.AI.slnx b/CommunityToolkit.AI.slnx
index 35e2b7c..a459dd0 100644
--- a/CommunityToolkit.AI.slnx
+++ b/CommunityToolkit.AI.slnx
@@ -13,6 +13,7 @@
+
@@ -32,6 +33,8 @@
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 4f15290..5684c14 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -17,6 +17,7 @@
+
@@ -46,6 +47,7 @@
+
diff --git a/MEVD/MEVD.slnf b/MEVD/MEVD.slnf
index 80d205a..ef1de46 100644
--- a/MEVD/MEVD.slnf
+++ b/MEVD/MEVD.slnf
@@ -8,6 +8,7 @@
"MEVD/src/AzureDocumentDB/AzureDocumentDB.csproj",
"MEVD/src/Chroma/Chroma.csproj",
"MEVD/src/InMemory/InMemory.csproj",
+ "MEVD/src/Milvus/Milvus.csproj",
"MEVD/src/PgVector/PgVector.csproj",
"MEVD/src/Qdrant/Qdrant.csproj",
"MEVD/src/Redis/Redis.csproj",
@@ -25,6 +26,8 @@
"MEVD/test/Chroma.ConformanceTests/Chroma.ConformanceTests.csproj",
"MEVD/test/InMemory.UnitTests/InMemory.UnitTests.csproj",
"MEVD/test/InMemory.ConformanceTests/InMemory.ConformanceTests.csproj",
+ "MEVD/test/Milvus.UnitTests/Milvus.UnitTests.csproj",
+ "MEVD/test/Milvus.ConformanceTests/Milvus.ConformanceTests.csproj",
"MEVD/test/PgVector.UnitTests/PgVector.UnitTests.csproj",
"MEVD/test/PgVector.ConformanceTests/PgVector.ConformanceTests.csproj",
"MEVD/test/Qdrant.UnitTests/Qdrant.UnitTests.csproj",
diff --git a/MEVD/src/Milvus/AssemblyInfo.cs b/MEVD/src/Milvus/AssemblyInfo.cs
new file mode 100644
index 0000000..53616c2
--- /dev/null
+++ b/MEVD/src/Milvus/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/Milvus/Milvus.csproj b/MEVD/src/Milvus/Milvus.csproj
new file mode 100644
index 0000000..c786b80
--- /dev/null
+++ b/MEVD/src/Milvus/Milvus.csproj
@@ -0,0 +1,31 @@
+
+
+
+ 1.0.0-preview
+ CommunityToolkit.VectorData.Milvus
+ $(AssemblyName)
+ net10.0;net8.0;netstandard2.0;net462
+
+ true
+
+ Milvus provider for Microsoft.Extensions.VectorData
+ Milvus provider for Microsoft.Extensions.VectorData by the .NET Community Toolkit
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MEVD/src/Milvus/MilvusCollection.cs b/MEVD/src/Milvus/MilvusCollection.cs
new file mode 100644
index 0000000..e2c3bd4
--- /dev/null
+++ b/MEVD/src/Milvus/MilvusCollection.cs
@@ -0,0 +1,597 @@
+// 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 Milvus.Client;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.VectorData;
+using Microsoft.Extensions.VectorData.ProviderServices;
+using Microsoft.Shared.Diagnostics;
+
+namespace CommunityToolkit.VectorData.Milvus;
+
+///
+/// Service for storing and retrieving vector records, that uses Milvus as the underlying storage.
+///
+/// The data type of the record key. Can be , or .
+/// 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 MilvusCollection : 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();
+
+ private static readonly HybridSearchOptions s_defaultHybridSearchOptions = new();
+
+ // The two searches of a hybrid search rank at least this many records each. When they rank only the first top + skip,
+ // a record found by both can miss one of the rankings, as when records tie in the vector search, and the fusion ranks it lower.
+ private const int HybridSearchCandidates = 10;
+
+ // The most records a Milvus search ranks.
+ private const int MaxSearchLimit = 16384;
+
+ // How long EnsureCollectionExistsAsync waits for the collection to load: a little longer than the load timeout of the
+ // server, 600 seconds in the default configuration of Milvus 2.6.25 (queryCoord.loadTimeoutSeconds), so that the
+ // provider does not give up on a load sooner than the server; a load that never ends still stops.
+ internal TimeSpan LoadTimeout { get; set; } = TimeSpan.FromMinutes(11);
+
+ /// The name of the upsert operation for telemetry purposes.
+ private const string UpsertName = "Upsert";
+
+ /// The name of the Delete operation for telemetry purposes.
+ private const string DeleteName = "Delete";
+
+ /// Milvus client that can be used to manage the collections and records in a Milvus store.
+ private readonly SharedMilvusClient _milvusClient;
+
+ /// Milvus client of the records of the collection.
+ private readonly global::Milvus.Client.MilvusCollection _records;
+
+ /// The model for this collection.
+ private readonly CollectionModel _model;
+
+ /// A mapper to use for converting between Milvus records and consumer models.
+ private readonly MilvusMapper _mapper;
+
+ /// Whether the collection was disposed: it releases its share of the client only once.
+ private int _disposed;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Milvus client that can be used to manage the collections and records in a Milvus store.
+ /// The name of the collection that this will access.
+ /// A value indicating whether is disposed when the collection is disposed.
+ /// Optional configuration options for this class.
+ /// Thrown if the is null.
+ /// Thrown for any misconfigured options.
+ [RequiresDynamicCode("This constructor is incompatible with NativeAOT. For dynamic mapping via Dictionary, instantiate MilvusDynamicCollection instead.")]
+ [RequiresUnreferencedCode("This constructor is incompatible with trimming. For dynamic mapping via Dictionary, instantiate MilvusDynamicCollection instead")]
+ public MilvusCollection(MilvusClient milvusClient, string name, bool ownsClient, MilvusCollectionOptions? options = null)
+ : this(() => new SharedMilvusClient(milvusClient, ownsClient), name, options)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Milvus client factory.
+ /// The name of the collection that this will access.
+ /// Optional configuration options for this class.
+ /// Thrown if the is null.
+ /// Thrown for any misconfigured options.
+ [RequiresDynamicCode("This constructor is incompatible with NativeAOT. For dynamic mapping via Dictionary, instantiate MilvusDynamicCollection instead.")]
+ [RequiresUnreferencedCode("This constructor is incompatible with trimming. For dynamic mapping via Dictionary, instantiate MilvusDynamicCollection instead")]
+ internal MilvusCollection(Func clientFactory, string name, MilvusCollectionOptions? options = null)
+ : this(
+ clientFactory,
+ name,
+ static options => typeof(TRecord) == typeof(Dictionary)
+ ? throw new NotSupportedException(VectorDataStrings.NonDynamicCollectionWithDictionaryNotSupported(typeof(MilvusDynamicCollection)))
+ : new MilvusModelBuilder().Build(typeof(TRecord), typeof(TKey), options.Definition, options.EmbeddingGenerator),
+ options)
+ {
+ }
+
+ internal MilvusCollection(Func clientFactory, string name, Func modelFactory, MilvusCollectionOptions? options)
+ {
+ // Verify.
+ Throw.IfNull(clientFactory);
+
+ // The collection holds the client from here: when the rest of the construction throws, it releases the client,
+ // so that a client it was to own is disposed, and a client it shares with its vector store is given back.
+ _milvusClient = clientFactory();
+ try
+ {
+ Throw.IfNullOrWhitespace(name);
+
+ if (typeof(TKey) != typeof(long) && typeof(TKey) != typeof(string) && typeof(TKey) != typeof(Guid) && typeof(TKey) != typeof(object))
+ {
+ throw new NotSupportedException("Only long, string and Guid keys are supported.");
+ }
+
+ options ??= MilvusCollectionOptions.Default;
+
+ // Assign.
+ Name = name;
+ _model = modelFactory(options);
+ _mapper = new MilvusMapper(_model);
+ }
+ catch
+ {
+ _milvusClient.Dispose();
+ throw;
+ }
+
+ _records = _milvusClient.Client.GetCollection(name);
+
+ _collectionMetadata = new()
+ {
+ VectorStoreSystemName = MilvusConstants.VectorStoreSystemName,
+ VectorStoreName = _milvusClient.DatabaseName,
+ CollectionName = name
+ };
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (Interlocked.Exchange(ref _disposed, 1) == 0)
+ {
+ _milvusClient.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+
+ ///
+ public override string Name { get; }
+
+ ///
+ public override Task CollectionExistsAsync(CancellationToken cancellationToken = default)
+ => RunOperationAsync("CollectionExists", () => _milvusClient.Client.HasCollectionAsync(Name, cancellationToken: cancellationToken));
+
+ ///
+ public override Task EnsureCollectionExistsAsync(CancellationToken cancellationToken = default)
+ => RunOperationAsync("EnsureCollectionExists", async () =>
+ {
+ // The indexes are created with the collection. Those of an existing collection stay as they are: Milvus rejects
+ // an index with other settings on an indexed field, or on a field the collection does not have.
+ if (!await _milvusClient.Client.HasCollectionAsync(Name, cancellationToken: cancellationToken).ConfigureAwait(false))
+ {
+ await _milvusClient.Client.CreateCollectionAsync(Name, MilvusCollectionCreateMapping.MapCollectionSchema(_model),
+ cancellationToken: cancellationToken).ConfigureAwait(false);
+ foreach (var vector in _model.VectorProperties)
+ {
+ var indexType = MilvusCollectionCreateMapping.GetIndexType(vector);
+ var parameters = indexType == IndexType.Hnsw
+ ? new Dictionary { ["M"] = "16", ["efConstruction"] = "200" } : null;
+ await _records.CreateIndexAsync(vector.StorageName, indexType,
+ MilvusCollectionCreateMapping.GetMetric(vector), extraParams: parameters,
+ cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
+ foreach (var property in MilvusCollectionCreateMapping.GetFullTextProperties(_model))
+ {
+ await _records.CreateIndexAsync(MilvusCollectionCreateMapping.GetBm25FieldName(property), IndexType.SparseInvertedIndex,
+ SimilarityMetricType.Bm25, cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
+ }
+ await _records.LoadAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
+ await _records.WaitForCollectionLoadAsync(timeout: LoadTimeout, cancellationToken: cancellationToken).ConfigureAwait(false);
+ });
+
+ ///
+ public override Task EnsureCollectionDeletedAsync(CancellationToken cancellationToken = default)
+ => RunOperationAsync("DeleteCollection", async () =>
+ {
+ if (await _milvusClient.Client.HasCollectionAsync(Name, cancellationToken: cancellationToken).ConfigureAwait(false))
+ {
+ await _records.DropAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
+ });
+
+ ///
+ public override async Task GetAsync(TKey key, RecordRetrievalOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ Throw.IfNull(key);
+
+ var records = await GetAsync([key], options, cancellationToken).ToListAsync(cancellationToken).ConfigureAwait(false);
+ return records.FirstOrDefault();
+ }
+
+ ///
+ public override async IAsyncEnumerable GetAsync(
+ IEnumerable keys,
+ RecordRetrievalOptions? options = default,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ const string OperationName = "Get";
+
+ Throw.IfNull(keys);
+
+ var includeVectors = options?.IncludeVectors ?? false;
+ if (includeVectors && _model.EmbeddingGenerationRequired)
+ {
+ throw new NotSupportedException(VectorDataStrings.IncludeVectorsNotSupportedWithEmbeddingGeneration);
+ }
+
+ var keyList = keys.ToList();
+ if (keyList.Count == 0)
+ {
+ yield break;
+ }
+ var parameters = CreateQueryParameters(includeVectors);
+ var entries = await RunOperationAsync(OperationName,
+ () => _records.QueryRowsAsync(KeyFilter(keyList), parameters, cancellationToken)).ConfigureAwait(false);
+ foreach (var entry in entries)
+ {
+ yield return _mapper.MapFromStorageToDataModel(entry, includeVectors);
+ }
+ }
+
+ ///
+ public override Task DeleteAsync(TKey key, CancellationToken cancellationToken = default)
+ {
+ Throw.IfNull(key);
+
+ return DeleteAsync([key], cancellationToken);
+ }
+
+ ///
+ public override Task DeleteAsync(IEnumerable keys, CancellationToken cancellationToken = default)
+ {
+ Throw.IfNull(keys);
+
+ var keyList = keys.ToList();
+ return keyList.Count == 0 ? Task.CompletedTask
+ : RunOperationAsync(DeleteName, () => _records.DeleteAsync(KeyFilter(keyList), cancellationToken: cancellationToken));
+ }
+
+ ///
+ public override async Task UpsertAsync(TRecord record, CancellationToken cancellationToken = default)
+ {
+ Throw.IfNull(record);
+
+ await UpsertAsync([record], cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ public override async Task UpsertAsync(IEnumerable records, CancellationToken cancellationToken = default)
+ {
+ Throw.IfNull(records);
+
+ IReadOnlyList? recordsList = null;
+
+ // If an embedding generator is defined, invoke it once per property for all records.
+ GeneratedEmbeddings>?[]? generatedEmbeddings = null;
+
+ var vectorPropertyCount = _model.VectorProperties.Count;
+ for (var i = 0; i < vectorPropertyCount; i++)
+ {
+ var vectorProperty = _model.VectorProperties[i];
+
+ if (MilvusModelBuilder.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);
+
+ if (recordsList is null)
+ {
+ recordsList = records is IReadOnlyList r ? r : records.ToList();
+
+ if (recordsList.Count == 0)
+ {
+ return;
+ }
+
+ 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 GeneratedEmbeddings>?[vectorPropertyCount];
+ generatedEmbeddings[i] = (GeneratedEmbeddings>)await vectorProperty.GenerateEmbeddingsAsync(records.Select(r => vectorProperty.GetValueAsObject(r)), cancellationToken).ConfigureAwait(false);
+ }
+
+ var keyProperty = _model.KeyProperty;
+ var rows = new List>();
+ var index = 0;
+ foreach (var record in records)
+ {
+ Throw.IfNull(record);
+ if (keyProperty.IsAutoGenerated && keyProperty.GetValue(record) == Guid.Empty)
+ {
+ keyProperty.SetValue(record, Guid.NewGuid());
+ }
+ rows.Add(_mapper.MapFromDataToStorageModel(record, index++, generatedEmbeddings));
+ }
+ if (rows.Count > 0)
+ {
+ await RunOperationAsync(UpsertName, () => _records.UpsertAsync(MilvusFieldMapping.CreateColumns(_model, rows), cancellationToken: cancellationToken)).ConfigureAwait(false);
+ }
+ }
+
+ #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;
+ if (options.IncludeVectors && _model.EmbeddingGenerationRequired)
+ {
+ throw new NotSupportedException(VectorDataStrings.IncludeVectorsNotSupportedWithEmbeddingGeneration);
+ }
+
+ var vectorProperty = _model.GetVectorPropertyOrSingle(options);
+
+ var vector = await GetSearchVectorAsync(searchValue, vectorProperty, cancellationToken).ConfigureAwait(false);
+ if (vector.Length != vectorProperty.Dimensions)
+ {
+ throw new ArgumentException($"Search vector must have {vectorProperty.Dimensions} dimensions.", nameof(searchValue));
+ }
+ var parameters = new SearchParameters
+ {
+ Offset = options.Skip,
+ Expression = options.Filter is null ? null : new MilvusFilterTranslator().Translate(options.Filter, _model),
+ ConsistencyLevel = ConsistencyLevel.Strong,
+ };
+ foreach (var field in GetOutputFields(options.IncludeVectors))
+ {
+ parameters.OutputFields.Add(field);
+ }
+ if (MilvusCollectionCreateMapping.GetIndexType(vectorProperty) == IndexType.Hnsw)
+ {
+ parameters.ExtraParameters["ef"] = Math.Max(64, top + options.Skip).ToString(System.Globalization.CultureInfo.InvariantCulture);
+ }
+ var results = await RunOperationAsync("Search", () => SearchAllowingEmptyResultsAsync(
+ () => _records.SearchAsync(vectorProperty.StorageName, [vector],
+ MilvusCollectionCreateMapping.GetMetric(vectorProperty), top, parameters, cancellationToken),
+ parameters.OutputFields)).ConfigureAwait(false);
+ if (IsEmpty(results))
+ {
+ yield break;
+ }
+ foreach (var hit in results.GetHits())
+ {
+ var score = MilvusCollectionSearchMapping.MapScore(hit.Score, vectorProperty.DistanceFunction);
+ if (MilvusCollectionSearchMapping.PassesThreshold(score, options.ScoreThreshold, vectorProperty.DistanceFunction))
+ {
+ yield return new VectorSearchResult(MapHit(hit, options.IncludeVectors), score);
+ }
+ }
+ }
+
+ ///
+ public async IAsyncEnumerable> HybridSearchAsync(
+ TInput searchValue,
+ ICollection keywords,
+ int top,
+ HybridSearchOptions? options = null,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ where TInput : notnull
+ {
+ Throw.IfNull(searchValue);
+ Throw.IfNull(keywords);
+ Throw.IfLessThan(top, 1);
+
+ options ??= s_defaultHybridSearchOptions;
+ if ((long)top + options.Skip > MaxSearchLimit)
+ {
+ throw new ArgumentOutOfRangeException(nameof(top),
+ $"Milvus hybrid search ranks at most {MaxSearchLimit} records, the skipped ones included: top + Skip is {(long)top + options.Skip}.");
+ }
+ if (options.IncludeVectors && _model.EmbeddingGenerationRequired)
+ {
+ throw new NotSupportedException(VectorDataStrings.IncludeVectorsNotSupportedWithEmbeddingGeneration);
+ }
+
+ var vectorProperty = _model.GetVectorPropertyOrSingle(new() { VectorProperty = options.VectorProperty });
+ var textProperty = _model.GetFullTextDataPropertyOrSingle(options.AdditionalProperty);
+ var filter = options.Filter is null ? null : new MilvusFilterTranslator().Translate(options.Filter, _model);
+
+ var vector = await GetSearchVectorAsync(searchValue, vectorProperty, cancellationToken).ConfigureAwait(false);
+ if (vector.Length != vectorProperty.Dimensions)
+ {
+ throw new ArgumentException($"Search vector must have {vectorProperty.Dimensions} dimensions.", nameof(searchValue));
+ }
+
+ // A vector search and a BM25 search of the keywords, both filtered, fused with Reciprocal Rank Fusion.
+ // Milvus has no offset for hybrid search: the skipped records are asked for too, and dropped.
+ var limit = top + options.Skip;
+ var candidates = Math.Max(limit, HybridSearchCandidates);
+ var vectorSearch = new VectorAnnSearchRequest(vectorProperty.StorageName, [vector],
+ MilvusCollectionCreateMapping.GetMetric(vectorProperty), candidates)
+ {
+ Expression = filter,
+ };
+ if (MilvusCollectionCreateMapping.GetIndexType(vectorProperty) == IndexType.Hnsw)
+ {
+ vectorSearch.ExtraParameters["ef"] = Math.Max(64, candidates).ToString(System.Globalization.CultureInfo.InvariantCulture);
+ }
+ var keywordSearch = new TextAnnSearchRequest(MilvusCollectionCreateMapping.GetBm25FieldName(textProperty),
+ [string.Join(" ", keywords)], candidates)
+ {
+ Expression = filter,
+ };
+ var parameters = new HybridSearchParameters { ConsistencyLevel = ConsistencyLevel.Strong };
+ foreach (var field in GetOutputFields(options.IncludeVectors))
+ {
+ parameters.OutputFields.Add(field);
+ }
+ var results = await RunOperationAsync("HybridSearch", () => SearchAllowingEmptyResultsAsync(
+ () => _records.HybridSearchAsync([vectorSearch, keywordSearch], new RrfReranker(60), limit, parameters, cancellationToken),
+ parameters.OutputFields)).ConfigureAwait(false);
+ if (IsEmpty(results))
+ {
+ yield break;
+ }
+ foreach (var hit in results.GetHits().Skip(options.Skip))
+ {
+ // A higher RRF score ranks higher: the threshold is a lower bound.
+ if (options.ScoreThreshold is not { } threshold || hit.Score >= threshold)
+ {
+ yield return new VectorSearchResult(MapHit(hit, options.IncludeVectors), hit.Score);
+ }
+ }
+ }
+
+ // Milvus.Client 2.6.0 cannot decode untyped output fields in an empty response.
+ // Retry without output fields to verify that this really is an empty search;
+ // do not hide the SDK error if the retry finds any records.
+ private static async Task SearchAllowingEmptyResultsAsync(Func> search, IList outputFields)
+ {
+ try
+ {
+ return await search().ConfigureAwait(false);
+ }
+ catch (NotSupportedException exception) when (exception.Message == "Cannot convert None FieldData to Field")
+ {
+ outputFields.Clear();
+ var retry = await search().ConfigureAwait(false);
+ if (!IsEmpty(retry))
+ {
+ throw;
+ }
+ return retry;
+ }
+ }
+
+ private static bool IsEmpty(SearchResults results) => (results.Ids.LongIds?.Count ?? results.Ids.StringIds?.Count ?? 0) == 0;
+
+ private TRecord MapHit(SearchHit hit, bool includeVectors)
+ {
+ var row = hit.Fields.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
+ row[_model.KeyProperty.StorageName] = hit.Id;
+ return _mapper.MapFromStorageToDataModel(row, includeVectors);
+ }
+
+ private static async ValueTask> GetSearchVectorAsync(TInput searchValue, VectorPropertyModel vectorProperty, CancellationToken cancellationToken)
+ where TInput : notnull
+ => searchValue switch
+ {
+ float[] array => array,
+ ReadOnlyMemory r => r,
+ Embedding e => e.Vector,
+ _ when vectorProperty.EmbeddingGenerationDispatcher is not null
+ => ((Embedding)await vectorProperty.GenerateEmbeddingAsync(searchValue, cancellationToken).ConfigureAwait(false)).Vector,
+
+ _ => vectorProperty.EmbeddingGenerator is null
+ ? throw new NotSupportedException(VectorDataStrings.InvalidSearchInputAndNoEmbeddingGeneratorWasConfigured(searchValue.GetType(), MilvusModelBuilder.SupportedVectorTypes))
+ : throw new InvalidOperationException(VectorDataStrings.IncompatibleEmbeddingGeneratorWasConfiguredForInputType(typeof(TInput), vectorProperty.EmbeddingGenerator.GetType()))
+ };
+
+ #endregion Search
+
+ ///
+ public override async IAsyncEnumerable GetAsync(Expression> filter, int top,
+ FilteredRecordRetrievalOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ Throw.IfNull(filter);
+ Throw.IfLessThan(top, 1);
+
+ options ??= new();
+
+ if (options.IncludeVectors && _model.EmbeddingGenerationRequired)
+ {
+ throw new NotSupportedException(VectorDataStrings.IncludeVectorsNotSupportedWithEmbeddingGeneration);
+ }
+
+ if (options.OrderBy?.Invoke(new()).Values is { Count: > 0 })
+ {
+ throw new NotSupportedException("Milvus does not support ordering.");
+ }
+
+ var milvusFilter = new MilvusFilterTranslator().Translate(filter, _model);
+ var parameters = CreateQueryParameters(options.IncludeVectors);
+ parameters.Limit = top;
+ parameters.Offset = options.Skip;
+ var entries = await RunOperationAsync("Get",
+ () => _records.QueryRowsAsync(milvusFilter, parameters, cancellationToken)).ConfigureAwait(false);
+ foreach (var entry in entries)
+ {
+ yield return _mapper.MapFromStorageToDataModel(entry, options.IncludeVectors);
+ }
+ }
+
+ ///
+ public override object? GetService(Type serviceType, object? serviceKey = null)
+ {
+ Throw.IfNull(serviceType);
+
+ return
+ serviceKey is not null ? null :
+ serviceType == typeof(VectorStoreCollectionMetadata) ? _collectionMetadata :
+ serviceType == typeof(MilvusClient) ? _milvusClient.Client :
+ // Hybrid search needs a full-text indexed property.
+ serviceType == typeof(IKeywordHybridSearchable) ? (MilvusCollectionCreateMapping.GetFullTextProperties(_model).Any() ? this : null) :
+ serviceType.IsInstanceOfType(this) ? this :
+ null;
+ }
+
+ private string KeyFilter(IEnumerable keys)
+ {
+ var literals = keys.Select(key =>
+ {
+ Throw.IfNull(key);
+ if (key.GetType() != _model.KeyProperty.Type)
+ {
+ throw new ArgumentException($"Expected key type '{_model.KeyProperty.Type}'.", nameof(keys));
+ }
+ return MilvusFieldMapping.Literal(key);
+ });
+ return $"{_model.KeyProperty.StorageName} in [{string.Join(", ", literals)}]";
+ }
+
+ private IEnumerable GetOutputFields(bool includeVectors)
+ => new[] { _model.KeyProperty.StorageName }.Concat(_model.DataProperties.Select(p => p.StorageName))
+ .Concat(includeVectors ? _model.VectorProperties.Select(p => p.StorageName) : []);
+
+ private QueryParameters CreateQueryParameters(bool includeVectors)
+ {
+ var parameters = new QueryParameters { ConsistencyLevel = ConsistencyLevel.Strong };
+ foreach (var field in GetOutputFields(includeVectors))
+ {
+ parameters.OutputFields.Add(field);
+ }
+ return parameters;
+ }
+
+ ///
+ /// Run the given operation and wrap any with .
+ ///
+ /// The type of database operation being run.
+ /// The operation to run.
+ /// The result of the operation.
+ private Task RunOperationAsync(string operationName, Func operation)
+ => MilvusErrorHandler.RunAsync(_collectionMetadata, operationName, operation);
+
+ ///
+ /// 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.
+ // Awaited here rather than returned: otherwise the NativeAOT compiler of .NET 10 fails on the state machine of the handler (dotnet/runtime#120847).
+ private async Task RunOperationAsync(string operationName, Func> operation)
+ => await MilvusErrorHandler.RunAsync(_collectionMetadata, operationName, operation).ConfigureAwait(false);
+}
diff --git a/MEVD/src/Milvus/MilvusCollectionCreateMapping.cs b/MEVD/src/Milvus/MilvusCollectionCreateMapping.cs
new file mode 100644
index 0000000..2c3ed98
--- /dev/null
+++ b/MEVD/src/Milvus/MilvusCollectionCreateMapping.cs
@@ -0,0 +1,93 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Milvus.Client;
+using Microsoft.Extensions.VectorData;
+using Microsoft.Extensions.VectorData.ProviderServices;
+
+namespace CommunityToolkit.VectorData.Milvus;
+
+internal static class MilvusCollectionCreateMapping
+{
+ public static IReadOnlyList MapCollectionDefinition(CollectionModel model)
+ {
+ var key = model.KeyProperty;
+ List fields = [key.Type == typeof(long)
+ ? FieldSchema.Create(key.StorageName, isPrimaryKey: true)
+ : FieldSchema.CreateVarchar(key.StorageName, 65535, isPrimaryKey: true)];
+ foreach (var property in model.DataProperties)
+ {
+ var type = Nullable.GetUnderlyingType(property.Type) ?? property.Type;
+ var nullable = !property.Type.IsValueType || Nullable.GetUnderlyingType(property.Type) is not null;
+ if (MilvusFieldMapping.GetElementType(type) is { } element)
+ {
+ var name = property.StorageName;
+ var field = element == typeof(string) ? FieldSchema.CreateVarcharArray(name, 4096, 65535, nullable: true)
+ : element == typeof(bool) ? FieldSchema.CreateArray(name, 4096, nullable: true)
+ : element == typeof(int) ? FieldSchema.CreateArray(name, 4096, nullable: true)
+ : element == typeof(long) ? FieldSchema.CreateArray(name, 4096, nullable: true)
+ : element == typeof(float) ? FieldSchema.CreateArray(name, 4096, nullable: true)
+ : element == typeof(double) ? FieldSchema.CreateArray(name, 4096, nullable: true)
+ : throw new NotSupportedException($"Unsupported Milvus array element type '{element}'.");
+ fields.Add(field);
+ continue;
+ }
+ fields.Add(type == typeof(string)
+ ? FieldSchema.CreateVarchar(property.StorageName, 65535, nullable: true, enableAnalyzer: property.IsFullTextIndexed)
+ : FieldSchema.Create(property.StorageName, MilvusFieldMapping.GetDataType(type), nullable: nullable));
+ }
+ foreach (var vector in model.VectorProperties)
+ {
+ fields.Add(FieldSchema.CreateFloatVector(vector.StorageName, vector.Dimensions));
+ }
+ foreach (var property in GetFullTextProperties(model))
+ {
+ fields.Add(FieldSchema.CreateSparseFloatVector(GetBm25FieldName(property)));
+ }
+ return fields;
+ }
+
+ ///
+ /// Maps the model to the schema of the collection: its fields, and for each full-text indexed property, a BM25
+ /// function that Milvus runs on the text to fill the hidden BM25 field.
+ ///
+ public static CollectionSchema MapCollectionSchema(CollectionModel model)
+ {
+ var schema = new CollectionSchema();
+ foreach (var field in MapCollectionDefinition(model))
+ {
+ schema.Fields.Add(field);
+ }
+ foreach (var property in GetFullTextProperties(model))
+ {
+ var bm25FieldName = GetBm25FieldName(property);
+ schema.Functions.Add(FunctionSchema.CreateBm25(bm25FieldName, property.StorageName, bm25FieldName));
+ }
+ return schema;
+ }
+
+ public static IEnumerable GetFullTextProperties(CollectionModel model)
+ => model.DataProperties.Where(p => p.IsFullTextIndexed);
+
+ ///
+ /// Gets the name of the hidden sparse field that holds the BM25 vectors of a full-text indexed property, and of
+ /// the BM25 function that fills it: the storage name of the property with _bm25.
+ ///
+ public static string GetBm25FieldName(DataPropertyModel property) => property.StorageName + "_bm25";
+
+ public static SimilarityMetricType GetMetric(VectorPropertyModel property) => property.DistanceFunction switch
+ {
+ null or DistanceFunction.CosineSimilarity => SimilarityMetricType.Cosine,
+ DistanceFunction.DotProductSimilarity => SimilarityMetricType.Ip,
+ DistanceFunction.EuclideanSquaredDistance or DistanceFunction.EuclideanDistance => SimilarityMetricType.L2,
+ _ => throw new NotSupportedException($"Milvus does not support distance function '{property.DistanceFunction}'."),
+ };
+
+ public static IndexType GetIndexType(VectorPropertyModel property) => property.IndexKind switch
+ {
+ null => IndexType.AutoIndex,
+ IndexKind.Flat => IndexType.Flat,
+ IndexKind.Hnsw => IndexType.Hnsw,
+ _ => throw new NotSupportedException($"Milvus does not support index kind '{property.IndexKind}'."),
+ };
+}
diff --git a/MEVD/src/Milvus/MilvusCollectionOptions.cs b/MEVD/src/Milvus/MilvusCollectionOptions.cs
new file mode 100644
index 0000000..1a05351
--- /dev/null
+++ b/MEVD/src/Milvus/MilvusCollectionOptions.cs
@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Extensions.VectorData;
+
+namespace CommunityToolkit.VectorData.Milvus;
+
+///
+/// Options when creating a .
+///
+public sealed class MilvusCollectionOptions : VectorStoreCollectionOptions
+{
+ internal static readonly MilvusCollectionOptions Default = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MilvusCollectionOptions()
+ {
+ }
+
+ internal MilvusCollectionOptions(MilvusCollectionOptions? source) : base(source)
+ {
+ }
+}
diff --git a/MEVD/src/Milvus/MilvusCollectionSearchMapping.cs b/MEVD/src/Milvus/MilvusCollectionSearchMapping.cs
new file mode 100644
index 0000000..12d98b8
--- /dev/null
+++ b/MEVD/src/Milvus/MilvusCollectionSearchMapping.cs
@@ -0,0 +1,17 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Extensions.VectorData;
+
+namespace CommunityToolkit.VectorData.Milvus;
+
+internal static class MilvusCollectionSearchMapping
+{
+ // For L2, Milvus returns the squared Euclidean distance: the Euclidean distance is its square root.
+ public static double MapScore(float score, string? distanceFunction)
+ => distanceFunction == DistanceFunction.EuclideanDistance ? Math.Sqrt(score) : score;
+
+ public static bool PassesThreshold(double score, double? threshold, string? distanceFunction)
+ => threshold is null || (distanceFunction is DistanceFunction.EuclideanSquaredDistance or DistanceFunction.EuclideanDistance
+ ? score <= threshold.Value : score >= threshold.Value);
+}
diff --git a/MEVD/src/Milvus/MilvusConstants.cs b/MEVD/src/Milvus/MilvusConstants.cs
new file mode 100644
index 0000000..877ea52
--- /dev/null
+++ b/MEVD/src/Milvus/MilvusConstants.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.Milvus;
+
+internal static class MilvusConstants
+{
+ internal const string VectorStoreSystemName = "milvus";
+}
diff --git a/MEVD/src/Milvus/MilvusDynamicCollection.cs b/MEVD/src/Milvus/MilvusDynamicCollection.cs
new file mode 100644
index 0000000..70be65a
--- /dev/null
+++ b/MEVD/src/Milvus/MilvusDynamicCollection.cs
@@ -0,0 +1,38 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Milvus.Client;
+
+namespace CommunityToolkit.VectorData.Milvus;
+
+///
+/// Represents a collection of vector store records in a Milvus database, mapped to a dynamic Dictionary<string, object?>.
+///
+#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
+public sealed class MilvusDynamicCollection : MilvusCollection