From c00840bf479452011b5ac27fe94964ea97075fc8 Mon Sep 17 00:00:00 2001
From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Date: Tue, 24 Sep 2024 11:04:15 -0700
Subject: [PATCH] .Net: Implemented generic data model support for Azure
CosmosDB MongoDB connector (#8967)
### Motivation and Context
Related: https://github.com/microsoft/semantic-kernel/issues/6522
- Implemented `AzureCosmosDBMongoDBGenericDataModelMapper` class.
- Added unit and integration tests.
### Contribution Checklist
- [x] The code builds clean without any errors or warnings
- [x] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [x] All unit tests pass, and I have added new tests where possible
- [x] I didn't break anyone :smile:
---
...mosDBMongoDBGenericDataModelMapperTests.cs | 310 ++++++++++++++++++
.../AzureAISearchGenericDataModelMapper.cs | 2 +-
.../AzureCosmosDBMongoDBConstants.cs | 38 +++
...reCosmosDBMongoDBGenericDataModelMapper.cs | 181 ++++++++++
...mosDBMongoDBVectorStoreRecordCollection.cs | 36 +-
...eCosmosDBMongoDBVectorStoreRecordMapper.cs | 40 +--
.../QdrantGenericDataModelMapper.cs | 2 +-
.../RedisHashSetGenericDataModelMapper.cs | 2 +-
.../RedisJsonGenericDataModelMapper.cs | 2 +-
.../AzureCosmosDBMongoDBVectorStoreFixture.cs | 5 +
...MongoDBVectorStoreRecordCollectionTests.cs | 48 +++
.../Data/VectorStoreRecordPropertyReader.cs | 26 +-
12 files changed, 638 insertions(+), 54 deletions(-)
create mode 100644 dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBGenericDataModelMapperTests.cs
create mode 100644 dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBGenericDataModelMapper.cs
diff --git a/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBGenericDataModelMapperTests.cs b/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBGenericDataModelMapperTests.cs
new file mode 100644
index 000000000000..e2b02c35a41f
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBGenericDataModelMapperTests.cs
@@ -0,0 +1,310 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.SemanticKernel.Connectors.AzureCosmosDBMongoDB;
+using Microsoft.SemanticKernel.Data;
+using MongoDB.Bson;
+using Xunit;
+
+namespace SemanticKernel.Connectors.AzureCosmosDBMongoDB.UnitTests;
+
+///
+/// Unit tests for class.
+///
+public sealed class AzureCosmosDBMongoDBGenericDataModelMapperTests
+{
+ private static readonly VectorStoreRecordDefinition s_vectorStoreRecordDefinition = new()
+ {
+ Properties = new List
+ {
+ new VectorStoreRecordKeyProperty("Key", typeof(string)),
+ new VectorStoreRecordDataProperty("BoolDataProp", typeof(bool)),
+ new VectorStoreRecordDataProperty("NullableBoolDataProp", typeof(bool?)),
+ new VectorStoreRecordDataProperty("StringDataProp", typeof(string)),
+ new VectorStoreRecordDataProperty("IntDataProp", typeof(int)),
+ new VectorStoreRecordDataProperty("NullableIntDataProp", typeof(int?)),
+ new VectorStoreRecordDataProperty("LongDataProp", typeof(long)),
+ new VectorStoreRecordDataProperty("NullableLongDataProp", typeof(long?)),
+ new VectorStoreRecordDataProperty("FloatDataProp", typeof(float)),
+ new VectorStoreRecordDataProperty("NullableFloatDataProp", typeof(float?)),
+ new VectorStoreRecordDataProperty("DoubleDataProp", typeof(double)),
+ new VectorStoreRecordDataProperty("NullableDoubleDataProp", typeof(double?)),
+ new VectorStoreRecordDataProperty("DecimalDataProp", typeof(decimal)),
+ new VectorStoreRecordDataProperty("NullableDecimalDataProp", typeof(decimal?)),
+ new VectorStoreRecordDataProperty("DateTimeDataProp", typeof(DateTime)),
+ new VectorStoreRecordDataProperty("NullableDateTimeDataProp", typeof(DateTime?)),
+ new VectorStoreRecordDataProperty("TagListDataProp", typeof(List)),
+ new VectorStoreRecordVectorProperty("FloatVector", typeof(ReadOnlyMemory)),
+ new VectorStoreRecordVectorProperty("NullableFloatVector", typeof(ReadOnlyMemory?)),
+ new VectorStoreRecordVectorProperty("DoubleVector", typeof(ReadOnlyMemory)),
+ new VectorStoreRecordVectorProperty("NullableDoubleVector", typeof(ReadOnlyMemory?)),
+ },
+ };
+
+ private static readonly float[] s_floatVector = [1.0f, 2.0f, 3.0f];
+ private static readonly double[] s_doubleVector = [1.0f, 2.0f, 3.0f];
+ private static readonly List s_taglist = ["tag1", "tag2"];
+
+ [Fact]
+ public void MapFromDataToStorageModelMapsAllSupportedTypes()
+ {
+ // Arrange
+ var sut = new AzureCosmosDBMongoDBGenericDataModelMapper(s_vectorStoreRecordDefinition);
+ var dataModel = new VectorStoreGenericDataModel("key")
+ {
+ Data =
+ {
+ ["BoolDataProp"] = true,
+ ["NullableBoolDataProp"] = false,
+ ["StringDataProp"] = "string",
+ ["IntDataProp"] = 1,
+ ["NullableIntDataProp"] = 2,
+ ["LongDataProp"] = 3L,
+ ["NullableLongDataProp"] = 4L,
+ ["FloatDataProp"] = 5.0f,
+ ["NullableFloatDataProp"] = 6.0f,
+ ["DoubleDataProp"] = 7.0,
+ ["NullableDoubleDataProp"] = 8.0,
+ ["DecimalDataProp"] = 9.0m,
+ ["NullableDecimalDataProp"] = 10.0m,
+ ["DateTimeDataProp"] = new DateTime(2021, 1, 1, 0, 0, 0).ToUniversalTime(),
+ ["NullableDateTimeDataProp"] = new DateTime(2021, 1, 1, 0, 0, 0).ToUniversalTime(),
+ ["TagListDataProp"] = s_taglist,
+ },
+ Vectors =
+ {
+ ["FloatVector"] = new ReadOnlyMemory(s_floatVector),
+ ["NullableFloatVector"] = new ReadOnlyMemory(s_floatVector),
+ ["DoubleVector"] = new ReadOnlyMemory(s_doubleVector),
+ ["NullableDoubleVector"] = new ReadOnlyMemory(s_doubleVector),
+ },
+ };
+
+ // Act
+ var storageModel = sut.MapFromDataToStorageModel(dataModel);
+
+ // Assert
+ Assert.Equal("key", storageModel["_id"]);
+ Assert.Equal(true, (bool?)storageModel["BoolDataProp"]);
+ Assert.Equal(false, (bool?)storageModel["NullableBoolDataProp"]);
+ Assert.Equal("string", (string?)storageModel["StringDataProp"]);
+ Assert.Equal(1, (int?)storageModel["IntDataProp"]);
+ Assert.Equal(2, (int?)storageModel["NullableIntDataProp"]);
+ Assert.Equal(3L, (long?)storageModel["LongDataProp"]);
+ Assert.Equal(4L, (long?)storageModel["NullableLongDataProp"]);
+ Assert.Equal(5.0f, (float?)storageModel["FloatDataProp"].AsDouble);
+ Assert.Equal(6.0f, (float?)storageModel["NullableFloatDataProp"].AsNullableDouble);
+ Assert.Equal(7.0, (double?)storageModel["DoubleDataProp"]);
+ Assert.Equal(8.0, (double?)storageModel["NullableDoubleDataProp"]);
+ Assert.Equal(9.0m, (decimal?)storageModel["DecimalDataProp"]);
+ Assert.Equal(10.0m, (decimal?)storageModel["NullableDecimalDataProp"]);
+ Assert.Equal(new DateTime(2021, 1, 1, 0, 0, 0).ToUniversalTime(), storageModel["DateTimeDataProp"].ToUniversalTime());
+ Assert.Equal(new DateTime(2021, 1, 1, 0, 0, 0).ToUniversalTime(), storageModel["NullableDateTimeDataProp"].ToUniversalTime());
+ Assert.Equal(s_taglist, storageModel["TagListDataProp"]!.AsBsonArray.Select(x => (string)x!).ToArray());
+ Assert.Equal(s_floatVector, storageModel["FloatVector"]!.AsBsonArray.Select(x => (float)x.AsDouble!).ToArray());
+ Assert.Equal(s_floatVector, storageModel["NullableFloatVector"]!.AsBsonArray.Select(x => (float)x.AsNullableDouble!).ToArray());
+ Assert.Equal(s_doubleVector, storageModel["DoubleVector"]!.AsBsonArray.Select(x => (double)x!).ToArray());
+ Assert.Equal(s_doubleVector, storageModel["NullableDoubleVector"]!.AsBsonArray.Select(x => (double)x!).ToArray());
+ }
+
+ [Fact]
+ public void MapFromDataToStorageModelMapsNullValues()
+ {
+ // Arrange
+ VectorStoreRecordDefinition vectorStoreRecordDefinition = new()
+ {
+ Properties = new List
+ {
+ new VectorStoreRecordKeyProperty("Key", typeof(string)),
+ new VectorStoreRecordDataProperty("StringDataProp", typeof(string)),
+ new VectorStoreRecordDataProperty("NullableIntDataProp", typeof(int?)),
+ new VectorStoreRecordVectorProperty("NullableFloatVector", typeof(ReadOnlyMemory?)),
+ },
+ };
+
+ var dataModel = new VectorStoreGenericDataModel("key")
+ {
+ Data =
+ {
+ ["StringDataProp"] = null,
+ ["NullableIntDataProp"] = null,
+ },
+ Vectors =
+ {
+ ["NullableFloatVector"] = null,
+ },
+ };
+
+ var sut = new AzureCosmosDBMongoDBGenericDataModelMapper(vectorStoreRecordDefinition);
+
+ // Act
+ var storageModel = sut.MapFromDataToStorageModel(dataModel);
+
+ // Assert
+ Assert.Equal(BsonNull.Value, storageModel["StringDataProp"]);
+ Assert.Equal(BsonNull.Value, storageModel["NullableIntDataProp"]);
+ Assert.Empty(storageModel["NullableFloatVector"].AsBsonArray);
+ }
+
+ [Fact]
+ public void MapFromStorageToDataModelMapsAllSupportedTypes()
+ {
+ // Arrange
+ var sut = new AzureCosmosDBMongoDBGenericDataModelMapper(s_vectorStoreRecordDefinition);
+ var storageModel = new BsonDocument
+ {
+ ["_id"] = "key",
+ ["BoolDataProp"] = true,
+ ["NullableBoolDataProp"] = false,
+ ["StringDataProp"] = "string",
+ ["IntDataProp"] = 1,
+ ["NullableIntDataProp"] = 2,
+ ["LongDataProp"] = 3L,
+ ["NullableLongDataProp"] = 4L,
+ ["FloatDataProp"] = 5.0f,
+ ["NullableFloatDataProp"] = 6.0f,
+ ["DoubleDataProp"] = 7.0,
+ ["NullableDoubleDataProp"] = 8.0,
+ ["DecimalDataProp"] = 9.0m,
+ ["NullableDecimalDataProp"] = 10.0m,
+ ["DateTimeDataProp"] = new DateTime(2021, 1, 1, 0, 0, 0).ToUniversalTime(),
+ ["NullableDateTimeDataProp"] = new DateTime(2021, 1, 1, 0, 0, 0).ToUniversalTime(),
+ ["TagListDataProp"] = BsonArray.Create(s_taglist),
+ ["FloatVector"] = BsonArray.Create(s_floatVector),
+ ["NullableFloatVector"] = BsonArray.Create(s_floatVector),
+ ["DoubleVector"] = BsonArray.Create(s_doubleVector),
+ ["NullableDoubleVector"] = BsonArray.Create(s_doubleVector)
+ };
+
+ // Act
+ var dataModel = sut.MapFromStorageToDataModel(storageModel, new StorageToDataModelMapperOptions { IncludeVectors = true });
+
+ // Assert
+ Assert.Equal("key", dataModel.Key);
+ Assert.Equal(true, dataModel.Data["BoolDataProp"]);
+ Assert.Equal(false, dataModel.Data["NullableBoolDataProp"]);
+ Assert.Equal("string", dataModel.Data["StringDataProp"]);
+ Assert.Equal(1, dataModel.Data["IntDataProp"]);
+ Assert.Equal(2, dataModel.Data["NullableIntDataProp"]);
+ Assert.Equal(3L, dataModel.Data["LongDataProp"]);
+ Assert.Equal(4L, dataModel.Data["NullableLongDataProp"]);
+ Assert.Equal(5.0f, dataModel.Data["FloatDataProp"]);
+ Assert.Equal(6.0f, dataModel.Data["NullableFloatDataProp"]);
+ Assert.Equal(7.0, dataModel.Data["DoubleDataProp"]);
+ Assert.Equal(8.0, dataModel.Data["NullableDoubleDataProp"]);
+ Assert.Equal(9.0m, dataModel.Data["DecimalDataProp"]);
+ Assert.Equal(10.0m, dataModel.Data["NullableDecimalDataProp"]);
+ Assert.Equal(new DateTime(2021, 1, 1, 0, 0, 0).ToUniversalTime(), dataModel.Data["DateTimeDataProp"]);
+ Assert.Equal(new DateTime(2021, 1, 1, 0, 0, 0).ToUniversalTime(), dataModel.Data["NullableDateTimeDataProp"]);
+ Assert.Equal(s_taglist, dataModel.Data["TagListDataProp"]);
+ Assert.Equal(s_floatVector, ((ReadOnlyMemory)dataModel.Vectors["FloatVector"]!).ToArray());
+ Assert.Equal(s_floatVector, ((ReadOnlyMemory)dataModel.Vectors["NullableFloatVector"]!)!.ToArray());
+ Assert.Equal(s_doubleVector, ((ReadOnlyMemory)dataModel.Vectors["DoubleVector"]!).ToArray());
+ Assert.Equal(s_doubleVector, ((ReadOnlyMemory)dataModel.Vectors["NullableDoubleVector"]!)!.ToArray());
+ }
+
+ [Fact]
+ public void MapFromStorageToDataModelMapsNullValues()
+ {
+ // Arrange
+ VectorStoreRecordDefinition vectorStoreRecordDefinition = new()
+ {
+ Properties = new List
+ {
+ new VectorStoreRecordKeyProperty("Key", typeof(string)),
+ new VectorStoreRecordDataProperty("StringDataProp", typeof(string)),
+ new VectorStoreRecordDataProperty("NullableIntDataProp", typeof(int?)),
+ new VectorStoreRecordVectorProperty("NullableFloatVector", typeof(ReadOnlyMemory?)),
+ },
+ };
+
+ var storageModel = new BsonDocument
+ {
+ ["_id"] = "key",
+ ["StringDataProp"] = BsonNull.Value,
+ ["NullableIntDataProp"] = BsonNull.Value,
+ ["NullableFloatVector"] = BsonNull.Value
+ };
+
+ var sut = new AzureCosmosDBMongoDBGenericDataModelMapper(vectorStoreRecordDefinition);
+
+ // Act
+ var dataModel = sut.MapFromStorageToDataModel(storageModel, new StorageToDataModelMapperOptions { IncludeVectors = true });
+
+ // Assert
+ Assert.Equal("key", dataModel.Key);
+ Assert.Null(dataModel.Data["StringDataProp"]);
+ Assert.Null(dataModel.Data["NullableIntDataProp"]);
+ Assert.Null(dataModel.Vectors["NullableFloatVector"]);
+ }
+
+ [Fact]
+ public void MapFromStorageToDataModelThrowsForMissingKey()
+ {
+ // Arrange
+ var sut = new AzureCosmosDBMongoDBGenericDataModelMapper(s_vectorStoreRecordDefinition);
+ var storageModel = new BsonDocument();
+
+ // Act & Assert
+ var exception = Assert.Throws(
+ () => sut.MapFromStorageToDataModel(storageModel, new StorageToDataModelMapperOptions { IncludeVectors = true }));
+ }
+
+ [Fact]
+ public void MapFromDataToStorageModelSkipsMissingProperties()
+ {
+ // Arrange
+ VectorStoreRecordDefinition vectorStoreRecordDefinition = new()
+ {
+ Properties = new List
+ {
+ new VectorStoreRecordKeyProperty("Key", typeof(string)),
+ new VectorStoreRecordDataProperty("StringDataProp", typeof(string)),
+ new VectorStoreRecordVectorProperty("FloatVector", typeof(ReadOnlyMemory)),
+ },
+ };
+
+ var dataModel = new VectorStoreGenericDataModel("key");
+ var sut = new AzureCosmosDBMongoDBGenericDataModelMapper(vectorStoreRecordDefinition);
+
+ // Act
+ var storageModel = sut.MapFromDataToStorageModel(dataModel);
+
+ // Assert
+ Assert.Equal("key", (string?)storageModel["_id"]);
+ Assert.False(storageModel.Contains("StringDataProp"));
+ Assert.False(storageModel.Contains("FloatVector"));
+ }
+
+ [Fact]
+ public void MapFromStorageToDataModelSkipsMissingProperties()
+ {
+ // Arrange
+ VectorStoreRecordDefinition vectorStoreRecordDefinition = new()
+ {
+ Properties = new List
+ {
+ new VectorStoreRecordKeyProperty("Key", typeof(string)),
+ new VectorStoreRecordDataProperty("StringDataProp", typeof(string)),
+ new VectorStoreRecordVectorProperty("FloatVector", typeof(ReadOnlyMemory)),
+ },
+ };
+
+ var storageModel = new BsonDocument
+ {
+ ["_id"] = "key"
+ };
+
+ var sut = new AzureCosmosDBMongoDBGenericDataModelMapper(vectorStoreRecordDefinition);
+
+ // Act
+ var dataModel = sut.MapFromStorageToDataModel(storageModel, new StorageToDataModelMapperOptions { IncludeVectors = true });
+
+ // Assert
+ Assert.Equal("key", dataModel.Key);
+ Assert.False(dataModel.Data.ContainsKey("StringDataProp"));
+ Assert.False(dataModel.Vectors.ContainsKey("FloatVector"));
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchGenericDataModelMapper.cs b/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchGenericDataModelMapper.cs
index 98f76f1142fe..33d995cf87e0 100644
--- a/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchGenericDataModelMapper.cs
+++ b/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchGenericDataModelMapper.cs
@@ -11,7 +11,7 @@
namespace Microsoft.SemanticKernel.Connectors.AzureAISearch;
///
-/// A mapper that maps between the generic semantic kernel data model and the model that the data is stored in in Azure AI Search.
+/// A mapper that maps between the generic Semantic Kernel data model and the model that the data is stored under, within Azure AI Search.
///
internal class AzureAISearchGenericDataModelMapper : IVectorStoreRecordMapper, JsonObject>
{
diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBConstants.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBConstants.cs
index ac78c98fabc2..197faf81f093 100644
--- a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBConstants.cs
+++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBConstants.cs
@@ -1,5 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
+using System;
+using System.Collections.Generic;
+
namespace Microsoft.SemanticKernel.Connectors.AzureCosmosDBMongoDB;
///
@@ -12,4 +15,39 @@ internal static class AzureCosmosDBMongoDBConstants
/// Reserved key property name in data model.
internal const string DataModelReservedKeyPropertyName = "Id";
+
+ /// A containing the supported key types.
+ internal static readonly HashSet SupportedKeyTypes =
+ [
+ typeof(string)
+ ];
+
+ /// A containing the supported data property types.
+ internal static readonly HashSet SupportedDataTypes =
+ [
+ typeof(bool),
+ typeof(bool?),
+ typeof(string),
+ typeof(int),
+ typeof(int?),
+ typeof(long),
+ typeof(long?),
+ typeof(float),
+ typeof(float?),
+ typeof(double),
+ typeof(double?),
+ typeof(decimal),
+ typeof(decimal?),
+ typeof(DateTime),
+ typeof(DateTime?),
+ ];
+
+ /// A containing the supported vector types.
+ internal static readonly HashSet SupportedVectorTypes =
+ [
+ typeof(ReadOnlyMemory),
+ typeof(ReadOnlyMemory?),
+ typeof(ReadOnlyMemory),
+ typeof(ReadOnlyMemory?)
+ ];
}
diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBGenericDataModelMapper.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBGenericDataModelMapper.cs
new file mode 100644
index 000000000000..e3ea3d2a12fc
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBGenericDataModelMapper.cs
@@ -0,0 +1,181 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.SemanticKernel.Data;
+using MongoDB.Bson;
+
+namespace Microsoft.SemanticKernel.Connectors.AzureCosmosDBMongoDB;
+
+///
+/// A mapper that maps between the generic Semantic Kernel data model and the model that the data is stored under, within Azure CosmosDB MongoDB.
+///
+internal sealed class AzureCosmosDBMongoDBGenericDataModelMapper : IVectorStoreRecordMapper, BsonDocument>
+{
+ /// A that defines the schema of the data in the database.
+ private readonly VectorStoreRecordDefinition _vectorStoreRecordDefinition;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A that defines the schema of the data in the database.
+ public AzureCosmosDBMongoDBGenericDataModelMapper(VectorStoreRecordDefinition vectorStoreRecordDefinition)
+ {
+ Verify.NotNull(vectorStoreRecordDefinition);
+
+ this._vectorStoreRecordDefinition = vectorStoreRecordDefinition;
+ }
+
+ ///
+ public BsonDocument MapFromDataToStorageModel(VectorStoreGenericDataModel dataModel)
+ {
+ Verify.NotNull(dataModel);
+
+ var document = new BsonDocument();
+
+ // Loop through all known properties and map each from the data model to the storage model.
+ foreach (var property in this._vectorStoreRecordDefinition.Properties)
+ {
+ var storagePropertyName = property.StoragePropertyName ?? property.DataModelPropertyName;
+
+ if (property is VectorStoreRecordKeyProperty keyProperty)
+ {
+ document[AzureCosmosDBMongoDBConstants.MongoReservedKeyPropertyName] = dataModel.Key;
+ }
+ else if (property is VectorStoreRecordDataProperty dataProperty)
+ {
+ if (dataModel.Data is not null && dataModel.Data.TryGetValue(dataProperty.DataModelPropertyName, out var dataValue))
+ {
+ document[storagePropertyName] = BsonValue.Create(dataValue);
+ }
+ }
+ else if (property is VectorStoreRecordVectorProperty vectorProperty)
+ {
+ if (dataModel.Vectors is not null && dataModel.Vectors.TryGetValue(vectorProperty.DataModelPropertyName, out var vectorValue))
+ {
+ document[storagePropertyName] = BsonArray.Create(GetVectorArray(vectorValue));
+ }
+ }
+ }
+
+ return document;
+ }
+
+ ///
+ public VectorStoreGenericDataModel MapFromStorageToDataModel(BsonDocument storageModel, StorageToDataModelMapperOptions options)
+ {
+ Verify.NotNull(storageModel);
+
+ // Create variables to store the response properties.
+ string? key = null;
+ var dataProperties = new Dictionary();
+ var vectorProperties = new Dictionary();
+
+ // Loop through all known properties and map each from the storage model to the data model.
+ foreach (var property in this._vectorStoreRecordDefinition.Properties)
+ {
+ var storagePropertyName = property.StoragePropertyName ?? property.DataModelPropertyName;
+
+ if (property is VectorStoreRecordKeyProperty keyProperty)
+ {
+ if (storageModel.TryGetValue(AzureCosmosDBMongoDBConstants.MongoReservedKeyPropertyName, out var keyValue))
+ {
+ key = keyValue.AsString;
+ }
+ }
+ else if (property is VectorStoreRecordDataProperty dataProperty)
+ {
+ if (!storageModel.TryGetValue(storagePropertyName, out var dataValue))
+ {
+ continue;
+ }
+
+ dataProperties.Add(dataProperty.DataModelPropertyName, GetDataPropertyValue(property.DataModelPropertyName, property.PropertyType, dataValue));
+ }
+ else if (property is VectorStoreRecordVectorProperty vectorProperty && options.IncludeVectors)
+ {
+ if (!storageModel.TryGetValue(storagePropertyName, out var vectorValue))
+ {
+ continue;
+ }
+
+ vectorProperties.Add(vectorProperty.DataModelPropertyName, GetVectorPropertyValue(property.DataModelPropertyName, property.PropertyType, vectorValue));
+ }
+ }
+
+ if (key is null)
+ {
+ throw new VectorStoreRecordMappingException("No key property was found in the record retrieved from storage.");
+ }
+
+ return new VectorStoreGenericDataModel(key) { Data = dataProperties, Vectors = vectorProperties };
+ }
+
+ #region private
+
+ private static object? GetDataPropertyValue(string propertyName, Type propertyType, BsonValue value)
+ {
+ if (value.IsBsonNull)
+ {
+ return null;
+ }
+
+ return propertyType switch
+ {
+ Type t when t == typeof(bool) => value.AsBoolean,
+ Type t when t == typeof(bool?) => value.AsNullableBoolean,
+ Type t when t == typeof(string) => value.AsString,
+ Type t when t == typeof(int) => value.AsInt32,
+ Type t when t == typeof(int?) => value.AsNullableInt32,
+ Type t when t == typeof(long) => value.AsInt64,
+ Type t when t == typeof(long?) => value.AsNullableInt64,
+ Type t when t == typeof(float) => ((float)value.AsDouble),
+ Type t when t == typeof(float?) => ((float?)value.AsNullableDouble),
+ Type t when t == typeof(double) => value.AsDouble,
+ Type t when t == typeof(double?) => value.AsNullableDouble,
+ Type t when t == typeof(decimal) => value.AsDecimal,
+ Type t when t == typeof(decimal?) => value.AsNullableDecimal,
+ Type t when t == typeof(DateTime) => value.ToUniversalTime(),
+ Type t when t == typeof(DateTime?) => value.ToNullableUniversalTime(),
+ Type t when typeof(IEnumerable).IsAssignableFrom(t) => value.AsBsonArray.Select(
+ item => GetDataPropertyValue(propertyName, VectorStoreRecordPropertyReader.GetCollectionElementType(t), item)),
+ _ => throw new NotSupportedException($"Mapping for property {propertyName} with type {propertyType.FullName} is not supported in generic data model.")
+ };
+ }
+
+ private static object? GetVectorPropertyValue(string propertyName, Type propertyType, BsonValue value)
+ {
+ if (value.IsBsonNull)
+ {
+ return null;
+ }
+
+ return propertyType switch
+ {
+ Type t when t == typeof(ReadOnlyMemory) || t == typeof(ReadOnlyMemory?) =>
+ new ReadOnlyMemory(value.AsBsonArray.Select(item => (float)item.AsDouble).ToArray()),
+ Type t when t == typeof(ReadOnlyMemory) || t == typeof(ReadOnlyMemory?) =>
+ new ReadOnlyMemory(value.AsBsonArray.Select(item => item.AsDouble).ToArray()),
+ _ => throw new NotSupportedException($"Mapping for property {propertyName} with type {propertyType.FullName} is not supported in generic data model.")
+ };
+ }
+
+ private static object GetVectorArray(object? vector)
+ {
+ if (vector is null)
+ {
+ return Array.Empty