diff --git a/.gitignore b/.gitignore
index 8092b3e..c7a08b5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ obj
*.user
.azure
appsettings.*.json
+testsettings.*.json
*.orig
test-results
[Tt]est[Rr]esults
diff --git a/CommunityToolkit.AI.slnx b/CommunityToolkit.AI.slnx
index acb7bee..35e2b7c 100644
--- a/CommunityToolkit.AI.slnx
+++ b/CommunityToolkit.AI.slnx
@@ -11,6 +11,7 @@
+
@@ -27,6 +28,8 @@
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 4c5f90e..442478e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -12,6 +12,7 @@
+
diff --git a/MEVD/MEVD.slnf b/MEVD/MEVD.slnf
index 2b48cd6..80d205a 100644
--- a/MEVD/MEVD.slnf
+++ b/MEVD/MEVD.slnf
@@ -6,6 +6,7 @@
"MEVD/src/AzureAISearch/AzureAISearch.csproj",
"MEVD/src/AzureCosmosDB/AzureCosmosDB.csproj",
"MEVD/src/AzureDocumentDB/AzureDocumentDB.csproj",
+ "MEVD/src/Chroma/Chroma.csproj",
"MEVD/src/InMemory/InMemory.csproj",
"MEVD/src/PgVector/PgVector.csproj",
"MEVD/src/Qdrant/Qdrant.csproj",
@@ -20,6 +21,8 @@
"MEVD/test/AzureCosmosDB.ConformanceTests/AzureCosmosDB.ConformanceTests.csproj",
"MEVD/test/AzureDocumentDB.UnitTests/AzureDocumentDB.UnitTests.csproj",
"MEVD/test/AzureDocumentDB.ConformanceTests/AzureDocumentDB.ConformanceTests.csproj",
+ "MEVD/test/Chroma.UnitTests/Chroma.UnitTests.csproj",
+ "MEVD/test/Chroma.ConformanceTests/Chroma.ConformanceTests.csproj",
"MEVD/test/InMemory.UnitTests/InMemory.UnitTests.csproj",
"MEVD/test/InMemory.ConformanceTests/InMemory.ConformanceTests.csproj",
"MEVD/test/PgVector.UnitTests/PgVector.UnitTests.csproj",
diff --git a/MEVD/src/Chroma/AssemblyInfo.cs b/MEVD/src/Chroma/AssemblyInfo.cs
new file mode 100644
index 0000000..d6395e8
--- /dev/null
+++ b/MEVD/src/Chroma/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/Chroma/Chroma.csproj b/MEVD/src/Chroma/Chroma.csproj
new file mode 100644
index 0000000..3a155c3
--- /dev/null
+++ b/MEVD/src/Chroma/Chroma.csproj
@@ -0,0 +1,26 @@
+
+
+
+ 1.0.0-preview
+ CommunityToolkit.VectorData.Chroma
+ $(AssemblyName)
+ net10.0;net8.0;netstandard2.0;net462
+ true
+
+ Chroma provider for Microsoft.Extensions.VectorData
+ Chroma provider for Microsoft.Extensions.VectorData by the .NET Community Toolkit
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MEVD/src/Chroma/ChromaCollection.cs b/MEVD/src/Chroma/ChromaCollection.cs
new file mode 100644
index 0000000..2e19dbe
--- /dev/null
+++ b/MEVD/src/Chroma/ChromaCollection.cs
@@ -0,0 +1,534 @@
+// 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 ChromaDB.Client;
+using ChromaDB.Client.Models;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.VectorData;
+using Microsoft.Extensions.VectorData.ProviderServices;
+using Microsoft.Shared.Diagnostics;
+
+namespace CommunityToolkit.VectorData.Chroma;
+
+///
+/// Service for storing and retrieving vector records, that uses Chroma as the underlying storage.
+///
+/// The data type of the record key. Can be either 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 ChromaCollection : 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 search.
+ private static readonly HybridSearchOptions s_defaultHybridSearchOptions = new();
+
+ /// 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";
+
+ /// Chroma client that can be used to manage the collections and records in a Chroma store.
+ private readonly SharedChromaClient _chromaClient;
+
+ /// Chroma client of the records of the collection.
+ private readonly ChromaCollectionClient _records;
+
+ /// The model for this collection.
+ private readonly CollectionModel _model;
+
+ /// A mapper to use for converting between Chroma records and consumer models.
+ private readonly ChromaMapper _mapper;
+
+ /// The properties to create a BM25 index for when the collection is created.
+ private readonly List _bm25Properties;
+
+ /// Whether the collection was disposed: it releases its share of the client only once.
+ private int _disposed;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Chroma client that can be used to manage the collections and records in a Chroma 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 ChromaDynamicCollection instead.")]
+ [RequiresUnreferencedCode("This constructor is incompatible with trimming. For dynamic mapping via Dictionary, instantiate ChromaDynamicCollection instead")]
+ public ChromaCollection(ChromaClient chromaClient, string name, bool ownsClient, ChromaCollectionOptions? options = null)
+ : this(() => new SharedChromaClient(chromaClient, ownsClient), name, options)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Chroma 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 ChromaDynamicCollection instead.")]
+ [RequiresUnreferencedCode("This constructor is incompatible with trimming. For dynamic mapping via Dictionary, instantiate ChromaDynamicCollection instead")]
+ internal ChromaCollection(Func clientFactory, string name, ChromaCollectionOptions? options = null)
+ : this(
+ clientFactory,
+ name,
+ static options => typeof(TRecord) == typeof(Dictionary)
+ ? throw new NotSupportedException(VectorDataStrings.NonDynamicCollectionWithDictionaryNotSupported(typeof(ChromaDynamicCollection)))
+ : new ChromaModelBuilder().Build(typeof(TRecord), typeof(TKey), options.Definition, options.EmbeddingGenerator),
+ options)
+ {
+ }
+
+ internal ChromaCollection(Func clientFactory, string name, Func modelFactory, ChromaCollectionOptions? options)
+ {
+ // Verify.
+ Throw.IfNull(clientFactory);
+ 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 ??= ChromaCollectionOptions.Default;
+
+ // Assign.
+ Name = name;
+ _model = modelFactory(options);
+ _mapper = new ChromaMapper(_model);
+ _bm25Properties = ChromaCollectionCreateMapping.GetBm25Properties(_model);
+
+ // The code above can throw, so we need to create the client after the model is built and verified.
+ // In case an exception is thrown, we don't need to dispose any resources.
+ _chromaClient = clientFactory();
+
+ var records = _chromaClient.Client.GetCollectionClient(name).WithMetadataValues(ChromaMetadataValues.Exact);
+ _records = _mapper.DocumentProperty is { } documentProperty ? records.WithDocumentCopyKey(documentProperty.StorageName) : records;
+
+ _collectionMetadata = new()
+ {
+ VectorStoreSystemName = ChromaConstants.VectorStoreSystemName,
+ VectorStoreName = _chromaClient.DatabaseName,
+ CollectionName = name
+ };
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (Interlocked.Exchange(ref _disposed, 1) == 0)
+ {
+ _chromaClient.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+
+ ///
+ public override string Name { get; }
+
+ ///
+ public override Task CollectionExistsAsync(CancellationToken cancellationToken = default)
+ => RunOperationAsync(
+ "CollectionExists",
+ () => _chromaClient.Client.CollectionExistsAsync(Name, cancellationToken: cancellationToken));
+
+ ///
+ public override Task EnsureCollectionExistsAsync(CancellationToken cancellationToken = default)
+ {
+ // Chroma indexes every metadata field for filtering, so IsIndexed has no effect.
+ var definition = ChromaCollectionCreateMapping.MapCollectionDefinition(Name, _model.VectorProperty, _bm25Properties, _mapper.DocumentProperty);
+
+ return RunOperationAsync(
+ "EnsureCollectionExists",
+ () => _chromaClient.Client.GetOrCreateCollectionAsync(definition, cancellationToken: cancellationToken));
+ }
+
+ ///
+ public override Task EnsureCollectionDeletedAsync(CancellationToken cancellationToken = default)
+ => RunOperationAsync(
+ "DeleteCollection",
+ // Chroma 1.5 gives the lists of the records of a deleted collection to the records of other collections.
+ () => _chromaClient.Client.DeleteCollectionIfExistsAsync(Name, deleteRecordsFirst: true, cancellationToken: cancellationToken));
+
+ ///
+ 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 ids = keys.Select(key => ChromaFieldMapping.ToId(key)).ToList();
+ if (ids.Count == 0)
+ {
+ yield break;
+ }
+
+ var entries = await RunOperationAsync(
+ OperationName,
+ () => _records.GetAsync(ids, include: GetInclude(includeVectors), cancellationToken: cancellationToken)).ConfigureAwait(false);
+
+ foreach (var entry in entries)
+ {
+ yield return _mapper.MapFromStorageToDataModel(entry.Id, entry.Embedding, entry.Metadata, entry.Document, 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 ids = keys.Select(key => ChromaFieldMapping.ToId(key)).ToList();
+ if (ids.Count == 0)
+ {
+ return Task.CompletedTask;
+ }
+
+ return RunOperationAsync(
+ DeleteName,
+ () => _records.DeleteAsync(ids, 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);
+
+ GeneratedEmbeddings>?[]? generatedEmbeddings = null;
+
+ var vectorProperty = _model.VectorProperty;
+ if (!ChromaModelBuilder.IsVectorPropertyTypeValidCore(vectorProperty.Type, out _))
+ {
+ // The vector property's type isn't natively supported - we need to generate embeddings.
+ Debug.Assert(vectorProperty.EmbeddingGenerator is not null);
+
+ var recordsList = records is IReadOnlyList r ? r : records.ToList();
+ if (recordsList.Count == 0)
+ {
+ return;
+ }
+
+ records = recordsList;
+ generatedEmbeddings = [(GeneratedEmbeddings>)await vectorProperty.GenerateEmbeddingsAsync(records.Select(r => vectorProperty.GetValueAsObject(r)), cancellationToken).ConfigureAwait(false)];
+ }
+
+ // Create the Chroma records.
+ var keyProperty = _model.KeyProperty;
+ var ids = new List();
+ var embeddings = new List>();
+ var metadatas = new List?>();
+ var documents = new List();
+ var recordIndex = 0;
+ foreach (var record in records)
+ {
+ if (keyProperty.IsAutoGenerated && keyProperty.GetValue(record) == Guid.Empty)
+ {
+ keyProperty.SetValue(record, Guid.NewGuid());
+ }
+
+ var storageRecord = _mapper.MapFromDataToStorageModel(record, recordIndex++, generatedEmbeddings);
+ ids.Add(storageRecord.Id);
+ embeddings.Add(storageRecord.Embedding);
+ metadatas.Add(storageRecord.Metadata);
+ documents.Add(storageRecord.Document);
+ }
+
+ if (ids.Count == 0)
+ {
+ return;
+ }
+
+ var chromaRecords = new ChromaRecords(ids)
+ {
+ Embeddings = embeddings,
+ Metadatas = metadatas,
+ Documents = _mapper.HasDocument ? documents : null,
+ NullDocumentsDelete = true,
+ };
+
+ await RunOperationAsync(
+ UpsertName,
+ () => _records.UpsertAsync(chromaRecords, 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 filter = options.Filter is not null
+ ? new ChromaFilterTranslator().Translate(options.Filter, _model, _mapper.DocumentProperty)
+ : ChromaWhereOperator.All;
+
+ var vector = await GetSearchVectorAsync(searchValue, vectorProperty, cancellationToken).ConfigureAwait(false);
+
+ var include = ChromaQueryInclude.Metadatas | ChromaQueryInclude.Distances;
+ if (_mapper.HasDocument)
+ {
+ include |= ChromaQueryInclude.Documents;
+ }
+ if (options.IncludeVectors)
+ {
+ include |= ChromaQueryInclude.Embeddings;
+ }
+
+ // The scores come from the distances in the space of the vector property.
+ var query = new ChromaQuery([vector])
+ {
+ NResults = top,
+ Offset = options.Skip,
+ Where = filter,
+ Include = include,
+ ExpectedSpace = ChromaCollectionCreateMapping.GetSpace(vectorProperty),
+ };
+ var entries = await RunOperationAsync(
+ "Query",
+ () => _records.QueryAsync(query, cancellationToken)).ConfigureAwait(false);
+
+ foreach (var entry in entries[0])
+ {
+ var score = ChromaCollectionSearchMapping.ToScore(entry.Distance!.Value, vectorProperty.DistanceFunction);
+ if (!ChromaCollectionSearchMapping.PassesThreshold(score, options.ScoreThreshold, vectorProperty.DistanceFunction))
+ {
+ continue;
+ }
+
+ yield return new VectorSearchResult(
+ _mapper.MapFromStorageToDataModel(entry.Id, entry.Embedding, entry.Metadata, entry.Document, 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 (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 not null
+ ? new ChromaFilterTranslator().Translate(options.Filter, _model, _mapper.DocumentProperty)
+ : ChromaWhereOperator.All;
+
+ var vector = await GetSearchVectorAsync(searchValue, vectorProperty, cancellationToken).ConfigureAwait(false);
+
+ List select = [ChromaSearchKeys.Metadata, ChromaSearchKeys.Score];
+ if (_mapper.HasDocument)
+ {
+ select.Add(ChromaSearchKeys.Document);
+ }
+ if (options.IncludeVectors)
+ {
+ select.Add(ChromaSearchKeys.Embedding);
+ }
+
+ var entries = await RunOperationAsync(
+ "Search",
+ async () =>
+ {
+ var index = await _records.FindBm25IndexAsync(textProperty.StorageName, cancellationToken).ConfigureAwait(false)
+ ?? throw new InvalidOperationException(
+ $"The Chroma collection '{Name}' has no BM25 index on the text of the property '{textProperty.ModelName}', which hybrid search needs. " +
+ "Only Chroma Cloud has BM25 indexes.");
+ return await _records.SearchAsync(
+ new ChromaSearch
+ {
+ Where = filter,
+ Rank = ChromaRank.HybridRrf(vector, string.Join(" ", keywords), index.Key, top + options.Skip),
+ Limit = top,
+ Offset = options.Skip,
+ Select = select,
+ },
+ cancellationToken: cancellationToken).ConfigureAwait(false);
+ }).ConfigureAwait(false);
+
+ foreach (var entry in entries)
+ {
+ var score = (double)entry.Score!.Value;
+ if (options.ScoreThreshold is { } threshold && score < threshold)
+ {
+ continue;
+ }
+
+ yield return new VectorSearchResult(
+ _mapper.MapFromStorageToDataModel(entry.Id, entry.Embedding, entry.Metadata, entry.Document, options.IncludeVectors),
+ score);
+ }
+ }
+
+ 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(), ChromaModelBuilder.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("Chroma does not support ordering.");
+ }
+
+ var chromaFilter = new ChromaFilterTranslator().Translate(filter, _model, _mapper.DocumentProperty);
+
+ var entries = await RunOperationAsync(
+ "Get",
+ () => _records.GetAsync(
+ ids: null,
+ chromaFilter,
+ whereDocument: null,
+ top,
+ options.Skip,
+ GetInclude(options.IncludeVectors),
+ cancellationToken)).ConfigureAwait(false);
+
+ foreach (var entry in entries)
+ {
+ yield return _mapper.MapFromStorageToDataModel(entry.Id, entry.Embedding, entry.Metadata, entry.Document, 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(ChromaClient) ? _chromaClient.Client :
+ // Hybrid search needs the BM25 indexes, which the client creates only on Chroma Cloud.
+ serviceType == typeof(IKeywordHybridSearchable) ? (_bm25Properties.Count > 0 && _chromaClient.Client.Options.IsChromaCloud ? this : null) :
+ serviceType.IsInstanceOfType(this) ? this :
+ null;
+ }
+
+ private ChromaGetInclude GetInclude(bool includeVectors)
+ => ChromaGetInclude.Metadatas
+ | (includeVectors ? ChromaGetInclude.Embeddings : 0)
+ | (_mapper.HasDocument ? ChromaGetInclude.Documents : 0);
+
+ ///
+ /// 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)
+ => VectorStoreErrorHandler.RunOperationAsync(_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 VectorStoreErrorHandler.RunOperationAsync(_collectionMetadata, operationName, operation).ConfigureAwait(false);
+}
diff --git a/MEVD/src/Chroma/ChromaCollectionCreateMapping.cs b/MEVD/src/Chroma/ChromaCollectionCreateMapping.cs
new file mode 100644
index 0000000..74c229b
--- /dev/null
+++ b/MEVD/src/Chroma/ChromaCollectionCreateMapping.cs
@@ -0,0 +1,65 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using ChromaDB.Client;
+using ChromaDB.Client.Models;
+using Microsoft.Extensions.VectorData;
+using Microsoft.Extensions.VectorData.ProviderServices;
+
+namespace CommunityToolkit.VectorData.Chroma;
+
+///
+/// Contains mapping helpers to use when creating a Chroma collection.
+///
+internal static class ChromaCollectionCreateMapping
+{
+ /// The distance functions Chroma supports, with its three spaces.
+ private const string SupportedDistanceFunctions =
+ $"{DistanceFunction.CosineSimilarity}, {DistanceFunction.CosineDistance}, {DistanceFunction.DotProductSimilarity}, " +
+ $"{DistanceFunction.NegativeDotProductSimilarity}, {DistanceFunction.EuclideanSquaredDistance}, {DistanceFunction.EuclideanDistance}";
+
+ ///
+ /// Maps the vector property and the BM25 indexes to the definition of the Chroma collection.
+ ///
+ /// The name of the collection.
+ /// The vector property.
+ /// The properties to create a BM25 index for.
+ /// The property stored as the document, whose BM25 index comes from the document, or .
+ /// The definition to create the collection with.
+ public static ChromaCollectionDefinition MapCollectionDefinition(string name, VectorPropertyModel vectorProperty, IReadOnlyList bm25Properties, DataPropertyModel? documentProperty = null)
+ {
+ ChromaCollectionSchema? schema = null;
+ foreach (var property in bm25Properties)
+ {
+ schema = (schema ?? new()).WithBm25Index(property == documentProperty ? ChromaSearchKeys.Document : property.StorageName, ifSupported: true);
+ }
+
+ return new(name) { Configuration = new() { Space = GetSpace(vectorProperty) }, Schema = schema };
+ }
+
+ ///
+ /// Get the string properties with full-text indexing, the ones a BM25 index can be created for.
+ ///
+ /// The model of the collection.
+ public static List GetBm25Properties(CollectionModel model)
+ => model.DataProperties.Where(ChromaFieldMapping.IsFullTextString).ToList();
+
+ ///
+ /// Get the Chroma distance function, called space, for the given .
+ /// If none is configured, the default is cosine.
+ ///
+ /// The vector property definition.
+ /// The Chroma space.
+ /// Thrown if a distance function is chosen that Chroma does not support.
+ public static ChromaSpace GetSpace(VectorPropertyModel vectorProperty)
+ => vectorProperty.DistanceFunction switch
+ {
+ DistanceFunction.CosineSimilarity or DistanceFunction.CosineDistance or null => ChromaSpace.Cosine,
+ DistanceFunction.DotProductSimilarity or DistanceFunction.NegativeDotProductSimilarity => ChromaSpace.InnerProduct,
+ DistanceFunction.EuclideanSquaredDistance or DistanceFunction.EuclideanDistance => ChromaSpace.L2,
+
+ _ => throw new NotSupportedException(
+ $"Distance function '{vectorProperty.DistanceFunction}' for {nameof(VectorStoreVectorProperty)} '{vectorProperty.ModelName}' is not supported by the Chroma VectorStore. " +
+ $"Supported distance functions: {SupportedDistanceFunctions}.")
+ };
+}
diff --git a/MEVD/src/Chroma/ChromaCollectionOptions.cs b/MEVD/src/Chroma/ChromaCollectionOptions.cs
new file mode 100644
index 0000000..9aadf4c
--- /dev/null
+++ b/MEVD/src/Chroma/ChromaCollectionOptions.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.Chroma;
+
+///
+/// Options when creating a .
+///
+public sealed class ChromaCollectionOptions : VectorStoreCollectionOptions
+{
+ internal static readonly ChromaCollectionOptions Default = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ChromaCollectionOptions()
+ {
+ }
+
+ internal ChromaCollectionOptions(ChromaCollectionOptions? source) : base(source)
+ {
+ }
+}
diff --git a/MEVD/src/Chroma/ChromaCollectionSearchMapping.cs b/MEVD/src/Chroma/ChromaCollectionSearchMapping.cs
new file mode 100644
index 0000000..56267ca
--- /dev/null
+++ b/MEVD/src/Chroma/ChromaCollectionSearchMapping.cs
@@ -0,0 +1,48 @@
+// 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 Microsoft.Extensions.VectorData;
+
+namespace CommunityToolkit.VectorData.Chroma;
+
+///
+/// Contains mapping helpers to use when searching for records using Chroma.
+///
+internal static class ChromaCollectionSearchMapping
+{
+ ///
+ /// Convert the distance Chroma returns to the score of the given distance function.
+ ///
+ ///
+ /// Chroma returns 1 - cosine similarity for cosine, 1 - dot product for ip, and the squared Euclidean distance for l2.
+ ///
+ /// The distance returned by Chroma.
+ /// The distance function of the vector property.
+ /// The score.
+ public static double ToScore(float distance, string? distanceFunction)
+ => distanceFunction switch
+ {
+ DistanceFunction.CosineSimilarity or null => 1 - distance,
+ DistanceFunction.CosineDistance => distance,
+ DistanceFunction.DotProductSimilarity => 1 - distance,
+ DistanceFunction.NegativeDotProductSimilarity => distance - 1,
+ DistanceFunction.EuclideanSquaredDistance => distance,
+ DistanceFunction.EuclideanDistance => Math.Sqrt(distance),
+
+ // The model builder rejects the other distance functions.
+ _ => throw new UnreachableException($"Distance function '{distanceFunction}' is not supported by the Chroma VectorStore.")
+ };
+
+ ///
+ /// Whether the score passes the threshold: a similarity must reach it, a distance must not go beyond it.
+ ///
+ /// The score.
+ /// The threshold, or for none.
+ /// The distance function of the vector property.
+ public static bool PassesThreshold(double score, double? threshold, string? distanceFunction)
+ => threshold is not { } t
+ || (distanceFunction is DistanceFunction.CosineSimilarity or DistanceFunction.DotProductSimilarity or null
+ ? score >= t
+ : score <= t);
+}
diff --git a/MEVD/src/Chroma/ChromaConstants.cs b/MEVD/src/Chroma/ChromaConstants.cs
new file mode 100644
index 0000000..5c78ec6
--- /dev/null
+++ b/MEVD/src/Chroma/ChromaConstants.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.Chroma;
+
+internal static class ChromaConstants
+{
+ internal const string VectorStoreSystemName = "chroma";
+}
diff --git a/MEVD/src/Chroma/ChromaDynamicCollection.cs b/MEVD/src/Chroma/ChromaDynamicCollection.cs
new file mode 100644
index 0000000..a4438e3
--- /dev/null
+++ b/MEVD/src/Chroma/ChromaDynamicCollection.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 ChromaDB.Client;
+
+namespace CommunityToolkit.VectorData.Chroma;
+
+///
+/// Represents a collection of vector store records in a Chroma database, mapped to a dynamic Dictionary<string, object?> .
+///
+#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
+public sealed class ChromaDynamicCollection : ChromaCollection>
+#pragma warning restore CA1711 // Identifiers should not have incorrect suffix
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Chroma client that can be used to manage the collections and records in a Chroma store.
+ /// The name of the collection.
+ /// A value indicating whether is disposed when the collection is disposed.
+ /// Optional configuration options for this class.
+ public ChromaDynamicCollection(ChromaClient chromaClient, string name, bool ownsClient, ChromaCollectionOptions options)
+ : this(() => new SharedChromaClient(chromaClient, ownsClient), name, options)
+ {
+ }
+
+ internal ChromaDynamicCollection(Func clientFactory, string name, ChromaCollectionOptions options)
+ : base(
+ clientFactory,
+ name,
+ static options => new ChromaModelBuilder()
+ .BuildDynamic(
+ options.Definition ?? throw new ArgumentException("Definition is required for dynamic collections"),
+ options.EmbeddingGenerator),
+ options)
+ {
+ }
+}
diff --git a/MEVD/src/Chroma/ChromaFieldMapping.cs b/MEVD/src/Chroma/ChromaFieldMapping.cs
new file mode 100644
index 0000000..bc6bc1f
--- /dev/null
+++ b/MEVD/src/Chroma/ChromaFieldMapping.cs
@@ -0,0 +1,40 @@
+// 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.ProviderServices;
+
+namespace CommunityToolkit.VectorData.Chroma;
+
+///
+/// Contains helper methods for mapping the properties of a record to a Chroma record.
+///
+internal static class ChromaFieldMapping
+{
+ ///
+ /// Get the property stored as the Chroma document: the only full-text indexed string property, or when
+ /// there is none or more than one.
+ ///
+ public static DataPropertyModel? GetDocumentProperty(CollectionModel model)
+ {
+ var fullTextProperties = model.DataProperties.Where(IsFullTextString).Take(2).ToList();
+ return fullTextProperties.Count == 1 ? fullTextProperties[0] : null;
+ }
+
+ ///
+ /// Whether the property is a string property with full-text indexing.
+ ///
+ public static bool IsFullTextString(DataPropertyModel property)
+ => property.IsFullTextIndexed && property.Type == typeof(string);
+
+ ///
+ /// Convert the given key to a Chroma record id.
+ ///
+ public static string ToId(object key)
+ => key switch
+ {
+ null => throw new ArgumentNullException(nameof(key)),
+ string id => id,
+ Guid id => id.ToString("D"),
+ _ => throw new NotSupportedException($"The provided key type '{key.GetType().Name}' is not supported by Chroma.")
+ };
+}
diff --git a/MEVD/src/Chroma/ChromaFilterTranslator.cs b/MEVD/src/Chroma/ChromaFilterTranslator.cs
new file mode 100644
index 0000000..09c8190
--- /dev/null
+++ b/MEVD/src/Chroma/ChromaFilterTranslator.cs
@@ -0,0 +1,342 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+using ChromaDB.Client;
+using Microsoft.Extensions.VectorData.ProviderServices;
+using Microsoft.Extensions.VectorData.ProviderServices.Filter;
+
+namespace CommunityToolkit.VectorData.Chroma;
+
+// https://docs.trychroma.com/docs/querying-collections/metadata-filtering
+internal sealed class ChromaFilterTranslator : FilterTranslatorBase
+{
+ private const string TextFilterNotSupported
+ = "Chroma filters text with Contains only on the single full-text indexed string property, which is stored as the document.";
+
+ private const string KeyFilterNotSupported
+ = "Chroma filters on the key only with == or Contains over a list of keys.";
+
+ private DataPropertyModel? _documentProperty;
+
+ ///
+ /// Translate the filter to the where clause of a Chroma request.
+ ///
+ /// The filter.
+ /// The model of the collection.
+ /// The property stored as the document, or .
+ internal ChromaWhereOperator Translate(LambdaExpression lambdaExpression, CollectionModel model, DataPropertyModel? documentProperty)
+ {
+ _documentProperty = documentProperty;
+ return Translate(PreprocessFilter(lambdaExpression, model, new FilterPreprocessingOptions()));
+ }
+
+ private static bool IsStringContains(Expression expression, [NotNullWhen(true)] out Expression? target, [NotNullWhen(true)] out Expression? argument)
+ {
+ if (expression is MethodCallExpression { Method.Name: nameof(string.Contains), Object: { } instance, Arguments: [var single] } call
+ && call.Method.DeclaringType == typeof(string)
+ && single.Type == typeof(string))
+ {
+ target = instance;
+ argument = single;
+ return true;
+ }
+
+ target = null;
+ argument = null;
+ return false;
+ }
+
+ private bool TryBindKey(Expression expression)
+ => TryBindProperty(expression, out var property) && property is KeyPropertyModel;
+
+ // The key is the Chroma id of a record, not a metadata field, and the vector is not in the metadata.
+ private bool TryBindDataProperty(Expression expression, [NotNullWhen(true)] out DataPropertyModel? property)
+ {
+ if (!TryBindProperty(expression, out var bound))
+ {
+ property = null;
+ return false;
+ }
+
+ property = bound switch
+ {
+ DataPropertyModel dataProperty => dataProperty,
+ KeyPropertyModel => throw new NotSupportedException(KeyFilterNotSupported),
+ _ => throw new NotSupportedException("Chroma does not filter on vector properties.")
+ };
+ return true;
+ }
+
+ // negated: whether the node is under an odd number of negations.
+ private ChromaWhereOperator Translate(Expression node, bool negated = false)
+ => node switch
+ {
+ BinaryExpression { NodeType: ExpressionType.Equal } equal => TranslateEqual(equal.Left, equal.Right),
+ BinaryExpression { NodeType: ExpressionType.NotEqual } notEqual => ChromaWhereOperator.Not(TranslateEqual(notEqual.Left, notEqual.Right)),
+
+ BinaryExpression { NodeType: ExpressionType.GreaterThan } comparison => TranslateComparison(comparison, greater: true, orEqual: false, negated),
+ BinaryExpression { NodeType: ExpressionType.GreaterThanOrEqual } comparison => TranslateComparison(comparison, greater: true, orEqual: true, negated),
+ BinaryExpression { NodeType: ExpressionType.LessThan } comparison => TranslateComparison(comparison, greater: false, orEqual: false, negated),
+ BinaryExpression { NodeType: ExpressionType.LessThanOrEqual } comparison => TranslateComparison(comparison, greater: false, orEqual: true, negated),
+
+ BinaryExpression { NodeType: ExpressionType.AndAlso } andAlso => Translate(andAlso.Left, negated) & Translate(andAlso.Right, negated),
+ BinaryExpression { NodeType: ExpressionType.OrElse } orElse => Translate(orElse.Left, negated) | Translate(orElse.Right, negated),
+
+ UnaryExpression { NodeType: ExpressionType.Not } not => ChromaWhereOperator.Not(Translate(not.Operand, !negated)),
+
+ // A bool property as a condition: r => r.Bool.
+ Expression when node.Type == typeof(bool) && TryBindDataProperty(node, out var property)
+ => ChromaWhereOperator.Equal(property.StorageName, true),
+
+ // r => true matches every record, which is useful for fetching all records, and r => false none.
+ ConstantExpression { Value: bool value } => value ? ChromaWhereOperator.All : ChromaWhereOperator.None,
+
+ MethodCallExpression methodCall => TranslateMethodCall(methodCall),
+
+ _ => throw new NotSupportedException("Chroma does not support the following NodeType in filters: " + node.NodeType)
+ };
+
+ private ChromaWhereOperator TranslateEqual(Expression left, Expression right)
+ {
+ // The key of a record is its Chroma id.
+ if (TryBindKey(left) && TryGetConstant(right, out var key) || TryBindKey(right) && TryGetConstant(left, out key))
+ {
+ return ChromaWhereOperator.In(ChromaSearchKeys.Id, ToId(key));
+ }
+
+ var (property, value) =
+ TryBindDataProperty(left, out var leftProperty) && TryGetConstant(right, out var rightConstant)
+ ? (leftProperty, rightConstant)
+ : TryBindDataProperty(right, out var rightProperty) && TryGetConstant(left, out var leftConstant)
+ ? (rightProperty, leftConstant)
+ : throw new NotSupportedException("Invalid equality/comparison");
+
+ // A property of a value type that is not nullable is never null: == null matches no record, and != null every record,
+ // with no need for the null values that Chroma metadata does not have.
+ if (value is null && property.Type.IsValueType && Nullable.GetUnderlyingType(property.Type) is null)
+ {
+ return ChromaWhereOperator.None;
+ }
+
+ return ChromaWhereOperator.Equal(property.StorageName, ToFilterValue(value));
+ }
+
+ private ChromaWhereOperator TranslateComparison(BinaryExpression comparison, bool greater, bool orEqual, bool negated)
+ {
+ // Normalize to property-on-the-left: 5 < r.Int is r.Int > 5.
+ DataPropertyModel? property;
+ object? value;
+ if (TryBindDataProperty(comparison.Left, out property) && TryGetConstant(comparison.Right, out value))
+ {
+ }
+ else if (TryBindDataProperty(comparison.Right, out property) && TryGetConstant(comparison.Left, out value))
+ {
+ greater = !greater;
+ }
+ else
+ {
+ throw new NotSupportedException("Comparison expression not supported by Chroma");
+ }
+
+ // A comparison with null is false whatever the value, as in C#: it matches no record.
+ if (value is null)
+ {
+ return ChromaWhereOperator.None;
+ }
+
+ // In C# the negation is true for a null value, which the negated comparison of Chroma does not match.
+ if (negated && Nullable.GetUnderlyingType(property.Type) is not null)
+ {
+ throw new NotSupportedException(
+ $"Chroma does not support a negated comparison on the nullable property '{property.ModelName}': it would leave out the records where the property is null.");
+ }
+
+ if (value is not (int or long or float or double))
+ {
+ throw new NotSupportedException($"Chroma supports comparisons on numbers only, not on '{value.GetType().Name}'.");
+ }
+
+ return (greater, orEqual) switch
+ {
+ (true, false) => ChromaWhereOperator.GreaterThan(property.StorageName, value),
+ (true, true) => ChromaWhereOperator.GreaterThanOrEqual(property.StorageName, value),
+ (false, false) => ChromaWhereOperator.LessThan(property.StorageName, value),
+ (false, true) => ChromaWhereOperator.LessThanOrEqual(property.StorageName, value)
+ };
+ }
+
+ // A constant compared with a nullable property is converted to the nullable type, e.g. r => r.NullableInt == 5, and a captured
+ // number to the type of the property, e.g. r => r.Double > intVariable, with the implicit conversions of C#.
+ private static bool TryGetConstant(Expression expression, out object? value)
+ {
+ switch (expression)
+ {
+ case ConstantExpression constant:
+ value = constant.Value;
+ return true;
+
+ case UnaryExpression { NodeType: ExpressionType.Convert, Operand: ConstantExpression constant } convert
+ when Nullable.GetUnderlyingType(convert.Type) == constant.Type:
+ value = constant.Value;
+ return true;
+
+ case UnaryExpression { NodeType: ExpressionType.Convert, Operand: ConstantExpression { Value: { } number } } convert
+ when Widen(number, Nullable.GetUnderlyingType(convert.Type) ?? convert.Type) is { } widened:
+ value = widened;
+ return true;
+
+ default:
+ value = null;
+ return false;
+ }
+ }
+
+ // The implicit numeric conversions of C# to the numbers Chroma compares; an explicit cast, which can change the value, is not one.
+ private static object? Widen(object number, Type type)
+ => type == typeof(double) ? number switch { int i => (double)i, long l => (double)l, float f => (double)f, _ => null }
+ : type == typeof(float) ? number switch { int i => (float)i, long l => (float)l, _ => null }
+ : type == typeof(long) ? number switch { int i => (long)i, _ => null }
+ : null;
+
+ private ChromaWhereOperator TranslateMethodCall(MethodCallExpression methodCall)
+ => methodCall switch
+ {
+ // string.Contains() on the property stored as the document; Chroma has no substring filter on metadata.
+ _ when IsStringContains(methodCall, out var target, out var argument) => TranslateTextContains(target, argument),
+
+ // Enumerable.Contains(), List.Contains(), MemoryExtensions.Contains()
+ _ when TryMatchContains(methodCall, out var source, out var item)
+ => TranslateContains(source, item),
+
+ // Enumerable.Any() with a Contains predicate (r => r.Strings.Any(s => array.Contains(s)))
+ { Method.Name: nameof(Enumerable.Any), Arguments: [var anySource, LambdaExpression lambda] } any
+ when any.Method.DeclaringType == typeof(Enumerable)
+ => TranslateAny(anySource, lambda),
+
+ _ => throw new NotSupportedException($"Unsupported method call: {methodCall.Method.DeclaringType?.Name}.{methodCall.Method.Name}")
+ };
+
+ private ChromaWhereOperator TranslateTextContains(Expression target, Expression argument)
+ {
+ if (_documentProperty is null || !TryBindProperty(target, out var property) || property != _documentProperty)
+ {
+ throw new NotSupportedException(TextFilterNotSupported);
+ }
+
+ if (!TryGetConstant(argument, out var value) || value is not string text)
+ {
+ throw new NotSupportedException("Chroma filters the text of the document with Contains over a constant string.");
+ }
+
+ return ChromaWhereOperator.Document(ChromaWhereDocumentOperator.Contains(text));
+ }
+
+ private ChromaWhereOperator TranslateContains(Expression source, Expression item)
+ {
+ // Contains over a list of keys: new[] { "a", "b" }.Contains(r.Key).
+ if (TryBindKey(item))
+ {
+ if (!TryGetValues(source, out var keys))
+ {
+ throw new NotSupportedException(KeyFilterNotSupported);
+ }
+
+ return ChromaWhereOperator.In(ChromaSearchKeys.Id, keys.Cast().Select(ToId).Distinct().ToArray());
+ }
+
+ // Contains over an array property: r.Strings.Contains("a").
+ if (TryBindDataProperty(source, out var property))
+ {
+ if (!TryGetConstant(item, out var value))
+ {
+ throw new NotSupportedException("Chroma supports Contains over an array property only with a constant item.");
+ }
+
+ return ChromaWhereOperator.Contains(property.StorageName, ToFilterValue(value));
+ }
+
+ // Contains over a list of values: new[] { "a", "b" }.Contains(r.String); over no values it matches no record.
+ if (TryGetValues(source, out var elements))
+ {
+ if (!TryBindDataProperty(item, out var itemProperty))
+ {
+ throw new NotSupportedException("Unsupported item type in Contains");
+ }
+
+ return ChromaWhereOperator.In(itemProperty.StorageName, elements.Cast().Select(ToFilterValue).Distinct().ToArray());
+ }
+
+ throw new NotSupportedException("Unsupported Contains expression");
+ }
+
+ // r.Strings.Any(s => array.Contains(s)) is true when the field contains at least one of the values; over no values it matches no record.
+ private ChromaWhereOperator TranslateAny(Expression source, LambdaExpression lambda)
+ {
+ if (!TryBindDataProperty(source, out var property)
+ || lambda.Body is not MethodCallExpression containsCall
+ || !TryMatchContains(containsCall, out var valuesExpression, out var itemExpression)
+ || itemExpression != lambda.Parameters[0]
+ || !TryGetValues(valuesExpression, out var values))
+ {
+ throw new NotSupportedException("Unsupported method call: Enumerable.Any");
+ }
+
+ return values.Cast()
+ .Select(ToFilterValue)
+ .Distinct()
+ .Aggregate(ChromaWhereOperator.None, (any, value) => any | ChromaWhereOperator.Contains(property.StorageName, value));
+ }
+
+ // The values of a list in a filter: an inline array, or a captured list or array, which the preprocessing makes a constant.
+ private static bool TryGetValues(Expression expression, [NotNullWhen(true)] out IEnumerable? values)
+ {
+ values = expression switch
+ {
+ NewArrayExpression newArray => GetInlineArrayElements(newArray),
+ ConstantExpression { Value: IEnumerable enumerable and not string } => enumerable,
+ _ => null
+ };
+ return values is not null;
+ }
+
+ // The elements of an inline array: new[] { "a", "b" }, or none for new string[0], whose only expression is its length.
+ private static object?[] GetInlineArrayElements(NewArrayExpression newArray)
+ => newArray switch
+ {
+ { NodeType: ExpressionType.NewArrayInit } => newArray.Expressions
+ .Select(element => element is ConstantExpression { Value: var value }
+ ? value
+ : throw new NotSupportedException("Inline array elements must be constants"))
+ .ToArray(),
+ { NodeType: ExpressionType.NewArrayBounds, Expressions: [ConstantExpression { Value: 0 }] } => [],
+ _ => throw new NotSupportedException("Unsupported inline array")
+ };
+
+ private static string ToId(object? key)
+ => key is null
+ ? throw new NotSupportedException("Chroma does not support filtering on a null key.")
+ : ChromaFieldMapping.ToId(key);
+
+ private static object ToFilterValue(object? value)
+ {
+ switch (value)
+ {
+ case null:
+ throw new NotSupportedException("Chroma does not support filtering on null values.");
+ case IEnumerable and not string:
+ throw new NotSupportedException("Chroma does not support comparing an array property with an array.");
+ }
+
+ try
+ {
+ return ChromaMetadataConvert.ToMetadataValue(value)!;
+ }
+ catch (ArgumentException exception)
+ {
+ throw new NotSupportedException($"Chroma does not support filtering on values of type '{value.GetType().Name}'.", exception);
+ }
+ }
+}
diff --git a/MEVD/src/Chroma/ChromaMapper.cs b/MEVD/src/Chroma/ChromaMapper.cs
new file mode 100644
index 0000000..7151b43
--- /dev/null
+++ b/MEVD/src/Chroma/ChromaMapper.cs
@@ -0,0 +1,120 @@
+// 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 ChromaDB.Client;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.VectorData.ProviderServices;
+
+namespace CommunityToolkit.VectorData.Chroma;
+
+///
+/// A record as Chroma stores it: an id, an embedding and metadata.
+///
+internal readonly record struct ChromaStorageRecord(string Id, ReadOnlyMemory Embedding, IReadOnlyDictionary? Metadata, string? Document);
+
+///
+/// Mapper between a Chroma record and the consumer data model.
+///
+/// The consumer data model to map to or from.
+internal sealed class ChromaMapper(CollectionModel model)
+ where TRecord : class
+{
+ private readonly DataPropertyModel? _documentProperty = ChromaFieldMapping.GetDocumentProperty(model);
+
+ /// Gets the property stored as the Chroma document: the only string property with full-text indexing, or .
+ public DataPropertyModel? DocumentProperty => _documentProperty;
+
+ /// Gets a value indicating whether the records have a Chroma document, from the full-text property.
+ public bool HasDocument => _documentProperty is not null;
+
+ public ChromaStorageRecord MapFromDataToStorageModel(TRecord dataModel, int recordIndex, GeneratedEmbeddings>?[]? generatedEmbeddings)
+ {
+ var keyProperty = model.KeyProperty;
+ var key = keyProperty.GetValueAsObject(dataModel)
+ ?? throw new InvalidOperationException($"Missing key property '{keyProperty.ModelName}' on provided record of type '{typeof(TRecord).Name}'.");
+
+ // The property stored as the document is the document.
+ var metadata = ChromaMetadataConvert.ToMetadata(model.DataProperties
+ .Where(property => property != _documentProperty)
+ .Select(property => new KeyValuePair(property.StorageName, property.GetValueAsObject(dataModel))));
+
+ // There is exactly one vector property, as verified by the model builder.
+ Debug.Assert(
+ generatedEmbeddings is null || generatedEmbeddings.Length == 1 && generatedEmbeddings[0] is not null,
+ "There should be exactly one generated embedding, for the single vector property.");
+ var embedding = GetVector(
+ model.VectorProperty,
+ generatedEmbeddings is null
+ ? model.VectorProperty.GetValueAsObject(dataModel)
+ : generatedEmbeddings[0]![recordIndex]);
+
+ var document = _documentProperty?.GetValueAsObject(dataModel) as string;
+
+ return new ChromaStorageRecord(ChromaFieldMapping.ToId(key), embedding, metadata, document);
+
+ // The model builder accepts these three vector types only, and the model checks the values of a dynamic record.
+ static ReadOnlyMemory GetVector(PropertyModel property, object? embedding)
+ => embedding switch
+ {
+ null => throw new InvalidOperationException($"Vector property '{property.ModelName}' on provided record of type '{typeof(TRecord).Name}' may not be null."),
+ Embedding e => e.Vector,
+ float[] a => a,
+ _ => (ReadOnlyMemory)embedding
+ };
+ }
+
+ public TRecord MapFromStorageToDataModel(string id, ReadOnlyMemory? embedding, IReadOnlyDictionary? metadata, string? document, bool includeVectors)
+ {
+ var outputRecord = model.CreateRecord()!;
+
+ // The model builder accepts string and Guid keys only.
+ model.KeyProperty.SetValueAsObject(outputRecord, model.KeyProperty.Type == typeof(Guid) ? Guid.Parse(id) : id);
+
+ if (includeVectors && embedding is { } vector)
+ {
+ // The model builder accepts ReadOnlyMemory, Embedding and float[] only.
+ var property = model.VectorProperty;
+ var vectorType = Nullable.GetUnderlyingType(property.Type) ?? property.Type;
+ property.SetValueAsObject(
+ outputRecord,
+ vectorType == typeof(Embedding) ? new Embedding(vector)
+ : vectorType == typeof(float[]) ? vector.ToArray()
+ : (object)vector);
+ }
+
+ foreach (var dataProperty in model.DataProperties)
+ {
+ if (dataProperty == _documentProperty)
+ {
+ dataProperty.SetValueAsObject(outputRecord, document);
+ }
+ else if (metadata is not null && metadata.TryGetValue(dataProperty.StorageName, out var value))
+ {
+ object? propertyValue;
+ try
+ {
+ propertyValue = ChromaMetadataConvert.FromMetadataValue(value, dataProperty.Type);
+ }
+ // Another Chroma client can write any value under the key of a property: a value of another type, a date that
+ // does not parse, or a number too large for the property.
+ catch (InvalidCastException exception)
+ {
+ throw ReadFailed(dataProperty, id, exception);
+ }
+
+ dataProperty.SetValueAsObject(outputRecord, propertyValue);
+ }
+ else if (!dataProperty.Type.IsValueType || Nullable.GetUnderlyingType(dataProperty.Type) is not null)
+ {
+ // A null value is not stored, since Chroma metadata has no null values; a missing one is null.
+ dataProperty.SetValueAsObject(outputRecord, null);
+ }
+ }
+
+ return outputRecord;
+
+ static InvalidOperationException ReadFailed(DataPropertyModel property, string id, Exception exception)
+ => new($"Failed to read the metadata key '{property.StorageName}' of the record '{id}' into the property '{property.ModelName}' of type '{property.Type.Name}'.", exception);
+ }
+}
diff --git a/MEVD/src/Chroma/ChromaModelBuilder.cs b/MEVD/src/Chroma/ChromaModelBuilder.cs
new file mode 100644
index 0000000..bbe4e47
--- /dev/null
+++ b/MEVD/src/Chroma/ChromaModelBuilder.cs
@@ -0,0 +1,110 @@
+// 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 Microsoft.Extensions.AI;
+using Microsoft.Extensions.VectorData;
+using Microsoft.Extensions.VectorData.ProviderServices;
+
+namespace CommunityToolkit.VectorData.Chroma;
+
+internal class ChromaModelBuilder() : CollectionModelBuilder(s_modelBuildingOptions)
+{
+ internal const string SupportedVectorTypes = "ReadOnlyMemory, Embedding, float[]";
+
+ // A Chroma record has exactly one embedding.
+ private static readonly CollectionModelBuildingOptions s_modelBuildingOptions = new()
+ {
+ RequiresAtLeastOneVector = true,
+ SupportsMultipleVectors = false,
+ };
+
+ protected override void ValidateKeyProperty(KeyPropertyModel keyProperty)
+ {
+ base.ValidateKeyProperty(keyProperty);
+
+ var type = keyProperty.Type;
+
+ if (type != typeof(string) && type != typeof(Guid))
+ {
+ throw new NotSupportedException(
+ $"Property '{keyProperty.ModelName}' has unsupported type '{type.Name}'. Key properties must be either string or Guid.");
+ }
+
+ // String and Guid keys are supported by the Chroma provider, but we support auto-generation of keys only for GUIDs
+ if (keyProperty.IsAutoGenerated && keyProperty.Type != typeof(Guid))
+ {
+ throw new NotSupportedException(
+ $"Auto-generation is only supported for key properties of type Guid. Property '{keyProperty.ModelName}' has type '{keyProperty.Type.Name}'.");
+ }
+ }
+
+ // Checked when the collection object is constructed, rather than when it is first used.
+ protected override void ValidateProperty(PropertyModel propertyModel, VectorStoreCollectionDefinition? definition)
+ {
+ base.ValidateProperty(propertyModel, definition);
+
+ switch (propertyModel)
+ {
+ case VectorPropertyModel vectorProperty:
+ if (vectorProperty.IndexKind is not null and not IndexKind.Hnsw)
+ {
+ throw new NotSupportedException(
+ $"Index kind '{vectorProperty.IndexKind}' for {nameof(VectorStoreVectorProperty)} '{vectorProperty.ModelName}' is not supported by the Chroma VectorStore. " +
+ $"Supported index kinds: {IndexKind.Hnsw}.");
+ }
+
+ // Throws for a distance function Chroma does not support.
+ _ = ChromaCollectionCreateMapping.GetSpace(vectorProperty);
+ break;
+
+ case DataPropertyModel { IsFullTextIndexed: true } dataProperty when dataProperty.Type != typeof(string):
+ throw new InvalidOperationException(
+ $"Property '{dataProperty.ModelName}' has {nameof(VectorStoreDataProperty.IsFullTextIndexed)} set, but is not a string: Chroma indexes the text of string properties only.");
+ }
+ }
+
+ protected override bool IsDataPropertyTypeValid(Type type, [NotNullWhen(false)] out string? supportedTypes)
+ {
+ supportedTypes = "string, int, long, double, float, bool, DateTime, DateTimeOffset,"
+#if NET
+ + " DateOnly,"
+#endif
+ + " or arrays/lists of these types";
+
+ if (Nullable.GetUnderlyingType(type) is Type underlyingType)
+ {
+ type = underlyingType;
+ }
+
+ return IsValid(type)
+ || (type.IsArray && IsValid(type.GetElementType()!))
+ || (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>) && IsValid(type.GenericTypeArguments[0]));
+
+ static bool IsValid(Type type)
+ => type == typeof(string) ||
+ type == typeof(int) ||
+ type == typeof(long) ||
+ type == typeof(double) ||
+ type == typeof(float) ||
+ type == typeof(bool) ||
+ type == typeof(DateTime) ||
+#if NET
+ type == typeof(DateOnly) ||
+#endif
+ type == typeof(DateTimeOffset);
+ }
+
+ protected override bool IsVectorPropertyTypeValid(Type type, [NotNullWhen(false)] out string? supportedTypes)
+ => IsVectorPropertyTypeValidCore(type, out supportedTypes);
+
+ internal static bool IsVectorPropertyTypeValidCore(Type type, [NotNullWhen(false)] out string? supportedTypes)
+ {
+ supportedTypes = SupportedVectorTypes;
+
+ return type == typeof(ReadOnlyMemory)
+ || type == typeof(ReadOnlyMemory?)
+ || type == typeof(Embedding)
+ || type == typeof(float[]);
+ }
+}
diff --git a/MEVD/src/Chroma/ChromaServiceCollectionExtensions.cs b/MEVD/src/Chroma/ChromaServiceCollectionExtensions.cs
new file mode 100644
index 0000000..74992e1
--- /dev/null
+++ b/MEVD/src/Chroma/ChromaServiceCollectionExtensions.cs
@@ -0,0 +1,276 @@
+// 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 ChromaDB.Client;
+using CommunityToolkit.VectorData.Chroma;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.VectorData;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Extensions.DependencyInjection;
+
+///
+/// Extension methods to register and instances on an .
+///
+public static class ChromaServiceCollectionExtensions
+{
+ private const string DynamicCodeMessage = "This method is incompatible with NativeAOT, consult the documentation for adding collections in a way that's compatible with NativeAOT.";
+ private const string UnreferencedCodeMessage = "This method is incompatible with trimming, consult the documentation for adding collections in a way that's compatible with NativeAOT.";
+
+ ///
+ /// Registers a as
+ /// with returned by
+ /// or retrieved from the dependency injection container if was not provided.
+ ///
+ ///
+ public static IServiceCollection AddChromaVectorStore(
+ this IServiceCollection services,
+ Func? clientProvider = default,
+ Func? optionsProvider = default,
+ ServiceLifetime lifetime = ServiceLifetime.Singleton)
+ => AddKeyedChromaVectorStore(services, serviceKey: null, clientProvider, optionsProvider, lifetime);
+
+ ///
+ /// Registers a keyed as
+ /// with returned by
+ /// or retrieved from the dependency injection container if was not provided.
+ ///
+ /// The to register the on.
+ /// The key with which to associate the vector store.
+ /// The provider.
+ /// Options provider to further configure the .
+ /// The service lifetime for the store. Defaults to .
+ /// Service collection.
+ public static IServiceCollection AddKeyedChromaVectorStore(
+ this IServiceCollection services,
+ object? serviceKey,
+ Func? clientProvider = default,
+ Func? optionsProvider = default,
+ ServiceLifetime lifetime = ServiceLifetime.Singleton)
+ {
+ Throw.IfNull(services);
+
+ return AddKeyedChromaVectorStore(services, serviceKey, lifetime, (sp, options) =>
+ {
+ var client = clientProvider is null ? sp.GetRequiredService() : clientProvider(sp);
+
+ // The client was restored from the DI container, so we do not own it.
+ return new ChromaVectorStore(client, ownsClient: false, options);
+ }, optionsProvider);
+ }
+
+ ///
+ /// Registers a as
+ /// that connects to Chroma with .
+ ///
+ ///
+ public static IServiceCollection AddChromaVectorStore(
+ this IServiceCollection services,
+ string connectionString,
+ ChromaVectorStoreOptions? options = default,
+ ServiceLifetime lifetime = ServiceLifetime.Singleton)
+ => AddKeyedChromaVectorStore(services, serviceKey: null, connectionString, options, lifetime);
+
+ ///
+ /// Registers a keyed as
+ /// that connects to Chroma with .
+ ///
+ /// The to register the on.
+ /// The key with which to associate the vector store.
+ /// Chroma connection string: the URI of the server, or Endpoint=...;Token=...;Tenant=...;Database=... .
+ /// Options to further configure the .
+ /// The service lifetime for the store. Defaults to .
+ /// Service collection.
+ public static IServiceCollection AddKeyedChromaVectorStore(
+ this IServiceCollection services,
+ object? serviceKey,
+ string connectionString,
+ ChromaVectorStoreOptions? options = default,
+ ServiceLifetime lifetime = ServiceLifetime.Singleton)
+ {
+ Throw.IfNull(services);
+ Throw.IfNullOrWhitespace(connectionString);
+
+ // The vector store creates the client, so it owns it.
+ var configurationOptions = ChromaConfigurationOptions.FromConnectionString(connectionString);
+ return AddKeyedChromaVectorStore(services, serviceKey, lifetime,
+ (_, storeOptions) => new ChromaVectorStore(new ChromaClient(configurationOptions), ownsClient: true, storeOptions),
+ _ => options!);
+ }
+
+ private static IServiceCollection AddKeyedChromaVectorStore(
+ IServiceCollection services,
+ object? serviceKey,
+ ServiceLifetime lifetime,
+ Func storeFactory,
+ Func? optionsProvider)
+ {
+ services.Add(new ServiceDescriptor(typeof(ChromaVectorStore), serviceKey,
+ (sp, _) => storeFactory(sp, GetStoreOptions(sp, optionsProvider)), lifetime));
+
+ services.Add(new ServiceDescriptor(typeof(VectorStore), serviceKey,
+ static (sp, key) => sp.GetRequiredKeyedService(key), lifetime));
+
+ return services;
+ }
+
+ ///
+ /// Registers a as
+ /// with returned by
+ /// or retrieved from the dependency injection container if was not provided.
+ ///
+ ///
+ [RequiresUnreferencedCode(UnreferencedCodeMessage)]
+ [RequiresDynamicCode(DynamicCodeMessage)]
+ public static IServiceCollection AddChromaCollection(
+ this IServiceCollection services,
+ string name,
+ Func? clientProvider = default,
+ Func? optionsProvider = default,
+ ServiceLifetime lifetime = ServiceLifetime.Singleton)
+ where TKey : notnull
+ where TRecord : class
+ => AddKeyedChromaCollection(services, serviceKey: null, name, clientProvider, optionsProvider, lifetime);
+
+ ///
+ /// Registers a keyed as
+ /// with returned by
+ /// or retrieved from the dependency injection container if was not provided.
+ ///
+ /// The to register the on.
+ /// The key with which to associate the collection.
+ /// The name of the collection.
+ /// The provider.
+ /// Options provider to further configure the .
+ /// The service lifetime for the collection. Defaults to .
+ /// Service collection.
+ [RequiresUnreferencedCode(UnreferencedCodeMessage)]
+ [RequiresDynamicCode(DynamicCodeMessage)]
+ public static IServiceCollection AddKeyedChromaCollection(
+ this IServiceCollection services,
+ object? serviceKey,
+ string name,
+ Func? clientProvider = default,
+ Func? optionsProvider = default,
+ ServiceLifetime lifetime = ServiceLifetime.Singleton)
+ where TKey : notnull
+ where TRecord : class
+ {
+ Throw.IfNull(services);
+ Throw.IfNullOrWhitespace(name);
+
+ return AddKeyedChromaCollection(services, serviceKey, lifetime, (sp, options) =>
+ {
+ var client = clientProvider is null ? sp.GetRequiredService() : clientProvider(sp);
+
+ // The client was restored from the DI container, so we do not own it.
+ return new ChromaCollection(client, name, ownsClient: false, options);
+ }, optionsProvider);
+ }
+
+ ///
+ /// Registers a as
+ /// that connects to Chroma with .
+ ///
+ ///
+ [RequiresUnreferencedCode(UnreferencedCodeMessage)]
+ [RequiresDynamicCode(DynamicCodeMessage)]
+ public static IServiceCollection AddChromaCollection(
+ this IServiceCollection services,
+ string name,
+ string connectionString,
+ ChromaCollectionOptions? options = default,
+ ServiceLifetime lifetime = ServiceLifetime.Singleton)
+ where TKey : notnull
+ where TRecord : class
+ => AddKeyedChromaCollection(services, serviceKey: null, name, connectionString, options, lifetime);
+
+ ///
+ /// Registers a keyed as
+ /// that connects to Chroma with .
+ ///
+ /// The to register the on.
+ /// The key with which to associate the collection.
+ /// The name of the collection.
+ /// Chroma connection string: the URI of the server, or Endpoint=...;Token=...;Tenant=...;Database=... .
+ /// Options to further configure the .
+ /// The service lifetime for the collection. Defaults to .
+ /// Service collection.
+ [RequiresUnreferencedCode(UnreferencedCodeMessage)]
+ [RequiresDynamicCode(DynamicCodeMessage)]
+ public static IServiceCollection AddKeyedChromaCollection(
+ this IServiceCollection services,
+ object? serviceKey,
+ string name,
+ string connectionString,
+ ChromaCollectionOptions? options = default,
+ ServiceLifetime lifetime = ServiceLifetime.Singleton)
+ where TKey : notnull
+ where TRecord : class
+ {
+ Throw.IfNull(services);
+ Throw.IfNullOrWhitespace(name);
+ Throw.IfNullOrWhitespace(connectionString);
+
+ // The collection creates the client, so it owns it.
+ var configurationOptions = ChromaConfigurationOptions.FromConnectionString(connectionString);
+ return AddKeyedChromaCollection(services, serviceKey, lifetime,
+ (_, collectionOptions) => new ChromaCollection(new ChromaClient(configurationOptions), name, ownsClient: true, collectionOptions),
+ _ => options!);
+ }
+
+ [RequiresUnreferencedCode(UnreferencedCodeMessage)]
+ [RequiresDynamicCode(DynamicCodeMessage)]
+ private static IServiceCollection AddKeyedChromaCollection(
+ IServiceCollection services,
+ object? serviceKey,
+ ServiceLifetime lifetime,
+ Func> collectionFactory,
+ Func? optionsProvider)
+ where TKey : notnull
+ where TRecord : class
+ {
+ services.Add(new ServiceDescriptor(typeof(ChromaCollection), serviceKey,
+ (sp, _) => collectionFactory(sp, GetCollectionOptions(sp, optionsProvider)), lifetime));
+
+ services.Add(new ServiceDescriptor(typeof(VectorStoreCollection), serviceKey,
+ static (sp, key) => sp.GetRequiredKeyedService>(key), lifetime));
+
+ services.Add(new ServiceDescriptor(typeof(IVectorSearchable), serviceKey,
+ static (sp, key) => sp.GetRequiredKeyedService>(key), lifetime));
+
+ services.Add(new ServiceDescriptor(typeof(IKeywordHybridSearchable), serviceKey,
+ static (sp, key) => sp.GetRequiredKeyedService>(key), lifetime));
+
+ return services;
+ }
+
+ private static ChromaVectorStoreOptions? GetStoreOptions(IServiceProvider sp, Func? optionsProvider)
+ {
+ var options = optionsProvider?.Invoke(sp);
+ if (options?.EmbeddingGenerator is not null)
+ {
+ return options; // The user has provided everything, there is nothing to change.
+ }
+
+ var embeddingGenerator = sp.GetService();
+ return embeddingGenerator is null
+ ? options // There is nothing to change.
+ : new(options) { EmbeddingGenerator = embeddingGenerator }; // Create a brand new copy in order to avoid modifying the original options.
+ }
+
+ private static ChromaCollectionOptions? GetCollectionOptions(IServiceProvider sp, Func? optionsProvider)
+ {
+ var options = optionsProvider?.Invoke(sp);
+ if (options?.EmbeddingGenerator is not null)
+ {
+ return options; // The user has provided everything, there is nothing to change.
+ }
+
+ var embeddingGenerator = sp.GetService();
+ return embeddingGenerator is null
+ ? options // There is nothing to change.
+ : new(options) { EmbeddingGenerator = embeddingGenerator }; // Create a brand new copy in order to avoid modifying the original options.
+ }
+}
diff --git a/MEVD/src/Chroma/ChromaVectorStore.cs b/MEVD/src/Chroma/ChromaVectorStore.cs
new file mode 100644
index 0000000..0d1c5b6
--- /dev/null
+++ b/MEVD/src/Chroma/ChromaVectorStore.cs
@@ -0,0 +1,152 @@
+// 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 System.Runtime.CompilerServices;
+using ChromaDB.Client;
+using ChromaDB.Client.Models;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.VectorData;
+using Microsoft.Extensions.VectorData.ProviderServices;
+using Microsoft.Shared.Diagnostics;
+
+namespace CommunityToolkit.VectorData.Chroma;
+
+///
+/// Class for accessing the list of collections in a Chroma vector store.
+///
+///
+/// This class can be used with collections of any schema type, but requires you to provide schema information when getting a collection.
+///
+public sealed class ChromaVectorStore : VectorStore
+{
+ /// Metadata about vector store.
+ private readonly VectorStoreMetadata _metadata;
+
+ /// Chroma client that can be used to manage the collections and records in a Chroma store.
+ private readonly SharedChromaClient _chromaClient;
+
+ /// A general purpose definition that can be used to construct a collection when needing to proxy schema agnostic operations.
+ private static readonly VectorStoreCollectionDefinition s_generalPurposeDefinition = new() { Properties = [new VectorStoreKeyProperty("Key", typeof(string)), new VectorStoreVectorProperty("Vector", typeof(ReadOnlyMemory), 1)] };
+
+ private readonly IEmbeddingGenerator? _embeddingGenerator;
+
+ /// Whether the store was disposed: it releases its share of the client only once.
+ private int _disposed;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Chroma client that can be used to manage the collections and records in a Chroma store.
+ /// A value indicating whether is disposed after the vector store and the collections it returns are all disposed.
+ /// Optional configuration options for this class.
+ public ChromaVectorStore(ChromaClient chromaClient, bool ownsClient, ChromaVectorStoreOptions? options = default)
+ : this(new SharedChromaClient(chromaClient, ownsClient), options)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Chroma client that can be used to manage the collections and records in a Chroma store.
+ /// Optional configuration options for this class.
+ internal ChromaVectorStore(SharedChromaClient chromaClient, ChromaVectorStoreOptions? options = default)
+ {
+ Throw.IfNull(chromaClient);
+
+ _chromaClient = chromaClient;
+
+ options ??= ChromaVectorStoreOptions.Default;
+ _embeddingGenerator = options.EmbeddingGenerator;
+
+ _metadata = new()
+ {
+ VectorStoreSystemName = ChromaConstants.VectorStoreSystemName,
+ VectorStoreName = chromaClient.DatabaseName
+ };
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (Interlocked.Exchange(ref _disposed, 1) == 0)
+ {
+ _chromaClient.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+
+#pragma warning disable IDE0090 // Use 'new(...)'
+ ///
+ [RequiresDynamicCode("This overload of GetCollection() is incompatible with NativeAOT. For dynamic mapping via Dictionary, call GetDynamicCollection() instead.")]
+ [RequiresUnreferencedCode("This overload of GetCollection() is incompatible with trimming. For dynamic mapping via Dictionary, call GetDynamicCollection() instead.")]
+#if NET
+ public override ChromaCollection GetCollection(string name, VectorStoreCollectionDefinition? definition = null)
+#else
+ public override VectorStoreCollection GetCollection(string name, VectorStoreCollectionDefinition? definition = null)
+#endif
+ => typeof(TRecord) == typeof(Dictionary)
+ ? throw new ArgumentException(VectorDataStrings.GetCollectionWithDictionaryNotSupported)
+ : new ChromaCollection(_chromaClient.Share, name, new()
+ {
+ Definition = definition,
+ EmbeddingGenerator = _embeddingGenerator
+ });
+
+ ///
+#if NET
+ public override ChromaDynamicCollection GetDynamicCollection(string name, VectorStoreCollectionDefinition definition)
+#else
+ public override VectorStoreCollection> GetDynamicCollection(string name, VectorStoreCollectionDefinition definition)
+#endif
+ => new ChromaDynamicCollection(_chromaClient.Share, name, new ChromaCollectionOptions()
+ {
+ Definition = definition,
+ EmbeddingGenerator = _embeddingGenerator
+ });
+#pragma warning restore IDE0090
+
+ ///
+ public override async IAsyncEnumerable ListCollectionNamesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ const string OperationName = "ListCollectionNames";
+
+ var collections = await VectorStoreErrorHandler.RunOperationAsync, ChromaException>(
+ _metadata,
+ OperationName,
+ () => _chromaClient.Client.ListCollectionsAsync(cancellationToken: cancellationToken)).ConfigureAwait(false);
+
+ foreach (var collection in collections)
+ {
+ yield return collection.Name;
+ }
+ }
+
+ ///
+ public override async Task CollectionExistsAsync(string name, CancellationToken cancellationToken = default)
+ {
+ using var collection = GetDynamicCollection(name, s_generalPurposeDefinition);
+ return await collection.CollectionExistsAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ public override async Task EnsureCollectionDeletedAsync(string name, CancellationToken cancellationToken = default)
+ {
+ using var collection = GetDynamicCollection(name, s_generalPurposeDefinition);
+ await collection.EnsureCollectionDeletedAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ public override object? GetService(Type serviceType, object? serviceKey = null)
+ {
+ Throw.IfNull(serviceType);
+
+ return
+ serviceKey is not null ? null :
+ serviceType == typeof(VectorStoreMetadata) ? _metadata :
+ serviceType == typeof(ChromaClient) ? _chromaClient.Client :
+ serviceType.IsInstanceOfType(this) ? this :
+ null;
+ }
+}
diff --git a/MEVD/src/Chroma/ChromaVectorStoreOptions.cs b/MEVD/src/Chroma/ChromaVectorStoreOptions.cs
new file mode 100644
index 0000000..3569cd9
--- /dev/null
+++ b/MEVD/src/Chroma/ChromaVectorStoreOptions.cs
@@ -0,0 +1,31 @@
+// 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.AI;
+
+namespace CommunityToolkit.VectorData.Chroma;
+
+///
+/// Options when creating a .
+///
+public sealed class ChromaVectorStoreOptions
+{
+ internal static readonly ChromaVectorStoreOptions Default = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ChromaVectorStoreOptions()
+ {
+ }
+
+ internal ChromaVectorStoreOptions(ChromaVectorStoreOptions? source)
+ {
+ EmbeddingGenerator = source?.EmbeddingGenerator;
+ }
+
+ ///
+ /// Gets or sets the default embedding generator to use when generating vector embeddings with this vector store.
+ ///
+ public IEmbeddingGenerator? EmbeddingGenerator { get; set; }
+}
diff --git a/MEVD/src/Chroma/README.md b/MEVD/src/Chroma/README.md
new file mode 100644
index 0000000..a84c90d
--- /dev/null
+++ b/MEVD/src/Chroma/README.md
@@ -0,0 +1,29 @@
+# CommunityToolkit.VectorData.Chroma
+
+Chroma provider for [Microsoft.Extensions.VectorData](https://learn.microsoft.com/dotnet/ai/vector-stores/overview), by the .NET Community Toolkit.
+
+[Chroma](https://www.trychroma.com/) is an open-source vector database for AI applications.
+
+The provider is built on [ChromaDotNet.Client](https://www.nuget.org/packages/ChromaDotNet.Client), a .NET client for Chroma and Chroma Cloud.
+
+## Quick start
+
+1. Run Chroma with Docker:
+
+```bash
+docker run -d --name chroma -p 8000:8000 chromadb/chroma
+```
+
+2. Install the NuGet package:
+
+```bash
+dotnet add package CommunityToolkit.VectorData.Chroma
+```
+
+For more information, see the [Microsoft.Extensions.VectorData documentation](https://learn.microsoft.com/dotnet/ai/vector-stores/overview).
+
+## Limitations
+
+- On Chroma Cloud, a vector search returns at most 300 results, `Skip` included, the default quota of Chroma Cloud: beyond that, Chroma Cloud answers with a quota error.
+- Chroma does not store empty lists, so an empty array or list comes back as `null`.
+- Array and list properties, like the namespaces of the `TextSearchStore` of Semantic Kernel, need Chroma 1.5.0 or later.
diff --git a/MEVD/src/Chroma/SharedChromaClient.cs b/MEVD/src/Chroma/SharedChromaClient.cs
new file mode 100644
index 0000000..1342c6c
--- /dev/null
+++ b/MEVD/src/Chroma/SharedChromaClient.cs
@@ -0,0 +1,62 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using ChromaDB.Client;
+using Microsoft.Shared.Diagnostics;
+
+namespace CommunityToolkit.VectorData.Chroma;
+
+///
+/// The of a vector store or of a collection. A vector store shares it with the collections it returns,
+/// and the it owns is disposed when the last of them is disposed.
+///
+internal sealed class SharedChromaClient : IDisposable
+{
+ private readonly ChromaClient? _ownedClient;
+ private int _referenceCount = 1;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Chroma client that can be used to manage the collections and records in a Chroma store.
+ /// A value indicating whether is disposed with the last of the vector store and its collections.
+ public SharedChromaClient(ChromaClient chromaClient, bool ownsClient)
+ {
+ Throw.IfNull(chromaClient);
+
+ Client = chromaClient;
+ _ownedClient = ownsClient ? chromaClient : null;
+ }
+
+ ///
+ /// Gets the client.
+ ///
+ public ChromaClient Client { get; }
+
+ ///
+ /// Gets the database the client works in, or for the default database of the server.
+ ///
+ public string? DatabaseName => Client.Options.Database;
+
+ ///
+ /// Gets this client for one more user, a collection of the vector store, which disposes it too.
+ ///
+ public SharedChromaClient Share()
+ {
+ if (_ownedClient is not null)
+ {
+ Interlocked.Increment(ref _referenceCount);
+ }
+
+ return this;
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (_ownedClient is not null && Interlocked.Decrement(ref _referenceCount) == 0)
+ {
+ _ownedClient.Dispose();
+ }
+ }
+}
diff --git a/MEVD/test/Chroma.ConformanceTests/Chroma.ConformanceTests.csproj b/MEVD/test/Chroma.ConformanceTests/Chroma.ConformanceTests.csproj
new file mode 100644
index 0000000..db4975c
--- /dev/null
+++ b/MEVD/test/Chroma.ConformanceTests/Chroma.ConformanceTests.csproj
@@ -0,0 +1,35 @@
+
+
+
+ net10.0;$(NetFrameworkTfm)
+ enable
+ enable
+ true
+ false
+ Chroma.ConformanceTests
+ b7762d10-e29b-4bb1-8b74-b6d69a667dd4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+ Always
+
+
+
+
diff --git a/MEVD/test/Chroma.ConformanceTests/ChromaCollectionManagementTests.cs b/MEVD/test/Chroma.ConformanceTests/ChromaCollectionManagementTests.cs
new file mode 100644
index 0000000..3a0bea8
--- /dev/null
+++ b/MEVD/test/Chroma.ConformanceTests/ChromaCollectionManagementTests.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Chroma.ConformanceTests.Support;
+using CommunityToolkit.VectorData.Chroma;
+using VectorData.ConformanceTests;
+using Xunit;
+
+namespace Chroma.ConformanceTests;
+
+public class ChromaCollectionManagementTests(ChromaFixture fixture)
+ : CollectionManagementTests(fixture), IClassFixture
+{
+}
diff --git a/MEVD/test/Chroma.ConformanceTests/ChromaDependencyInjectionTests.cs b/MEVD/test/Chroma.ConformanceTests/ChromaDependencyInjectionTests.cs
new file mode 100644
index 0000000..0039a61
--- /dev/null
+++ b/MEVD/test/Chroma.ConformanceTests/ChromaDependencyInjectionTests.cs
@@ -0,0 +1,94 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using ChromaDB.Client;
+using CommunityToolkit.VectorData.Chroma;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using VectorData.ConformanceTests;
+using Xunit;
+
+namespace Chroma.ConformanceTests;
+
+public class ChromaDependencyInjectionTests
+ : DependencyInjectionTests.Record>, string, DependencyInjectionTests.Record>
+{
+ private const string ConnectionString = "Endpoint=http://localhost:8000;Token=fakeToken";
+
+ protected override void PopulateConfiguration(ConfigurationManager configuration, object? serviceKey = null)
+ => configuration.AddInMemoryCollection(
+ [
+ new(CreateConfigKey("Chroma", serviceKey, "ConnectionString"), ConnectionString),
+ ]);
+
+ private static string ConnectionStringProvider(IServiceProvider sp, object? serviceKey = null)
+ => sp.GetRequiredService().GetRequiredSection(CreateConfigKey("Chroma", serviceKey, "ConnectionString")).Value!;
+
+ private static ChromaClient CreateClient(string connectionString)
+ => new(ChromaConfigurationOptions.FromConnectionString(connectionString));
+
+ public override IEnumerable> CollectionDelegates
+ {
+ get
+ {
+ yield return (services, serviceKey, name, lifetime) => serviceKey is null
+ ? services
+ .AddSingleton(sp => CreateClient(ConnectionString))
+ .AddChromaCollection(name, lifetime: lifetime)
+ : services
+ .AddSingleton(sp => CreateClient(ConnectionString))
+ .AddKeyedChromaCollection(serviceKey, name, lifetime: lifetime);
+
+ yield return (services, serviceKey, name, lifetime) => serviceKey is null
+ ? services.AddChromaCollection(name, ConnectionString, lifetime: lifetime)
+ : services.AddKeyedChromaCollection(serviceKey, name, ConnectionString, lifetime: lifetime);
+
+ yield return (services, serviceKey, name, lifetime) => serviceKey is null
+ ? services.AddChromaCollection(
+ name, sp => CreateClient(ConnectionStringProvider(sp)), lifetime: lifetime)
+ : services.AddKeyedChromaCollection(
+ serviceKey, name, sp => CreateClient(ConnectionStringProvider(sp, serviceKey)), lifetime: lifetime);
+ }
+ }
+
+ public override IEnumerable> StoreDelegates
+ {
+ get
+ {
+ yield return (services, serviceKey, lifetime) => serviceKey is null
+ ? services.AddChromaVectorStore(ConnectionString, lifetime: lifetime)
+ : services.AddKeyedChromaVectorStore(serviceKey, ConnectionString, lifetime: lifetime);
+
+ yield return (services, serviceKey, lifetime) => serviceKey is null
+ ? services
+ .AddSingleton(sp => CreateClient(ConnectionString))
+ .AddChromaVectorStore(lifetime: lifetime)
+ : services
+ .AddSingleton(sp => CreateClient(ConnectionString))
+ .AddKeyedChromaVectorStore(serviceKey, lifetime: lifetime);
+
+ yield return (services, serviceKey, lifetime) => serviceKey is null
+ ? services.AddChromaVectorStore(
+ sp => CreateClient(ConnectionStringProvider(sp)), lifetime: lifetime)
+ : services.AddKeyedChromaVectorStore(
+ serviceKey, sp => CreateClient(ConnectionStringProvider(sp, serviceKey)), lifetime: lifetime);
+ }
+ }
+
+ [Fact]
+ public void ConnectionStringCantBeNullOrEmpty()
+ {
+ IServiceCollection services = new ServiceCollection();
+
+ Assert.Throws(() => services.AddChromaVectorStore(connectionString: null!));
+ Assert.Throws(() => services.AddKeyedChromaVectorStore(serviceKey: "notNull", connectionString: null!));
+ Assert.Throws(() => services.AddChromaCollection(
+ name: "notNull", connectionString: null!));
+ Assert.Throws(() => services.AddChromaCollection(
+ name: "notNull", connectionString: ""));
+ Assert.Throws(() => services.AddKeyedChromaCollection(
+ serviceKey: "notNull", name: "notNull", connectionString: null!));
+ Assert.Throws(() => services.AddKeyedChromaCollection(
+ serviceKey: "notNull", name: "notNull", connectionString: ""));
+ }
+}
diff --git a/MEVD/test/Chroma.ConformanceTests/ChromaDistanceFunctionTests.cs b/MEVD/test/Chroma.ConformanceTests/ChromaDistanceFunctionTests.cs
new file mode 100644
index 0000000..55f44a5
--- /dev/null
+++ b/MEVD/test/Chroma.ConformanceTests/ChromaDistanceFunctionTests.cs
@@ -0,0 +1,22 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Chroma.ConformanceTests.Support;
+using VectorData.ConformanceTests;
+using VectorData.ConformanceTests.Support;
+using Xunit;
+
+namespace Chroma.ConformanceTests;
+
+public class ChromaDistanceFunctionTests(ChromaDistanceFunctionTests.Fixture fixture)
+ : DistanceFunctionTests(fixture), IClassFixture
+{
+ // Chroma supports cosine, inner product (ip) and squared Euclidean (l2) only
+ public override Task ManhattanDistance() => Assert.ThrowsAsync(base.ManhattanDistance);
+ public override Task HammingDistance() => Assert.ThrowsAsync(base.HammingDistance);
+
+ public new class Fixture() : DistanceFunctionTests.Fixture
+ {
+ public override TestStore TestStore => ChromaTestStore.Instance;
+ }
+}
diff --git a/MEVD/test/Chroma.ConformanceTests/ChromaEmbeddingGenerationTests.cs b/MEVD/test/Chroma.ConformanceTests/ChromaEmbeddingGenerationTests.cs
new file mode 100644
index 0000000..97cddc3
--- /dev/null
+++ b/MEVD/test/Chroma.ConformanceTests/ChromaEmbeddingGenerationTests.cs
@@ -0,0 +1,62 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Chroma.ConformanceTests.Support;
+using ChromaDB.Client;
+using CommunityToolkit.VectorData.Chroma;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.VectorData;
+using VectorData.ConformanceTests;
+using VectorData.ConformanceTests.Support;
+using Xunit;
+
+namespace Chroma.ConformanceTests;
+
+public class ChromaEmbeddingGenerationTests(ChromaEmbeddingGenerationTests.StringVectorFixture stringVectorFixture, ChromaEmbeddingGenerationTests.RomOfFloatVectorFixture romOfFloatVectorFixture)
+ : EmbeddingGenerationTests(stringVectorFixture, romOfFloatVectorFixture), IClassFixture, IClassFixture
+{
+ public new class StringVectorFixture : EmbeddingGenerationTests.StringVectorFixture
+ {
+ public override TestStore TestStore => ChromaTestStore.Instance;
+
+ public override VectorStore CreateVectorStore(IEmbeddingGenerator? embeddingGenerator)
+ => ChromaTestStore.Instance.GetVectorStore(new() { EmbeddingGenerator = embeddingGenerator });
+
+ public override Func[] DependencyInjectionStoreRegistrationDelegates =>
+ [
+ services => services
+ .AddSingleton(ChromaTestStore.Instance.Client)
+ .AddChromaVectorStore()
+ ];
+
+ public override Func[] DependencyInjectionCollectionRegistrationDelegates =>
+ [
+ services => services
+ .AddSingleton(ChromaTestStore.Instance.Client)
+ .AddChromaCollection(this.CollectionName)
+ ];
+ }
+
+ public new class RomOfFloatVectorFixture : EmbeddingGenerationTests.RomOfFloatVectorFixture
+ {
+ public override TestStore TestStore => ChromaTestStore.Instance;
+
+ public override VectorStore CreateVectorStore(IEmbeddingGenerator? embeddingGenerator)
+ => ChromaTestStore.Instance.GetVectorStore(new() { EmbeddingGenerator = embeddingGenerator });
+
+ public override Func[] DependencyInjectionStoreRegistrationDelegates =>
+ [
+ services => services
+ .AddSingleton(ChromaTestStore.Instance.Client)
+ .AddChromaVectorStore()
+ ];
+
+ public override Func[] DependencyInjectionCollectionRegistrationDelegates =>
+ [
+ services => services
+ .AddSingleton(ChromaTestStore.Instance.Client)
+ .AddChromaCollection(this.CollectionName)
+ ];
+ }
+}
diff --git a/MEVD/test/Chroma.ConformanceTests/ChromaFilterTests.cs b/MEVD/test/Chroma.ConformanceTests/ChromaFilterTests.cs
new file mode 100644
index 0000000..8644ead
--- /dev/null
+++ b/MEVD/test/Chroma.ConformanceTests/ChromaFilterTests.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 Chroma.ConformanceTests.Support;
+using VectorData.ConformanceTests;
+using VectorData.ConformanceTests.Support;
+using Xunit;
+
+namespace Chroma.ConformanceTests;
+
+public class ChromaFilterTests(ChromaFilterTests.Fixture fixture)
+ : FilterTests(fixture), IClassFixture
+{
+ #region Null checking
+
+ // Chroma metadata has no null values, and Chroma has no filter for a missing field
+ public override Task Equal_with_null_reference_type()
+ => Assert.ThrowsAsync(() => base.Equal_with_null_reference_type());
+
+ public override Task Equal_with_null_captured()
+ => Assert.ThrowsAsync(() => base.Equal_with_null_captured());
+
+ public override Task NotEqual_with_null_reference_type()
+ => Assert.ThrowsAsync(() => base.NotEqual_with_null_reference_type());
+
+ public override Task NotEqual_with_null_captured()
+ => Assert.ThrowsAsync(() => base.NotEqual_with_null_captured());
+
+ #endregion
+
+ public new class Fixture : FilterTests.Fixture
+ {
+ public override TestStore TestStore => ChromaTestStore.Instance;
+ }
+}
diff --git a/MEVD/test/Chroma.ConformanceTests/ChromaHybridSearchTests.cs b/MEVD/test/Chroma.ConformanceTests/ChromaHybridSearchTests.cs
new file mode 100644
index 0000000..eee271e
--- /dev/null
+++ b/MEVD/test/Chroma.ConformanceTests/ChromaHybridSearchTests.cs
@@ -0,0 +1,73 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Chroma.ConformanceTests.Support;
+using VectorData.ConformanceTests;
+using VectorData.ConformanceTests.Support;
+using Xunit;
+
+namespace Chroma.ConformanceTests;
+
+///
+/// Hybrid search needs the Search API and the sparse vector indexes of Chroma, which only Chroma Cloud has:
+/// these tests run only when Chroma:ConnectionString has a Chroma Cloud address, like https://api.trychroma.com, and are skipped otherwise. See the README for the settings.
+///
+public class ChromaHybridSearchTests(ChromaHybridSearchTests.VectorAndStringFixture vectorAndStringFixture, ChromaHybridSearchTests.MultiTextFixture multiTextFixture)
+ : HybridSearchTests(vectorAndStringFixture, multiTextFixture),
+ IClassFixture,
+ IClassFixture
+{
+ private const string SkipReason = "Hybrid search needs Chroma Cloud: set Chroma:ConnectionString to a Chroma Cloud address, like https://api.trychroma.com to run it.";
+
+ public override Task HybridSearchAsync()
+ {
+ Assert.SkipUnless(ChromaTestStore.IsChromaCloud, SkipReason);
+ return base.HybridSearchAsync();
+ }
+
+ public override Task HybridSearchAsync_with_filter()
+ {
+ Assert.SkipUnless(ChromaTestStore.IsChromaCloud, SkipReason);
+ return base.HybridSearchAsync_with_filter();
+ }
+
+ public override Task HybridSearchAsync_with_top()
+ {
+ Assert.SkipUnless(ChromaTestStore.IsChromaCloud, SkipReason);
+ return base.HybridSearchAsync_with_top();
+ }
+
+ public override Task HybridSearchAsync_with_Skip()
+ {
+ Assert.SkipUnless(ChromaTestStore.IsChromaCloud, SkipReason);
+ return base.HybridSearchAsync_with_Skip();
+ }
+
+ public override Task HybridSearchAsync_with_multiple_keywords_ranks_matched_keywords_higher()
+ {
+ Assert.SkipUnless(ChromaTestStore.IsChromaCloud, SkipReason);
+ return base.HybridSearchAsync_with_multiple_keywords_ranks_matched_keywords_higher();
+ }
+
+ public override Task HybridSearchAsync_with_multiple_text_properties()
+ {
+ Assert.SkipUnless(ChromaTestStore.IsChromaCloud, SkipReason);
+ return base.HybridSearchAsync_with_multiple_text_properties();
+ }
+
+ public override Task HybridSearchAsync_without_explicitly_specified_property_fails()
+ {
+ Assert.SkipUnless(ChromaTestStore.IsChromaCloud, SkipReason);
+ return base.HybridSearchAsync_without_explicitly_specified_property_fails();
+ }
+
+ public new class VectorAndStringFixture : HybridSearchTests.VectorAndStringFixture
+ {
+ public override TestStore TestStore => ChromaTestStore.Instance;
+ }
+
+ public new class MultiTextFixture : HybridSearchTests.MultiTextFixture
+ {
+ public override TestStore TestStore => ChromaTestStore.Instance;
+ }
+}
diff --git a/MEVD/test/Chroma.ConformanceTests/ChromaIndexKindTests.cs b/MEVD/test/Chroma.ConformanceTests/ChromaIndexKindTests.cs
new file mode 100644
index 0000000..b9a7658
--- /dev/null
+++ b/MEVD/test/Chroma.ConformanceTests/ChromaIndexKindTests.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Chroma.ConformanceTests.Support;
+using Microsoft.Extensions.VectorData;
+using VectorData.ConformanceTests;
+using VectorData.ConformanceTests.Support;
+using Xunit;
+
+namespace Chroma.ConformanceTests;
+
+public class ChromaIndexKindTests(ChromaIndexKindTests.Fixture fixture)
+ : IndexKindTests(fixture), IClassFixture
+{
+ // Chroma does not support index-less searching
+ public override Task Flat() => Assert.ThrowsAsync(base.Flat);
+
+ [Fact]
+ public virtual Task Hnsw()
+ => this.Test(IndexKind.Hnsw);
+
+ public new class Fixture() : IndexKindTests.Fixture
+ {
+ public override TestStore TestStore => ChromaTestStore.Instance;
+ }
+}
diff --git a/MEVD/test/Chroma.ConformanceTests/ChromaTestSuiteImplementationTests.cs b/MEVD/test/Chroma.ConformanceTests/ChromaTestSuiteImplementationTests.cs
new file mode 100644
index 0000000..c61619c
--- /dev/null
+++ b/MEVD/test/Chroma.ConformanceTests/ChromaTestSuiteImplementationTests.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 VectorData.ConformanceTests;
+using VectorData.ConformanceTests.ModelTests;
+
+namespace Chroma.ConformanceTests;
+
+public class ChromaTestSuiteImplementationTests : TestSuiteImplementationTests
+{
+ protected override ICollection IgnoredTestBases { get; } =
+ [
+ // A Chroma record has exactly one vector
+ typeof(MultiVectorModelTests<>),
+ typeof(NoVectorModelTests<>),
+ ];
+}
diff --git a/MEVD/test/Chroma.ConformanceTests/ModelTests/ChromaBasicModelTests.cs b/MEVD/test/Chroma.ConformanceTests/ModelTests/ChromaBasicModelTests.cs
new file mode 100644
index 0000000..1c193b5
--- /dev/null
+++ b/MEVD/test/Chroma.ConformanceTests/ModelTests/ChromaBasicModelTests.cs
@@ -0,0 +1,36 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Chroma.ConformanceTests.Support;
+using VectorData.ConformanceTests.ModelTests;
+using VectorData.ConformanceTests.Support;
+using Xunit;
+
+namespace Chroma.ConformanceTests.ModelTests;
+
+public class ChromaBasicModelTests(ChromaBasicModelTests.Fixture fixture)
+ : BasicModelTests(fixture), IClassFixture
+{
+ public override async Task GetAsync_with_filter_and_OrderBy()
+ {
+ var exception = await Assert.ThrowsAsync(base.GetAsync_with_filter_and_OrderBy);
+ Assert.Equal("Chroma does not support ordering.", exception.Message);
+ }
+
+ public override async Task GetAsync_with_filter_and_OrderBy_and_Skip()
+ {
+ var exception = await Assert.ThrowsAsync(base.GetAsync_with_filter_and_OrderBy_and_Skip);
+ Assert.Equal("Chroma does not support ordering.", exception.Message);
+ }
+
+ public override async Task GetAsync_with_filter_and_multiple_OrderBys()
+ {
+ var exception = await Assert.ThrowsAsync(base.GetAsync_with_filter_and_multiple_OrderBys);
+ Assert.Equal("Chroma does not support ordering.", exception.Message);
+ }
+
+ public new class Fixture : BasicModelTests.Fixture
+ {
+ public override TestStore TestStore => ChromaTestStore.Instance;
+ }
+}
diff --git a/MEVD/test/Chroma.ConformanceTests/ModelTests/ChromaDynamicModelTests.cs b/MEVD/test/Chroma.ConformanceTests/ModelTests/ChromaDynamicModelTests.cs
new file mode 100644
index 0000000..290d0f2
--- /dev/null
+++ b/MEVD/test/Chroma.ConformanceTests/ModelTests/ChromaDynamicModelTests.cs
@@ -0,0 +1,36 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Chroma.ConformanceTests.Support;
+using VectorData.ConformanceTests.ModelTests;
+using VectorData.ConformanceTests.Support;
+using Xunit;
+
+namespace Chroma.ConformanceTests.ModelTests;
+
+public class ChromaDynamicModelTests(ChromaDynamicModelTests.Fixture fixture)
+ : DynamicModelTests(fixture), IClassFixture
+{
+ public override async Task GetAsync_with_filter_and_OrderBy()
+ {
+ var exception = await Assert.ThrowsAsync(base.GetAsync_with_filter_and_OrderBy);
+ Assert.Equal("Chroma does not support ordering.", exception.Message);
+ }
+
+ public override async Task GetAsync_with_filter_and_OrderBy_and_Skip()
+ {
+ var exception = await Assert.ThrowsAsync(base.GetAsync_with_filter_and_OrderBy_and_Skip);
+ Assert.Equal("Chroma does not support ordering.", exception.Message);
+ }
+
+ public override async Task GetAsync_with_filter_and_multiple_OrderBys()
+ {
+ var exception = await Assert.ThrowsAsync(base.GetAsync_with_filter_and_multiple_OrderBys);
+ Assert.Equal("Chroma does not support ordering.", exception.Message);
+ }
+
+ public new class Fixture : DynamicModelTests.Fixture
+ {
+ public override TestStore TestStore => ChromaTestStore.Instance;
+ }
+}
diff --git a/MEVD/test/Chroma.ConformanceTests/ModelTests/ChromaNoDataModelTests.cs b/MEVD/test/Chroma.ConformanceTests/ModelTests/ChromaNoDataModelTests.cs
new file mode 100644
index 0000000..70c8239
--- /dev/null
+++ b/MEVD/test/Chroma.ConformanceTests/ModelTests/ChromaNoDataModelTests.cs
@@ -0,0 +1,18 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Chroma.ConformanceTests.Support;
+using VectorData.ConformanceTests.ModelTests;
+using VectorData.ConformanceTests.Support;
+using Xunit;
+
+namespace Chroma.ConformanceTests.ModelTests;
+
+public class ChromaNoDataModelTests(ChromaNoDataModelTests.Fixture fixture)
+ : NoDataModelTests(fixture), IClassFixture
+{
+ public new class Fixture : NoDataModelTests.Fixture
+ {
+ public override TestStore TestStore => ChromaTestStore.Instance;
+ }
+}
diff --git a/MEVD/test/Chroma.ConformanceTests/README.md b/MEVD/test/Chroma.ConformanceTests/README.md
new file mode 100644
index 0000000..2c5a6e2
--- /dev/null
+++ b/MEVD/test/Chroma.ConformanceTests/README.md
@@ -0,0 +1,54 @@
+# Chroma Vector Store Conformance Tests
+
+This project contains conformance tests for the Chroma Vector Store implementation.
+
+## Running the Tests
+
+By default, the tests will automatically use a testcontainer to spin up a Chroma instance. Docker must be running on your machine for this to work.
+
+The hybrid search tests need Chroma Cloud, the only Chroma with the sparse vector indexes that hybrid search uses: they run only when `Chroma:ConnectionString` has a Chroma Cloud address, like `https://api.trychroma.com`, and are skipped otherwise.
+
+### Using an External Chroma Instance
+
+If you want to run the tests against an external Chroma instance (e.g., Chroma Cloud, a local Chroma server, or any other instance), you can provide a connection string through one of the following methods:
+
+#### Option 1: Environment Variable
+
+Set the `Chroma__ConnectionString` environment variable:
+
+```bash
+# Bash/Linux/macOS
+export Chroma__ConnectionString="Endpoint=https://api.trychroma.com;Token=ck-...;Tenant=my-tenant;Database=my-test-database"
+
+# PowerShell
+$env:Chroma__ConnectionString = "Endpoint=https://api.trychroma.com;Token=ck-...;Tenant=my-tenant;Database=my-test-database"
+```
+
+#### Option 2: Configuration File
+
+Create a `testsettings.development.json` file in this directory with the following content:
+
+```json
+{
+ "Chroma": {
+ "ConnectionString": "Endpoint=https://api.trychroma.com;Token=ck-...;Tenant=my-tenant;Database=my-test-database"
+ }
+}
+```
+
+This file is git-ignored and safe for local development.
+
+#### Option 3: User Secrets
+
+```bash
+cd MEVD/test/Chroma.ConformanceTests
+dotnet user-secrets set "Chroma:ConnectionString" "Endpoint=https://api.trychroma.com;Token=ck-...;Tenant=my-tenant;Database=my-test-database"
+```
+
+## Benefits of Using an External Instance
+
+Using an external Chroma instance can be beneficial when:
+- You want to avoid the overhead of spinning up Docker containers
+- You need to test against Chroma Cloud specifically
+- You want faster test execution (no container startup time)
+- You're running tests in an environment where Docker is not available
diff --git a/MEVD/test/Chroma.ConformanceTests/Support/ChromaFixture.cs b/MEVD/test/Chroma.ConformanceTests/Support/ChromaFixture.cs
new file mode 100644
index 0000000..ec497ee
--- /dev/null
+++ b/MEVD/test/Chroma.ConformanceTests/Support/ChromaFixture.cs
@@ -0,0 +1,11 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using VectorData.ConformanceTests.Support;
+
+namespace Chroma.ConformanceTests.Support;
+
+public class ChromaFixture : VectorStoreFixture
+{
+ public override TestStore TestStore => ChromaTestStore.Instance;
+}
diff --git a/MEVD/test/Chroma.ConformanceTests/Support/ChromaTestEnvironment.cs b/MEVD/test/Chroma.ConformanceTests/Support/ChromaTestEnvironment.cs
new file mode 100644
index 0000000..d546825
--- /dev/null
+++ b/MEVD/test/Chroma.ConformanceTests/Support/ChromaTestEnvironment.cs
@@ -0,0 +1,28 @@
+// 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.Configuration;
+
+namespace Chroma.ConformanceTests.Support;
+
+#pragma warning disable CA1810 // Initialize all static fields when those fields are declared
+
+internal static class ChromaTestEnvironment
+{
+ public static readonly string? ConnectionString;
+
+ public static bool IsConnectionStringDefined => !string.IsNullOrEmpty(ConnectionString);
+
+ static ChromaTestEnvironment()
+ {
+ var configuration = new ConfigurationBuilder()
+ .AddJsonFile(path: "testsettings.json", optional: true)
+ .AddJsonFile(path: "testsettings.development.json", optional: true)
+ .AddEnvironmentVariables()
+ .AddUserSecrets()
+ .Build();
+
+ var chromaSection = configuration.GetSection("Chroma");
+ ConnectionString = chromaSection["ConnectionString"];
+ }
+}
diff --git a/MEVD/test/Chroma.ConformanceTests/Support/ChromaTestStore.cs b/MEVD/test/Chroma.ConformanceTests/Support/ChromaTestStore.cs
new file mode 100644
index 0000000..cbbb30c
--- /dev/null
+++ b/MEVD/test/Chroma.ConformanceTests/Support/ChromaTestStore.cs
@@ -0,0 +1,86 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using ChromaDB.Client;
+using CommunityToolkit.VectorData.Chroma;
+using DotNet.Testcontainers.Builders;
+using DotNet.Testcontainers.Containers;
+using Microsoft.Extensions.VectorData;
+using VectorData.ConformanceTests.Support;
+
+namespace Chroma.ConformanceTests.Support;
+
+#pragma warning disable CA1001 // Type owns disposable fields but is not disposable
+
+internal sealed class ChromaTestStore : TestStore
+{
+ private const ushort ChromaPort = 8000;
+
+ public static ChromaTestStore Instance { get; } = new();
+
+ // Chroma indexes vectors with HNSW only
+ public override string DefaultIndexKind => IndexKind.Hnsw;
+
+ // Only when no external instance is configured, see ChromaTestEnvironment.
+ private readonly IContainer? _container = ChromaTestEnvironment.IsConnectionStringDefined
+ ? null
+ : new ContainerBuilder("chromadb/chroma:1.5.9")
+ .WithPortBinding(ChromaPort, assignRandomHostPort: true)
+ .WithWaitStrategy(Wait.ForUnixContainer()
+ .UntilHttpRequestIsSucceeded(request => request.ForPath("/api/v2/heartbeat").ForPort(ChromaPort)))
+ .Build();
+
+ private ChromaClient? _client;
+
+ ///
+ /// Chroma normalizes the vectors of cosine collections, so the vectors it returns
+ /// can differ from the upserted ones in the last digits; we can only check that
+ /// a vector was returned.
+ ///
+ public override bool VectorsComparable => false;
+
+ public ChromaClient Client => this._client ?? throw new InvalidOperationException("Not initialized");
+
+ ///
+ /// Whether the tests run against Chroma Cloud, the only Chroma with the Search API and the sparse vector indexes that hybrid search needs.
+ ///
+ public static bool IsChromaCloud => ChromaTestEnvironment.IsConnectionStringDefined
+ && ChromaConfigurationOptions.FromConnectionString(ChromaTestEnvironment.ConnectionString!).Uri.Host.EndsWith(".trychroma.com", StringComparison.OrdinalIgnoreCase);
+
+ public ChromaVectorStore GetVectorStore(ChromaVectorStoreOptions options)
+ => new(this.Client, ownsClient: false, options); // The client is shared, it's not owned by the vector store.
+
+ private ChromaTestStore()
+ {
+ }
+
+ protected override async Task StartAsync()
+ {
+ ChromaConfigurationOptions options;
+ if (this._container is not null)
+ {
+ await this._container.StartAsync();
+ options = new ChromaConfigurationOptions(
+ new UriBuilder(Uri.UriSchemeHttp, this._container.Hostname, this._container.GetMappedPublicPort(ChromaPort)).ToString());
+ }
+ else
+ {
+ options = ChromaConfigurationOptions.FromConnectionString(ChromaTestEnvironment.ConnectionString!);
+ }
+
+ this._client = new ChromaClient(options);
+
+ // It's a shared static instance, we don't want any of the tests to dispose the client.
+ this.DefaultVectorStore = new ChromaVectorStore(this._client, ownsClient: false);
+ }
+
+ protected override async Task StopAsync()
+ {
+ this._client?.Dispose();
+
+ if (this._container is not null)
+ {
+ await this._container.StopAsync();
+ }
+ }
+}
diff --git a/MEVD/test/Chroma.ConformanceTests/TypeTests/ChromaDataTypeTests.cs b/MEVD/test/Chroma.ConformanceTests/TypeTests/ChromaDataTypeTests.cs
new file mode 100644
index 0000000..59946f4
--- /dev/null
+++ b/MEVD/test/Chroma.ConformanceTests/TypeTests/ChromaDataTypeTests.cs
@@ -0,0 +1,41 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Chroma.ConformanceTests.Support;
+using VectorData.ConformanceTests.Support;
+using VectorData.ConformanceTests.TypeTests;
+using Xunit;
+
+namespace Chroma.ConformanceTests.TypeTests;
+
+public class ChromaDataTypeTests(ChromaDataTypeTests.Fixture fixture)
+ : DataTypeTests.DefaultRecord>(fixture), IClassFixture
+{
+ // Chroma does not compare an array field with an array
+ [Fact]
+ public override Task String_array()
+ => this.Test(
+ "StringArray",
+ ["foo", "bar"],
+ ["foo", "baz"],
+ isFilterable: false);
+
+ public new class Fixture : DataTypeTests.DefaultRecord>.Fixture
+ {
+ public override TestStore TestStore => ChromaTestStore.Instance;
+
+ // Chroma metadata has no null values, and Chroma has no filter for a missing field
+ public override bool IsNullFilteringSupported => false;
+
+ public override Type[] UnsupportedDefaultTypes { get; } =
+ [
+ typeof(byte),
+ typeof(short),
+ typeof(decimal),
+ typeof(Guid),
+#if NET
+ typeof(TimeOnly)
+#endif
+ ];
+ }
+}
diff --git a/MEVD/test/Chroma.ConformanceTests/TypeTests/ChromaEmbeddingTypeTests.cs b/MEVD/test/Chroma.ConformanceTests/TypeTests/ChromaEmbeddingTypeTests.cs
new file mode 100644
index 0000000..645beda
--- /dev/null
+++ b/MEVD/test/Chroma.ConformanceTests/TypeTests/ChromaEmbeddingTypeTests.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Chroma.ConformanceTests.Support;
+using VectorData.ConformanceTests.Support;
+using VectorData.ConformanceTests.TypeTests;
+using Xunit;
+
+#pragma warning disable CA2000 // Dispose objects before losing scope
+
+namespace Chroma.ConformanceTests.TypeTests;
+
+public class ChromaEmbeddingTypeTests(ChromaEmbeddingTypeTests.Fixture fixture)
+ : EmbeddingTypeTests(fixture), IClassFixture
+{
+ public new class Fixture : EmbeddingTypeTests.Fixture
+ {
+ public override TestStore TestStore => ChromaTestStore.Instance;
+ }
+}
diff --git a/MEVD/test/Chroma.ConformanceTests/TypeTests/ChromaKeyTypeTests.cs b/MEVD/test/Chroma.ConformanceTests/TypeTests/ChromaKeyTypeTests.cs
new file mode 100644
index 0000000..f236497
--- /dev/null
+++ b/MEVD/test/Chroma.ConformanceTests/TypeTests/ChromaKeyTypeTests.cs
@@ -0,0 +1,21 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Chroma.ConformanceTests.Support;
+using VectorData.ConformanceTests.Support;
+using VectorData.ConformanceTests.TypeTests;
+using Xunit;
+
+namespace Chroma.ConformanceTests.TypeTests;
+
+public class ChromaKeyTypeTests(ChromaKeyTypeTests.Fixture fixture)
+ : KeyTypeTests(fixture), IClassFixture
+{
+ [Fact]
+ public virtual Task String() => this.Test("foo", "bar");
+
+ public new class Fixture : KeyTypeTests.Fixture
+ {
+ public override TestStore TestStore => ChromaTestStore.Instance;
+ }
+}
diff --git a/MEVD/test/Chroma.ConformanceTests/testsettings.json b/MEVD/test/Chroma.ConformanceTests/testsettings.json
new file mode 100644
index 0000000..f3a1e9d
--- /dev/null
+++ b/MEVD/test/Chroma.ConformanceTests/testsettings.json
@@ -0,0 +1,8 @@
+{
+ // Optional: Provide a connection string to use an external Chroma instance instead of testcontainers
+ // This is useful for running tests against Chroma Cloud, which hybrid search needs
+ // If not specified, tests will automatically use a testcontainer
+ "Chroma": {
+ "ConnectionString": ""
+ }
+}
diff --git a/MEVD/test/Chroma.UnitTests/.editorconfig b/MEVD/test/Chroma.UnitTests/.editorconfig
new file mode 100644
index 0000000..394eef6
--- /dev/null
+++ b/MEVD/test/Chroma.UnitTests/.editorconfig
@@ -0,0 +1,6 @@
+# Suppressing errors for Test projects under dotnet folder
+[*.cs]
+dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task
+dotnet_diagnostic.VSTHRD111.severity = none # Use .ConfigureAwait(bool) is hidden by default, set to none to prevent IDE from changing on autosave
+dotnet_diagnostic.CS1591.severity = none # Missing XML comment for publicly visible type or member
+dotnet_diagnostic.IDE1006.severity = warning # Naming rule violations
diff --git a/MEVD/test/Chroma.UnitTests/Chroma.UnitTests.csproj b/MEVD/test/Chroma.UnitTests/Chroma.UnitTests.csproj
new file mode 100644
index 0000000..cf1a76f
--- /dev/null
+++ b/MEVD/test/Chroma.UnitTests/Chroma.UnitTests.csproj
@@ -0,0 +1,29 @@
+
+
+
+ CommunityToolkit.VectorData.Chroma.UnitTests
+ CommunityToolkit.VectorData.Chroma.UnitTests
+ net10.0
+ true
+ enable
+ disable
+ false
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MEVD/test/Chroma.UnitTests/ChromaCollectionCreateMappingTests.cs b/MEVD/test/Chroma.UnitTests/ChromaCollectionCreateMappingTests.cs
new file mode 100644
index 0000000..c119772
--- /dev/null
+++ b/MEVD/test/Chroma.UnitTests/ChromaCollectionCreateMappingTests.cs
@@ -0,0 +1,92 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Linq;
+using System.Text.Json.Nodes;
+using ChromaDB.Client;
+using CommunityToolkit.VectorData.Chroma;
+using Microsoft.Extensions.VectorData;
+using Microsoft.Extensions.VectorData.ProviderServices;
+using Xunit;
+
+namespace Chroma.UnitTests;
+
+///
+/// Contains tests for the class.
+///
+public class ChromaCollectionCreateMappingTests
+{
+ [Theory]
+ [InlineData(null, ChromaSpace.Cosine)]
+ [InlineData(DistanceFunction.CosineSimilarity, ChromaSpace.Cosine)]
+ [InlineData(DistanceFunction.CosineDistance, ChromaSpace.Cosine)]
+ [InlineData(DistanceFunction.DotProductSimilarity, ChromaSpace.InnerProduct)]
+ [InlineData(DistanceFunction.NegativeDotProductSimilarity, ChromaSpace.InnerProduct)]
+ [InlineData(DistanceFunction.EuclideanDistance, ChromaSpace.L2)]
+ [InlineData(DistanceFunction.EuclideanSquaredDistance, ChromaSpace.L2)]
+ public void MapCollectionDefinitionSetsTheSpace(string? distanceFunction, ChromaSpace expectedSpace)
+ {
+ // Arrange.
+ var vectorProperty = new VectorPropertyModel("Vector", typeof(ReadOnlyMemory)) { DistanceFunction = distanceFunction };
+
+ // Act.
+ var definition = ChromaCollectionCreateMapping.MapCollectionDefinition("hotels", vectorProperty, []);
+
+ // Assert.
+ Assert.Equal("hotels", definition.Name);
+ Assert.Equal(expectedSpace, definition.Configuration?.Space);
+ Assert.Null(definition.Schema);
+ }
+
+ [Theory]
+ [InlineData(DistanceFunction.ManhattanDistance)]
+ [InlineData(DistanceFunction.HammingDistance)]
+ public void MapCollectionDefinitionThrowsForUnsupportedDistanceFunction(string distanceFunction)
+ {
+ // Arrange.
+ var vectorProperty = new VectorPropertyModel("Vector", typeof(ReadOnlyMemory)) { DistanceFunction = distanceFunction };
+
+ // Act and assert.
+ Assert.Throws(() => ChromaCollectionCreateMapping.MapCollectionDefinition("hotels", vectorProperty, []));
+ }
+
+ [Fact]
+ public void MapCollectionDefinitionAddsASchemaForTheBm25Properties()
+ {
+ // Arrange.
+ var model = ChromaTestModel.Build();
+
+ // Act.
+ var definition = ChromaCollectionCreateMapping.MapCollectionDefinition("hotels", model.VectorProperty, ChromaCollectionCreateMapping.GetBm25Properties(model));
+
+ // Assert.
+ var expected =
+ """{"defaults":{},"keys":{"Description_bm25":"""
+ + """{"sparse_vector":{"sparse_vector_index":{"enabled":true,"config":{"source_key":"Description","bm25":true,"embedding_function":{"type":"known","name":"chroma_bm25","config":{"k":1.2,"b":0.75,"avg_doc_length":256,"token_max_length":40,"include_tokens":false}}}}}},"Review_bm25":"""
+ + """{"sparse_vector":{"sparse_vector_index":{"enabled":true,"config":{"source_key":"Review","bm25":true,"embedding_function":{"type":"known","name":"chroma_bm25","config":{"k":1.2,"b":0.75,"avg_doc_length":256,"token_max_length":40,"include_tokens":false}}}}}}}}""";
+ var actual = definition.Schema!.ToString();
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)), actual);
+ }
+
+ [Fact]
+ public void MapCollectionDefinitionTakesTheBm25VectorsOfTheDocumentPropertyFromTheDocument()
+ {
+ // Arrange: the only full-text property is stored as the document.
+ var model = ChromaTestModel.Build();
+
+ // Act.
+ var definition = ChromaCollectionCreateMapping.MapCollectionDefinition("hotels", model.VectorProperty, ChromaCollectionCreateMapping.GetBm25Properties(model), ChromaFieldMapping.GetDocumentProperty(model));
+
+ // Assert.
+ Assert.Contains("\"document_bm25\":{\"sparse_vector\":{\"sparse_vector_index\":{\"enabled\":true,\"config\":{", definition.Schema!.ToString());
+ Assert.Contains("\"source_key\":\"#document\"", definition.Schema.ToString());
+ }
+
+ [Fact]
+ public void GetBm25PropertiesTakesTheStringPropertiesWithFullTextIndexing()
+ {
+ Assert.Equal(["Description", "Review"], ChromaCollectionCreateMapping.GetBm25Properties(ChromaTestModel.Build()).Select(p => p.ModelName));
+ Assert.Empty(ChromaCollectionCreateMapping.GetBm25Properties(ChromaTestModel.Build>()));
+ }
+}
diff --git a/MEVD/test/Chroma.UnitTests/ChromaCollectionSearchMappingTests.cs b/MEVD/test/Chroma.UnitTests/ChromaCollectionSearchMappingTests.cs
new file mode 100644
index 0000000..ad6e68e
--- /dev/null
+++ b/MEVD/test/Chroma.UnitTests/ChromaCollectionSearchMappingTests.cs
@@ -0,0 +1,52 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Diagnostics;
+using CommunityToolkit.VectorData.Chroma;
+using Microsoft.Extensions.VectorData;
+using Xunit;
+
+namespace Chroma.UnitTests;
+
+///
+/// Contains tests for the class.
+///
+public class ChromaCollectionSearchMappingTests
+{
+ [Theory]
+ [InlineData(null, 0.25f, 0.75)]
+ [InlineData(DistanceFunction.CosineSimilarity, 0.25f, 0.75)]
+ [InlineData(DistanceFunction.CosineDistance, 0.25f, 0.25)]
+ [InlineData(DistanceFunction.DotProductSimilarity, 0.25f, 0.75)]
+ [InlineData(DistanceFunction.NegativeDotProductSimilarity, 0.25f, -0.75)]
+ [InlineData(DistanceFunction.EuclideanSquaredDistance, 4f, 4)]
+ [InlineData(DistanceFunction.EuclideanDistance, 4f, 2)]
+ public void ToScoreConvertsTheChromaDistance(string? distanceFunction, float distance, double expectedScore)
+ => Assert.Equal(expectedScore, ChromaCollectionSearchMapping.ToScore(distance, distanceFunction), precision: 6);
+
+ [Theory]
+ [InlineData(DistanceFunction.CosineSimilarity, 0.8, 0.7, true)]
+ [InlineData(DistanceFunction.CosineSimilarity, 0.6, 0.7, false)]
+ [InlineData(DistanceFunction.CosineDistance, 0.2, 0.3, true)]
+ [InlineData(DistanceFunction.CosineDistance, 0.4, 0.3, false)]
+ [InlineData(DistanceFunction.EuclideanDistance, 1.5, 2, true)]
+ [InlineData(DistanceFunction.EuclideanDistance, 2.5, 2, false)]
+ [InlineData(null, 0.8, 0.7, true)]
+ [InlineData(DistanceFunction.DotProductSimilarity, 0.8, 0.7, true)]
+ [InlineData(DistanceFunction.DotProductSimilarity, 0.6, 0.7, false)]
+ [InlineData(DistanceFunction.NegativeDotProductSimilarity, -0.8, -0.7, true)]
+ [InlineData(DistanceFunction.NegativeDotProductSimilarity, -0.6, -0.7, false)]
+ [InlineData(DistanceFunction.EuclideanSquaredDistance, 2, 4, true)]
+ [InlineData(DistanceFunction.EuclideanSquaredDistance, 5, 4, false)]
+ public void PassesThresholdKeepsTheSimilarAndTheNear(string? distanceFunction, double score, double threshold, bool expected)
+ => Assert.Equal(expected, ChromaCollectionSearchMapping.PassesThreshold(score, threshold, distanceFunction));
+
+ [Fact]
+ public void ToScoreThrowsForADistanceFunctionTheModelBuilderRejects()
+ => Assert.Throws(() => ChromaCollectionSearchMapping.ToScore(0.25f, DistanceFunction.ManhattanDistance));
+
+ [Fact]
+ public void PassesThresholdWithoutThresholdKeepsEverything()
+ => Assert.True(ChromaCollectionSearchMapping.PassesThreshold(-100, null, DistanceFunction.CosineSimilarity));
+}
diff --git a/MEVD/test/Chroma.UnitTests/ChromaCollectionTests.cs b/MEVD/test/Chroma.UnitTests/ChromaCollectionTests.cs
new file mode 100644
index 0000000..5eb6224
--- /dev/null
+++ b/MEVD/test/Chroma.UnitTests/ChromaCollectionTests.cs
@@ -0,0 +1,518 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using ChromaDB.Client;
+using ChromaDB.Client.Models;
+using CommunityToolkit.VectorData.Chroma;
+using InMemory.UnitTests;
+using Microsoft.Extensions.VectorData;
+using Moq;
+using Moq.Protected;
+using Xunit;
+
+namespace Chroma.UnitTests;
+
+///
+/// Contains tests for the class.
+///
+public class ChromaCollectionTests
+{
+ private const string TestCollectionName = "testcollection";
+
+ private readonly Mock _chromaClientMock = new(MockBehavior.Strict);
+ private readonly Mock _collectionClientMock = new(MockBehavior.Strict);
+
+ // A token that is not the default one, so that the strict mocks also check that it reaches the client.
+ private readonly CancellationToken _testCancellationToken = TestContext.Current.CancellationToken;
+
+ public ChromaCollectionTests()
+ {
+ this._chromaClientMock
+ .Setup(x => x.Options)
+ .Returns(new ChromaConfigurationOptions("http://localhost:8000"));
+ this._chromaClientMock
+ .Setup(x => x.GetCollectionClient(TestCollectionName))
+ .Returns(this._collectionClientMock.Object);
+ this._collectionClientMock
+ .Setup(x => x.WithDocumentCopyKey(It.IsAny()))
+ .Returns(this._collectionClientMock.Object);
+ this._collectionClientMock
+ .Setup(x => x.WithMetadataValues(ChromaMetadataValues.Exact))
+ .Returns(this._collectionClientMock.Object);
+ }
+
+ #region Construction and services
+
+ [Fact]
+ public void RejectsADictionaryRecord()
+ => Assert.Throws(() => new ChromaCollection>(this._chromaClientMock.Object, TestCollectionName, ownsClient: false));
+
+ [Fact]
+ public void DynamicCollectionsTakeAClientAndADefinition()
+ {
+ // Arrange.
+ var definition = new VectorStoreCollectionDefinition
+ {
+ Properties = [new VectorStoreKeyProperty("Key", typeof(string)), new VectorStoreVectorProperty("Vector", typeof(ReadOnlyMemory), 4)]
+ };
+
+ // Act.
+ using var sut = new ChromaDynamicCollection(this._chromaClientMock.Object, TestCollectionName, ownsClient: false, new() { Definition = definition });
+
+ // Assert.
+ Assert.Equal(TestCollectionName, sut.Name);
+ Assert.Same(this._chromaClientMock.Object, sut.GetService(typeof(ChromaClient)));
+ }
+
+ [Fact]
+ public void ReadsTheRecordsWithExactMetadataValues()
+ {
+ // The mapper reads strings as strings, and lists as lists of values, also with a client that infers them.
+ this._chromaClientMock
+ .Setup(x => x.Options)
+ .Returns(new ChromaConfigurationOptions("http://localhost:8000").WithMetadataValues(ChromaMetadataValues.Inferred));
+
+ using var sut = new ChromaCollection>(this._chromaClientMock.Object, TestCollectionName, ownsClient: false);
+
+ this._collectionClientMock.Verify(x => x.WithMetadataValues(ChromaMetadataValues.Exact), Times.Once);
+ }
+
+ [Fact]
+ public void DynamicCollectionsRequireADefinition()
+ => Assert.Throws(() => new ChromaDynamicCollection(this._chromaClientMock.Object, TestCollectionName, ownsClient: false, new()));
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void DisposeDisposesTheClientOnlyWhenTheCollectionOwnsIt(bool ownsClient)
+ {
+ // Arrange.
+ this._chromaClientMock.Protected().Setup("Dispose", ItExpr.IsAny());
+ var sut = new ChromaCollection>(this._chromaClientMock.Object, TestCollectionName, ownsClient);
+
+ // Act.
+ sut.Dispose();
+
+ // Assert.
+ this._chromaClientMock.Protected().Verify("Dispose", ownsClient ? Times.Once() : Times.Never(), ItExpr.IsAny());
+ }
+
+ [Fact]
+ public void GetServiceReturnsTheMetadataTheClientAndTheCollection()
+ {
+ using var sut = this.CreateCollection>();
+
+ Assert.Equal(TestCollectionName, Assert.IsType(sut.GetService(typeof(VectorStoreCollectionMetadata))).CollectionName);
+ Assert.Same(this._chromaClientMock.Object, sut.GetService(typeof(ChromaClient)));
+ Assert.Same(sut, sut.GetService(typeof(VectorStoreCollection>)));
+ Assert.Null(sut.GetService(typeof(VectorStoreCollection>), "key"));
+ Assert.Null(sut.GetService(typeof(string)));
+ }
+
+ [Theory]
+ [InlineData("https://api.trychroma.com", true)]
+ [InlineData("http://localhost:8000", false)]
+ public void GetServiceOffersHybridSearchOnlyWithFullTextPropertiesOnChromaCloud(string uri, bool offered)
+ {
+ // Arrange.
+ this._chromaClientMock
+ .Setup(x => x.Options)
+ .Returns(new ChromaConfigurationOptions(uri));
+ using var fullText = this.CreateCollection();
+ using var noFullText = this.CreateCollection>();
+
+ // Act and assert.
+ Assert.Equal(offered, fullText.GetService(typeof(IKeywordHybridSearchable)) is not null);
+ Assert.Null(noFullText.GetService(typeof(IKeywordHybridSearchable>)));
+ }
+
+ #endregion
+
+ #region Get by key
+
+ [Fact]
+ public async Task GetReadsTheVectorsAndTheDocumentWhenAskedAsync()
+ {
+ // Arrange.
+ using var sut = this.CreateCollection();
+ this._collectionClientMock
+ .Setup(x => x.GetAsync(new List { "h1" }, null, null, null, null, ChromaGetInclude.Metadatas | ChromaGetInclude.Embeddings | ChromaGetInclude.Documents, this._testCancellationToken))
+ .ReturnsAsync([new ChromaCollectionEntry("h1") { Embedding = new float[] { 1, 2, 3, 4 }, Document = "A pool" }]);
+
+ // Act.
+ var hotel = await sut.GetAsync("h1", new() { IncludeVectors = true }, this._testCancellationToken);
+
+ // Assert.
+ Assert.Equal(new float[] { 1, 2, 3, 4 }, hotel!.Embedding!.Value.ToArray());
+ Assert.Equal("A pool", hotel.Description);
+ }
+
+ #endregion
+
+ #region Upsert
+
+ [Fact]
+ public async Task UpsertDeletesTheNullValuesAndCopiesTheDocumentAsync()
+ {
+ // Arrange.
+ using var sut = this.CreateCollection();
+ var upserted = this.CaptureUpsert();
+
+ // Act.
+ await sut.UpsertAsync(
+ [
+ new FullTextHotel { HotelId = "h1", Description = null, Rating = 4, Embedding = new float[] { 1, 2, 3, 4 } },
+ new FullTextHotel { HotelId = "h2", Description = "A pool", Embedding = new float[] { 1, 2, 3, 4 } },
+ ], this._testCancellationToken);
+
+ // Assert.
+ var records = upserted();
+ Assert.True(records.NullDocumentsDelete);
+ this._collectionClientMock.Verify(x => x.WithDocumentCopyKey("Description"), Times.Once);
+ Assert.Equal(new[] { null, "A pool" }, records.Documents!.ToArray());
+ Assert.All(records.Metadatas!, metadata => Assert.False(metadata!.ContainsKey("Description")));
+ Assert.Equal(4, records.Metadatas![0]!["Rating"]);
+ Assert.Equal(0, records.Metadatas[1]!["Rating"]);
+ }
+
+ #endregion
+
+ #region Vector search
+
+ [Fact]
+ public async Task SearchSkipsConvertsAndFiltersTheResultsAsync()
+ {
+ // Arrange.
+ using var sut = this.CreateCollection>();
+ var query = this.SetupQuery(
+ new ChromaCollectionQueryEntry("kept") { Distance = 0.2f },
+ new ChromaCollectionQueryEntry("too far") { Distance = 0.6f });
+
+ // Act.
+ var results = await sut.SearchAsync(new ReadOnlyMemory([1, 2, 3, 4]), top: 2, new() { Skip = 1, ScoreThreshold = 0.5 }, this._testCancellationToken).ToListAsync();
+
+ // Assert.
+ Assert.Equal((2, 1), (query().NResults, query().Offset));
+ Assert.Same(ChromaWhereOperator.All, query().Where);
+ Assert.Equal(ChromaQueryInclude.Metadatas | ChromaQueryInclude.Distances, query().Include);
+ var result = Assert.Single(results);
+ Assert.Equal("kept", result.Record.HotelId);
+ Assert.Equal(0.8, result.Score!.Value, precision: 6);
+ }
+
+ [Fact]
+ public async Task SearchSendsTheKeysOfTheFilterInTheWhereClauseAsync()
+ {
+ // Arrange.
+ using var sut = this.CreateCollection>();
+ var query = this.SetupQuery(new ChromaCollectionQueryEntry("h1") { Distance = 0.1f });
+
+ // Act.
+ var results = await sut.SearchAsync(new ReadOnlyMemory([1, 2, 3, 4]), top: 2, new() { Filter = h => new[] { "h1", "h2" }.Contains(h.HotelId) }, this._testCancellationToken).ToListAsync();
+
+ // Assert.
+ Assert.Equal("""{"#id":{"$in":["h1","h2"]}}""", query().Where!.ToString());
+ Assert.Equal("h1", Assert.Single(results).Record.HotelId);
+ }
+
+ [Fact]
+ public async Task SearchReadsTheVectorsAndTheDocumentWhenAskedAsync()
+ {
+ // Arrange.
+ using var sut = this.CreateCollection();
+ var query = this.SetupQuery(new ChromaCollectionQueryEntry("h1") { Distance = 0.1f, Embedding = new float[] { 1, 2, 3, 4 }, Document = "A pool" });
+
+ // Act.
+ var result = Assert.Single(await sut.SearchAsync(new float[] { 1, 2, 3, 4 }, top: 1, new() { IncludeVectors = true }, this._testCancellationToken).ToListAsync());
+
+ // Assert.
+ Assert.Equal(ChromaQueryInclude.Metadatas | ChromaQueryInclude.Distances | ChromaQueryInclude.Documents | ChromaQueryInclude.Embeddings, query().Include);
+ Assert.Equal("A pool", result.Record.Description);
+ Assert.Equal(new float[] { 1, 2, 3, 4 }, result.Record.Embedding!.Value.ToArray());
+ }
+
+ [Fact]
+ public async Task SearchExpectsTheSpaceOfTheDistanceFunctionAsync()
+ {
+ // Arrange.
+ using var dotProduct = this.CreateCollection();
+ using var byDefault = this.CreateCollection>();
+ var query = this.SetupQuery();
+
+ // Act and assert.
+ await dotProduct.SearchAsync(new ReadOnlyMemory([1, 2, 3, 4]), top: 1, cancellationToken: this._testCancellationToken).ToListAsync();
+ Assert.Equal(ChromaSpace.InnerProduct, query().ExpectedSpace);
+ await byDefault.SearchAsync(new ReadOnlyMemory