Skip to content

Commit 585eceb

Browse files
committed
Enhance Vector Record Serialization and Deserialization
- Updated VectorRecord and QdrantVectorRecord classes to support AOT compatibility by adding overloads for JSON serialization and deserialization that accept JsonSerializerOptions and JsonTypeInfo. - Modified the Create methods in both classes to utilize the new serialization options. - Introduced runtime-configured dimensions and distance functions in PostgresVectorIndexer and QdrantVectorIndexer by creating VectorStoreCollectionDefinition. - Added a PromptInjectionDetector class to identify potential prompt injection patterns in user input, enhancing security measures. - Updated SemanticSearchDataSource to log potential injection attempts and sanitize inputs accordingly. - Improved documentation and comments throughout the code for clarity and security practices. - Adjusted unit tests to reflect changes in method signatures and added necessary reflection logic for testing.
1 parent ab1f53e commit 585eceb

33 files changed

Lines changed: 1318 additions & 67 deletions

Directory.Build.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
<PackageTags>blazor;autocomplete;component;ai;semantic-search;performance;trimming;aot;wcag;accessibility;theming;virtualization</PackageTags>
2525
<PackageReadmeFile>README.md</PackageReadmeFile>
2626
<PackageIcon>icon.png</PackageIcon>
27-
<Version>1.0.6</Version>
28-
<PackageReleaseNotes>Version 1.0.6 - Updated all package READMEs with focused documentation for each package. Added READMEs for vector database providers (PostgreSQL, Azure AI Search, Pinecone, Qdrant, CosmosDB).</PackageReleaseNotes>
27+
<Version>1.0.7</Version>
28+
<PackageReleaseNotes>Version 1.0.7 - Added AOT-compatible JSON serialization overloads for all vector database providers. Fixed exception handling in GetItem to return default on deserialization failure. Added cancellation token propagation in Azure Search hybrid search. Improved robustness and trimming compatibility.</PackageReleaseNotes>
2929
<Copyright>Copyright (c) 2025 EasyAppDev</Copyright>
3030

3131
<!-- Documentation -->

src/EasyAppDev.Blazor.AutoComplete.AI.AzureSearch/AzureSearchVectorIndexer.cs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using Azure.Search.Documents;
22
using Microsoft.Extensions.AI;
3+
using Microsoft.Extensions.VectorData;
34
using Microsoft.SemanticKernel.Connectors.AzureAISearch;
5+
using VectorDataDistanceFunction = Microsoft.Extensions.VectorData.DistanceFunction;
46
using EasyAppDev.Blazor.AutoComplete.AI.Abstractions;
57
using EasyAppDev.Blazor.AutoComplete.AI.Models;
68
using EasyAppDev.Blazor.AutoComplete.AI.AzureSearch.Models;
@@ -53,7 +55,46 @@ public AzureSearchVectorIndexer(
5355
_textSelector = textSelector;
5456
_titleSelector = titleSelector;
5557
_idSelector = idSelector;
56-
_collection = vectorStore.GetCollection<string, AzureSearchVectorRecord>(options.IndexName);
58+
59+
// Create record definition with runtime-configured dimensions and distance function
60+
var definition = CreateRecordDefinition(options);
61+
_collection = vectorStore.GetCollection<string, AzureSearchVectorRecord>(options.IndexName, definition);
62+
}
63+
64+
/// <summary>
65+
/// Creates a VectorStoreCollectionDefinition with runtime-configured dimensions and distance function.
66+
/// This overrides the hardcoded values in the AzureSearchVectorRecord attributes.
67+
/// </summary>
68+
private static VectorStoreCollectionDefinition CreateRecordDefinition(AzureSearchVectorSearchOptions options)
69+
{
70+
return new VectorStoreCollectionDefinition
71+
{
72+
Properties =
73+
[
74+
new VectorStoreKeyProperty("Id", typeof(string)),
75+
new VectorStoreDataProperty("ItemJson", typeof(string)),
76+
new VectorStoreDataProperty("Content", typeof(string)),
77+
new VectorStoreDataProperty("Title", typeof(string)),
78+
new VectorStoreVectorProperty("Embedding", typeof(ReadOnlyMemory<float>), options.EmbeddingDimensions)
79+
{
80+
DistanceFunction = MapDistanceFunction(options.DistanceFunction)
81+
}
82+
]
83+
};
84+
}
85+
86+
/// <summary>
87+
/// Maps the library's DistanceFunction enum to Semantic Kernel's DistanceFunction string.
88+
/// </summary>
89+
private static string MapDistanceFunction(AI.Models.DistanceFunction distanceFunction)
90+
{
91+
return distanceFunction switch
92+
{
93+
AI.Models.DistanceFunction.Cosine => VectorDataDistanceFunction.CosineSimilarity,
94+
AI.Models.DistanceFunction.Euclidean => VectorDataDistanceFunction.EuclideanDistance,
95+
AI.Models.DistanceFunction.DotProduct => VectorDataDistanceFunction.DotProductSimilarity,
96+
_ => VectorDataDistanceFunction.CosineSimilarity
97+
};
5798
}
5899

59100
/// <inheritdoc />

src/EasyAppDev.Blazor.AutoComplete.AI.AzureSearch/AzureSearchVectorSearchProvider.cs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using Azure.Search.Documents;
22
using Azure.Search.Documents.Models;
3+
using Microsoft.Extensions.VectorData;
34
using Microsoft.SemanticKernel.Connectors.AzureAISearch;
5+
using VectorDataDistanceFunction = Microsoft.Extensions.VectorData.DistanceFunction;
46
using EasyAppDev.Blazor.AutoComplete.AI.Abstractions;
57
using EasyAppDev.Blazor.AutoComplete.AI.AzureSearch.Models;
68

@@ -38,7 +40,46 @@ public AzureSearchVectorSearchProvider(
3840

3941
_searchClient = searchClient;
4042
_options = options;
41-
_collection = vectorStore.GetCollection<string, AzureSearchVectorRecord>(options.IndexName);
43+
44+
// Create record definition with runtime-configured dimensions and distance function
45+
var definition = CreateRecordDefinition(options);
46+
_collection = vectorStore.GetCollection<string, AzureSearchVectorRecord>(options.IndexName, definition);
47+
}
48+
49+
/// <summary>
50+
/// Creates a VectorStoreCollectionDefinition with runtime-configured dimensions and distance function.
51+
/// This overrides the hardcoded values in the AzureSearchVectorRecord attributes.
52+
/// </summary>
53+
private static VectorStoreCollectionDefinition CreateRecordDefinition(AzureSearchVectorSearchOptions options)
54+
{
55+
return new VectorStoreCollectionDefinition
56+
{
57+
Properties =
58+
[
59+
new VectorStoreKeyProperty("Id", typeof(string)),
60+
new VectorStoreDataProperty("ItemJson", typeof(string)),
61+
new VectorStoreDataProperty("Content", typeof(string)),
62+
new VectorStoreDataProperty("Title", typeof(string)),
63+
new VectorStoreVectorProperty("Embedding", typeof(ReadOnlyMemory<float>), options.EmbeddingDimensions)
64+
{
65+
DistanceFunction = MapDistanceFunction(options.DistanceFunction)
66+
}
67+
]
68+
};
69+
}
70+
71+
/// <summary>
72+
/// Maps the library's DistanceFunction enum to Semantic Kernel's DistanceFunction string.
73+
/// </summary>
74+
private static string MapDistanceFunction(AI.Models.DistanceFunction distanceFunction)
75+
{
76+
return distanceFunction switch
77+
{
78+
AI.Models.DistanceFunction.Cosine => VectorDataDistanceFunction.CosineSimilarity,
79+
AI.Models.DistanceFunction.Euclidean => VectorDataDistanceFunction.EuclideanDistance,
80+
AI.Models.DistanceFunction.DotProduct => VectorDataDistanceFunction.DotProductSimilarity,
81+
_ => VectorDataDistanceFunction.CosineSimilarity
82+
};
4283
}
4384

4485
/// <inheritdoc />
@@ -141,7 +182,9 @@ public AzureSearchVectorSearchProvider(
141182

142183
var results = new List<AI.Models.VectorSearchResult<TItem>>();
143184

144-
await foreach (var result in response.Value.GetResultsAsync().ConfigureAwait(false))
185+
await foreach (var result in response.Value.GetResultsAsync()
186+
.WithCancellation(cancellationToken)
187+
.ConfigureAwait(false))
145188
{
146189
var score = (float)(result.Score ?? 0);
147190

src/EasyAppDev.Blazor.AutoComplete.AI.AzureSearch/EasyAppDev.Blazor.AutoComplete.AI.AzureSearch.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
<ItemGroup>
3333
<!-- Semantic Kernel Azure AI Search connector (prerelease) -->
3434
<PackageReference Include="Microsoft.SemanticKernel.Connectors.AzureAISearch" Version="*-*" />
35+
<PackageReference Include="Microsoft.Extensions.VectorData.Abstractions" Version="9.*-*" />
3536
<PackageReference Include="Azure.Search.Documents" Version="11.*" />
3637
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" Condition="'$(TargetFramework)' == 'net8.0'" />
3738
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.0" Condition="'$(TargetFramework)' == 'net9.0'" />

src/EasyAppDev.Blazor.AutoComplete.AI.AzureSearch/Models/AzureSearchVectorRecord.cs

Lines changed: 146 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Text.Json;
2+
using System.Text.Json.Serialization.Metadata;
23
using Microsoft.Extensions.VectorData;
34

45
namespace EasyAppDev.Blazor.AutoComplete.AI.AzureSearch.Models;
@@ -8,6 +9,10 @@ namespace EasyAppDev.Blazor.AutoComplete.AI.AzureSearch.Models;
89
/// The TItem is serialized as JSON for flexible storage.
910
/// Note: Must be public for Azure Search SDK JSON serialization to work.
1011
/// </summary>
12+
/// <remarks>
13+
/// For AOT compatibility, use the overloads that accept <see cref="JsonSerializerOptions"/>
14+
/// or <see cref="JsonTypeInfo{T}"/> with a source-generated context.
15+
/// </remarks>
1116
public sealed class AzureSearchVectorRecord
1217
{
1318
/// <summary>
@@ -36,7 +41,7 @@ public sealed class AzureSearchVectorRecord
3641

3742
/// <summary>
3843
/// The embedding vector.
39-
/// Dimensions are configured at runtime.
44+
/// Dimensions are configured at runtime via VectorStoreCollectionDefinition.
4045
/// </summary>
4146
[VectorStoreVector(Dimensions: 1536, DistanceFunction = DistanceFunction.CosineSimilarity)]
4247
public required ReadOnlyMemory<float> Embedding { get; init; }
@@ -45,13 +50,73 @@ public sealed class AzureSearchVectorRecord
4550
/// Deserializes the item from JSON.
4651
/// </summary>
4752
/// <typeparam name="TItem">The item type to deserialize to.</typeparam>
48-
/// <returns>The deserialized item, or null if deserialization fails.</returns>
53+
/// <returns>The deserialized item, or default if deserialization fails.</returns>
54+
/// <remarks>
55+
/// This method uses reflection-based deserialization and is NOT AOT-compatible.
56+
/// For AOT scenarios, use <see cref="GetItem{TItem}(JsonSerializerOptions)"/> or
57+
/// <see cref="GetItem{TItem}(JsonTypeInfo{TItem})"/> instead.
58+
/// </remarks>
4959
public TItem? GetItem<TItem>()
60+
{
61+
return GetItemCore<TItem>(null, null);
62+
}
63+
64+
/// <summary>
65+
/// Deserializes the item from JSON using the specified options.
66+
/// </summary>
67+
/// <typeparam name="TItem">The item type to deserialize to.</typeparam>
68+
/// <param name="options">JSON serializer options with a configured TypeInfoResolver for AOT compatibility.</param>
69+
/// <returns>The deserialized item, or default if deserialization fails.</returns>
70+
/// <remarks>
71+
/// For AOT compatibility, configure options with a source-generated JsonSerializerContext:
72+
/// <code>
73+
/// var options = new JsonSerializerOptions { TypeInfoResolver = MyJsonContext.Default };
74+
/// var item = record.GetItem&lt;MyType&gt;(options);
75+
/// </code>
76+
/// </remarks>
77+
public TItem? GetItem<TItem>(JsonSerializerOptions options)
78+
{
79+
ArgumentNullException.ThrowIfNull(options);
80+
return GetItemCore<TItem>(options, null);
81+
}
82+
83+
/// <summary>
84+
/// Deserializes the item from JSON using the specified type info.
85+
/// </summary>
86+
/// <typeparam name="TItem">The item type to deserialize to.</typeparam>
87+
/// <param name="jsonTypeInfo">The JSON type info from a source-generated context.</param>
88+
/// <returns>The deserialized item, or default if deserialization fails.</returns>
89+
/// <remarks>
90+
/// This is the preferred method for AOT-compatible deserialization:
91+
/// <code>
92+
/// var item = record.GetItem(MyJsonContext.Default.MyType);
93+
/// </code>
94+
/// </remarks>
95+
public TItem? GetItem<TItem>(JsonTypeInfo<TItem> jsonTypeInfo)
96+
{
97+
ArgumentNullException.ThrowIfNull(jsonTypeInfo);
98+
return GetItemCore<TItem>(null, jsonTypeInfo);
99+
}
100+
101+
private TItem? GetItemCore<TItem>(JsonSerializerOptions? options, JsonTypeInfo<TItem>? jsonTypeInfo)
50102
{
51103
if (string.IsNullOrEmpty(ItemJson))
52104
return default;
53105

54-
return JsonSerializer.Deserialize<TItem>(ItemJson);
106+
try
107+
{
108+
if (jsonTypeInfo is not null)
109+
return JsonSerializer.Deserialize(ItemJson, jsonTypeInfo);
110+
111+
if (options is not null)
112+
return JsonSerializer.Deserialize<TItem>(ItemJson, options);
113+
114+
return JsonSerializer.Deserialize<TItem>(ItemJson);
115+
}
116+
catch (JsonException)
117+
{
118+
return default;
119+
}
55120
}
56121

57122
/// <summary>
@@ -64,17 +129,94 @@ public sealed class AzureSearchVectorRecord
64129
/// <param name="title">Optional title for filtering.</param>
65130
/// <param name="embedding">The embedding vector.</param>
66131
/// <returns>A new AzureSearchVectorRecord instance.</returns>
132+
/// <remarks>
133+
/// This method uses reflection-based serialization and is NOT AOT-compatible.
134+
/// For AOT scenarios, use <see cref="Create{TItem}(string, TItem, string, string?, ReadOnlyMemory{float}, JsonSerializerOptions)"/>
135+
/// or <see cref="Create{TItem}(string, TItem, string, string?, ReadOnlyMemory{float}, JsonTypeInfo{TItem})"/> instead.
136+
/// </remarks>
67137
public static AzureSearchVectorRecord Create<TItem>(
68138
string id,
69139
TItem item,
70140
string content,
71141
string? title,
72142
ReadOnlyMemory<float> embedding)
73143
{
144+
return CreateCore(id, item, content, title, embedding, null, null);
145+
}
146+
147+
/// <summary>
148+
/// Creates an AzureSearchVectorRecord from an item using the specified options.
149+
/// </summary>
150+
/// <typeparam name="TItem">The item type.</typeparam>
151+
/// <param name="id">Unique identifier.</param>
152+
/// <param name="item">The item to store.</param>
153+
/// <param name="content">Text content for full-text search.</param>
154+
/// <param name="title">Optional title for filtering.</param>
155+
/// <param name="embedding">The embedding vector.</param>
156+
/// <param name="options">JSON serializer options with a configured TypeInfoResolver for AOT compatibility.</param>
157+
/// <returns>A new AzureSearchVectorRecord instance.</returns>
158+
public static AzureSearchVectorRecord Create<TItem>(
159+
string id,
160+
TItem item,
161+
string content,
162+
string? title,
163+
ReadOnlyMemory<float> embedding,
164+
JsonSerializerOptions options)
165+
{
166+
ArgumentNullException.ThrowIfNull(options);
167+
return CreateCore(id, item, content, title, embedding, options, null);
168+
}
169+
170+
/// <summary>
171+
/// Creates an AzureSearchVectorRecord from an item using the specified type info.
172+
/// </summary>
173+
/// <typeparam name="TItem">The item type.</typeparam>
174+
/// <param name="id">Unique identifier.</param>
175+
/// <param name="item">The item to store.</param>
176+
/// <param name="content">Text content for full-text search.</param>
177+
/// <param name="title">Optional title for filtering.</param>
178+
/// <param name="embedding">The embedding vector.</param>
179+
/// <param name="jsonTypeInfo">The JSON type info from a source-generated context.</param>
180+
/// <returns>A new AzureSearchVectorRecord instance.</returns>
181+
/// <remarks>
182+
/// This is the preferred method for AOT-compatible serialization:
183+
/// <code>
184+
/// var record = AzureSearchVectorRecord.Create(id, item, content, title, embedding, MyJsonContext.Default.MyType);
185+
/// </code>
186+
/// </remarks>
187+
public static AzureSearchVectorRecord Create<TItem>(
188+
string id,
189+
TItem item,
190+
string content,
191+
string? title,
192+
ReadOnlyMemory<float> embedding,
193+
JsonTypeInfo<TItem> jsonTypeInfo)
194+
{
195+
ArgumentNullException.ThrowIfNull(jsonTypeInfo);
196+
return CreateCore(id, item, content, title, embedding, null, jsonTypeInfo);
197+
}
198+
199+
private static AzureSearchVectorRecord CreateCore<TItem>(
200+
string id,
201+
TItem item,
202+
string content,
203+
string? title,
204+
ReadOnlyMemory<float> embedding,
205+
JsonSerializerOptions? options,
206+
JsonTypeInfo<TItem>? jsonTypeInfo)
207+
{
208+
string itemJson;
209+
if (jsonTypeInfo is not null)
210+
itemJson = JsonSerializer.Serialize(item, jsonTypeInfo);
211+
else if (options is not null)
212+
itemJson = JsonSerializer.Serialize(item, options);
213+
else
214+
itemJson = JsonSerializer.Serialize(item);
215+
74216
return new AzureSearchVectorRecord
75217
{
76218
Id = id,
77-
ItemJson = JsonSerializer.Serialize(item),
219+
ItemJson = itemJson,
78220
Content = content,
79221
Title = title,
80222
Embedding = embedding

0 commit comments

Comments
 (0)