From 3694415a393b4a46f092f82a0da016c5b4bd5e24 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Mon, 23 Mar 2026 22:17:14 +0100 Subject: [PATCH 1/5] Cleanup in model building --- .../CosmosNoSql/CosmosNoSqlMapper.cs | 6 +- .../CollectionJsonModelBuilder.cs | 4 +- .../ProviderServices/CollectionModel.cs | 8 +- .../CollectionModelBuilder.cs | 172 +++++++----------- .../ProviderServices/KeyPropertyModel.cs | 9 + .../ProviderServices/PropertyModel.cs | 105 +++++------ .../ProviderServices/VectorPropertyModel.cs | 8 +- 7 files changed, 137 insertions(+), 175 deletions(-) diff --git a/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlMapper.cs b/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlMapper.cs index 8a8644b80198..6f0ffdaea91b 100644 --- a/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlMapper.cs +++ b/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlMapper.cs @@ -41,8 +41,8 @@ public JsonObject MapFromDataToStorageModel(TRecord dataModel, int recordIndex, // The key property in Azure CosmosDB NoSQL is always named 'id'. // But the external JSON serializer used just above isn't aware of that, and will produce a JSON object with another name, taking into - // account e.g. naming policies. TemporaryStorageName gets populated in the model builder - containing that name - once VectorStoreModelBuildingOptions.ReservedKeyPropertyName is set - RenameJsonProperty(jsonObject, this._keyProperty.TemporaryStorageName!, CosmosNoSqlConstants.ReservedKeyPropertyName); + // account e.g. naming policies. SerializedKeyName gets populated in the model builder - containing that name - once VectorStoreModelBuildingOptions.ReservedKeyPropertyName is set + RenameJsonProperty(jsonObject, this._keyProperty.SerializedKeyName!, CosmosNoSqlConstants.ReservedKeyPropertyName); // Go over the vector properties; inject any generated embeddings to overwrite the JSON serialized above. // Also, for Embedding properties we also need to overwrite with a simple array (since Embedding gets serialized as a complex object). @@ -116,7 +116,7 @@ public JsonObject MapFromDataToStorageModel(TRecord dataModel, int recordIndex, public TRecord MapFromStorageToDataModel(JsonObject storageModel, bool includeVectors) { // See above comment. - RenameJsonProperty(storageModel, CosmosNoSqlConstants.ReservedKeyPropertyName, this._keyProperty.TemporaryStorageName!); + RenameJsonProperty(storageModel, CosmosNoSqlConstants.ReservedKeyPropertyName, this._keyProperty.SerializedKeyName!); foreach (var vectorProperty in this._model.VectorProperties) { diff --git a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/CollectionJsonModelBuilder.cs b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/CollectionJsonModelBuilder.cs index 2b7584b8dd0c..90a2db9433f3 100644 --- a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/CollectionJsonModelBuilder.cs +++ b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/CollectionJsonModelBuilder.cs @@ -89,13 +89,11 @@ protected override void Customize() if (keyPropertyWithReservedName) { - // Somewhat hacky: // Some providers (Weaviate, Cosmos NoSQL) have a fixed, reserved storage name for keys (id), and at the same time use an external // JSON serializer to serialize the entire user POCO. Since the serializer is unaware of the reserved storage name, it will produce // a storage name as usual, based on the .NET property's name, possibly with a naming policy applied to it. The connector then needs // to look that up and replace with the reserved name. - // So we store the policy-transformed name, as StorageName contains the reserved name. - property.TemporaryStorageName = storageName; + ((KeyPropertyModel)property).SerializedKeyName = storageName; } else { diff --git a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/CollectionModel.cs b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/CollectionModel.cs index 1d67a7aa2026..7f837038dfa6 100644 --- a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/CollectionModel.cs +++ b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/CollectionModel.cs @@ -19,7 +19,7 @@ namespace Microsoft.Extensions.VectorData.ProviderServices; public sealed class CollectionModel { private readonly Type _recordType; - private readonly IRecordCreator _recordCreator; + private readonly Func _recordFactory; private KeyPropertyModel? _singleKeyProperty; private VectorPropertyModel? _singleVectorProperty; @@ -57,14 +57,14 @@ public sealed class CollectionModel internal CollectionModel( Type recordType, - IRecordCreator recordCreator, + Func recordFactory, IReadOnlyList keyProperties, IReadOnlyList dataProperties, IReadOnlyList vectorProperties, IReadOnlyDictionary propertyMap) { this._recordType = recordType; - this._recordCreator = recordCreator; + this._recordFactory = recordFactory; this.KeyProperties = keyProperties; this.DataProperties = dataProperties; @@ -97,7 +97,7 @@ public TRecord CreateRecord() { Debug.Assert(typeof(TRecord) == this._recordType, "Type mismatch between record type and model type."); - return this._recordCreator.Create(); + return (TRecord)this._recordFactory(); } /// diff --git a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/CollectionModelBuilder.cs b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/CollectionModelBuilder.cs index 4932b81cdcb9..7b2f544fa74d 100644 --- a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/CollectionModelBuilder.cs +++ b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/CollectionModelBuilder.cs @@ -92,19 +92,20 @@ public virtual CollectionModel Build(Type recordType, Type keyType, VectorStoreC this.ProcessRecordDefinition(definition, recordType); } - // Go over the properties, set the PropertyInfos to point to the .NET type's properties and validate type compatibility. + // Go over the properties, configure POCO accessors and validate type compatibility. foreach (var property in this.Properties) { - // When we have a CLR type (POCO, not dynamic mapping), get the .NET property's type and make sure it matches the definition. - property.PropertyInfo = recordType.GetProperty(property.ModelName) + var clrProperty = recordType.GetProperty(property.ModelName) ?? throw new InvalidOperationException($"Property '{property.ModelName}' not found on CLR type '{recordType.FullName}'."); - var clrPropertyType = property.PropertyInfo.PropertyType; + var clrPropertyType = clrProperty.PropertyType; if ((Nullable.GetUnderlyingType(clrPropertyType) ?? clrPropertyType) != (Nullable.GetUnderlyingType(property.Type) ?? property.Type)) { throw new InvalidOperationException( - $"Property '{property.ModelName}' has a different CLR type in the record definition ('{property.Type.Name}') and on the .NET property ('{property.PropertyInfo.PropertyType}')."); + $"Property '{property.ModelName}' has a different CLR type in the record definition ('{property.Type.Name}') and on the .NET property ('{clrProperty.PropertyType}')."); } + + property.ConfigurePocoAccessors(clrProperty); } this.Customize(); @@ -116,7 +117,7 @@ public virtual CollectionModel Build(Type recordType, Type keyType, VectorStoreC throw new NotSupportedException($"Type '{recordType.Name}' must have a parameterless constructor."); } - return new(recordType, new ActivatorBasedRecordCreator(), this.KeyProperties, this.DataProperties, this.VectorProperties, this.PropertyMap); + return new(recordType, () => Activator.CreateInstance(recordType)!, this.KeyProperties, this.DataProperties, this.VectorProperties, this.PropertyMap); } /// @@ -136,7 +137,12 @@ public virtual CollectionModel BuildDynamic(VectorStoreCollectionDefinition defi this.Customize(); this.Validate(type: null, definition); - return new(typeof(Dictionary), new DynamicRecordCreator(), this.KeyProperties, this.DataProperties, this.VectorProperties, this.PropertyMap); + foreach (var property in this.Properties) + { + property.ConfigureDynamicAccessors(); + } + + return new(typeof(Dictionary), static () => new Dictionary(), this.KeyProperties, this.DataProperties, this.VectorProperties, this.PropertyMap); } /// @@ -201,50 +207,7 @@ protected virtual void ProcessTypeProperties(Type type, VectorStoreCollectionDef vectorProperty.IndexKind = vectorAttribute.IndexKind; vectorProperty.DistanceFunction = vectorAttribute.DistanceFunction; - // Set up the embedding generator for the property. For this pass over .NET properties, we only have the default embedding generator (configured) - // at the collection/store level) - this may get overridden later by the record definition. - - // 1. We also attempt to set the EmbeddingType for the property. If the type is natively supported (e.g. ReadOnlyMemory), we use that. - // 2. If an embedding generator is configured, we try to resolve the embedding type from that. This allows users to just e.g. stick an - // IEmbeddingGenerator in DI, define a string property as their vector property, and as long as the embedding generator is compatible (supports - // string and ROM, assuming that's what the connector requires), everything just works. - // Note that inferring the embedding type from the IEmbeddingGenerator isn't trivial, involving both connector logic (around which embedding - // types are supported/preferred), as well as the vector property type (which knows about supported input types). - // 3. Otherwise, if we can't infer the embedding type from the generator (no generator or the default generator isn't compatible), we leave it - // null to allow it to get configured later (e.g. via a property-specific generator configured in the record definition). - - vectorProperty.EmbeddingGenerator = this.DefaultEmbeddingGenerator; - - if (this.IsVectorPropertyTypeValid(clrProperty.PropertyType, out _)) - { - vectorProperty.EmbeddingType = clrProperty.PropertyType; - - // Even for native types, if an embedding generator is configured, resolve the handler - // so that search can convert arbitrary inputs (e.g. string) to embeddings. - // Since the property type is a native vector type (not the input type for the generator), - // we use CanGenerateEmbedding which checks if the generator can produce the embedding output type - // regardless of input type. - if (this.DefaultEmbeddingGenerator is not null) - { - vectorProperty.EmbeddingGenerationDispatcher = this.ResolveSearchOnlyEmbeddingHandler(vectorProperty, this.DefaultEmbeddingGenerator); - } - } - else if (this.DefaultEmbeddingGenerator is not null) - { - // The property type isn't a valid embedding type (e.g. ReadOnlyMemory), but an embedding generator is configured. - // Try to resolve the embedding type from that: if the configured generator supports translating the input type (e.g. string) to - // an output type supported by the provider, we set that as the embedding type. - // Note that this can fail (if the configured generator doesn't support the required translation). In that case, EmbeddingType - // remains null, and we may succeed configuring it later (e.g. from the record definition). If that fails, we throw in validation at the end. - var (embeddingType, handler) = this.ResolveEmbeddingType(vectorProperty, this.DefaultEmbeddingGenerator, userRequestedEmbeddingType: null); - vectorProperty.EmbeddingType = embeddingType; - vectorProperty.EmbeddingGenerationDispatcher = handler; - } - else - { - // If the property type isn't valid and there's no embedding generator, that's an error. - // However, we throw only later in validation, to allow e.g. for arbitrary provider customization after this step. - } + this.ConfigureVectorPropertyEmbedding(vectorProperty, this.DefaultEmbeddingGenerator, userRequestedEmbeddingType: null); this.VectorProperties.Add(vectorProperty); storageName = vectorAttribute.StorageName; @@ -354,45 +317,10 @@ protected virtual void ProcessRecordDefinition(VectorStoreCollectionDefinition d vectorProperty.DistanceFunction = definitionVectorProperty.DistanceFunction; } - // See comment above in ProcessTypeProperties() on embedding generation. - - vectorProperty.EmbeddingGenerator = definitionVectorProperty.EmbeddingGenerator ?? this.DefaultEmbeddingGenerator; - - if (this.IsVectorPropertyTypeValid(vectorProperty.Type, out _)) - { - if (definitionVectorProperty.EmbeddingType is not null && definitionVectorProperty.EmbeddingType != vectorProperty.Type) - { - throw new InvalidOperationException(VectorDataStrings.DifferentEmbeddingTypeSpecifiedForNativelySupportedType(vectorProperty, definitionVectorProperty.EmbeddingType)); - } - - vectorProperty.EmbeddingType = definitionVectorProperty.Type; - - // Even for native types, if an embedding generator is configured, resolve the handler - // so that search can convert arbitrary inputs (e.g. string) to embeddings. - // Since the property type is a native vector type (not the input type for the generator), - // we use CanGenerateEmbedding which checks if the generator can produce the embedding output type - // regardless of input type. - if (vectorProperty.EmbeddingGenerator is not null) - { - vectorProperty.EmbeddingGenerationDispatcher = this.ResolveSearchOnlyEmbeddingHandler(vectorProperty, vectorProperty.EmbeddingGenerator); - } - } - else if (vectorProperty.EmbeddingGenerator is not null) - { - // The property type isn't a valid embedding type (e.g. ReadOnlyMemory), but an embedding generator is configured. - // Try to resolve the embedding type from the generator: if the configured generator supports translating the input type (e.g. string) to - // an output type supported by the provider, we set that as the embedding type. - // Note that this can fail (if the configured generator doesn't support the required translation). In that case, EmbeddingType - // remains null - we defer throwing to the validation phase at the end, to allow for possible later provider customization later. - var (embeddingType, handler) = this.ResolveEmbeddingType(vectorProperty, vectorProperty.EmbeddingGenerator, definitionVectorProperty.EmbeddingType); - vectorProperty.EmbeddingType = embeddingType; - vectorProperty.EmbeddingGenerationDispatcher = handler; - } - else - { - // If the property type isn't valid and there's no embedding generator, that's an error. - // However, we throw only later in validation, to allow e.g. for arbitrary provider customization after this step. - } + this.ConfigureVectorPropertyEmbedding( + vectorProperty, + definitionVectorProperty.EmbeddingGenerator ?? this.DefaultEmbeddingGenerator, + definitionVectorProperty.EmbeddingType); break; @@ -475,6 +403,55 @@ private void SetPropertyStorageName(PropertyModel property, string? storageName, return null; } + /// + /// Configures embedding generation for a vector property. Sets the embedding generator, resolves the embedding type, + /// and assigns the appropriate . + /// + /// + /// If the property's type is natively supported (e.g. of ), the embedding type + /// is set to the property's type; if a generator is also configured, a search-only dispatcher is resolved so that search can convert + /// arbitrary inputs (e.g. string) to embeddings. + /// Otherwise, if a generator is configured, the embedding type is resolved from it. If resolution fails, the embedding type remains + /// and an error is deferred to the validation phase. + /// + private void ConfigureVectorPropertyEmbedding( + VectorPropertyModel vectorProperty, + IEmbeddingGenerator? embeddingGenerator, + Type? userRequestedEmbeddingType) + { + vectorProperty.EmbeddingGenerator = embeddingGenerator; + + if (this.IsVectorPropertyTypeValid(vectorProperty.Type, out _)) + { + if (userRequestedEmbeddingType is not null && userRequestedEmbeddingType != vectorProperty.Type) + { + throw new InvalidOperationException(VectorDataStrings.DifferentEmbeddingTypeSpecifiedForNativelySupportedType(vectorProperty, userRequestedEmbeddingType)); + } + + vectorProperty.EmbeddingType = vectorProperty.Type; + + // Even for native types, if an embedding generator is configured, resolve the dispatcher + // so that search can convert arbitrary inputs (e.g. string) to embeddings. + if (embeddingGenerator is not null) + { + vectorProperty.EmbeddingGenerationDispatcher = this.ResolveSearchOnlyEmbeddingHandler(vectorProperty, embeddingGenerator); + } + } + else if (embeddingGenerator is not null) + { + // The property type isn't a valid embedding type, but an embedding generator is configured. + // Try to resolve the embedding type from it: if the configured generator supports translating the input type (e.g. string) to + // an output type supported by the provider, we set that as the embedding type. + // If this fails, EmbeddingType remains null and we defer the error to the validation phase. + var (embeddingType, handler) = this.ResolveEmbeddingType(vectorProperty, embeddingGenerator, userRequestedEmbeddingType); + vectorProperty.EmbeddingType = embeddingType; + vectorProperty.EmbeddingGenerationDispatcher = handler; + } + + // If the property type isn't valid and there's no embedding generator, that's an error. + // But we throw later, in validation, to allow for provider customization to correct this invalid state after this step. + } + /// /// Extension hook for connectors to be able to customize the model. /// @@ -628,19 +605,4 @@ protected virtual void ValidateKeyProperty(KeyPropertyModel keyProperty) /// Validates that the .NET type for a vector property is supported by the provider. /// protected abstract bool IsVectorPropertyTypeValid(Type type, [NotNullWhen(false)] out string? supportedTypes); - - [RequiresUnreferencedCode("This record creator is incompatible with trimming and is only used in non-trimming compatible codepaths")] - private sealed class ActivatorBasedRecordCreator : IRecordCreator - { - public TRecord Create() - => Activator.CreateInstance() ?? throw new InvalidOperationException($"Failed to instantiate record of type '{typeof(TRecord).Name}'."); - } - - private sealed class DynamicRecordCreator : IRecordCreator - { - public TRecord Create() - => typeof(TRecord) == typeof(Dictionary) - ? (TRecord)(object)new Dictionary() - : throw new UnreachableException($"Dynamic record creator only supports Dictionary, but got {typeof(TRecord).Name}."); - } } diff --git a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/KeyPropertyModel.cs b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/KeyPropertyModel.cs index 76f90822f7a5..5d0879dfc2da 100644 --- a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/KeyPropertyModel.cs +++ b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/KeyPropertyModel.cs @@ -17,6 +17,15 @@ public class KeyPropertyModel(string modelName, Type type) : PropertyModel(model /// public bool IsAutoGenerated { get; set; } + /// + /// Gets or sets the name that the JSON serializer will produce for this key property. + /// This is needed for connectors that use an external JSON serializer combined with a reserved key storage name + /// (e.g. CosmosDB NoSQL uses "id"): the serializer produces a JSON object with the policy-transformed name, and + /// the connector needs to find and replace it with the reserved storage name. + /// + [Experimental("MEVD9001")] + public string? SerializedKeyName { get; set; } + /// public override string ToString() => $"{this.ModelName} (Key, {this.Type.Name}{(this.IsAutoGenerated ? ", auto-generated" : "")})"; diff --git a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/PropertyModel.cs b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/PropertyModel.cs index 2815847a757d..7b3635911e95 100644 --- a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/PropertyModel.cs +++ b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/PropertyModel.cs @@ -16,6 +16,8 @@ namespace Microsoft.Extensions.VectorData.ProviderServices; public abstract class PropertyModel(string modelName, Type type) { private string? _storageName; + private Func? _getter; + private Action? _setter; /// /// Gets or sets the model name of the property. If the property corresponds to a .NET property, this name is the name of that property. @@ -31,15 +33,6 @@ public string StorageName set => this._storageName = value; } - // See comment in VectorStoreJsonModelBuilder - // TODO: Spend more time thinking about this, there may be a less hacky way to handle it. - - /// - /// Gets or sets the temporary storage name for the property, for use during the serialization process by certain connectors. - /// - [Experimental("MEVD9001")] - public string? TemporaryStorageName { get; set; } - /// /// Gets or sets the CLR type of the property. /// @@ -93,79 +86,77 @@ public bool IsNullable } /// - /// Reads the property from the given , returning the value as an . + /// Configures the property accessors using a CLR for POCO mapping. /// - public virtual object? GetValueAsObject(object record) + // TODO: Implement compiled delegates for better performance, #11122 + // TODO: Implement source-generated accessors for NativeAOT, #10256 + internal void ConfigurePocoAccessors(PropertyInfo propertyInfo) { - if (this.PropertyInfo is null) + this.PropertyInfo = propertyInfo; + this._getter = propertyInfo.GetValue; + this._setter = (record, value) => { - if (record is Dictionary dictionary) + // If the value is null, no need to set the property (it's the CLR default) + if (value is not null) { - var value = dictionary.TryGetValue(this.ModelName, out var tempValue) - ? tempValue - : null; - - if (value is not null && value.GetType() != (Nullable.GetUnderlyingType(this.Type) ?? this.Type)) - { - throw new InvalidCastException($"Property '{this.ModelName}' has a value of type '{value.GetType().Name}', but its configured type is '{this.Type.Name}'."); - } - - return value; + propertyInfo.SetValue(record, value); } - - throw new UnreachableException("Non-dynamic mapping but PropertyInfo is null."); - } - - // We have a CLR property (non-dynamic POCO mapping) - - // TODO: Implement compiled delegates for better performance, #11122 - // TODO: Implement source-generated accessors for NativeAOT, #10256 - - return this.PropertyInfo.GetValue(record); + }; } /// - /// Writes the property from the given , accepting the value to write as an . - /// s - public virtual void SetValueAsObject(object record, object? value) + /// Configures the property accessors for dynamic mapping using . + /// + internal void ConfigureDynamicAccessors() { - if (this.PropertyInfo is null) + var modelName = this.ModelName; + var propertyType = this.Type; + + this._getter = record => { - if (record.GetType() == typeof(Dictionary)) + var dictionary = (Dictionary)record; + var value = dictionary.TryGetValue(modelName, out var tempValue) ? tempValue : null; + + if (value is not null && value.GetType() != (Nullable.GetUnderlyingType(propertyType) ?? propertyType)) { - var dictionary = (Dictionary)record; - dictionary[this.ModelName] = value; - return; + throw new InvalidCastException($"Property '{modelName}' has a value of type '{value.GetType().Name}', but its configured type is '{propertyType.Name}'."); } - throw new UnreachableException("Non-dynamic mapping but ClrProperty is null."); - } + return value; + }; - // We have a CLR property (non-dynamic POCO mapping) + this._setter = (record, value) => ((Dictionary)record)[modelName] = value; + } - // TODO: Implement compiled delegates for better performance, #11122 - // TODO: Implement source-generated accessors for NativeAOT, #10256 + /// + /// Reads the property from the given , returning the value as an . + /// + public object? GetValueAsObject(object record) + { + Debug.Assert(this._getter is not null, "Property accessors have not been configured."); + return this._getter!(record); + } - // If the value is null, no need to set the property (it's the CLR default) - if (value is not null) - { - this.PropertyInfo.SetValue(record, value); - } + /// + /// Writes the property from the given , accepting the value to write as an . + /// + public void SetValueAsObject(object record, object? value) + { + Debug.Assert(this._setter is not null, "Property accessors have not been configured."); + this._setter!(record, value); } /// /// Reads the property from the given . /// // TODO: actually implement the generic accessors to avoid boxing, and make use of them in connectors - public virtual T GetValue(object record) + public T GetValue(object record) => (T)(object)this.GetValueAsObject(record)!; /// /// Writes the property from the given . - /// s + /// // TODO: actually implement the generic accessors to avoid boxing, and make use of them in connectors - public virtual void SetValue(object record, T value) - { - this.SetValueAsObject(record, value); - } + public void SetValue(object record, T value) + => this.SetValueAsObject(record, value); } diff --git a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/VectorPropertyModel.cs b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/VectorPropertyModel.cs index 0ba7b33ef0ea..75d3a1057ff9 100644 --- a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/VectorPropertyModel.cs +++ b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/VectorPropertyModel.cs @@ -62,10 +62,12 @@ public int Dimensions /// /// Gets or sets the type representing the embedding stored in the database if is set. - /// Otherwise, this property is identical to . + /// Otherwise, this property is identical to . /// - // TODO: sort out the nullability story here: EmbeddingType must be non-null after model building is complete, but can be null during - // model building as we're figuring things out (i.e. introduce a provider-facing interface where the property is non-nullable). + /// + /// This property may be during model building while the embedding type is being resolved, + /// but is guaranteed to be non-null after building completes (validation ensures this). + /// [AllowNull] public Type EmbeddingType { get; set; } = null!; From 9bfbb389dcbd2b7308c6db21fda045476bb651eb Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Mon, 23 Mar 2026 22:49:16 +0100 Subject: [PATCH 2/5] Expose read-only interface views of properties from the model Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Memory/MongoDB/MongoDynamicMapper.cs | 6 +- .../AzureAISearch/AzureAISearchCollection.cs | 10 +-- .../AzureAISearchCollectionCreateMapping.cs | 10 +-- .../AzureAISearchDynamicMapper.cs | 8 +-- .../VectorData/Common/SqlFilterTranslator.cs | 4 +- .../CosmosMongoCollectionCreateMapping.cs | 4 +- .../CosmosMongoFilterTranslator.cs | 2 +- .../CosmosNoSql/CosmosNoSqlCollection.cs | 6 +- .../CosmosNoSqlCollectionQueryBuilder.cs | 8 +-- .../CosmosNoSql/CosmosNoSqlDynamicMapper.cs | 6 +- .../CosmosNoSqlFilterTranslator.cs | 6 +- .../CosmosNoSql/CosmosNoSqlMapper.cs | 2 +- .../src/VectorData/MongoDB/MongoCollection.cs | 2 +- .../MongoDB/MongoCollectionCreateMapping.cs | 6 +- .../MongoDB/MongoFilterTranslator.cs | 2 +- .../VectorData/PgVector/PostgresCollection.cs | 6 +- .../PgVector/PostgresFilterTranslator.cs | 2 +- .../src/VectorData/PgVector/PostgresMapper.cs | 2 +- .../PgVector/PostgresPropertyExtensions.cs | 4 +- .../PgVector/PostgresPropertyMapping.cs | 14 ++-- .../VectorData/PgVector/PostgresSqlBuilder.cs | 24 +++---- .../VectorData/Pinecone/PineconeCollection.cs | 2 +- .../Pinecone/PineconeFilterTranslator.cs | 2 +- .../src/VectorData/Qdrant/QdrantCollection.cs | 2 +- .../Qdrant/QdrantCollectionCreateMapping.cs | 6 +- dotnet/src/VectorData/Qdrant/QdrantMapper.cs | 4 +- .../Redis/RedisCollectionCreateMapping.cs | 14 ++-- .../Redis/RedisCollectionSearchMapping.cs | 4 +- .../src/VectorData/Redis/RedisJsonMapper.cs | 2 +- .../SqlServer/SqlServerCollection.cs | 8 +-- .../SqlServer/SqlServerCommandBuilder.cs | 48 ++++++------- .../SqlServer/SqlServerFilterTranslator.cs | 4 +- .../VectorData/SqlServer/SqlServerMapper.cs | 2 +- .../VectorData/SqliteVec/SqliteCollection.cs | 6 +- .../SqliteVec/SqliteCommandBuilder.cs | 14 ++-- .../SqliteVec/SqliteFilterTranslator.cs | 2 +- .../SqliteVec/SqlitePropertyMapping.cs | 16 ++--- .../ProviderServices/CollectionModel.cs | 38 +++++----- .../ProviderServices/DataPropertyModel.cs | 2 +- .../Filter/FilterTranslatorBase.cs | 2 +- .../ProviderServices/IDataPropertyModel.cs | 23 ++++++ .../ProviderServices/IKeyPropertyModel.cs | 23 ++++++ .../ProviderServices/IPropertyModel.cs | 70 +++++++++++++++++++ .../ProviderServices/IRecordCreator.cs | 8 --- .../ProviderServices/IVectorPropertyModel.cs | 63 +++++++++++++++++ .../ProviderServices/KeyPropertyModel.cs | 2 +- .../ProviderServices/PropertyModel.cs | 2 +- .../ProviderServices/VectorPropertyModel.cs | 2 +- .../VectorData/Weaviate/WeaviateCollection.cs | 2 +- .../src/VectorData/Weaviate/WeaviateMapper.cs | 2 +- .../Weaviate/WeaviateQueryBuilder.cs | 4 +- .../PostgresPropertyMappingTests.cs | 2 +- .../RedisCollectionCreateMappingTests.cs | 2 +- .../SqlitePropertyMappingTests.cs | 2 +- .../PropertyModelTests.cs | 2 +- 55 files changed, 346 insertions(+), 175 deletions(-) create mode 100644 dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/IDataPropertyModel.cs create mode 100644 dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/IKeyPropertyModel.cs create mode 100644 dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/IPropertyModel.cs delete mode 100644 dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/IRecordCreator.cs create mode 100644 dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/IVectorPropertyModel.cs diff --git a/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/MongoDynamicMapper.cs b/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/MongoDynamicMapper.cs index 791de1ea231c..20aa67ffdc56 100644 --- a/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/MongoDynamicMapper.cs +++ b/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/MongoDynamicMapper.cs @@ -90,7 +90,7 @@ Embedding e { switch (property) { - case KeyPropertyModel keyProperty: + case IKeyPropertyModel keyProperty: if (!storageModel.TryGetValue(MongoConstants.MongoReservedKeyPropertyName, out var keyValue)) { throw new InvalidOperationException("No key property was found in the record retrieved from storage."); @@ -109,14 +109,14 @@ Embedding e continue; - case DataPropertyModel dataProperty: + case IDataPropertyModel dataProperty: if (storageModel.TryGetValue(dataProperty.StorageName, out var dataValue)) { result.Add(dataProperty.ModelName, GetDataPropertyValue(property.ModelName, property.Type, dataValue)); } continue; - case VectorPropertyModel vectorProperty: + case IVectorPropertyModel vectorProperty: if (includeVectors && storageModel.TryGetValue(vectorProperty.StorageName, out var vectorValue)) { result.Add( diff --git a/dotnet/src/VectorData/AzureAISearch/AzureAISearchCollection.cs b/dotnet/src/VectorData/AzureAISearch/AzureAISearchCollection.cs index 16ed75d6c6ee..00eb8a3c485e 100644 --- a/dotnet/src/VectorData/AzureAISearch/AzureAISearchCollection.cs +++ b/dotnet/src/VectorData/AzureAISearch/AzureAISearchCollection.cs @@ -154,15 +154,15 @@ public override async Task EnsureCollectionExistsAsync(CancellationToken cancell { switch (property) { - case KeyPropertyModel p: + case IKeyPropertyModel p: searchFields.Add(AzureAISearchCollectionCreateMapping.MapKeyField(p)); break; - case DataPropertyModel p: + case IDataPropertyModel p: searchFields.Add(AzureAISearchCollectionCreateMapping.MapDataField(p)); break; - case VectorPropertyModel p: + case IVectorPropertyModel p: (VectorSearchField vectorSearchField, VectorSearchAlgorithmConfiguration algorithmConfiguration, VectorSearchProfile vectorSearchProfile) = AzureAISearchCollectionCreateMapping.MapVectorField(p); // Add the search field, plus its profile and algorithm configuration to the search config. @@ -361,7 +361,7 @@ public override IAsyncEnumerable GetAsync(Expression?> GetSearchVectorAsync(TInput searchValue, VectorPropertyModel vectorProperty, CancellationToken cancellationToken) + private static async ValueTask?> GetSearchVectorAsync(TInput searchValue, IVectorPropertyModel vectorProperty, CancellationToken cancellationToken) where TInput : notnull => searchValue switch { diff --git a/dotnet/src/VectorData/AzureAISearch/AzureAISearchCollectionCreateMapping.cs b/dotnet/src/VectorData/AzureAISearch/AzureAISearchCollectionCreateMapping.cs index c21053e15dda..1ddb39fdc196 100644 --- a/dotnet/src/VectorData/AzureAISearch/AzureAISearchCollectionCreateMapping.cs +++ b/dotnet/src/VectorData/AzureAISearch/AzureAISearchCollectionCreateMapping.cs @@ -18,7 +18,7 @@ internal static class AzureAISearchCollectionCreateMapping /// /// The key property definition. /// The for the provided property definition. - public static SearchableField MapKeyField(KeyPropertyModel keyProperty) + public static SearchableField MapKeyField(IKeyPropertyModel keyProperty) { return new SearchableField(keyProperty.StorageName) { IsKey = true, IsFilterable = true }; } @@ -29,7 +29,7 @@ public static SearchableField MapKeyField(KeyPropertyModel keyProperty) /// The data property definition. /// The for the provided property definition. /// Throws when the definition is missing required information. - public static SimpleField MapDataField(DataPropertyModel dataProperty) + public static SimpleField MapDataField(IDataPropertyModel dataProperty) { if (dataProperty.IsFullTextIndexed) { @@ -61,7 +61,7 @@ public static SimpleField MapDataField(DataPropertyModel dataProperty) /// The vector property definition. /// The and required index configuration. /// Throws when the definition is missing required information, or unsupported options are configured. - public static (VectorSearchField vectorSearchField, VectorSearchAlgorithmConfiguration algorithmConfiguration, VectorSearchProfile vectorSearchProfile) MapVectorField(VectorPropertyModel vectorProperty) + public static (VectorSearchField vectorSearchField, VectorSearchAlgorithmConfiguration algorithmConfiguration, VectorSearchProfile vectorSearchProfile) MapVectorField(IVectorPropertyModel vectorProperty) { // Build a name for the profile and algorithm configuration based on the property name // since we'll just create a separate one for each vector property. @@ -91,7 +91,7 @@ public static (VectorSearchField vectorSearchField, VectorSearchAlgorithmConfigu /// /// The vector property definition. /// The configured or default . - public static string GetSKIndexKind(VectorPropertyModel vectorProperty) + public static string GetSKIndexKind(IVectorPropertyModel vectorProperty) => vectorProperty.IndexKind ?? IndexKind.Hnsw; /// @@ -101,7 +101,7 @@ public static string GetSKIndexKind(VectorPropertyModel vectorProperty) /// The vector property definition. /// The chosen . /// Thrown if a distance function is chosen that isn't supported by Azure AI Search. - public static VectorSearchAlgorithmMetric GetSDKDistanceAlgorithm(VectorPropertyModel vectorProperty) + public static VectorSearchAlgorithmMetric GetSDKDistanceAlgorithm(IVectorPropertyModel vectorProperty) => vectorProperty.DistanceFunction switch { DistanceFunction.CosineSimilarity or null => VectorSearchAlgorithmMetric.Cosine, diff --git a/dotnet/src/VectorData/AzureAISearch/AzureAISearchDynamicMapper.cs b/dotnet/src/VectorData/AzureAISearch/AzureAISearchDynamicMapper.cs index 5b8024e5505d..2991add6a64d 100644 --- a/dotnet/src/VectorData/AzureAISearch/AzureAISearchDynamicMapper.cs +++ b/dotnet/src/VectorData/AzureAISearch/AzureAISearchDynamicMapper.cs @@ -99,7 +99,7 @@ public JsonObject MapFromDataToStorageModel(Dictionary dataMode { switch (property) { - case KeyPropertyModel keyProperty: + case IKeyPropertyModel keyProperty: var key = (string?)storageModel[keyProperty.StorageName] ?? throw new InvalidOperationException($"The key property '{keyProperty.StorageName}' is missing from the record retrieved from storage."); @@ -112,7 +112,7 @@ public JsonObject MapFromDataToStorageModel(Dictionary dataMode continue; - case DataPropertyModel dataProperty: + case IDataPropertyModel dataProperty: { if (storageModel.TryGetPropertyValue(dataProperty.StorageName, out var value)) { @@ -121,7 +121,7 @@ public JsonObject MapFromDataToStorageModel(Dictionary dataMode continue; } - case VectorPropertyModel vectorProperty when includeVectors: + case IVectorPropertyModel vectorProperty when includeVectors: { if (storageModel.TryGetPropertyValue(vectorProperty.StorageName, out var value)) { @@ -147,7 +147,7 @@ public JsonObject MapFromDataToStorageModel(Dictionary dataMode continue; } - case VectorPropertyModel vectorProperty when !includeVectors: + case IVectorPropertyModel vectorProperty when !includeVectors: break; default: diff --git a/dotnet/src/VectorData/Common/SqlFilterTranslator.cs b/dotnet/src/VectorData/Common/SqlFilterTranslator.cs index 0652d7675049..a2af5d775169 100644 --- a/dotnet/src/VectorData/Common/SqlFilterTranslator.cs +++ b/dotnet/src/VectorData/Common/SqlFilterTranslator.cs @@ -196,7 +196,7 @@ private void TranslateMember(MemberExpression memberExpression, bool isSearchCon throw new NotSupportedException($"Member access for '{memberExpression.Member.Name}' is unsupported - only member access over the filter parameter are supported"); } - protected virtual void GenerateColumn(PropertyModel property, bool isSearchCondition = false) + protected virtual void GenerateColumn(IPropertyModel property, bool isSearchCondition = false) // StorageName is considered to be a safe input, we quote and escape it mostly to produce valid SQL. => this._sql.Append('"').Append(property.StorageName.Replace("\"", "\"\"")).Append('"'); @@ -332,7 +332,7 @@ private void TranslateAny(Expression source, LambdaExpression lambda) } } - protected abstract void TranslateAnyContainsOverArrayColumn(PropertyModel property, object? values); + protected abstract void TranslateAnyContainsOverArrayColumn(IPropertyModel property, object? values); private void TranslateUnary(UnaryExpression unary, bool isSearchCondition) { diff --git a/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoCollectionCreateMapping.cs b/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoCollectionCreateMapping.cs index 08ab4c859e7e..14f3944356b8 100644 --- a/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoCollectionCreateMapping.cs +++ b/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoCollectionCreateMapping.cs @@ -21,7 +21,7 @@ internal static class CosmosMongoCollectionCreateMapping /// Number of clusters that the inverted file (IVF) index uses to group the vector data. /// The size of the dynamic candidate list for constructing the graph. public static BsonArray GetVectorIndexes( - IReadOnlyList vectorProperties, + IReadOnlyList vectorProperties, HashSet uniqueIndexes, int numLists, int efConstruction) @@ -71,7 +71,7 @@ public static BsonArray GetVectorIndexes( /// Collection of data properties for index creation. /// Collection of unique existing indexes to avoid creating duplicates. public static BsonArray GetFilterableDataIndexes( - IReadOnlyList dataProperties, + IReadOnlyList dataProperties, HashSet uniqueIndexes) { var indexArray = new BsonArray(); diff --git a/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoFilterTranslator.cs b/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoFilterTranslator.cs index 5fca84cbe3d3..dbd99d06d6f4 100644 --- a/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoFilterTranslator.cs +++ b/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoFilterTranslator.cs @@ -62,7 +62,7 @@ private BsonDocument TranslateEqualityComparison(BinaryExpression binary) ? this.GenerateEqualityComparison(property, leftConstant, binary.NodeType) : throw new NotSupportedException("Invalid equality/comparison"); - private BsonDocument GenerateEqualityComparison(PropertyModel property, object? value, ExpressionType nodeType) + private BsonDocument GenerateEqualityComparison(IPropertyModel property, object? value, ExpressionType nodeType) { if (value is null) { diff --git a/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlCollection.cs b/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlCollection.cs index d535a16e6d31..fee83a1c5b5b 100644 --- a/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlCollection.cs +++ b/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlCollection.cs @@ -56,7 +56,7 @@ public class CosmosNoSqlCollection : VectorStoreCollectionThe properties to use as partition key (supports hierarchical partition keys up to 3 levels). - private readonly List _partitionKeyProperties; + private readonly List _partitionKeyProperties; /// The mapper to use when mapping between the consumer data model and the Azure CosmosDB NoSQL record. private readonly ICosmosNoSqlMapper _mapper; @@ -181,7 +181,7 @@ internal CosmosNoSqlCollection( throw new ArgumentException("Cosmos DB supports at most 3 levels of hierarchical partition keys."); } - this._partitionKeyProperties = new List(options.PartitionKeyProperties.Count); + this._partitionKeyProperties = new List(options.PartitionKeyProperties.Count); foreach (var propertyName in options.PartitionKeyProperties) { @@ -563,7 +563,7 @@ public override async IAsyncEnumerable> SearchAsync< } } - private static async ValueTask GetSearchVectorAsync(TInput searchValue, VectorPropertyModel vectorProperty, CancellationToken cancellationToken) + private static async ValueTask GetSearchVectorAsync(TInput searchValue, IVectorPropertyModel vectorProperty, CancellationToken cancellationToken) where TInput : notnull => searchValue switch { diff --git a/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlCollectionQueryBuilder.cs b/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlCollectionQueryBuilder.cs index 0a07992b582b..fba7248a67b6 100644 --- a/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlCollectionQueryBuilder.cs +++ b/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlCollectionQueryBuilder.cs @@ -40,10 +40,10 @@ public static QueryDefinition BuildSearchQuery( var tableVariableName = CosmosNoSqlConstants.ContainerAlias; - IEnumerable projectionProperties = model.Properties; + IEnumerable projectionProperties = model.Properties; if (!includeVectors) { - projectionProperties = projectionProperties.Where(p => p is not VectorPropertyModel); + projectionProperties = projectionProperties.Where(p => p is not IVectorPropertyModel); } var fieldsArgument = projectionProperties.Select(p => GeneratePropertyAccess(tableVariableName, p.StorageName)); var vectorDistanceArgument = $"VectorDistance({GeneratePropertyAccess(tableVariableName, vectorPropertyName)}, {VectorVariableName})"; @@ -167,10 +167,10 @@ internal static QueryDefinition BuildSearchQuery( { var tableVariableName = CosmosNoSqlConstants.ContainerAlias; - IEnumerable projectionProperties = model.Properties; + IEnumerable projectionProperties = model.Properties; if (!filterOptions.IncludeVectors) { - projectionProperties = projectionProperties.Where(p => p is not VectorPropertyModel); + projectionProperties = projectionProperties.Where(p => p is not IVectorPropertyModel); } var fieldsArgument = projectionProperties.Select(field => GeneratePropertyAccess(tableVariableName, field.StorageName)); diff --git a/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlDynamicMapper.cs b/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlDynamicMapper.cs index 31720476142a..641a86743ba6 100644 --- a/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlDynamicMapper.cs +++ b/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlDynamicMapper.cs @@ -123,7 +123,7 @@ static bool TryGetReadOnlyMemory(object value, [NotNullWhen(true)] out ReadOn { switch (property) { - case KeyPropertyModel keyProperty: + case IKeyPropertyModel keyProperty: var key = (string?)storageModel[CosmosNoSqlConstants.ReservedKeyPropertyName] ?? throw new InvalidOperationException($"The key property '{keyProperty.StorageName}' is missing from the record retrieved from storage."); @@ -136,14 +136,14 @@ static bool TryGetReadOnlyMemory(object value, [NotNullWhen(true)] out ReadOn continue; - case DataPropertyModel dataProperty: + case IDataPropertyModel dataProperty: if (storageModel.TryGetPropertyValue(dataProperty.StorageName, out var dataValue)) { result.Add(property.ModelName, dataValue.Deserialize(property.Type, jsonSerializerOptions)); } continue; - case VectorPropertyModel vectorProperty: + case IVectorPropertyModel vectorProperty: if (includeVectors && storageModel.TryGetPropertyValue(vectorProperty.StorageName, out var vectorValue)) { if (vectorValue is not null) diff --git a/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlFilterTranslator.cs b/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlFilterTranslator.cs index 63f173a8cf45..b6ebb358b7e2 100644 --- a/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlFilterTranslator.cs +++ b/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlFilterTranslator.cs @@ -300,7 +300,7 @@ private void TranslateAny(Expression source, LambdaExpression lambda) } } - private void GenerateAnyContains(PropertyModel property, object? values) + private void GenerateAnyContains(IPropertyModel property, object? values) { this._sql.Append("EXISTS(SELECT VALUE t FROM t IN "); this.GeneratePropertyAccess(property); @@ -309,7 +309,7 @@ private void GenerateAnyContains(PropertyModel property, object? values) this._sql.Append(", t))"); } - private void GenerateAnyContains(PropertyModel property, QueryParameterExpression queryParameter) + private void GenerateAnyContains(IPropertyModel property, QueryParameterExpression queryParameter) { this._sql.Append("EXISTS(SELECT VALUE t FROM t IN "); this.GeneratePropertyAccess(property); @@ -361,7 +361,7 @@ protected void TranslateQueryParameter(string name, object? value) this._sql.Append(name); } - protected virtual void GeneratePropertyAccess(PropertyModel property) + protected virtual void GeneratePropertyAccess(IPropertyModel property) => this._sql .Append(CosmosNoSqlConstants.ContainerAlias) .Append("[\"") diff --git a/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlMapper.cs b/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlMapper.cs index 6f0ffdaea91b..fdf20e1f74ab 100644 --- a/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlMapper.cs +++ b/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlMapper.cs @@ -19,7 +19,7 @@ internal sealed class CosmosNoSqlMapper : ICosmosNoSqlMapper where TRecord : class { private readonly CollectionModel _model; - private readonly KeyPropertyModel _keyProperty; + private readonly IKeyPropertyModel _keyProperty; private readonly JsonSerializerOptions _jsonSerializerOptions; public CosmosNoSqlMapper(CollectionModel model, JsonSerializerOptions? jsonSerializerOptions) diff --git a/dotnet/src/VectorData/MongoDB/MongoCollection.cs b/dotnet/src/VectorData/MongoDB/MongoCollection.cs index 7cb8f234ec16..feff16d46873 100644 --- a/dotnet/src/VectorData/MongoDB/MongoCollection.cs +++ b/dotnet/src/VectorData/MongoDB/MongoCollection.cs @@ -426,7 +426,7 @@ public override async IAsyncEnumerable> SearchAsync< } } - private static async ValueTask GetSearchVectorArrayAsync(TInput searchValue, VectorPropertyModel vectorProperty, CancellationToken cancellationToken) + private static async ValueTask GetSearchVectorArrayAsync(TInput searchValue, IVectorPropertyModel vectorProperty, CancellationToken cancellationToken) where TInput : notnull { if (searchValue is float[] array) diff --git a/dotnet/src/VectorData/MongoDB/MongoCollectionCreateMapping.cs b/dotnet/src/VectorData/MongoDB/MongoCollectionCreateMapping.cs index 31fa5bd7ba4d..c16950464462 100644 --- a/dotnet/src/VectorData/MongoDB/MongoCollectionCreateMapping.cs +++ b/dotnet/src/VectorData/MongoDB/MongoCollectionCreateMapping.cs @@ -17,7 +17,7 @@ internal static class MongoCollectionCreateMapping /// Returns an array of indexes to create for vector properties. /// /// Collection of vector properties for index creation. - public static BsonArray GetVectorIndexFields(IReadOnlyList vectorProperties) + public static BsonArray GetVectorIndexFields(IReadOnlyList vectorProperties) { var indexArray = new BsonArray(); @@ -42,7 +42,7 @@ public static BsonArray GetVectorIndexFields(IReadOnlyList /// Returns an array of indexes to create for filterable data properties. /// /// Collection of data properties for index creation. - public static BsonArray GetFilterableDataIndexFields(IReadOnlyList dataProperties) + public static BsonArray GetFilterableDataIndexFields(IReadOnlyList dataProperties) { var indexArray = new BsonArray(); @@ -68,7 +68,7 @@ public static BsonArray GetFilterableDataIndexFields(IReadOnlyList /// Collection of data properties for index creation. - public static List GetFullTextSearchableDataIndexFields(IReadOnlyList dataProperties) + public static List GetFullTextSearchableDataIndexFields(IReadOnlyList dataProperties) { var fieldElements = new List(); diff --git a/dotnet/src/VectorData/MongoDB/MongoFilterTranslator.cs b/dotnet/src/VectorData/MongoDB/MongoFilterTranslator.cs index 9f32e3d0f4be..d2d08137c742 100644 --- a/dotnet/src/VectorData/MongoDB/MongoFilterTranslator.cs +++ b/dotnet/src/VectorData/MongoDB/MongoFilterTranslator.cs @@ -62,7 +62,7 @@ private BsonDocument TranslateEqualityComparison(BinaryExpression binary) ? this.GenerateEqualityComparison(property, leftConstant, binary.NodeType) : throw new NotSupportedException("Invalid equality/comparison"); - private BsonDocument GenerateEqualityComparison(PropertyModel property, object? value, ExpressionType nodeType) + private BsonDocument GenerateEqualityComparison(IPropertyModel property, object? value, ExpressionType nodeType) { if (value is null) { diff --git a/dotnet/src/VectorData/PgVector/PostgresCollection.cs b/dotnet/src/VectorData/PgVector/PostgresCollection.cs index 79d47b7f794c..1142f7e2549b 100644 --- a/dotnet/src/VectorData/PgVector/PostgresCollection.cs +++ b/dotnet/src/VectorData/PgVector/PostgresCollection.cs @@ -179,7 +179,7 @@ public override async Task UpsertAsync(IEnumerable records, Cancellatio IReadOnlyList? recordsList = null; // If an embedding generator is defined, invoke it once per property for all records. - Dictionary>? generatedEmbeddings = null; + Dictionary>? generatedEmbeddings = null; var vectorPropertyCount = this._model.VectorProperties.Count; for (var i = 0; i < vectorPropertyCount; i++) @@ -209,7 +209,7 @@ public override async Task UpsertAsync(IEnumerable records, Cancellatio // TODO: Ideally we'd group together vector properties using the same generator (and with the same input and output properties), // and generate embeddings for them in a single batch. That's some more complexity though. - generatedEmbeddings ??= new Dictionary>(vectorPropertyCount); + generatedEmbeddings ??= new Dictionary>(vectorPropertyCount); generatedEmbeddings[vectorProperty] = await vectorProperty.GenerateEmbeddingsAsync(records.Select(r => vectorProperty.GetValueAsObject(r)), cancellationToken).ConfigureAwait(false); } @@ -554,7 +554,7 @@ private Task RunOperationAsync(string operationName, Func> operati /// /// Converts a search input value to a PostgreSQL vector representation, generating embeddings if necessary. /// - private async Task ConvertSearchInputToVectorAsync(TInput searchValue, VectorPropertyModel vectorProperty, CancellationToken cancellationToken) + private async Task ConvertSearchInputToVectorAsync(TInput searchValue, IVectorPropertyModel vectorProperty, CancellationToken cancellationToken) where TInput : notnull { object vector = searchValue switch diff --git a/dotnet/src/VectorData/PgVector/PostgresFilterTranslator.cs b/dotnet/src/VectorData/PgVector/PostgresFilterTranslator.cs index eb6d38f9ddb8..9a4e495f0488 100644 --- a/dotnet/src/VectorData/PgVector/PostgresFilterTranslator.cs +++ b/dotnet/src/VectorData/PgVector/PostgresFilterTranslator.cs @@ -105,7 +105,7 @@ protected override void TranslateContainsOverParameterizedArray(Expression sourc this._sql.Append(')'); } - protected override void TranslateAnyContainsOverArrayColumn(PropertyModel property, object? values) + protected override void TranslateAnyContainsOverArrayColumn(IPropertyModel property, object? values) { // Translate r.Strings.Any(s => array.Contains(s)) to: column && ARRAY[values] // The && operator checks if the two arrays have any elements in common diff --git a/dotnet/src/VectorData/PgVector/PostgresMapper.cs b/dotnet/src/VectorData/PgVector/PostgresMapper.cs index 57d4c2b56a18..4b13b67c3399 100644 --- a/dotnet/src/VectorData/PgVector/PostgresMapper.cs +++ b/dotnet/src/VectorData/PgVector/PostgresMapper.cs @@ -103,7 +103,7 @@ public TRecord MapFromStorageToDataModel(NpgsqlDataReader reader, bool includeVe return record; } - private static void PopulateProperty(PropertyModel property, NpgsqlDataReader reader, TRecord record) + private static void PopulateProperty(IPropertyModel property, NpgsqlDataReader reader, TRecord record) { int ordinal = reader.GetOrdinal(property.StorageName); diff --git a/dotnet/src/VectorData/PgVector/PostgresPropertyExtensions.cs b/dotnet/src/VectorData/PgVector/PostgresPropertyExtensions.cs index 1ef1ef9071d7..5893a6026624 100644 --- a/dotnet/src/VectorData/PgVector/PostgresPropertyExtensions.cs +++ b/dotnet/src/VectorData/PgVector/PostgresPropertyExtensions.cs @@ -50,7 +50,7 @@ public static VectorStoreDataProperty WithFullTextSearchLanguage(this VectorStor /// /// The data property model to read from. /// The configured language, or the default language ("english") if not set. - internal static string GetFullTextSearchLanguageOrDefault(this DataPropertyModel property) + internal static string GetFullTextSearchLanguageOrDefault(this IDataPropertyModel property) => property.ProviderAnnotations?.TryGetValue(FullTextSearchLanguageKey, out var value) == true && value is string language ? language : PostgresConstants.DefaultFullTextSearchLanguage; @@ -109,7 +109,7 @@ public static TProperty WithStoreType(this TProperty property, string /// /// Gets whether the property model has been configured with a timestamp (without time zone) store type. /// - internal static bool IsTimestampWithoutTimezone(this PropertyModel property) + internal static bool IsTimestampWithoutTimezone(this IPropertyModel property) => property.ProviderAnnotations?.TryGetValue(StoreTypeKey, out var value) == true && value is string storeType && IsTimestampStoreType(storeType); diff --git a/dotnet/src/VectorData/PgVector/PostgresPropertyMapping.cs b/dotnet/src/VectorData/PgVector/PostgresPropertyMapping.cs index 15e872ec70b2..426517583f39 100644 --- a/dotnet/src/VectorData/PgVector/PostgresPropertyMapping.cs +++ b/dotnet/src/VectorData/PgVector/PostgresPropertyMapping.cs @@ -41,7 +41,7 @@ internal static class PostgresPropertyMapping /// /// Gets the NpgsqlDbType for a property, taking into account any store type annotation. /// - internal static NpgsqlDbType? GetNpgsqlDbType(PropertyModel property) + internal static NpgsqlDbType? GetNpgsqlDbType(IPropertyModel property) => (Nullable.GetUnderlyingType(property.Type) ?? property.Type) switch { Type t when t == typeof(bool) => NpgsqlDbType.Boolean, @@ -72,7 +72,7 @@ internal static class PostgresPropertyMapping /// /// Maps a .NET type to a PostgreSQL type name, taking into account any store type annotation on the property. /// - internal static (string PgType, bool IsNullable) GetPostgresTypeName(PropertyModel property) + internal static (string PgType, bool IsNullable) GetPostgresTypeName(IPropertyModel property) { static bool TryGetBaseType(Type type, [NotNullWhen(true)] out string? typeName) { @@ -142,7 +142,7 @@ static bool TryGetBaseType(Type type, [NotNullWhen(true)] out string? typeName) /// /// The vector property. /// The PostgreSQL vector type name. - public static (string PgType, bool IsNullable) GetPgVectorTypeName(VectorPropertyModel vectorProperty) + public static (string PgType, bool IsNullable) GetPgVectorTypeName(IVectorPropertyModel vectorProperty) { var unwrappedEmbeddingType = Nullable.GetUnderlyingType(vectorProperty.EmbeddingType) ?? vectorProperty.EmbeddingType; @@ -181,18 +181,18 @@ public static NpgsqlParameter GetNpgsqlParameter(object? value) /// /// The default index kind is "Flat", which prevents the creation of an index. /// - public static List<(string column, string kind, string function, bool isVector, bool isFullText, string? fullTextLanguage)> GetIndexInfo(IReadOnlyList properties) + public static List<(string column, string kind, string function, bool isVector, bool isFullText, string? fullTextLanguage)> GetIndexInfo(IReadOnlyList properties) { var vectorIndexesToCreate = new List<(string column, string kind, string function, bool isVector, bool isFullText, string? fullTextLanguage)>(); foreach (var property in properties) { switch (property) { - case KeyPropertyModel: + case IKeyPropertyModel: // There is no need to create a separate index for the key property. break; - case VectorPropertyModel vectorProperty: + case IVectorPropertyModel vectorProperty: var indexKind = vectorProperty.IndexKind ?? PostgresConstants.DefaultIndexKind; var distanceFunction = vectorProperty.DistanceFunction ?? PostgresConstants.DefaultDistanceFunction; @@ -215,7 +215,7 @@ public static NpgsqlParameter GetNpgsqlParameter(object? value) break; - case DataPropertyModel dataProperty: + case IDataPropertyModel dataProperty: if (dataProperty.IsIndexed) { vectorIndexesToCreate.Add((dataProperty.StorageName, kind: "", function: "", isVector: false, isFullText: false, fullTextLanguage: null)); diff --git a/dotnet/src/VectorData/PgVector/PostgresSqlBuilder.cs b/dotnet/src/VectorData/PgVector/PostgresSqlBuilder.cs index c5374b053d78..317b99707bba 100644 --- a/dotnet/src/VectorData/PgVector/PostgresSqlBuilder.cs +++ b/dotnet/src/VectorData/PgVector/PostgresSqlBuilder.cs @@ -189,7 +189,7 @@ internal static bool BuildUpsertCommand( string tableName, CollectionModel model, IEnumerable records, - Dictionary>? generatedEmbeddings) + Dictionary>? generatedEmbeddings) { // Note: since keys may be auto-generated, we can't use a single multi-value INSERT statement, since that would return // the generated keys in random order. Use a batch of single-value INSERT statements instead. @@ -209,14 +209,14 @@ internal static bool BuildUpsertCommand( foreach (var property in model.Properties) { - if (property is KeyPropertyModel && autoGeneratedKey) + if (property is IKeyPropertyModel && autoGeneratedKey) { continue; } var value = property.GetValueAsObject(record); - if (property is VectorPropertyModel vectorProperty) + if (property is IVectorPropertyModel vectorProperty) { if (generatedEmbeddings?[vectorProperty] is IReadOnlyList ge) { @@ -268,7 +268,7 @@ string GenerateSingleUpsertSql(bool autoGeneratedKey) var i = 0; foreach (var property in model.Properties) { - if (property is KeyPropertyModel && autoGeneratedKey) + if (property is IKeyPropertyModel && autoGeneratedKey) { continue; } @@ -288,7 +288,7 @@ string GenerateSingleUpsertSql(bool autoGeneratedKey) i = 0; foreach (var property in model.Properties) { - if (property is KeyPropertyModel && autoGeneratedKey) + if (property is IKeyPropertyModel && autoGeneratedKey) { continue; } @@ -322,7 +322,7 @@ string GenerateSingleUpsertSql(bool autoGeneratedKey) i = 0; foreach (var property in model.Properties) { - if (property is KeyPropertyModel) + if (property is IKeyPropertyModel) { continue; } @@ -380,7 +380,7 @@ internal static void BuildGetBatchCommand(NpgsqlCommand command, string? s var first = true; foreach (var property in model.Properties) { - if (!includeVectors && property is VectorPropertyModel) + if (!includeVectors && property is IVectorPropertyModel) { continue; } @@ -418,7 +418,7 @@ internal static void BuildDeleteCommand(NpgsqlCommand command, string? sch } /// - internal static void BuildDeleteBatchCommand(NpgsqlCommand command, string? schema, string tableName, KeyPropertyModel keyProperty, List keys) + internal static void BuildDeleteBatchCommand(NpgsqlCommand command, string? schema, string tableName, IKeyPropertyModel keyProperty, List keys) { NpgsqlDbType? keyType = PostgresPropertyMapping.GetNpgsqlDbType(keyProperty) ?? throw new ArgumentException($"Unsupported key type {typeof(TKey).Name}"); @@ -491,7 +491,7 @@ private static (string Condition, List Parameters) GenerateFilterConditi /// internal static void BuildGetNearestMatchCommand( - NpgsqlCommand command, string? schema, string tableName, CollectionModel model, VectorPropertyModel vectorProperty, object vectorValue, + NpgsqlCommand command, string? schema, string tableName, CollectionModel model, IVectorPropertyModel vectorProperty, object vectorValue, Expression>? filter, int? skip, bool includeVectors, int limit, double? scoreThreshold = null) { @@ -603,7 +603,7 @@ internal static void BuildSelectWhereCommand( var first = true; foreach (var property in model.Properties) { - if (options.IncludeVectors || property is not VectorPropertyModel) + if (options.IncludeVectors || property is not IVectorPropertyModel) { if (!first) { @@ -683,7 +683,7 @@ private static StringBuilder AppendTableName(this StringBuilder sb, string? sche /// internal static void BuildHybridSearchCommand( NpgsqlCommand command, string? schema, string tableName, CollectionModel model, - VectorPropertyModel vectorProperty, DataPropertyModel textProperty, + IVectorPropertyModel vectorProperty, IDataPropertyModel textProperty, object vectorValue, ICollection keywords, Expression>? filter, int? skip, bool includeVectors, int top, double? scoreThreshold = null) @@ -695,7 +695,7 @@ internal static void BuildHybridSearchCommand( StringBuilder columns = new(); for (var i = 0; i < model.Properties.Count; i++) { - if (!includeVectors && model.Properties[i] is VectorPropertyModel) + if (!includeVectors && model.Properties[i] is IVectorPropertyModel) { continue; } diff --git a/dotnet/src/VectorData/Pinecone/PineconeCollection.cs b/dotnet/src/VectorData/Pinecone/PineconeCollection.cs index 66a303936849..4192d3dd2ca6 100644 --- a/dotnet/src/VectorData/Pinecone/PineconeCollection.cs +++ b/dotnet/src/VectorData/Pinecone/PineconeCollection.cs @@ -596,7 +596,7 @@ private static ServerlessSpecCloud MapCloud(string serverlessIndexCloud) _ => throw new ArgumentException($"Invalid serverless index cloud: {serverlessIndexCloud}.", nameof(serverlessIndexCloud)) }; - private static CreateIndexRequestMetric MapDistanceFunction(VectorPropertyModel vectorProperty) + private static CreateIndexRequestMetric MapDistanceFunction(IVectorPropertyModel vectorProperty) => vectorProperty.DistanceFunction switch { DistanceFunction.CosineSimilarity or null => CreateIndexRequestMetric.Cosine, diff --git a/dotnet/src/VectorData/Pinecone/PineconeFilterTranslator.cs b/dotnet/src/VectorData/Pinecone/PineconeFilterTranslator.cs index 0fe0875e0b81..d0012d451de9 100644 --- a/dotnet/src/VectorData/Pinecone/PineconeFilterTranslator.cs +++ b/dotnet/src/VectorData/Pinecone/PineconeFilterTranslator.cs @@ -71,7 +71,7 @@ private Metadata TranslateEqualityComparison(BinaryExpression binary) ? this.GenerateEqualityComparison(property, leftConstant, binary.NodeType) : throw new NotSupportedException("Invalid equality/comparison"); - private Metadata GenerateEqualityComparison(PropertyModel property, object? value, ExpressionType nodeType) + private Metadata GenerateEqualityComparison(IPropertyModel property, object? value, ExpressionType nodeType) { if (value is null) { diff --git a/dotnet/src/VectorData/Qdrant/QdrantCollection.cs b/dotnet/src/VectorData/Qdrant/QdrantCollection.cs index d37f90a60d76..7e9390379539 100644 --- a/dotnet/src/VectorData/Qdrant/QdrantCollection.cs +++ b/dotnet/src/VectorData/Qdrant/QdrantCollection.cs @@ -580,7 +580,7 @@ public override async IAsyncEnumerable> SearchAsync< } } - private static async ValueTask GetSearchVectorArrayAsync(TInput searchValue, VectorPropertyModel vectorProperty, CancellationToken cancellationToken) + private static async ValueTask GetSearchVectorArrayAsync(TInput searchValue, IVectorPropertyModel vectorProperty, CancellationToken cancellationToken) where TInput : notnull { if (searchValue is float[] array) diff --git a/dotnet/src/VectorData/Qdrant/QdrantCollectionCreateMapping.cs b/dotnet/src/VectorData/Qdrant/QdrantCollectionCreateMapping.cs index c03145ce8fb5..3840d8ea5c80 100644 --- a/dotnet/src/VectorData/Qdrant/QdrantCollectionCreateMapping.cs +++ b/dotnet/src/VectorData/Qdrant/QdrantCollectionCreateMapping.cs @@ -61,7 +61,7 @@ internal static class QdrantCollectionCreateMapping /// The property to map. /// The mapped . /// Thrown if the property is missing information or has unsupported options specified. - public static VectorParams MapSingleVector(VectorPropertyModel vectorProperty) + public static VectorParams MapSingleVector(IVectorPropertyModel vectorProperty) { if (vectorProperty!.IndexKind is not null and not IndexKind.Hnsw) { @@ -77,7 +77,7 @@ public static VectorParams MapSingleVector(VectorPropertyModel vectorProperty) /// The properties to map. /// THe mapped . /// Thrown if the property is missing information or has unsupported options specified. - public static VectorParamsMap MapNamedVectors(IEnumerable vectorProperties) + public static VectorParamsMap MapNamedVectors(IEnumerable vectorProperties) { var vectorParamsMap = new VectorParamsMap(); @@ -97,7 +97,7 @@ public static VectorParamsMap MapNamedVectors(IEnumerable v /// The vector property definition. /// The chosen . /// Thrown if a distance function is chosen that isn't supported by qdrant. - public static Distance GetSDKDistanceAlgorithm(VectorPropertyModel vectorProperty) + public static Distance GetSDKDistanceAlgorithm(IVectorPropertyModel vectorProperty) => vectorProperty.DistanceFunction switch { DistanceFunction.CosineSimilarity or null => Distance.Cosine, diff --git a/dotnet/src/VectorData/Qdrant/QdrantMapper.cs b/dotnet/src/VectorData/Qdrant/QdrantMapper.cs index 992b5fa05891..991c85458aef 100644 --- a/dotnet/src/VectorData/Qdrant/QdrantMapper.cs +++ b/dotnet/src/VectorData/Qdrant/QdrantMapper.cs @@ -86,7 +86,7 @@ generatedEmbeddings is null return pointStruct; - Vector GetVector(PropertyModel property, object? embedding) + Vector GetVector(IPropertyModel property, object? embedding) => embedding switch { ReadOnlyMemory m => m.ToArray(), @@ -128,7 +128,7 @@ public TRecord MapFromStorageToDataModel(PointId pointId, MapField data = value switch { diff --git a/dotnet/src/VectorData/Redis/RedisCollectionCreateMapping.cs b/dotnet/src/VectorData/Redis/RedisCollectionCreateMapping.cs index f276e8a3a429..abedad8a8dfb 100644 --- a/dotnet/src/VectorData/Redis/RedisCollectionCreateMapping.cs +++ b/dotnet/src/VectorData/Redis/RedisCollectionCreateMapping.cs @@ -51,7 +51,7 @@ internal static class RedisCollectionCreateMapping /// A value indicating whether to include $. prefix for field names as required in JSON mode. /// The mapped Redis . /// Thrown if there are missing required or unsupported configuration options set. - public static Schema MapToSchema(IEnumerable properties, bool useDollarPrefix) + public static Schema MapToSchema(IEnumerable properties, bool useDollarPrefix) { var schema = new Schema(); var fieldNamePrefix = useDollarPrefix ? "$." : string.Empty; @@ -63,11 +63,11 @@ public static Schema MapToSchema(IEnumerable properties, bool use switch (property) { - case KeyPropertyModel keyProperty: + case IKeyPropertyModel keyProperty: // Do nothing, since key is not stored as part of the payload and therefore doesn't have to be added to the index. continue; - case DataPropertyModel dataProperty when dataProperty.IsIndexed || dataProperty.IsFullTextIndexed: + case IDataPropertyModel dataProperty when dataProperty.IsIndexed || dataProperty.IsFullTextIndexed: if (dataProperty.IsIndexed && dataProperty.IsFullTextIndexed) { throw new InvalidOperationException($"Property '{dataProperty.ModelName}' has both {nameof(VectorStoreDataProperty.IsIndexed)} and {nameof(VectorStoreDataProperty.IsFullTextIndexed)} set to true, and this is not supported by the Redis VectorStore."); @@ -109,7 +109,7 @@ public static Schema MapToSchema(IEnumerable properties, bool use continue; - case VectorPropertyModel vectorProperty: + case IVectorPropertyModel vectorProperty: var indexKind = GetSDKIndexKind(vectorProperty); var vectorType = GetSDKVectorType(vectorProperty); var dimensions = vectorProperty.Dimensions.ToString(CultureInfo.InvariantCulture); @@ -139,7 +139,7 @@ static bool IsTagsType(Type type) /// The vector property definition. /// The chosen . /// Thrown if a index type was chosen that isn't supported by Redis. - public static Schema.VectorField.VectorAlgo GetSDKIndexKind(VectorPropertyModel vectorProperty) + public static Schema.VectorField.VectorAlgo GetSDKIndexKind(IVectorPropertyModel vectorProperty) => vectorProperty.IndexKind switch { IndexKind.Hnsw or null => Schema.VectorField.VectorAlgo.HNSW, @@ -154,7 +154,7 @@ public static Schema.VectorField.VectorAlgo GetSDKIndexKind(VectorPropertyModel /// The vector property definition. /// The chosen distance metric. /// Thrown if a distance function is chosen that isn't supported by Redis. - public static string GetSDKDistanceAlgorithm(VectorPropertyModel vectorProperty) + public static string GetSDKDistanceAlgorithm(IVectorPropertyModel vectorProperty) => vectorProperty.DistanceFunction switch { DistanceFunction.CosineSimilarity or null => "COSINE", @@ -171,7 +171,7 @@ public static string GetSDKDistanceAlgorithm(VectorPropertyModel vectorProperty) /// The vector property definition. /// The SDK required vector type. /// Thrown if the property data type is not supported by the connector. - public static string GetSDKVectorType(VectorPropertyModel vectorProperty) + public static string GetSDKVectorType(IVectorPropertyModel vectorProperty) => (Nullable.GetUnderlyingType(vectorProperty.EmbeddingType) ?? vectorProperty.EmbeddingType) switch { Type t when t == typeof(ReadOnlyMemory) => "FLOAT32", diff --git a/dotnet/src/VectorData/Redis/RedisCollectionSearchMapping.cs b/dotnet/src/VectorData/Redis/RedisCollectionSearchMapping.cs index e9c36fd87605..d037fe5effbd 100644 --- a/dotnet/src/VectorData/Redis/RedisCollectionSearchMapping.cs +++ b/dotnet/src/VectorData/Redis/RedisCollectionSearchMapping.cs @@ -47,7 +47,7 @@ public static byte[] ValidateVectorAndConvertToBytes(TVector vector, st /// The vector property. /// The set of fields to limit the results to. Null for all. /// The . - public static Query BuildQuery(byte[] vectorBytes, int top, VectorSearchOptions options, CollectionModel model, VectorPropertyModel vectorProperty, string[]? selectFields) + public static Query BuildQuery(byte[] vectorBytes, int top, VectorSearchOptions options, CollectionModel model, IVectorPropertyModel vectorProperty, string[]? selectFields) { // Build search query. var redisLimit = top + options.Skip; @@ -101,7 +101,7 @@ internal static Query BuildQuery(Expression> filter /// /// The vector property to be used. /// The distance function for the vector we want to search. - public static string ResolveDistanceFunction(VectorPropertyModel vectorProperty) + public static string ResolveDistanceFunction(IVectorPropertyModel vectorProperty) => vectorProperty.DistanceFunction ?? DistanceFunction.CosineSimilarity; /// diff --git a/dotnet/src/VectorData/Redis/RedisJsonMapper.cs b/dotnet/src/VectorData/Redis/RedisJsonMapper.cs index 7755866b1c16..16c2d863fdaa 100644 --- a/dotnet/src/VectorData/Redis/RedisJsonMapper.cs +++ b/dotnet/src/VectorData/Redis/RedisJsonMapper.cs @@ -114,7 +114,7 @@ public TConsumerDataModel MapFromStorageToDataModel((string Key, JsonNode Node) JsonArray and [JsonObject arrayEntryJsonObject] => arrayEntryJsonObject, JsonValue when model.DataProperties.Count + (includeVectors ? model.VectorProperties.Count : 0) == 1 => new JsonObject { - [model.DataProperties.Concat(model.VectorProperties).First().StorageName] = storageModel.Node + [model.DataProperties.Concat(model.VectorProperties).First().StorageName] = storageModel.Node }, _ => throw new InvalidOperationException($"Invalid data format for document with key '{storageModel.Key}'") }; diff --git a/dotnet/src/VectorData/SqlServer/SqlServerCollection.cs b/dotnet/src/VectorData/SqlServer/SqlServerCollection.cs index 470be57ab494..bfc5ed3b22dd 100644 --- a/dotnet/src/VectorData/SqlServer/SqlServerCollection.cs +++ b/dotnet/src/VectorData/SqlServer/SqlServerCollection.cs @@ -352,7 +352,7 @@ public override async Task UpsertAsync(TRecord record, CancellationToken cancell { Verify.NotNull(record); - Dictionary>? generatedEmbeddings = null; + Dictionary>? generatedEmbeddings = null; var vectorPropertyCount = this._model.VectorProperties.Count; for (var i = 0; i < vectorPropertyCount; i++) @@ -369,7 +369,7 @@ public override async Task UpsertAsync(TRecord record, CancellationToken cancell // TODO: Ideally we'd group together vector properties using the same generator (and with the same input and output properties), // and generate embeddings for them in a single batch. That's some more complexity though. - generatedEmbeddings ??= new Dictionary>(vectorPropertyCount); + generatedEmbeddings ??= new Dictionary>(vectorPropertyCount); generatedEmbeddings[vectorProperty] = [await vectorProperty.GenerateEmbeddingAsync(vectorProperty.GetValueAsObject(record), cancellationToken).ConfigureAwait(false)]; } @@ -414,7 +414,7 @@ public override async Task UpsertAsync(IEnumerable records, Cancellatio IReadOnlyList? recordsList = null; // If an embedding generator is defined, invoke it once per property for all records. - Dictionary>? generatedEmbeddings = null; + Dictionary>? generatedEmbeddings = null; var vectorPropertyCount = this._model.VectorProperties.Count; for (var i = 0; i < vectorPropertyCount; i++) @@ -445,7 +445,7 @@ public override async Task UpsertAsync(IEnumerable records, Cancellatio // TODO: Ideally we'd group together vector properties using the same generator (and with the same input and output properties), // and generate embeddings for them in a single batch. That's some more complexity though. - generatedEmbeddings ??= new Dictionary>(vectorPropertyCount); + generatedEmbeddings ??= new Dictionary>(vectorPropertyCount); generatedEmbeddings[vectorProperty] = await vectorProperty.GenerateEmbeddingsAsync(records.Select(r => vectorProperty.GetValueAsObject(r)), cancellationToken).ConfigureAwait(false); } diff --git a/dotnet/src/VectorData/SqlServer/SqlServerCommandBuilder.cs b/dotnet/src/VectorData/SqlServer/SqlServerCommandBuilder.cs index eabfc794f34e..9a3bf47a6ef0 100644 --- a/dotnet/src/VectorData/SqlServer/SqlServerCommandBuilder.cs +++ b/dotnet/src/VectorData/SqlServer/SqlServerCommandBuilder.cs @@ -103,7 +103,7 @@ internal static List CreateTable( } // Create full-text catalog and index for properties marked as IsFullTextIndexed - var fullTextProperties = new List(); + var fullTextProperties = new List(); foreach (var dataProperty in model.DataProperties) { if (dataProperty.IsFullTextIndexed) @@ -228,7 +228,7 @@ FROM INFORMATION_SCHEMA.TABLES /// Checks if the key property uses SQL Server IDENTITY (for int/bigint) as opposed to DEFAULT (for GUID). /// IDENTITY columns require SET IDENTITY_INSERT ON to insert explicit values. /// - private static bool UsesIdentity(KeyPropertyModel keyProperty) + private static bool UsesIdentity(IKeyPropertyModel keyProperty) { if (!keyProperty.IsAutoGenerated) { @@ -249,7 +249,7 @@ internal static bool Upsert( CollectionModel model, IEnumerable records, int firstRecordIndex, - Dictionary>? generatedEmbeddings) + Dictionary>? generatedEmbeddings) { var keyProperty = model.KeyProperty; StringBuilder sb = new(500); @@ -282,14 +282,14 @@ internal static bool Upsert( foreach (var property in model.Properties) { // Skip key in VALUES when auto-generating - if (property is KeyPropertyModel && skipKeyInInsert) + if (property is IKeyPropertyModel && skipKeyInInsert) { continue; } sb.AppendParameterName(property, ref paramIndex, out var paramName).Append(','); - var value = property is VectorPropertyModel vectorProperty && generatedEmbeddings?.TryGetValue(vectorProperty, out var ge) == true + var value = property is IVectorPropertyModel vectorProperty && generatedEmbeddings?.TryGetValue(vectorProperty, out var ge) == true ? ge[firstRecordIndex + rowIndex] : property.GetValueAsObject(record); @@ -314,7 +314,7 @@ internal static bool Upsert( sb.Append("UPDATE SET "); foreach (var property in model.Properties) { - if (property is not KeyPropertyModel) // don't update the key + if (property is not IKeyPropertyModel) // don't update the key { sb.Append("t.").AppendIdentifier(property.StorageName).Append(" = s.").AppendIdentifier(property.StorageName).Append(','); } @@ -356,7 +356,7 @@ internal static bool Upsert( internal static SqlCommand DeleteSingle( SqlConnection connection, string? schema, string tableName, - KeyPropertyModel keyProperty, object key) + IKeyPropertyModel keyProperty, object key) { SqlCommand command = connection.CreateCommand(); @@ -374,7 +374,7 @@ internal static SqlCommand DeleteSingle( internal static bool DeleteMany( SqlCommand command, string? schema, string tableName, - KeyPropertyModel keyProperty, IEnumerable keys) + IKeyPropertyModel keyProperty, IEnumerable keys) { StringBuilder sb = new(100); sb.Append("DELETE FROM "); @@ -444,7 +444,7 @@ internal static bool SelectMany( internal static SqlCommand SelectVector( SqlConnection connection, string? schema, string tableName, - VectorPropertyModel vectorProperty, + IVectorPropertyModel vectorProperty, CollectionModel model, int top, VectorSearchOptions options, @@ -460,7 +460,7 @@ internal static SqlCommand SelectVector( private static SqlCommand SelectVectorWithVectorDistance( SqlConnection connection, string? schema, string tableName, - VectorPropertyModel vectorProperty, + IVectorPropertyModel vectorProperty, CollectionModel model, int top, VectorSearchOptions options, @@ -527,7 +527,7 @@ private static SqlCommand SelectVectorWithVectorDistance( /// private static SqlCommand SelectVectorWithVectorSearch( SqlConnection connection, string? schema, string tableName, - VectorPropertyModel vectorProperty, + IVectorPropertyModel vectorProperty, CollectionModel model, int top, VectorSearchOptions options, @@ -577,8 +577,8 @@ private static SqlCommand SelectVectorWithVectorSearch( internal static SqlCommand SelectHybrid( SqlConnection connection, string? schema, string tableName, - VectorPropertyModel vectorProperty, - DataPropertyModel textProperty, + IVectorPropertyModel vectorProperty, + IDataPropertyModel textProperty, CollectionModel model, int top, HybridSearchOptions options, @@ -718,7 +718,7 @@ internal static SqlCommand SelectHybrid( sb.Append("SELECT "); foreach (var property in model.Properties) { - if (!options.IncludeVectors && property is VectorPropertyModel) + if (!options.IncludeVectors && property is IVectorPropertyModel) { continue; } @@ -806,7 +806,7 @@ internal static SqlCommand SelectWhere( return command; } - internal static StringBuilder AppendParameterName(this StringBuilder sb, PropertyModel property, ref int paramIndex, out string parameterName) + internal static StringBuilder AppendParameterName(this StringBuilder sb, IPropertyModel property, ref int paramIndex, out string parameterName) { // In SQL Server, parameter names cannot be just a number like "@1". // Parameter names must start with an alphabetic character or an underscore @@ -865,7 +865,7 @@ internal static StringBuilder AppendIdentifier(this StringBuilder sb, string ide } private static StringBuilder AppendIdentifiers(this StringBuilder sb, - IEnumerable properties, + IEnumerable properties, string? prefix = null, bool includeVectors = true, bool skipKey = false) @@ -873,12 +873,12 @@ private static StringBuilder AppendIdentifiers(this StringBuilder sb, bool any = false; foreach (var property in properties) { - if (!includeVectors && property is VectorPropertyModel) + if (!includeVectors && property is IVectorPropertyModel) { continue; } - if (skipKey && property is KeyPropertyModel) + if (skipKey && property is IKeyPropertyModel) { continue; } @@ -900,7 +900,7 @@ private static StringBuilder AppendIdentifiers(this StringBuilder sb, } private static StringBuilder AppendKeyParameterList(this StringBuilder sb, - IEnumerable keys, SqlCommand command, KeyPropertyModel keyProperty, out bool emptyKeys) + IEnumerable keys, SqlCommand command, IKeyPropertyModel keyProperty, out bool emptyKeys) { int keyIndex = 0; foreach (TKey key in keys) @@ -957,7 +957,7 @@ private static SqlCommand CreateCommand(this SqlConnection connection, StringBui return command; } - private static void AddParameter(this SqlCommand command, PropertyModel? property, string name, object? value) + private static void AddParameter(this SqlCommand command, IPropertyModel? property, string name, object? value) { switch (value) { @@ -998,7 +998,7 @@ private static void AddParameter(this SqlCommand command, PropertyModel? propert } } - private static string Map(PropertyModel property) + private static string Map(IPropertyModel property) => (Nullable.GetUnderlyingType(property.Type) ?? property.Type) switch { Type t when t == typeof(byte) => "TINYINT", @@ -1006,8 +1006,8 @@ private static string Map(PropertyModel property) Type t when t == typeof(int) => "INT", Type t when t == typeof(long) => "BIGINT", Type t when t == typeof(Guid) => "UNIQUEIDENTIFIER", - Type t when t == typeof(string) && property is KeyPropertyModel => "NVARCHAR(4000)", - Type t when t == typeof(string) && property is DataPropertyModel { IsIndexed: true } => "NVARCHAR(4000)", + Type t when t == typeof(string) && property is IKeyPropertyModel => "NVARCHAR(4000)", + Type t when t == typeof(string) && property is IDataPropertyModel { IsIndexed: true } => "NVARCHAR(4000)", Type t when t == typeof(string) => "NVARCHAR(MAX)", Type t when t == typeof(byte[]) => "VARBINARY(MAX)", Type t when t == typeof(bool) => "BIT", @@ -1043,6 +1043,6 @@ private static string Map(PropertyModel property) /// Returns whether VECTOR_SEARCH() (approximate/indexed search) should be used for the given vector property, /// as opposed to VECTOR_DISTANCE() (exact/brute-force search). /// - private static bool UseVectorSearch(VectorPropertyModel vectorProperty) + private static bool UseVectorSearch(IVectorPropertyModel vectorProperty) => vectorProperty.IndexKind is not (null or "" or IndexKind.Flat); } diff --git a/dotnet/src/VectorData/SqlServer/SqlServerFilterTranslator.cs b/dotnet/src/VectorData/SqlServer/SqlServerFilterTranslator.cs index 0ccacd33334c..7ff4fdebc3ad 100644 --- a/dotnet/src/VectorData/SqlServer/SqlServerFilterTranslator.cs +++ b/dotnet/src/VectorData/SqlServer/SqlServerFilterTranslator.cs @@ -65,7 +65,7 @@ protected override void TranslateConstant(object? value, bool isSearchCondition) } } - protected override void GenerateColumn(PropertyModel property, bool isSearchCondition = false) + protected override void GenerateColumn(IPropertyModel property, bool isSearchCondition = false) { // StorageName is considered to be a safe input, we quote and escape it mostly to produce valid SQL. if (this._tableAlias is not null) @@ -124,7 +124,7 @@ protected override void TranslateContainsOverParameterizedArray(Expression sourc this._sql.Append(')'); } - protected override void TranslateAnyContainsOverArrayColumn(PropertyModel property, object? values) + protected override void TranslateAnyContainsOverArrayColumn(IPropertyModel property, object? values) { // Translate r.Strings.Any(s => array.Contains(s)) to: // EXISTS(SELECT 1 FROM OPENJSON(column) WHERE value IN ('a', 'b', 'c')) diff --git a/dotnet/src/VectorData/SqlServer/SqlServerMapper.cs b/dotnet/src/VectorData/SqlServer/SqlServerMapper.cs index 684d07e251fb..515e4774d878 100644 --- a/dotnet/src/VectorData/SqlServer/SqlServerMapper.cs +++ b/dotnet/src/VectorData/SqlServer/SqlServerMapper.cs @@ -61,7 +61,7 @@ public TRecord MapFromStorageToDataModel(SqlDataReader reader, bool includeVecto return record; - static void PopulateValue(SqlDataReader reader, PropertyModel property, object record) + static void PopulateValue(SqlDataReader reader, IPropertyModel property, object record) { try { diff --git a/dotnet/src/VectorData/SqliteVec/SqliteCollection.cs b/dotnet/src/VectorData/SqliteVec/SqliteCollection.cs index 03d7849c1ddf..432923460f8c 100644 --- a/dotnet/src/VectorData/SqliteVec/SqliteCollection.cs +++ b/dotnet/src/VectorData/SqliteVec/SqliteCollection.cs @@ -546,7 +546,7 @@ private async Task DoUpsertAsync(IEnumerable records, CancellationToken records = recordsList; // If an embedding generator is defined, invoke it once per property for all records. - Dictionary>>? generatedEmbeddings = null; + Dictionary>>? generatedEmbeddings = null; var vectorPropertyCount = this._model.VectorProperties.Count; for (var i = 0; i < vectorPropertyCount; i++) @@ -563,7 +563,7 @@ private async Task DoUpsertAsync(IEnumerable records, CancellationToken // TODO: Ideally we'd group together vector properties using the same generator (and with the same input and output properties), // and generate embeddings for them in a single batch. That's some more complexity though. - generatedEmbeddings ??= new Dictionary>>(vectorPropertyCount); + generatedEmbeddings ??= new Dictionary>>(vectorPropertyCount); generatedEmbeddings[vectorProperty] = (IReadOnlyList>)await vectorProperty.GenerateEmbeddingsAsync(records.Select(r => vectorProperty.GetValueAsObject(r)), cancellationToken).ConfigureAwait(false); } @@ -588,7 +588,7 @@ private async Task DoUpsertAsync(IEnumerable records, CancellationToken { // If the key property is auto-generated, we need to read the generated keys from the database and inject them into the records // (except for GUIDs which are generated client-side and have already been injected). - if (keyProperty is KeyPropertyModel { IsAutoGenerated: true } && keyProperty.Type != typeof(Guid)) + if (keyProperty is IKeyPropertyModel { IsAutoGenerated: true } && keyProperty.Type != typeof(Guid)) { int? keyOrdinal = null; diff --git a/dotnet/src/VectorData/SqliteVec/SqliteCommandBuilder.cs b/dotnet/src/VectorData/SqliteVec/SqliteCommandBuilder.cs index 22ea94bdea9b..60d060281c5f 100644 --- a/dotnet/src/VectorData/SqliteVec/SqliteCommandBuilder.cs +++ b/dotnet/src/VectorData/SqliteVec/SqliteCommandBuilder.cs @@ -116,7 +116,7 @@ public static DbCommand BuildInsertCommand( string tableName, CollectionModel model, IReadOnlyList records, - Dictionary>>? generatedEmbeddings, + Dictionary>>? generatedEmbeddings, bool data, bool replaceIfExists = false) { @@ -125,7 +125,7 @@ public static DbCommand BuildInsertCommand( var recordIndex = 0; - var properties = model.KeyProperties.Concat(data ? model.DataProperties : (IEnumerable)model.VectorProperties).ToList(); + var properties = model.KeyProperties.Concat(data ? model.DataProperties : (IEnumerable)model.VectorProperties).ToList(); var keyProperty = model.KeyProperty; var isKeyPossiblyDatabaseGenerated = keyProperty.IsAutoGenerated && (keyProperty.Type == typeof(int) || keyProperty.Type == typeof(long)); @@ -148,7 +148,7 @@ public static DbCommand BuildInsertCommand( var propertyIndex = 0; foreach (var property in properties) { - if (property is KeyPropertyModel && isRecordKeyDatabaseGenerated) + if (property is IKeyPropertyModel && isRecordKeyDatabaseGenerated) { continue; } @@ -172,7 +172,7 @@ public static DbCommand BuildInsertCommand( switch (property) { - case KeyPropertyModel { IsAutoGenerated: true }: + case IKeyPropertyModel { IsAutoGenerated: true }: { switch (value) { @@ -206,7 +206,7 @@ public static DbCommand BuildInsertCommand( break; } - case VectorPropertyModel vectorProperty: + case IVectorPropertyModel vectorProperty: { if (generatedEmbeddings?[vectorProperty] is IReadOnlyList ge) { @@ -397,13 +397,13 @@ internal static StringBuilder AppendIdentifier(this StringBuilder sb, string ide #region private - private static StringBuilder AppendColumnNames(this StringBuilder builder, bool includeVectors, IReadOnlyList properties, + private static StringBuilder AppendColumnNames(this StringBuilder builder, bool includeVectors, IReadOnlyList properties, string? vectorTableName = null, string? dataTableName = null) { foreach (var property in properties) { string? tableName = dataTableName; - if (property is VectorPropertyModel) + if (property is IVectorPropertyModel) { if (!includeVectors) { diff --git a/dotnet/src/VectorData/SqliteVec/SqliteFilterTranslator.cs b/dotnet/src/VectorData/SqliteVec/SqliteFilterTranslator.cs index 3baed486eead..046d11dcde89 100644 --- a/dotnet/src/VectorData/SqliteVec/SqliteFilterTranslator.cs +++ b/dotnet/src/VectorData/SqliteVec/SqliteFilterTranslator.cs @@ -52,7 +52,7 @@ protected override void TranslateContainsOverArrayColumn(Expression source, Expr => throw new NotSupportedException("Unsupported Contains expression"); // TODO: support Any over array fields (#10343) - protected override void TranslateAnyContainsOverArrayColumn(PropertyModel property, object? values) + protected override void TranslateAnyContainsOverArrayColumn(IPropertyModel property, object? values) => throw new NotSupportedException("Unsupported method call: Enumerable.Any"); protected override void TranslateContainsOverParameterizedArray(Expression source, Expression item, object? value) diff --git a/dotnet/src/VectorData/SqliteVec/SqlitePropertyMapping.cs b/dotnet/src/VectorData/SqliteVec/SqlitePropertyMapping.cs index 4c4ac08859ee..ce81dda07535 100644 --- a/dotnet/src/VectorData/SqliteVec/SqlitePropertyMapping.cs +++ b/dotnet/src/VectorData/SqliteVec/SqlitePropertyMapping.cs @@ -24,7 +24,7 @@ public static byte[] MapVectorForStorageModel(ReadOnlyMemory memory) return byteArray; } - public static List GetColumns(IReadOnlyList properties, bool data) + public static List GetColumns(IReadOnlyList properties, bool data) { const string DistanceMetricConfigurationName = "distance_metric"; @@ -37,7 +37,7 @@ public static List GetColumns(IReadOnlyList propert string propertyType; Dictionary? configuration = null; - if (property is VectorPropertyModel vectorProperty) + if (property is IVectorPropertyModel vectorProperty) { if (data) { @@ -50,7 +50,7 @@ public static List GetColumns(IReadOnlyList propert [DistanceMetricConfigurationName] = GetDistanceMetric(vectorProperty) }; } - else if (property is DataPropertyModel dataProperty) + else if (property is IDataPropertyModel dataProperty) { if (!data) { @@ -62,7 +62,7 @@ public static List GetColumns(IReadOnlyList propert else { // The Key column in included in both Vector and Data tables. - Debug.Assert(property is KeyPropertyModel, "property is VectorStoreRecordKeyPropertyModel"); + Debug.Assert(property is IKeyPropertyModel, "property is VectorStoreRecordIKeyPropertyModel"); propertyType = GetStorageDataPropertyType(property); isPrimary = true; @@ -72,7 +72,7 @@ public static List GetColumns(IReadOnlyList propert { IsNullable = property.IsNullable, Configuration = configuration, - HasIndex = property is DataPropertyModel { IsIndexed: true } + HasIndex = property is IDataPropertyModel { IsIndexed: true } }; columns.Add(column); @@ -93,7 +93,7 @@ public static List GetColumns(IReadOnlyList propert #region private - private static string GetStorageDataPropertyType(PropertyModel property) + private static string GetStorageDataPropertyType(IPropertyModel property) => property.Type switch { // Integer types @@ -129,7 +129,7 @@ private static string GetStorageDataPropertyType(PropertyModel property) _ => throw new NotSupportedException($"Property '{property.ModelName}' has type '{property.Type.Name}', which is not supported by SQLite connector.") }; - private static string GetDistanceMetric(VectorPropertyModel vectorProperty) + private static string GetDistanceMetric(IVectorPropertyModel vectorProperty) => vectorProperty.DistanceFunction switch { DistanceFunction.CosineDistance or null => "cosine", @@ -138,7 +138,7 @@ private static string GetDistanceMetric(VectorPropertyModel vectorProperty) _ => throw new NotSupportedException($"Distance function '{vectorProperty.DistanceFunction}' for {nameof(VectorStoreVectorProperty)} '{vectorProperty.ModelName}' is not supported by the SQLite connector.") }; - private static string GetStorageVectorPropertyType(VectorPropertyModel vectorProperty) + private static string GetStorageVectorPropertyType(IVectorPropertyModel vectorProperty) => $"FLOAT[{vectorProperty.Dimensions}]"; #endregion diff --git a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/CollectionModel.cs b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/CollectionModel.cs index 7f837038dfa6..06a130704a44 100644 --- a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/CollectionModel.cs +++ b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/CollectionModel.cs @@ -21,34 +21,34 @@ public sealed class CollectionModel private readonly Type _recordType; private readonly Func _recordFactory; - private KeyPropertyModel? _singleKeyProperty; - private VectorPropertyModel? _singleVectorProperty; - private DataPropertyModel? _singleFullTextSearchProperty; + private IKeyPropertyModel? _singleKeyProperty; + private IVectorPropertyModel? _singleVectorProperty; + private IDataPropertyModel? _singleFullTextSearchProperty; /// /// Gets the key properties of the record. /// - public IReadOnlyList KeyProperties { get; } + public IReadOnlyList KeyProperties { get; } /// /// Gets the data properties of the record. /// - public IReadOnlyList DataProperties { get; } + public IReadOnlyList DataProperties { get; } /// /// Gets the vector properties of the record. /// - public IReadOnlyList VectorProperties { get; } + public IReadOnlyList VectorProperties { get; } /// /// Gets all properties of the record, of all types. /// - public IReadOnlyList Properties { get; } + public IReadOnlyList Properties { get; } /// /// Gets all properties of the record, of all types, indexed by their model name. /// - public IReadOnlyDictionary PropertyMap { get; } + public IReadOnlyDictionary PropertyMap { get; } /// /// Gets a value that indicates whether any of the vector properties in the model require embedding generation. @@ -69,8 +69,8 @@ internal CollectionModel( this.KeyProperties = keyProperties; this.DataProperties = dataProperties; this.VectorProperties = vectorProperties; - this.PropertyMap = propertyMap; - this.Properties = propertyMap.Values.ToList(); + this.PropertyMap = propertyMap.ToDictionary(kvp => kvp.Key, kvp => (IPropertyModel)kvp.Value); + this.Properties = this.PropertyMap.Values.ToList(); this.EmbeddingGenerationRequired = vectorProperties.Any(p => p.EmbeddingType != p.Type); } @@ -78,13 +78,13 @@ internal CollectionModel( /// /// Returns the single key property in the model, and throws if there are multiple key properties. /// - public KeyPropertyModel KeyProperty => this._singleKeyProperty ??= this.KeyProperties.Single(); + public IKeyPropertyModel KeyProperty => this._singleKeyProperty ??= this.KeyProperties.Single(); /// /// Returns the single vector property in the model, and throws if there are multiple vector properties. /// Suitable for connectors where validation is in place for single vectors only (). /// - public VectorPropertyModel VectorProperty => this._singleVectorProperty ??= this.VectorProperties.Single(); + public IVectorPropertyModel VectorProperty => this._singleVectorProperty ??= this.VectorProperties.Single(); /// /// Instantiates a new record of the specified type. @@ -106,11 +106,11 @@ public TRecord CreateRecord() /// /// The search options, which defines the vector property name. /// The provided property name is not a valid text data property name.ORNo name was provided and there's more than one vector property. - public VectorPropertyModel GetVectorPropertyOrSingle(VectorSearchOptions searchOptions) + public IVectorPropertyModel GetVectorPropertyOrSingle(VectorSearchOptions searchOptions) { if (searchOptions.VectorProperty is not null) { - return this.GetMatchingProperty(searchOptions.VectorProperty, data: false); + return this.GetMatchingProperty(searchOptions.VectorProperty, data: false); } // If vector property name is not provided, check if there is a single vector property, or throw if there are no vectors or more than one. @@ -140,11 +140,11 @@ public VectorPropertyModel GetVectorPropertyOrSingle(VectorSearchOption /// /// The full text search property selector. /// The provided property name is not a valid text data property name.ORNo name was provided and there's more than one text data property with full text search indexing enabled. - public DataPropertyModel GetFullTextDataPropertyOrSingle(Expression>? expression) + public IDataPropertyModel GetFullTextDataPropertyOrSingle(Expression>? expression) { if (expression is not null) { - var property = this.GetMatchingProperty(expression, data: true); + var property = this.GetMatchingProperty(expression, data: true); return property.IsFullTextIndexed ? property @@ -184,11 +184,11 @@ public DataPropertyModel GetFullTextDataPropertyOrSingle(Expression /// The property selector. /// The provided property name is not a valid data or key property name. - public PropertyModel GetDataOrKeyProperty(Expression> expression) - => this.GetMatchingProperty(expression, data: true); + public IPropertyModel GetDataOrKeyProperty(Expression> expression) + => this.GetMatchingProperty(expression, data: true); private TProperty GetMatchingProperty(Expression> expression, bool data) - where TProperty : PropertyModel + where TProperty : IPropertyModel { var node = expression.Body; diff --git a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/DataPropertyModel.cs b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/DataPropertyModel.cs index b2d800245054..da03ddbd5e1c 100644 --- a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/DataPropertyModel.cs +++ b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/DataPropertyModel.cs @@ -10,7 +10,7 @@ namespace Microsoft.Extensions.VectorData.ProviderServices; /// This is an internal support type meant for use by connectors only and not by applications. /// [Experimental("MEVD9001")] -public class DataPropertyModel(string modelName, Type type) : PropertyModel(modelName, type) +public class DataPropertyModel(string modelName, Type type) : PropertyModel(modelName, type), IDataPropertyModel { /// /// Gets or sets a value indicating whether this data property is indexed. diff --git a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/Filter/FilterTranslatorBase.cs b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/Filter/FilterTranslatorBase.cs index ad847b34b1d1..7c4655a048da 100644 --- a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/Filter/FilterTranslatorBase.cs +++ b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/Filter/FilterTranslatorBase.cs @@ -109,7 +109,7 @@ protected static bool TryMatchContains( /// The expression to bind. /// When successful, the property model that was bound. /// if the expression was successfully bound to a property; otherwise, . - protected virtual bool TryBindProperty(Expression expression, [NotNullWhen(true)] out PropertyModel? propertyModel) + protected virtual bool TryBindProperty(Expression expression, [NotNullWhen(true)] out IPropertyModel? propertyModel) { var unwrappedExpression = expression; while (unwrappedExpression is UnaryExpression { NodeType: ExpressionType.Convert } convert) diff --git a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/IDataPropertyModel.cs b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/IDataPropertyModel.cs new file mode 100644 index 000000000000..cf96acfcc689 --- /dev/null +++ b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/IDataPropertyModel.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.VectorData.ProviderServices; + +/// +/// Represents a read-only view of a data property on a vector store record. +/// This is an internal support type meant for use by connectors only and not by applications. +/// +[Experimental("MEVD9001")] +public interface IDataPropertyModel : IPropertyModel +{ + /// + /// Gets a value indicating whether this data property is indexed. + /// + bool IsIndexed { get; } + + /// + /// Gets a value indicating whether this data property is indexed for full-text search. + /// + bool IsFullTextIndexed { get; } +} diff --git a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/IKeyPropertyModel.cs b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/IKeyPropertyModel.cs new file mode 100644 index 000000000000..69e34bd99c81 --- /dev/null +++ b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/IKeyPropertyModel.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.VectorData.ProviderServices; + +/// +/// Represents a read-only view of a key property on a vector store record. +/// This is an internal support type meant for use by connectors only and not by applications. +/// +[Experimental("MEVD9001")] +public interface IKeyPropertyModel : IPropertyModel +{ + /// + /// Gets whether this key property's value is auto-generated or not. + /// + bool IsAutoGenerated { get; } + + /// + /// Gets the name that the JSON serializer will produce for this key property. + /// + string? SerializedKeyName { get; } +} diff --git a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/IPropertyModel.cs b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/IPropertyModel.cs new file mode 100644 index 000000000000..a8e2634f1327 --- /dev/null +++ b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/IPropertyModel.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace Microsoft.Extensions.VectorData.ProviderServices; + +/// +/// Represents a read-only view of a property on a vector store record. +/// This is an internal support type meant for use by connectors only and not by applications. +/// +[Experimental("MEVD9001")] +public interface IPropertyModel +{ + /// + /// Gets the model name of the property. If the property corresponds to a .NET property, this name is the name of that property. + /// + string ModelName { get; } + + /// + /// Gets the storage name of the property. This is the name to which the property is mapped in the vector store. + /// + string StorageName { get; } + + /// + /// Gets the CLR type of the property. + /// + Type Type { get; } + + /// + /// Gets the reflection for the .NET property. + /// + /// + /// The reflection for the .NET property. + /// when using dynamic mapping. + /// + PropertyInfo? PropertyInfo { get; } + + /// + /// Gets whether the property type is nullable. + /// + bool IsNullable { get; } + + /// + /// Gets a dictionary of provider-specific annotations for this property. + /// + Dictionary? ProviderAnnotations { get; } + + /// + /// Reads the property from the given , returning the value as an . + /// + object? GetValueAsObject(object record); + + /// + /// Writes the property from the given , accepting the value to write as an . + /// + void SetValueAsObject(object record, object? value); + + /// + /// Reads the property from the given . + /// + T GetValue(object record); + + /// + /// Writes the property from the given . + /// + void SetValue(object record, T value); +} diff --git a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/IRecordCreator.cs b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/IRecordCreator.cs deleted file mode 100644 index 079659ce5bcb..000000000000 --- a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/IRecordCreator.cs +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace Microsoft.Extensions.VectorData.ProviderServices; - -internal interface IRecordCreator -{ - TRecord Create(); -} diff --git a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/IVectorPropertyModel.cs b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/IVectorPropertyModel.cs new file mode 100644 index 000000000000..2580c0bf73b7 --- /dev/null +++ b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/IVectorPropertyModel.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; + +namespace Microsoft.Extensions.VectorData.ProviderServices; + +/// +/// Represents a read-only view of a vector property on a vector store record. +/// This is an internal support type meant for use by connectors only and not by applications. +/// +[Experimental("MEVD9001")] +public interface IVectorPropertyModel : IPropertyModel +{ + /// + /// Gets the number of dimensions that the vector has. + /// + int Dimensions { get; } + + /// + /// Gets the kind of index to use. + /// + string? IndexKind { get; } + + /// + /// Gets the distance function to use when comparing vectors. + /// + string? DistanceFunction { get; } + + /// + /// Gets the type representing the embedding stored in the database. + /// + /// + /// This is guaranteed to be non-null after model building completes. + /// If is set, this is the output embedding type; + /// otherwise it is identical to . + /// + Type EmbeddingType { get; } + + /// + /// Gets the embedding generator to use for this property. + /// + IEmbeddingGenerator? EmbeddingGenerator { get; } + + /// + /// Gets the that was resolved for this property during model building. + /// + EmbeddingGenerationDispatcher? EmbeddingGenerationDispatcher { get; } + + /// + /// Generates embeddings for the given , using the configured . + /// + Task> GenerateEmbeddingsAsync(IEnumerable values, CancellationToken cancellationToken); + + /// + /// Generates a single embedding for the given , using the configured . + /// + Task GenerateEmbeddingAsync(object? value, CancellationToken cancellationToken); +} diff --git a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/KeyPropertyModel.cs b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/KeyPropertyModel.cs index 5d0879dfc2da..504ebb3bd78d 100644 --- a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/KeyPropertyModel.cs +++ b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/KeyPropertyModel.cs @@ -10,7 +10,7 @@ namespace Microsoft.Extensions.VectorData.ProviderServices; /// This is an internal support type meant for use by connectors only and not by applications. /// [Experimental("MEVD9001")] -public class KeyPropertyModel(string modelName, Type type) : PropertyModel(modelName, type) +public class KeyPropertyModel(string modelName, Type type) : PropertyModel(modelName, type), IKeyPropertyModel { /// /// Gets or sets whether this key property's value is auto-generated or not. diff --git a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/PropertyModel.cs b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/PropertyModel.cs index 7b3635911e95..2ed95faf997e 100644 --- a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/PropertyModel.cs +++ b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/PropertyModel.cs @@ -13,7 +13,7 @@ namespace Microsoft.Extensions.VectorData.ProviderServices; /// This is an internal support type meant for use by connectors only and not by applications. /// [Experimental("MEVD9001")] -public abstract class PropertyModel(string modelName, Type type) +public abstract class PropertyModel(string modelName, Type type) : IPropertyModel { private string? _storageName; private Func? _getter; diff --git a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/VectorPropertyModel.cs b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/VectorPropertyModel.cs index 75d3a1057ff9..4c80ce62d321 100644 --- a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/VectorPropertyModel.cs +++ b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/VectorPropertyModel.cs @@ -16,7 +16,7 @@ namespace Microsoft.Extensions.VectorData.ProviderServices; /// This is an internal support type meant for use by connectors only and not by applications. /// [Experimental("MEVD9001")] -public class VectorPropertyModel(string modelName, Type type) : PropertyModel(modelName, type) +public class VectorPropertyModel(string modelName, Type type) : PropertyModel(modelName, type), IVectorPropertyModel { private int _dimensions; diff --git a/dotnet/src/VectorData/Weaviate/WeaviateCollection.cs b/dotnet/src/VectorData/Weaviate/WeaviateCollection.cs index 900c865c6396..600e993c4b5b 100644 --- a/dotnet/src/VectorData/Weaviate/WeaviateCollection.cs +++ b/dotnet/src/VectorData/Weaviate/WeaviateCollection.cs @@ -356,7 +356,7 @@ public override async IAsyncEnumerable> SearchAsync< } } - private static async ValueTask> GetSearchVectorAsync(TInput searchValue, VectorPropertyModel vectorProperty, CancellationToken cancellationToken) + private static async ValueTask> GetSearchVectorAsync(TInput searchValue, IVectorPropertyModel vectorProperty, CancellationToken cancellationToken) where TInput : notnull => searchValue switch { diff --git a/dotnet/src/VectorData/Weaviate/WeaviateMapper.cs b/dotnet/src/VectorData/Weaviate/WeaviateMapper.cs index 730199f0a87c..f365fac75f21 100644 --- a/dotnet/src/VectorData/Weaviate/WeaviateMapper.cs +++ b/dotnet/src/VectorData/Weaviate/WeaviateMapper.cs @@ -175,7 +175,7 @@ public TRecord MapFromStorageToDataModel(JsonObject storageModel, bool includeVe return record; - static void PopulateVectorProperty(TRecord record, object? value, VectorPropertyModel property) + static void PopulateVectorProperty(TRecord record, object? value, IVectorPropertyModel property) { switch (value) { diff --git a/dotnet/src/VectorData/Weaviate/WeaviateQueryBuilder.cs b/dotnet/src/VectorData/Weaviate/WeaviateQueryBuilder.cs index 58aee493a149..f1694793b32d 100644 --- a/dotnet/src/VectorData/Weaviate/WeaviateQueryBuilder.cs +++ b/dotnet/src/VectorData/Weaviate/WeaviateQueryBuilder.cs @@ -122,8 +122,8 @@ public static string BuildHybridSearchQuery( string keywords, string collectionName, CollectionModel model, - VectorPropertyModel vectorProperty, - DataPropertyModel textProperty, + IVectorPropertyModel vectorProperty, + IDataPropertyModel textProperty, JsonSerializerOptions jsonSerializerOptions, HybridSearchOptions searchOptions, bool hasNamedVectors) diff --git a/dotnet/test/VectorData/PgVector.UnitTests/PostgresPropertyMappingTests.cs b/dotnet/test/VectorData/PgVector.UnitTests/PostgresPropertyMappingTests.cs index 64dafc41de81..56beee094119 100644 --- a/dotnet/test/VectorData/PgVector.UnitTests/PostgresPropertyMappingTests.cs +++ b/dotnet/test/VectorData/PgVector.UnitTests/PostgresPropertyMappingTests.cs @@ -93,7 +93,7 @@ public void GetPropertyValueReturnsCorrectNullableValue() public void GetIndexInfoReturnsCorrectValues() { // Arrange - List vectorProperties = + List vectorProperties = [ new VectorPropertyModel("vector1", typeof(ReadOnlyMemory?)) { IndexKind = IndexKind.Hnsw, Dimensions = 1000 }, new VectorPropertyModel("vector2", typeof(ReadOnlyMemory?)) { IndexKind = IndexKind.Flat, Dimensions = 3000 }, diff --git a/dotnet/test/VectorData/Redis.UnitTests/RedisCollectionCreateMappingTests.cs b/dotnet/test/VectorData/Redis.UnitTests/RedisCollectionCreateMappingTests.cs index 8d218a3566b4..d56c05b59d69 100644 --- a/dotnet/test/VectorData/Redis.UnitTests/RedisCollectionCreateMappingTests.cs +++ b/dotnet/test/VectorData/Redis.UnitTests/RedisCollectionCreateMappingTests.cs @@ -21,7 +21,7 @@ public class RedisCollectionCreateMappingTests public void MapToSchemaCreatesSchema(bool useDollarPrefix) { // Arrange. - PropertyModel[] properties = + IPropertyModel[] properties = [ new KeyPropertyModel("Key", typeof(string)), diff --git a/dotnet/test/VectorData/SqliteVec.UnitTests/SqlitePropertyMappingTests.cs b/dotnet/test/VectorData/SqliteVec.UnitTests/SqlitePropertyMappingTests.cs index fef4557ec29f..1899612fa628 100644 --- a/dotnet/test/VectorData/SqliteVec.UnitTests/SqlitePropertyMappingTests.cs +++ b/dotnet/test/VectorData/SqliteVec.UnitTests/SqlitePropertyMappingTests.cs @@ -34,7 +34,7 @@ public void MapVectorForStorageModelReturnsByteArray() public void GetColumnsReturnsCollectionOfColumns(bool data) { // Arrange - var properties = new List() + var properties = new List() { new KeyPropertyModel("Key", typeof(string)) { StorageName = "Key" }, new DataPropertyModel("Data", typeof(int)) { StorageName = "my_data", IsIndexed = true }, diff --git a/dotnet/test/VectorData/VectorData.UnitTests/PropertyModelTests.cs b/dotnet/test/VectorData/VectorData.UnitTests/PropertyModelTests.cs index aad7b64beee3..e9e24824c56b 100644 --- a/dotnet/test/VectorData/VectorData.UnitTests/PropertyModelTests.cs +++ b/dotnet/test/VectorData/VectorData.UnitTests/PropertyModelTests.cs @@ -6,7 +6,7 @@ namespace VectorData.UnitTests; -public class PropertyModelTests +public class IPropertyModelTests { #region Value type nullability From 4855cbf8a4ec5d6d98de33e974c95868aa6b5680 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Tue, 24 Mar 2026 19:53:59 +0100 Subject: [PATCH 3/5] Address review comments --- dotnet/src/VectorData/SqliteVec/SqlitePropertyMapping.cs | 2 +- .../VectorData.Abstractions/ProviderServices/IPropertyModel.cs | 2 +- .../ProviderServices/KeyPropertyModel.cs | 1 - .../VectorData.Abstractions/ProviderServices/PropertyModel.cs | 2 ++ .../test/VectorData/VectorData.UnitTests/PropertyModelTests.cs | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/dotnet/src/VectorData/SqliteVec/SqlitePropertyMapping.cs b/dotnet/src/VectorData/SqliteVec/SqlitePropertyMapping.cs index ce81dda07535..bde38e812eae 100644 --- a/dotnet/src/VectorData/SqliteVec/SqlitePropertyMapping.cs +++ b/dotnet/src/VectorData/SqliteVec/SqlitePropertyMapping.cs @@ -62,7 +62,7 @@ public static List GetColumns(IReadOnlyList proper else { // The Key column in included in both Vector and Data tables. - Debug.Assert(property is IKeyPropertyModel, "property is VectorStoreRecordIKeyPropertyModel"); + Debug.Assert(property is IKeyPropertyModel, "property is not an IKeyPropertyModel"); propertyType = GetStorageDataPropertyType(property); isPrimary = true; diff --git a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/IPropertyModel.cs b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/IPropertyModel.cs index a8e2634f1327..c2d6a6ce536a 100644 --- a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/IPropertyModel.cs +++ b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/IPropertyModel.cs @@ -46,7 +46,7 @@ public interface IPropertyModel /// /// Gets a dictionary of provider-specific annotations for this property. /// - Dictionary? ProviderAnnotations { get; } + IReadOnlyDictionary? ProviderAnnotations { get; } /// /// Reads the property from the given , returning the value as an . diff --git a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/KeyPropertyModel.cs b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/KeyPropertyModel.cs index 504ebb3bd78d..a14382be0d2c 100644 --- a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/KeyPropertyModel.cs +++ b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/KeyPropertyModel.cs @@ -23,7 +23,6 @@ public class KeyPropertyModel(string modelName, Type type) : PropertyModel(model /// (e.g. CosmosDB NoSQL uses "id"): the serializer produces a JSON object with the policy-transformed name, and /// the connector needs to find and replace it with the reserved storage name. /// - [Experimental("MEVD9001")] public string? SerializedKeyName { get; set; } /// diff --git a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/PropertyModel.cs b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/PropertyModel.cs index 2ed95faf997e..3ca86c60e844 100644 --- a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/PropertyModel.cs +++ b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/PropertyModel.cs @@ -55,6 +55,8 @@ public string StorageName /// public Dictionary? ProviderAnnotations { get; set; } + IReadOnlyDictionary? IPropertyModel.ProviderAnnotations => this.ProviderAnnotations; + /// /// Gets whether the property type is nullable. For value types, this is when the type is /// . For reference types on .NET 6+, this uses NRT annotations via diff --git a/dotnet/test/VectorData/VectorData.UnitTests/PropertyModelTests.cs b/dotnet/test/VectorData/VectorData.UnitTests/PropertyModelTests.cs index e9e24824c56b..aad7b64beee3 100644 --- a/dotnet/test/VectorData/VectorData.UnitTests/PropertyModelTests.cs +++ b/dotnet/test/VectorData/VectorData.UnitTests/PropertyModelTests.cs @@ -6,7 +6,7 @@ namespace VectorData.UnitTests; -public class IPropertyModelTests +public class PropertyModelTests { #region Value type nullability From 8db508e9de2183ca55760130cc4f7930744a1fc9 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Wed, 25 Mar 2026 18:01:35 +0100 Subject: [PATCH 4/5] Merge Type and definition processing into a single method --- .../Memory/MongoDB/MongoModelBuilder.cs | 13 +- .../CollectionModelBuilder.cs | 291 +++++++++--------- 2 files changed, 151 insertions(+), 153 deletions(-) diff --git a/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/MongoModelBuilder.cs b/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/MongoModelBuilder.cs index 69f974cffd19..25b77f90839b 100644 --- a/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/MongoModelBuilder.cs +++ b/dotnet/src/InternalUtilities/connectors/Memory/MongoDB/MongoModelBuilder.cs @@ -27,17 +27,14 @@ internal class MongoModelBuilder() : CollectionModelBuilder(s_validationOptions) UsesExternalSerializer = true, }; - [RequiresUnreferencedCode("Traverses the CLR type's properties with reflection, so not compatible with trimming")] - protected override void ProcessTypeProperties(Type type, VectorStoreCollectionDefinition? definition) + protected override void ProcessProperty(PropertyInfo? clrProperty, VectorStoreProperty? definitionProperty, Type? type) { - base.ProcessTypeProperties(type, definition); + base.ProcessProperty(clrProperty, definitionProperty, type); - foreach (var property in this.Properties) + if (clrProperty?.GetCustomAttribute() is { } bsonElementAttribute + && this.PropertyMap.TryGetValue(clrProperty.Name, out var property)) { - if (property.PropertyInfo?.GetCustomAttribute() is { } bsonElementAttribute) - { - property.StorageName = bsonElementAttribute.ElementName; - } + property.StorageName = bsonElementAttribute.ElementName; } } diff --git a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/CollectionModelBuilder.cs b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/CollectionModelBuilder.cs index 7b2f544fa74d..3b6f6ccc6b51 100644 --- a/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/CollectionModelBuilder.cs +++ b/dotnet/src/VectorData/VectorData.Abstractions/ProviderServices/CollectionModelBuilder.cs @@ -85,11 +85,27 @@ public virtual CollectionModel Build(Type recordType, Type keyType, VectorStoreC this.DefaultEmbeddingGenerator = definition?.EmbeddingGenerator ?? defaultEmbeddingGenerator; - this.ProcessTypeProperties(recordType, definition); - + // Build a lookup of definition properties by name for matching with CLR properties. + Dictionary? definitionByName = null; if (definition is not null) { - this.ProcessRecordDefinition(definition, recordType); + definitionByName = []; + foreach (var p in definition.Properties) + { + definitionByName[p.Name] = p; + } + } + + // Process CLR properties, matching to definition properties where available. + // TODO: This traverses the CLR type's properties, making it incompatible with trimming (and NativeAOT). + // TODO: We could put [DynamicallyAccessedMembers] to preserve all properties, but that approach wouldn't + // TODO: work with hierarchical data models (#10957). + foreach (var clrProperty in recordType.GetProperties()) + { + VectorStoreProperty? definitionProperty = null; + _ = definitionByName?.TryGetValue(clrProperty.Name, out definitionProperty); + + this.ProcessProperty(clrProperty, definitionProperty, recordType); } // Go over the properties, configure POCO accessors and validate type compatibility. @@ -133,7 +149,12 @@ public virtual CollectionModel BuildDynamic(VectorStoreCollectionDefinition defi } this.DefaultEmbeddingGenerator = defaultEmbeddingGenerator; - this.ProcessRecordDefinition(definition, type: null); + + foreach (var defProp in definition.Properties) + { + this.ProcessProperty(clrProperty: null, defProp, type: null); + } + this.Customize(); this.Validate(type: null, definition); @@ -146,188 +167,168 @@ public virtual CollectionModel BuildDynamic(VectorStoreCollectionDefinition defi } /// - /// As part of building the model, this method processes the properties of the given , - /// detecting and reading attributes that affect the model. Not called for dynamic mapping scenarios. + /// As part of building the model, this method processes a single property, accepting both a CLR + /// (from which attributes are read) and a from the user-provided record definition. + /// Either may be , but not both. + /// When both are provided, the record definition values override attribute-configured values. /// - // TODO: This traverses the CLR type's properties, making it incompatible with trimming (and NativeAOT). - // TODO: We could put [DynamicallyAccessedMembers] to preserve all properties, but that approach wouldn't - // TODO: work with hierarchical data models (#10957). - [RequiresUnreferencedCode("Traverses the CLR type's properties with reflection, so not compatible with trimming")] - protected virtual void ProcessTypeProperties(Type type, VectorStoreCollectionDefinition? definition) + protected virtual void ProcessProperty(PropertyInfo? clrProperty, VectorStoreProperty? definitionProperty, Type? type) { - // We want to allow the user-provided record definition to override anything configured via attributes - // (allowing the same CLR type + attributes to be used with different record definitions). - foreach (var clrProperty in type.GetProperties()) + Debug.Assert(clrProperty is not null || definitionProperty is not null); + + VectorStoreKeyAttribute? keyAttribute = null; + VectorStoreDataAttribute? dataAttribute = null; + VectorStoreVectorAttribute? vectorAttribute = null; + + if (clrProperty is not null) { - PropertyModel? property = null; - string? storageName = null; + // Read attributes from CLR property. + keyAttribute = clrProperty.GetCustomAttribute(); + dataAttribute = clrProperty.GetCustomAttribute(); + vectorAttribute = clrProperty.GetCustomAttribute(); - if (clrProperty.GetCustomAttribute() is { } keyAttribute) + // Validate that at most one mapping attribute is present. + if ((keyAttribute is not null ? 1 : 0) + (dataAttribute is not null ? 1 : 0) + (vectorAttribute is not null ? 1 : 0) > 1) { - var keyProperty = new KeyPropertyModel(clrProperty.Name, clrProperty.PropertyType); - keyProperty.IsAutoGenerated = keyAttribute.IsAutoGeneratedNullable ?? this.SupportsKeyAutoGeneration(keyProperty.Type); - this.KeyProperties.Add(keyProperty); - storageName = keyAttribute.StorageName; - property = keyProperty; + throw new InvalidOperationException( + $"Property '{type!.Name}.{clrProperty.Name}' has multiple of {nameof(VectorStoreKeyAttribute)}, {nameof(VectorStoreDataAttribute)} or {nameof(VectorStoreVectorAttribute)}. Only one of these attributes can be specified on a property."); } - if (clrProperty.GetCustomAttribute() is { } dataAttribute) + // If no mapping attribute and no definition, skip this property. + if (keyAttribute is null && dataAttribute is null && vectorAttribute is null && definitionProperty is null) { - if (property is not null) - { - // TODO: Test - throw new InvalidOperationException($"Property '{type.Name}.{clrProperty.Name}' has multiple of {nameof(VectorStoreKeyAttribute)}, {nameof(VectorStoreDataAttribute)} or {nameof(VectorStoreVectorAttribute)}. Only one of these attributes can be specified on a property."); - } + return; + } - var dataProperty = new DataPropertyModel(clrProperty.Name, clrProperty.PropertyType) + // Validate kind compatibility between attribute and definition. + if (definitionProperty is not null + && ((keyAttribute is not null && definitionProperty is not VectorStoreKeyProperty) + || (dataAttribute is not null && definitionProperty is not VectorStoreDataProperty) + || (vectorAttribute is not null && definitionProperty is not VectorStoreVectorProperty))) + { + string definitionKind = definitionProperty switch { - IsIndexed = dataAttribute.IsIndexed, - IsFullTextIndexed = dataAttribute.IsFullTextIndexed, + VectorStoreKeyProperty => "key", + VectorStoreDataProperty => "data", + VectorStoreVectorProperty => "vector", + _ => throw new ArgumentException($"Unknown type '{definitionProperty.GetType().FullName}' in vector store record definition.") }; - this.DataProperties.Add(dataProperty); - storageName = dataAttribute.StorageName; - property = dataProperty; + throw new InvalidOperationException( + $"Property '{clrProperty.Name}' is present in the {nameof(VectorStoreCollectionDefinition)} as a {definitionKind} property, but the .NET property on type '{type?.Name}' has an incompatible attribute."); } + } - if (clrProperty.GetCustomAttribute() is { } vectorAttribute) - { - if (property is not null) - { - throw new InvalidOperationException($"Only one of {nameof(VectorStoreKeyAttribute)}, {nameof(VectorStoreDataAttribute)} and {nameof(VectorStoreVectorAttribute)} can be applied to a property."); - } - - // If a record definition exists for the property, we must instantiate it via that definition, as the user may be using - // a generic VectorStoreRecordVectorProperty for a custom input type. - var vectorProperty = definition?.Properties.FirstOrDefault(p => p.Name == clrProperty.Name) is VectorStoreVectorProperty definitionVectorProperty - ? definitionVectorProperty.CreatePropertyModel() - : new VectorPropertyModel(clrProperty.Name, clrProperty.PropertyType); + string propertyName = clrProperty?.Name ?? definitionProperty!.Name; + Type propertyType = clrProperty?.PropertyType + ?? definitionProperty!.Type + ?? throw new InvalidOperationException(VectorDataStrings.MissingTypeOnPropertyDefinition(definitionProperty!)); - vectorProperty.Dimensions = vectorAttribute.Dimensions; - vectorProperty.IndexKind = vectorAttribute.IndexKind; - vectorProperty.DistanceFunction = vectorAttribute.DistanceFunction; + PropertyModel property; + string? attributeStorageName = null; - this.ConfigureVectorPropertyEmbedding(vectorProperty, this.DefaultEmbeddingGenerator, userRequestedEmbeddingType: null); + if (keyAttribute is not null || definitionProperty is VectorStoreKeyProperty) + { + var keyProperty = new KeyPropertyModel(propertyName, propertyType); - this.VectorProperties.Add(vectorProperty); - storageName = vectorAttribute.StorageName; - property = vectorProperty; + if (keyAttribute is not null) + { + keyProperty.IsAutoGenerated = keyAttribute.IsAutoGeneratedNullable ?? this.SupportsKeyAutoGeneration(keyProperty.Type); + attributeStorageName = keyAttribute.StorageName; } - if (property is null) + // Definition values override attribute values. + if (definitionProperty is VectorStoreKeyProperty defKey) { - // No mapping attribute was found, ignore this property. - continue; + keyProperty.IsAutoGenerated = defKey.IsAutoGenerated ?? this.SupportsKeyAutoGeneration(keyProperty.Type); } - this.SetPropertyStorageName(property, storageName, type); - - property.PropertyInfo = clrProperty; - this.PropertyMap.Add(clrProperty.Name, property); + this.KeyProperties.Add(keyProperty); + property = keyProperty; } - } - - /// - /// Processes the given as part of building the model. - /// - protected virtual void ProcessRecordDefinition(VectorStoreCollectionDefinition definition, Type? type) - { - foreach (VectorStoreProperty definitionProperty in definition.Properties) + else if (dataAttribute is not null || definitionProperty is VectorStoreDataProperty) { - if (!this.PropertyMap.TryGetValue(definitionProperty.Name, out var property)) + var dataProperty = new DataPropertyModel(propertyName, propertyType); + + if (dataAttribute is not null) { - // Property wasn't found attribute-annotated on the CLR type, so we need to add it. + dataProperty.IsIndexed = dataAttribute.IsIndexed; + dataProperty.IsFullTextIndexed = dataAttribute.IsFullTextIndexed; + attributeStorageName = dataAttribute.StorageName; + } - var propertyType = definitionProperty.Type - ?? throw new InvalidOperationException(VectorDataStrings.MissingTypeOnPropertyDefinition(definitionProperty)); - switch (definitionProperty) - { - case VectorStoreKeyProperty definitionKeyProperty: - var keyProperty = new KeyPropertyModel(definitionKeyProperty.Name, propertyType); - this.KeyProperties.Add(keyProperty); - this.PropertyMap.Add(definitionKeyProperty.Name, keyProperty); - property = keyProperty; - break; - case VectorStoreDataProperty definitionDataProperty: - var dataProperty = new DataPropertyModel(definitionDataProperty.Name, propertyType); - this.DataProperties.Add(dataProperty); - this.PropertyMap.Add(definitionDataProperty.Name, dataProperty); - property = dataProperty; - break; - case VectorStoreVectorProperty definitionVectorProperty: - var vectorProperty = definitionVectorProperty.CreatePropertyModel(); - this.VectorProperties.Add(vectorProperty); - this.PropertyMap.Add(definitionVectorProperty.Name, vectorProperty); - property = vectorProperty; - break; - default: - throw new ArgumentException($"Unknown type '{definitionProperty.GetType().FullName}' in vector store record definition."); - } + // Definition values override attribute values. + if (definitionProperty is VectorStoreDataProperty defData) + { + dataProperty.IsIndexed = defData.IsIndexed; + dataProperty.IsFullTextIndexed = defData.IsFullTextIndexed; } - this.SetPropertyStorageName(property, definitionProperty.StorageName, type); + this.DataProperties.Add(dataProperty); + property = dataProperty; + } + else if (vectorAttribute is not null || definitionProperty is VectorStoreVectorProperty) + { + // If a definition exists, create via the definition to preserve generic type info (VectorStoreVectorProperty). + var vectorProperty = definitionProperty is VectorStoreVectorProperty defVec + ? defVec.CreatePropertyModel() + : new VectorPropertyModel(propertyName, propertyType); - // Copy provider-specific properties if present - if (definitionProperty.ProviderAnnotations is not null) + if (vectorAttribute is not null) { - property.ProviderAnnotations = new Dictionary(definitionProperty.ProviderAnnotations); + vectorProperty.Dimensions = vectorAttribute.Dimensions; + vectorProperty.IndexKind = vectorAttribute.IndexKind; + vectorProperty.DistanceFunction = vectorAttribute.DistanceFunction; + attributeStorageName = vectorAttribute.StorageName; } - switch (definitionProperty) + // Definition values override attribute values. + if (definitionProperty is VectorStoreVectorProperty defVectorProp) { - case VectorStoreKeyProperty definitionKeyProperty: - if (property is not KeyPropertyModel keyPropertyModel) - { - throw new InvalidOperationException( - $"Property '{property.ModelName}' is present in the {nameof(VectorStoreCollectionDefinition)} as a key property, but the .NET property on type '{type?.Name}' has an incompatible attribute."); - } + vectorProperty.Dimensions = defVectorProp.Dimensions; - keyPropertyModel.IsAutoGenerated = definitionKeyProperty.IsAutoGenerated ?? this.SupportsKeyAutoGeneration(keyPropertyModel.Type); - - break; - - case VectorStoreDataProperty definitionDataProperty: - if (property is not DataPropertyModel dataProperty) - { - throw new InvalidOperationException( - $"Property '{property.ModelName}' is present in the {nameof(VectorStoreCollectionDefinition)} as a data property, but the .NET property on type '{type?.Name}' has an incompatible attribute."); - } - - dataProperty.IsIndexed = definitionDataProperty.IsIndexed; - dataProperty.IsFullTextIndexed = definitionDataProperty.IsFullTextIndexed; - - break; - - case VectorStoreVectorProperty definitionVectorProperty: - if (property is not VectorPropertyModel vectorProperty) - { - throw new InvalidOperationException( - $"Property '{property.ModelName}' is present in the {nameof(VectorStoreCollectionDefinition)} as a vector property, but the .NET property on type '{type?.Name}' has an incompatible attribute."); - } + if (defVectorProp.IndexKind is not null) + { + vectorProperty.IndexKind = defVectorProp.IndexKind; + } - vectorProperty.Dimensions = definitionVectorProperty.Dimensions; + if (defVectorProp.DistanceFunction is not null) + { + vectorProperty.DistanceFunction = defVectorProp.DistanceFunction; + } + } - if (definitionVectorProperty.IndexKind is not null) - { - vectorProperty.IndexKind = definitionVectorProperty.IndexKind; - } + this.ConfigureVectorPropertyEmbedding( + vectorProperty, + (definitionProperty as VectorStoreVectorProperty)?.EmbeddingGenerator ?? this.DefaultEmbeddingGenerator, + (definitionProperty as VectorStoreVectorProperty)?.EmbeddingType); - if (definitionVectorProperty.DistanceFunction is not null) - { - vectorProperty.DistanceFunction = definitionVectorProperty.DistanceFunction; - } + this.VectorProperties.Add(vectorProperty); + property = vectorProperty; + } + else + { + throw new UnreachableException(); + } - this.ConfigureVectorPropertyEmbedding( - vectorProperty, - definitionVectorProperty.EmbeddingGenerator ?? this.DefaultEmbeddingGenerator, - definitionVectorProperty.EmbeddingType); + // Apply storage name: attribute first, then definition (which takes precedence). + this.SetPropertyStorageName(property, attributeStorageName, type); + if (definitionProperty is not null) + { + this.SetPropertyStorageName(property, definitionProperty.StorageName, type); + } - break; + if (definitionProperty?.ProviderAnnotations is not null) + { + property.ProviderAnnotations = new Dictionary(definitionProperty.ProviderAnnotations); + } - default: - throw new ArgumentException($"Unknown type '{definitionProperty.GetType().FullName}' in vector store record definition."); - } + if (clrProperty is not null) + { + property.PropertyInfo = clrProperty; } + + this.PropertyMap.Add(propertyName, property); } private void SetPropertyStorageName(PropertyModel property, string? storageName, Type? type) From 8c27f72e032d1542fd65ecee42233b256fda20f3 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Wed, 25 Mar 2026 18:24:39 +0100 Subject: [PATCH 5/5] Bump Scriban (security vulnerability) --- dotnet/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index e2e795ecd269..05e02f82df8f 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -102,7 +102,7 @@ - +