forked from dotnet/extensions
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCollectionModel.cs
More file actions
246 lines (208 loc) · 11.2 KB
/
Copy pathCollectionModel.cs
File metadata and controls
246 lines (208 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Extensions.VectorData.ProviderServices;
/// <summary>
/// Represents a record in a vector store collection.
/// This is an internal support type meant for use by providers only and not by applications.
/// </summary>
[Experimental(DiagnosticIds.Experiments.VectorDataProviderServices, UrlFormat = DiagnosticIds.UrlFormat)]
public sealed class CollectionModel
{
private readonly Type _recordType;
private readonly Func<object> _recordFactory;
private VectorPropertyModel? _singleVectorProperty;
private DataPropertyModel? _singleFullTextSearchProperty;
/// <summary>
/// Gets the key properties of the record.
/// </summary>
public IReadOnlyList<KeyPropertyModel> KeyProperties { get; }
/// <summary>
/// Gets the data properties of the record.
/// </summary>
public IReadOnlyList<DataPropertyModel> DataProperties { get; }
/// <summary>
/// Gets the vector properties of the record.
/// </summary>
public IReadOnlyList<VectorPropertyModel> VectorProperties { get; }
/// <summary>
/// Gets all properties of the record, of all types.
/// </summary>
public IReadOnlyList<PropertyModel> Properties { get; }
/// <summary>
/// Gets all properties of the record, of all types, indexed by their model name.
/// </summary>
public IReadOnlyDictionary<string, PropertyModel> PropertyMap { get; }
/// <summary>
/// Gets a value indicating whether any of the vector properties in the model require embedding generation.
/// </summary>
public bool EmbeddingGenerationRequired { get; }
internal CollectionModel(
Type recordType,
Func<object> recordFactory,
IReadOnlyList<KeyPropertyModel> keyProperties,
IReadOnlyList<DataPropertyModel> dataProperties,
IReadOnlyList<VectorPropertyModel> vectorProperties,
IReadOnlyDictionary<string, PropertyModel> propertyMap)
{
_recordType = recordType;
_recordFactory = recordFactory;
KeyProperties = keyProperties;
DataProperties = dataProperties;
VectorProperties = vectorProperties;
PropertyMap = propertyMap;
Properties = propertyMap.Values.ToList();
EmbeddingGenerationRequired = vectorProperties.Any(p => p.EmbeddingType != p.Type);
}
/// <summary>
/// Gets the single key property in the model, and throws if there are multiple key properties.
/// </summary>
public KeyPropertyModel KeyProperty => field ??= KeyProperties.Single();
/// <summary>
/// Gets the single vector property in the model, and throws if there are multiple vector properties.
/// Suitable for providers where validation is in place for single vectors only (<see cref="CollectionModelBuildingOptions.SupportsMultipleVectors"/>).
/// </summary>
public VectorPropertyModel VectorProperty => _singleVectorProperty ??= VectorProperties.Single();
// TODO: the pattern of first instantiating via parameterless constructor and then populating the properties isn't compatible
// with read-only types, where properties have no setters. Supporting those would be problematic given the that different
// providers have completely different representations of the data coming back from the database, and which needs to be
// populated.
/// <summary>
/// Instantiates a new record of the specified type.
/// </summary>
/// <typeparam name="TRecord">The type of the record to create.</typeparam>
/// <returns>A new instance of the specified record type.</returns>
public TRecord CreateRecord<TRecord>()
{
Debug.Assert(typeof(TRecord) == _recordType, "Type mismatch between record type and model type.");
return (TRecord)_recordFactory();
}
/// <summary>
/// Gets the vector property with the provided name if a name is provided, and falls back
/// to a vector property in the schema if not.
/// </summary>
/// <typeparam name="TRecord">The type of the record.</typeparam>
/// <param name="searchOptions">The search options, which defines the vector property name.</param>
/// <returns>The matching <see cref="VectorPropertyModel"/>, or the single vector property if none is specified.</returns>
/// <exception cref="InvalidOperationException">
/// The provided property name is not a valid text data property name, or no name was provided and there's more than one vector
/// property.
/// </exception>
public VectorPropertyModel GetVectorPropertyOrSingle<TRecord>(VectorSearchOptions<TRecord> searchOptions)
{
_ = Throw.IfNull(searchOptions);
if (searchOptions.VectorProperty is not null)
{
return GetMatchingProperty<TRecord, VectorPropertyModel>(searchOptions.VectorProperty);
}
// 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.
_singleVectorProperty ??= VectorProperties switch
{
[var singleProperty] => singleProperty,
{ Count: 0 } => throw new InvalidOperationException($"The '{_recordType.Name}' type does not have any vector properties."),
_ => throw new InvalidOperationException($"The '{_recordType.Name}' type has multiple vector properties, please specify your chosen property via options."),
};
return _singleVectorProperty;
}
/// <summary>
/// Gets the text data property with the provided name that has full text search indexing enabled, or falls back
/// to a text data property in the schema if no name is provided.
/// </summary>
/// <typeparam name="TRecord">The type of the record.</typeparam>
/// <param name="expression">The full text search property selector.</param>
/// <returns>The matching <see cref="DataPropertyModel"/> with full text search indexing enabled.</returns>
/// <exception cref="InvalidOperationException">
/// The provided property name is not a valid text data property name, or no name was provided and there's more than one text data property with full text search indexing enabled.
/// </exception>
public DataPropertyModel GetFullTextDataPropertyOrSingle<TRecord>(Expression<Func<TRecord, object?>>? expression)
{
if (expression is not null)
{
var property = GetMatchingProperty<TRecord, DataPropertyModel>(expression);
return property.IsFullTextIndexed
? property
: throw new InvalidOperationException($"The property '{property.ModelName}' on '{_recordType.Name}' must have full text search indexing enabled.");
}
if (_singleFullTextSearchProperty is null)
{
// If text data property name is not provided, check if a single full text indexed text property exists or throw otherwise.
var fullTextStringProperties = DataProperties
.Where(l => l.Type == typeof(string) && l.IsFullTextIndexed)
.ToList();
// If text data property name is not provided, check if a single full text indexed text property exists or throw otherwise.
_singleFullTextSearchProperty = fullTextStringProperties switch
{
[var singleProperty] => singleProperty,
{ Count: 0 } => throw new InvalidOperationException($"The '{_recordType.Name}' type does not have any text data properties that have full text indexing enabled."),
_ => throw new InvalidOperationException($"The '{_recordType.Name}' type has multiple text data properties that have full text indexing enabled, please specify your chosen property via options."),
};
}
return _singleFullTextSearchProperty;
}
/// <summary>
/// Gets the data or key property selected by the provided expression.
/// </summary>
/// <typeparam name="TRecord">The type of the record.</typeparam>
/// <param name="expression">The property selector.</param>
/// <returns>The matching <see cref="PropertyModel"/>.</returns>
/// <exception cref="InvalidOperationException">The provided property name is not a valid data or key property name.</exception>
public PropertyModel GetDataOrKeyProperty<TRecord>(Expression<Func<TRecord, object?>> expression)
{
_ = Throw.IfNull(expression);
return GetMatchingProperty<TRecord, PropertyModel>(expression);
}
private TProperty GetMatchingProperty<TRecord, TProperty>(Expression<Func<TRecord, object?>> expression)
where TProperty : PropertyModel
{
var node = expression.Body;
// First, unwrap any object convert node: r => (object)r.PropertyName becomes r => r.PropertyName
if (expression.Body is UnaryExpression { NodeType: ExpressionType.Convert } convert
&& convert.Type == typeof(object))
{
node = convert.Operand;
}
var propertyName = node switch
{
// Simple member expression over the lambda parameter (r => r.PropertyName)
MemberExpression { Member: PropertyInfo clrProperty } member when member.Expression == expression.Parameters[0]
=> clrProperty.Name,
// Dictionary access over the lambda parameter, in dynamic mapping (r => r["PropertyName"])
MethodCallExpression { Method.Name: "get_Item", Arguments: [var keyExpression] } methodCall
=> keyExpression switch
{
ConstantExpression { Value: string text } => text,
MemberExpression field when TryGetCapturedValue(field, out object? capturedValue) && capturedValue is string text => text,
_ => throw new InvalidOperationException("Invalid dictionary key expression")
},
_ => throw new InvalidOperationException("Property selector lambda is invalid")
};
if (!PropertyMap.TryGetValue(propertyName, out var property))
{
throw new InvalidOperationException($"Property '{propertyName}' could not be found.");
}
return property is TProperty typedProperty
? typedProperty
: throw new InvalidOperationException($"Property '{propertyName}' isn't of type '{typeof(TProperty).Name}'.");
static bool TryGetCapturedValue(Expression expression, out object? capturedValue)
{
if (expression is MemberExpression { Expression: ConstantExpression constant, Member: FieldInfo fieldInfo }
&& constant.Type.Attributes.HasFlag(TypeAttributes.NestedPrivate)
&& Attribute.IsDefined(constant.Type, typeof(CompilerGeneratedAttribute), inherit: true))
{
capturedValue = fieldInfo.GetValue(constant.Value);
return true;
}
capturedValue = null;
return false;
}
}
}